2019独角兽企业重金招聘Python工程师标准>>>
前言
说明一下需求,最近做的平台,有多张用户表,怎么根据不同用户登录去执行自己查询不同数据库并实现认证的业务逻辑呢?
博主参与的产品开发进入阶段性完成期,有时间将过程中遇到的相关问题一一总结。
总结
实现本需求,首先是从Subject入手,它是完成shiro登录过程的入口,login(UsernamePasswordToken)方法完成用户名密码传递,后面自己实现Realm去认证登录,关键就在如何区分这些用户名密码是对应哪个数据库表,若有一个状态去判断它们,则可以解决问题。
设计上的反思
其实从实际参与这个大产品开发之后,越来越发现,它不便于我们对各类用户的管理,虽然做了很多针对shiro的扩展去实现自己想要的功能,但渐渐明白为什么shiro不提供这样的解决方案。
这里,博主也建议,用户表可以有多个,但登录认证的表其实只保留一个就好,将你的多Realm抽象出来一个关系表映射,将各种状态加入,登录等认证交由统一维护,具体信息查询等封装抽象,下面做对应实现即可,这样才应该是跨平台的,以后也只需要存储跟别的平台的用户关系绑定,就完成了登录。
正文
shiro标准的登录过程是用户在Controller里创建UsernamePasswordToken对象,然后绑定上前端访问过来的账号密码,之后由Subject.login(UsernamePasswordToken)完成登录,自己实现AuthorizingRealm完成登录认证,里面插入操作Service、DAO代码;(业务代码省略)
———-
若要区分不同用户登录查询哪个表,若有3个用户表,那么对于Service、DAO应该是有3种不同的代码片,毕竟业务不同,绑定字段不同,查询数据库表不同。如此,在最开始阶段,用户登录时,我们需要标记登录去查哪个表,标记后让系统动态处理,创建一个枚举或者静态常量类都行:
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是需要用户自行去访问对应数据库(它也不知道访问哪),下面实现了我的产品里厂商平台用户登录。
/**
* 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 urlSet = shiro_factoryMenu.findMenuUrlByUserId(shiroUser.getId());
Set 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);
}
}
关注这段代码:
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后,要让它在认证和校验过程中自动绑定,交由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,加载认证的时候,Shiro会进入这个方法org.apache.shiro.authc.pam.ModularRealmAuthenticator,将多个Realm都读取到,并加载这些认证信息。说到这里,应该就知道了,我们只需要在读取这些信息的时候,判定使用哪个Realm就行。
步骤是:1、重写ModularRealmAuthenticator;2、针对多Realm,找到指定待认证的Realm信息;3、手工调用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 definedRealms;
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 强制转换回自定义的CustomizedToken
CustomLoginToken token = (CustomLoginToken) authenticationToken;
// 找到当前登录人的登录类型
String loginType = token.getLoginType();
// 所有Realm
Collection realms = getRealms();
// 找到登录类型对应的指定Realm
Collection typeRealms = new ArrayList();
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 getDefinedRealms() {
return this.definedRealms;
}
public void setDefinedRealms(Map definedRealms) {
this.definedRealms = definedRealms;
}
}
上述代码中完成了对应步骤,接下来要去做对应的xml绑定初始化,重写的对象一定都要去配置,否则shiro压根不知道,还是会执行它自己那套源码,配置如下:
关于这里提到的2个认证类型,博主都加上了,but只启用了单一认证策略,这也是我要的,不需要全部认证通过才算。
再将自定义认证信息和参与认证的Realm加入安全管理器securityManager
,配置如下:
这样,shiro就能识别对多Realms如何精确指向需要指定认证的Realm处理,改写了shiro的代码就完成。