前言
說明一下需求,最近作的平臺,有多張用戶表,怎麼根據不一樣用戶登陸去執行本身查詢不一樣數據庫並實現認證的業務邏輯呢?前端
博主參與的產品開發進入階段性完成期,有時間將過程當中遇到的相關問題一一總結。java
總結
實現本需求,首先是從Subject入手,它是完成shiro登陸過程的入口,login(UsernamePasswordToken)方法完成用戶名密碼傳遞,後面本身實現Realm去認證登陸,關鍵就在如何區分這些用戶名密碼是對應哪一個數據庫表,如有一個狀態去判斷它們,則能夠解決問題。web
設計上的反思
其實從實際參與這個大產品開發以後,愈來愈發現,它不便於咱們對各種用戶的管理,雖然作了不少針對shiro的擴展去實現本身想要的功能,但漸漸明白爲何shiro不提供這樣的解決方案。
這裏,博主也建議,用戶表能夠有多個,但登陸認證的表其實只保留一個就好,將你的多Realm抽象出來一個關係表映射,將各類狀態加入,登陸等認證交由統一維護,具體信息查詢等封裝抽象,下面作對應實現便可,這樣才應該是跨平臺的,之後也只須要存儲跟別的平臺的用戶關係綁定,就完成了登陸。redis
正文
shiro標準的登陸過程是用戶在Controller裏建立UsernamePasswordToken對象,而後綁定上前端訪問過來的帳號密碼,以後由Subject.login(UsernamePasswordToken)完成登陸,本身實現AuthorizingRealm完成登陸認證,裏面插入操做Service、DAO代碼;(業務代碼省略)數據庫
———-
若要區分不一樣用戶登陸查詢哪一個表,如有3個用戶表,那麼對於Service、DAO應該是有3種不一樣的代碼片,畢竟業務不一樣,綁定字段不一樣,查詢數據庫表不一樣。如此,在最開始階段,用戶登陸時,咱們須要標記登陸去查哪一個表,標記後讓系統動態處理,建立一個枚舉或者靜態常量類都行:apache
public class UserType { /** 經銷商平臺 */ public static final String AGENCY = "agency"; /** 廠商平臺 */ public static final String FACTORY = "factory"; /** 系統平臺 */ public static final String SYSTEM = "system"; /** 消費者平臺 */ public static final String PERSON = "person"; /** 遊客 */ public static final String GUEST = "guest"; }
接下來擴展UsernamePasswordToken
,讓其攜帶咱們上面加的類型到Realm認證中,這樣才便於判斷用戶類型:緩存
/** * Description:自定義shiro-token重寫類,用於多類型用戶校驗 * @author around * @date 2017年8月15日上午9:50:42 */ public class CustomLoginToken extends UsernamePasswordToken { private static final long serialVersionUID = 2020457391511655213L; private String loginType; public CustomLoginToken() {} public CustomLoginToken(final String username, final String password, final String loginType) { super(username, password); this.loginType = loginType; } public String getLoginType() { return loginType; } public void setLoginType(String loginType) { this.loginType = loginType; } }
如此,後面咱們在用戶登陸時,再也不調用系統的UsernamePasswordToken類綁定用戶密碼,而是調用CustomLoginToken進行綁定,而且還能夠多攜帶參數loginType。安全
———-
接下來完成登陸操做,shiro是須要用戶自行去訪問對應數據庫(它也不知道訪問哪),下面實現了個人產品裏廠商平臺用戶登陸。
session
/** * Description:廠商平臺自定義shiro認證模塊 * @author around * @date 2017年8月15日上午11:33:20 */ public class FactoryRealm extends AuthorizingRealm { private static Logger LOGGER = LoggerFactory.getLogger(FactoryRealm.class); @Autowired private FactoryUserService shiro_factoryUser; @Autowired private RoleService shiro_factoryRole; @Autowired private MenuService shiro_factoryMenu; @Override public String getName() { return UserType.FACTORY; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { LOGGER.info("Shiro-factory登陸認證"); //getAuthenticationCache(); CustomLoginToken token = (CustomLoginToken) authcToken; FactoryUserVo user = shiro_factoryUser.selectUserByUserName(token.getUsername()); //帳號不存在 if (user == null) { throw new UnknownAccountException(); } //密碼錯誤 if (!user.getPassword().equals(String.valueOf(token.getPassword()))) { throw new IncorrectCredentialsException(); } //帳號未啓用 if (user.getStatus() != 1 || user.getIsDeleted() != 1) { throw new DisabledAccountException(); } ShiroUser shiroUser = new ShiroUser(user.getId(), user.getUserName(), user.getTrueName(), getName()); user.setPassword(null); //修改用戶session setCurrentUser(user); // 認證緩存信息,不作自定義加鹽密碼認證 return new SimpleAuthenticationInfo(shiroUser, token.getPassword(), getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //校驗當前用戶類型是否正確,正確則進入處理角色權限問題,不然跳出 if (!principals.getRealmNames().contains(getName())) return null; //獲取當前登陸的用戶 ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal(); Set<String> urlSet = shiro_factoryMenu.findMenuUrlByUserId(shiroUser.getId()); Set<String> roles = shiro_factoryRole.findByUserId(shiroUser.getId()); shiroUser.urlSet = urlSet; SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(urlSet); info.addRoles(roles); //追加廠商角色 info.addRole(UserType.FACTORY); return info; } private void setCurrentUser(Object user){ UserUtils.setCurrentUser(user); } public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } @Override protected void clearCache(PrincipalCollection principals) { super.clearCache(principals); } @Override protected void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override protected void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } }
關注這段代碼:ide
CustomLoginToken token = (CustomLoginToken) authcToken;
它將登陸認證方法doGetAuthorizationInfo返回的AuthenticationToken轉換爲咱們自定義的CustomLoginToken,能夠看看繼承樹,UsernamePasswordToken是AuthenticationToken的子集。因此這麼轉換,咱們必定拿獲得本身要的CustomLoginToken,而後再取出loginType判斷訪問哪一個數據庫?
Question
若咱們直接在Realm裏作對應的Service或者DAO訪問,那就是寫一堆if-else或者switch,是否是代碼很low?這屬於面向過程式編碼,而且還得作不一樣的doGetAuthorizationInfo權限認證,又得判斷。因此每一個用戶只對應一個Realm是最好的辦法,而且後面管理一個平臺的在線用戶和活躍度很方便。
這裏,博主將多用戶表對應成多Realm對象,如圖1,實現內容跟上面一致,都是作對應業務的用戶、權限查詢。
接着,到shiro.xml添加配置
<!-- 項目自定義的Realm --> <bean id="agencyRealm" class="com.fg.cloud.framework.shiro.realm.AgencyRealm"> <property name="cachingEnabled" value="true" /> <!-- <property name="cacheManager" ref="redisCacheManager"></property> --> </bean> <bean id="factoryRealm" class="com.fg.cloud.framework.shiro.realm.FactoryRealm"> <property name="cachingEnabled" value="true" /> <!-- <property name="cacheManager" ref="redisCacheManager"></property> --> </bean> <!-- 系統用戶 --> <bean id="systemRealm" class="com.fg.cloud.framework.shiro.realm.SystemRealm"> <property name="cachingEnabled" value="true" /> </bean> <bean id="guestRealm" class="com.fg.cloud.framework.shiro.realm.GuestRealm"></bean>
———-
明確了每一個用戶表對應的Realm後,要讓它在認證和校驗過程當中自動綁定,交由shiro完成的話,可它不知道怎麼完成!這就是須要繼續擴展了(深坑,後續會總結說明一下這類問題),下面咱們來擴展它。
擴展以前先說一下Shiro的Realm如何工做的,當用戶登陸後,shiro首先去訪問安全管理器securityManager,通常web項目都用這個
org.apache.shiro.web.mgt.DefaultWebSecurityManager
安全管理器作認證使用的,那麼用戶須要將本身實現的Realm寫入,若只有一個Realm,則設置屬性綁定realm,如有多個,則用realms。
而設置只是讓Shiro知道你的這個項目有幾個Realm,它管理認證校驗時,必定會將多個Realm都參與認證。意思就是,按上面的來講,即便我知道本身使用的FactoryRealm認證,而Shiro不知道,依舊會把上述博主添加的全部Realm所有去校驗,因此這裏得有本身的代碼,讓它只校驗對應的Realm。
按照shiro的源碼,若安全管理器只配置一個Realm,則使用doSingleRealmAuthentication方法進入Realm作單獨認證;如有多個Realm時,則使用doMultiRealmAuthentication方法,加載Collection<Realm>進行全部的Realm認證。
由於博主添加了多個Realm,加載認證的時候,Shiro會進入這個方法org.apache.shiro.authc.pam.ModularRealmAuthenticator,將多個Realm都讀取到,並加載這些認證信息。說到這裏,應該就知道了,咱們只須要在讀取這些信息的時候,斷定使用哪一個Realm就行。
步驟是:一、重寫ModularRealmAuthenticator;二、針對多Realm,找到指定待認證的Realm信息;三、手工調用doSingleRealmAuthentication讓其只認證指定須要認證的那個,可經過loginType;
import java.util.ArrayList; import java.util.Collection; import java.util.Map; import org.apache.shiro.ShiroException; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.pam.ModularRealmAuthenticator; import org.apache.shiro.realm.Realm; import org.apache.shiro.util.CollectionUtils; import com.fg.cloud.common.shiro.CustomLoginToken; /** * Description:全局shiro攔截分發realm * @author around * @date 2017年8月15日上午11:34:05 */ public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator { private Map<String, Object> definedRealms; @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { // 判斷getRealms()是否返回爲空 assertRealmsConfigured(); // 強制轉換回自定義的CustomizedToken CustomLoginToken token = (CustomLoginToken) authenticationToken; // 找到當前登陸人的登陸類型 String loginType = token.getLoginType(); // 全部Realm Collection<Realm> realms = getRealms(); // 找到登陸類型對應的指定Realm Collection<Realm> typeRealms = new ArrayList<Realm>(); for (Realm realm : realms) { if (realm.getName().toLowerCase().contains(loginType)) typeRealms.add(realm); } // 判斷是單Realm仍是多Realm if (typeRealms.size() == 1) return doSingleRealmAuthentication(typeRealms.iterator().next(), token); else return doMultiRealmAuthentication(typeRealms, token); } /** * 判斷realm是否爲空 */ @Override protected void assertRealmsConfigured() throws IllegalStateException { this.definedRealms = this.getDefinedRealms(); if (CollectionUtils.isEmpty(this.definedRealms)) { throw new ShiroException("值傳遞錯誤!"); } } public Map<String, Object> getDefinedRealms() { return this.definedRealms; } public void setDefinedRealms(Map<String, Object> definedRealms) { this.definedRealms = definedRealms; } }
上述代碼中完成了對應步驟,接下來要去作對應的xml綁定初始化,重寫的對象必定都要去配置,不然shiro壓根不知道,仍是會執行它本身那套源碼,配置以下:
<!-- 配置使用自定義認證器,能夠實現多Realm認證,而且能夠指定特定Realm處理特定類型的驗證 --> <bean id="authenticator" class="com.fg.cloud.framework.shiro.realm.CustomModularRealmAuthenticator"> <property name="definedRealms"> <map> <entry key="agency" value-ref="agencyRealm" /> <entry key="factory" value-ref="factoryRealm" /> <entry key="guest" value-ref="guestRealm" /> <!-- 系統用戶 --> <entry key="system" value-ref="systemRealm" /> </map> </property> <property name="authenticationStrategy"> <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy" /> <!-- 配置認證策略,只要有一個Realm認證成功便可,而且返回全部認證成功信息 --> <!-- <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy" /> --> </property> </bean>
關於這裏提到的2個認證類型,博主都加上了,but只啓用了單一認證策略,這也是我要的,不須要所有認證經過纔算。
再將自定義認證信息和參與認證的Realm加入安全管理器securityManager
,配置以下:
<!--安全管理器--> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 設置自定義Realm --> <property name="authenticator" ref="authenticator"></property> <property name="realms"> <list> <ref bean="factoryRealm"/> <ref bean="agencyRealm" /> <ref bean="guestRealm" /> <ref bean="systemRealm" /> </list> </property> <!--將緩存管理器,交給安全管理器--> <!-- <property name="cacheManager" ref="redisCacheManager" /> --> <property name="rememberMeManager" ref="rememberMeManager"/> <property name="sessionManager" ref="sessionManager" /> </bean>
這樣,shiro就能識別對多Realms如何精確指向須要指定認證的Realm處理,改寫了shiro的代碼就完成。