最近作者被安排的活是给一个系统添加权限接入到已有的用户管理中心去。作者在之前写过一篇Shiro日记,介绍了Shiro大概的思路,但是现在拾起来再看,已经是云里雾里。
从中可以说明两点情况:
一、上篇文章写得实在很差
二、作者当时对Shiro的认知还很浅,只会只言片语
这次觉得对Shiro重新做一个梳理,作为自己的笔记留存。
注意
如果你正在接入已有的用户权限管理系统,或者正在规划建设有多个系统组成,各系统均要接入统一的用户权限管理系统,这篇文章可以帮助你,使你对整个架构有大致的了解。
系统架构简介
因为目前公司在进行微服务改造及前后端分离,因此这次的权限管理功能是独立的,有一个专门的用户权限管理中心负责。业务系统也通过服务调用的形式于用户权限管理中心进行交互。大致的架构图及流程如下:

流程详解
1-2.前端向后端请求资源、后端返回未鉴权错误以便让前端根据错误跳转
为了实现这一点,在所有的接口上添加@RequiresPermissions注解即可,给注解附上唯一标识,并将该标识在用户权限管理系统中登记。
当然,需要将所有请求添加过滤,为此在项目中添加ShiroConfig,配置相关信息。
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(UcenterProperties ucenterProperties, SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//过滤
Map<String, Filter> filters = Maps.newHashMap();
filters.put("shiro", new ShiroAuthenticatingFilter(ucenterProperties));
shiroFilter.setFilters(filters);
Map<String, String> filterMap = Maps.newLinkedHashMap();
filterMap.put("/dhl/static/**", "anon");
filterMap.put("/dhl/**", "shiro");
filterMap.put("/uc/profile", "shiro");
filterMap.put("/**", "anon");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
从中可以看到,作者注册一个shiroFilter Bean。
其功能有两个:1是绑定securityManager;2是定义哪些url资源需要shiro过滤
securityManager是shiro的核心,这个重要的Bean我在下面过程中,一步步拆解它,看看它具体在哪个过程中起到作用。

3-4.业务前端根据【2】错误跳转页面至用户权限管理统一的登录页面、用户权限管理系统根据输入的用户名密码进行鉴权,如果鉴权通过后生成token并返回给前端
步骤3由前端进行操作,步骤4是由权限管理系统来实现。
该接口主要做三件事:
1.验证用户名密码
2.生成token
3.将token缓存至redis中,并设置过期时常
如若登录成果,将token结果返回给前端。
5-6.业务系统将token带上向业务后端请求资源、业务后端将根据token向用户管理系统进行查询,查询是否存在该token对应的用户
这里业务后端是怎么实现的呢。看一下大致流程:

这里就涉及到鉴权,鉴权顾名思义就是验证用户的身份,在这里也就是验证token的有效性。而认证、授权、会话、缓存全部由securityManager来搞定。ShiroConfig中是这样配置的
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdCookieEnabled(true);
return sessionManager;
}
@Bean
public ShiroAuthorizingRealm shiroAuthorizingRealm(UcenterProperties ucenterProperties) {
ShiroAuthorizingRealm shiroAuthorizingRealm=new ShiroAuthorizingRealm(ucenterProperties);
return shiroAuthorizingRealm;
}
@Bean
public SecurityManager securityManager(ShiroAuthorizingRealm shiroAuthorizingRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroAuthorizingRealm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
我们定义来会话管理和认证授权管理,并赋给securityManager。我们重点关注一下shiroAuthorizingRealm,它完成了鉴权和授权的操作。
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserVo sysUserVo = (SysUserVo)principals.getPrimaryPrincipal();
SysRolePermVo roles = ShiroCacheKit.getSysUserRoles(sysUserVo.getToken());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(roles.getPerms());
return info;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String)token.getPrincipal();
SysUserVo sysUserVo = ShiroCacheKit.getSysUserVo(accessToken);
if (sysUserVo == null) {
throw new IncorrectCredentialsException("token失效,请重新登录");
} else {
log.info("认证{}", accessToken);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(sysUserVo, accessToken, this.getName());
return info;
}
}
doGetAuthenticationInfo就是进行鉴权操作,doGetAuthorizationInfo就是授权操作。
星期五了,未完待续,回家吃饭!
接着谈
在进行鉴权和授权操作时,我们用到来一个工具类,ShiroCacheKit,该工具类的主要任务就是查询本地缓存中有没有命中的结果。如果在本地缓存没有命中结果,调用另一个工具类,ShiroRemoteKit,向用户权限管理系统进行查询,如果查到,将结果缓存至本地并返回给前端结果。
@Slf4j
public class ShiroCacheKit {
private static Cache<String,Object> cache= CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)//给定时间内没有写访问则回收,30分钟
.ticker(Ticker.systemTicker()) //定义缓存对象失效的时间精度为纳秒级
.build();
private ShiroCacheKit(){
}
protected static SysUserVo getSysUserVo(String token){
SysUserVo sysUserVo=(SysUserVo)cache.getIfPresent("user:"+token);
if(sysUserVo==null){
sysUserVo=ShiroRemoteKit.getSystemUserInfo(token);
if(sysUserVo!=null){
sysUserVo.setLastActive(System.currentTimeMillis());
cache.put("user:"+token,sysUserVo);
}
}else{
sysUserVo.setLastActive(System.currentTimeMillis());
cache.put("user:"+token,sysUserVo);
}
return sysUserVo;
}
protected static SysRolePermVo getSysUserRoles(String token){
SysRolePermVo roles=(SysRolePermVo)cache.getIfPresent("role:"+token);
if(roles==null){
roles=ShiroRemoteKit.getSystemUserRoles(token);
if(roles!=null){
cache.put("role:"+token,roles);
}
}
return roles;
}
}
@Slf4j
public class ShiroRemoteKit {
private static UcenterProperties ucenterProperties;
protected static void setUcenterProperties(UcenterProperties ucenterProperties){
ShiroRemoteKit.ucenterProperties=ucenterProperties;
}
/**
* 通过 token获取用户信息
* @param token
* @return
*/
protected static SysUserVo getSystemUserInfo(String token){
log.info("远程查询用户信息:{},{}",token,ucenterProperties.getApiUrl());
String body=HttpKit.getRequest(ucenterProperties.getApiUrl()+"/api/user_info?token="+token);
JSONObject jsonObject=JSON.parseObject(body);
Integer code=jsonObject.getInteger("code");
if(code.equals(0)){
SysUserVo sysUserVo=jsonObject.getObject("data",SysUserVo.class);
return sysUserVo;
}
log.warn("获取远程用户失败 token:{},resp:{}",token,body);
return null;
}
/**
* 通过token获取权限
* @param token
* @return
*/
protected static SysRolePermVo getSystemUserRoles(String token){
log.info("远程查询权限信息:{},{}",token,ucenterProperties.getApiUrl());
String body=HttpKit.getRequest(ucenterProperties.getApiUrl()+"/api/user_roles?app="+ucenterProperties.getAppCode()+"&token="+token);
JSONObject jsonObject=JSON.parseObject(body);
Integer code=jsonObject.getInteger("code");
if(code.equals(0)){
SysRolePermVo sysRolePermVo=JSON.parseObject(jsonObject.getString("data"),SysRolePermVo.class);
return sysRolePermVo;
}
log.warn("远程获取权限失败 token:{},resp:{}",token,body);
return null;
}
}
长时间未会话,如何让token失效?
微服务改造后,我们有的应用应该是无状态的,在这里如何让会话无状态?
这里的会话、token管理很有意思,有必要拿出来细谈。
首先,让我们看看,在业务系统里的缓存是什么样的。
private static Cache<String,Object> cache= CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)//给定时间内没有写访问则回收,30分钟
.ticker(Ticker.systemTicker()) //定义缓存对象失效的时间精度为纳秒级
.build();
它是一个google cache实例,而不是直连到redis中。而真正管理token是否有效的应该是用户权限管理系统。

业务系统里的cache只不过是一个更新不算即时的副本,为了实现即时监听到token的有效性,业务系统内有两个定时任务。
1.每5分钟检查一次session活跃情况,并上报给用户权限管理系统
2.每分钟批量检查一次token是否有效,即时更新本地副本token的状态
在ShiroCacheKit工具类中,我们可以看到,用户通过token请求资源时,每次鉴权时都会更新其最后活跃时间【sysUserVo.setLastActive(System.currentTimeMillis())】
这样活跃的用户就能保证每5分钟的定时活跃检查能够被检查到,并报备给用户权限管理系统。
看看这个定时任务,定义在ShiroCacheKit工具类中
/**
* 每5分钟检查一次session活跃
*/
@Scheduled(fixedDelay=300000)
private static void scheduled(){
Set<String> tokens=cache.asMap().values().stream()
.filter(val->val instanceof SysUserVo)
.map(val->(SysUserVo)val)
.filter(val->(System.currentTimeMillis()-val.getLastActive())<300000)
.map(SysUserVo::getToken)
.collect(Collectors.toSet());
boolean result=ShiroRemoteKit.batchActiveTokens(tokens);
}
报备给远程的用户权限管理中心是调用远程接口,其定义在ShiroRemoteKit类中
/**
* 远程批量增加token有效期
* @param tokens
* @return
*/
protected static boolean batchActiveTokens( Set<String> tokens){
log.info("远程批量增加token有效期:{},{}",tokens,ucenterProperties.getApiUrl());
String body=HttpKit.postRequest(ucenterProperties.getApiUrl()+"/api/tokens_active",tokens);
JSONObject jsonObject=JSON.parseObject(body);
Integer code=jsonObject.getInteger("code");
if(code.equals(0)){
return true;
}
log.warn("远程批量增加token有效期 tokens:{},resp:{}",tokens,body);
return false;
}
为了保证业务系统内的token和用户权限管理中心的准同步,另一个定时任务更为频繁,每一分钟定时检查一次,将失效的token及时剔除
在ShiroCacheKit工具类中另一个定时任务如下:
protected static void invalidateToken(String token){
cache.invalidate("user:"+token);
cache.invalidate("role:"+token);
}
/**
* 每分钟批量检查一次token是否有效
*/
@Scheduled(fixedDelay=60000)
private static void scheduledClean(){
Set<String> tokens=cache.asMap().keySet().stream().filter(key->key.startsWith("user:")).collect(Collectors.toSet());
Set<String> availableTokens=ShiroRemoteKit.batchCheckTokens(tokens);
Sets.difference(availableTokens,tokens).forEach(token->{
invalidateToken(token);
});
}
而远程check用户权限管理中心的接口同样定义在ShiroRemoteKit类中
/**
* 远程批量校验token
* @param tokens
* @return
*/
protected static Set<String> batchCheckTokens( Set<String> tokens){
log.info("远程批量校验token:{},{}",tokens,ucenterProperties.getApiUrl());
String body=HttpKit.postRequest(ucenterProperties.getApiUrl()+"/api/tokens_check",tokens);
JSONObject jsonObject=JSON.parseObject(body);
Integer code=jsonObject.getInteger("code");
if(code.equals(0)){
Set<String> availableTokens=Sets.newHashSet();
JSONArray jsonArray=jsonObject.getJSONArray("data");
for (int i = 0; i < jsonArray.size(); i++) {
availableTokens.add(jsonArray.getString(i));
}
return availableTokens;
}
log.warn("远程批量校验token失败 tokens:{},resp:{}",tokens,body);
return null;
}
先写到这,若有读者感兴趣,接着往下写。剩下的部分也只剩业务系统内用户退出的处理操作了。
网友评论