1、背景
公司内部的OA系统,慢慢的迭代升级,最终转型成为【综合业务系统】。很多大型系统最终都将走向类似于【企业微信】这样的大型系统发展。
前期:系统只存在,如:【用户(人)】这样的使用者进行登录和权限认证过程。慢慢的,【xx应用】、【xx设备】、【xx其他使用类型】都需要用到系统功能,比如【xx应用】需要使用【内部合同模块】,【xx设备】需要使用【内部门锁模块】等等情况。
问题:这时候如果只靠模拟用户登录,很多很多地方都接入了,都用了某个用户登录信息,那到底是不是这个人操作等问题就出现了。特别是:单点登录这种常用功能,就会出现用户挤兑现象。
总结:从管理学角度,其实谁【使用者】用了什么东西,最好严格区分,如果出现问题,也方便排查,责任划分也更方便。从设计学角度,这样的设计也算是数据分离,对于维护是有好处的。
2、一期的简单改造
其他系统,如:【xx应用】也需要接入系统,也需要登录和授权。那么,可以尝试在:【用户认证+权限检测】处进行统一分发处理,使用策略模式进行改造。
3、代码实现
首先做个BaseToken类,用于继承Token,方便给各个实现扩展
public class BaseToken implements Serializable {
protected static final long serialVersionUID = 1L;
// token类型
protected String tokenType = Const.DEFAUTL_TOKEN_TYPE;
// 登录对象ID
protected String principalId;
//用户ID
protected Integer userId;
//token
protected String token;
//过期时间
protected Date expireTime;
//更新时间
protected Date updateTime;
// 过期时间
protected Integer expire;
}
再做一个PrincipalInfo,用户继承使用者信息,如:userInfo
public class PrincipalInfo {
protected static final long serialVersionUID = 1L;
/**
* 使用类型
*/
protected String tokenType = "user_device_token";
/**
* 登录对象ID
*/
protected String principalId = "";
/**
* 用户ID
*/
protected Integer userId = 0;
/**
* 用户名(账户)
*/
protected String userName = "";
/**
* 昵称
*/
protected String nickName = "";
/**
* 用户类型 1-系统用户,2-客户
*/
protected Integer userType;
/**
* 手机号码
*/
protected String mobile;
/**
* 状态 0-禁用 1-正常
*/
protected Integer status;
/**
* 是否锁定:1-是 0-否
*/
protected Integer isLock;
protected String avatar;
}
中转工具统一封装一下,这里拆分为两个类:一个是token工厂类,用于产生token;另一个是认证类,用于用户和权限认证。
@Slf4j
@Component
public class TokenServiceUtil {
@Resource
ApplicationContext applicationContext;
@Resource
RedisUtil redisUtil;
@Component
public class Factory{
public BaseToken createToken(String beanName, JSONObject param){
try{
TokenConfigBaseService util = (TokenConfigBaseService) applicationContext.getBean(beanName);
if(null == util){
throw new KgoaException(Result.TOKEN_UTIL_NOT_EXIST);
}
// 创建token
return util.createToken(beanName, param);
}catch (NoSuchBeanDefinitionException e){
throw new KgoaException(Result.TOKEN_UTIL_NOT_EXIST);
}
}
}
@Component
public class Auth{
/**
* 鉴权token
*/
public PrincipalInfo authPrincipal(String token){
try{
// 检查token
BaseToken tokenEntity = redisUtil.getCacheObject(RedisConst.TOKEN_PREFIX + token);
if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
throw new KgoaException(Result.TOKEN_INVALID);
}
// 获取响应工具
TokenConfigBaseService util = (TokenConfigBaseService) applicationContext.getBean(tokenEntity.getTokenType());
if(null == util){
throw new KgoaException(Result.TOKEN_UTIL_NOT_EXIST);
}
// 鉴权token
return util.authPrincipal(token);
}catch (NoSuchBeanDefinitionException e){
throw new KgoaException(Result.TOKEN_UTIL_NOT_EXIST);
}
}
/**
* 获取授权
*/
public Set<String> getPermissions(PrincipalInfo principal){
// 获取响应工具
TokenConfigBaseService util = (TokenConfigBaseService) applicationContext.getBean(principal.getTokenType());
if(null == util){
throw new KgoaException(Result.TOKEN_UTIL_NOT_EXIST);
}
return util.getPermissions(principal);
}
}
}
然后就是,抽象接口,用于作为通用实现的上层处理。
public interface TokenConfigBaseService {
/**
* 创建token
*/
BaseToken createToken(String beanName, JSONObject param);
/**
* 检查主体
*/
PrincipalInfo authPrincipal(String token);
/**
* 获取权限
* @return
*/
Set<String> getPermissions(PrincipalInfo principal);
}
通用实现
@Service("user_device_token")
public class UserDeviceImpl implements TokenConfigBaseService {
@Autowired
private UserTokenService userTokenService;
@Autowired
private ShiroService shiroService;
/**
* 创建 token
*/
@Override
public BaseToken createToken(String beanName, JSONObject param) {
Integer userId = param.getInt("user_id");
LoginEndEnum loginEnd = LoginEndEnum.getLoginEndByCode(param.getInt("login_end"));
return userTokenService.createToken(userId, loginEnd);
}
/**
* 健全token
*/
@Override
public PrincipalInfo authPrincipal(String token) {
//查询系统用户信息
R res = shiroService.checkToken(token);
//检查用户异常状态
new GetAuthUserInfo().getAuthUserInfo(res);
UserInfo userInfo = (UserInfo) res.getData();
return userInfo;
}
/**
* 获取权限
*/
@Override
public Set<String> getPermissions(PrincipalInfo principal) {
Integer userId = principal.getUserId();
//用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(userId);
return permsSet;
}
以上就是基础组件的代码,整体设计追求高耦合,需要将系统强关联起来,这样如果有一环出错,也能够顺利排查错误。
接下来就是权限认证的时候了,采用了springboot+shiro,贴出来AuthorizingRealm的代码,分别是:用户认证、权限认证
@Slf4j
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Resource
private TokenServiceUtil.Auth auth;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
PrincipalInfo principal = (PrincipalInfo) principals.getPrimaryPrincipal();
if (null == principal) {
throw new KgoaException(Result.PRINCIPALS_NOT_EXIST);
}
//用户权限列表
Set<String> permsSet = auth.getPermissions(principal);
info.setStringPermissions(permsSet);
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
PrincipalInfo principal = auth.authPrincipal(accessToken);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, accessToken, getName());
return info;
}
}
4、总结
上面的这种简单设计模式,可能还会存在隐藏的bug,比如说:多模块或者微服务架构下,其他模块直接调用
UserInfo user = (UserInfo) SecurityUtils.getSubject().getPrincipal();
直接获取用户信息,会出现和父亲类:PrincipalInfo,冲突的情况,或者当前方法用到了用户信息,APP登录却没有用户这种情况。
所以需要在详细优化这部分功能。