前言
说明一下需求,最近作的平台,有多张用户表,怎么根据不一样用户登陆去执行本身查询不一样数据库并实现认证的业务逻辑呢?前端
博主参与的产品开发进入阶段性完成期,有时间将过程当中遇到的相关问题一一总结。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的代码就完成。