就够了
Posted 张子行的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了就够了相关的知识,希望对你有一定的参考价值。
本文目录
前言
最近整合了一套shiro脚手架出现了很多问题…比较多的问题其实还是我自测出来的,主要是关于一些shiro 中内置 session 污染、缓存污染方面的问题,在排查 bug的过程中我都一度想禁用shiro中的session功能了,但是想想研究研究这个东西万一以后用的上呢?因为不是说你开发的每一个项目都是分布式项目。都可以用到重量级的 spring security。当然不是说 spring security不好,为了体现出本文的价值,我想说:shiro天下第一、shiro是天底下最好的权限框架
shiro几大核心组件
先来说一下shiro中的几大核心组件间以及各组件的核心职能吧
- ShiroFilterFactoryBean:shiro提供的一个工厂Bean,通过这个Bean我们可以对整个权限系统的接口做一个管控,包括接口级权限设置、自定义过滤器设置,最终目的就是定义路由的跳转规则,其他会话级别的安全保证是由 SecurityManager 来控制的
- SecurityManager:安全管理器,可以利用 SecurityManager 配置哪些 Realm、SessionManager、CacheManager 会生效
- SessionManager:会话管理器,主要就是对 shiro.session 的 CRUD,可以使用 shiro 默认提供的 SessionManager,也可以使用自己实现的 SessionManager,但是都需注入对应的 SessionDAO,同样可以使用默认的 SessionDAO或者是自己实现的 SessionDAO
- CacheManager:缓存管理器,一般用来缓存 Realm 返回的信息,Realm 中会返回 AuthenticationInfo(认证信息)、AuthorizationInfo(授权信息),缓存管理器的目的就是为了缓存这些信息
shiro配置信息
我个人是不建议自己编写CacheManager来管理缓存的(大佬请忽略我这句话),如果有人不信的话很多意想不到的bug在等着你(笔者亲测、主要是关于多人登录时出现的session污染上的bug),下文中的RedisCacheManager是我整合第三方的包(shiro-redis),里面实现了一整套对Cache的增删改查逻辑,其中包括读者关心的对Session序列化与反序列化的问题、shiro整合Token的问题…
Cookie被禁用了还可以使用Shiro框架吗?
下文中的SessionManager是我自定义实现的一个会话管理器,使用默认的也行,如果系统不是一个Web项目那么你还坚持使用DefaultWebSessionManager,由于cookie的被禁用,系统的权限这块就瘫痪了,因为DefaultWebSessionManager默认是从前端传过来的Cookie中获取SessionId的,正是因为shiro是通过内置session+cookie来实现的权限控制的原因,所以我实现了自定义的SessionManager(重写getSessionId方法),shiro如果可以检索到对应的SessionId,那么shiro就无需重新创建session,可以通过检索到的SessionId获取对应的Session,如果没有重写getSessionId方法,由于Cookie的被禁用Session将会被反复创建,这也是Session污染的来源之一,所以可以通过重写SessionManager中的getSessionId方法,来控制Session污染。同时只要是浏览器能正确的携带正确的SessionId过来,Shiro就能正常使用,至于携带SessionId的的媒介可以选择Token、Jwt、字符串、Cookie,前端只要将这个媒介放入Herder即可
Cookie过期了会自动删除缓存的Session信息吗?
服务端虽然可以根据Cookie中的SessionId进行删除Redis中的session操作,但是cookie过期了,在客户端每次发起请求的时候将不会携带Cookie了,是无法带动删除session的操作的,要么就是设置超时Session时间、要么就是设置Redis缓存过期时间(不建议这么做、授权、认证、session信息的超时时间是捆绑在一块的)来管理过期的session
@Configuration
public class ShiroConfig
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Value("$shiro.cache.authenticationCache")
private String AUTHENTICATIONCACHEPREFIX;
@Value("$shiro.cache.authorizationCache")
private String AUTHORIZATIONCACHEPREFIX;
@Bean(name = "shiroCacheManager")
public RedisCacheManager shiroCacheManager()
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
//redis中根据userName来缓存用户
redisCacheManager.setPrincipalIdFieldName("userName");
//用户权限信息缓存时间
redisCacheManager.setExpire(200000);
return redisCacheManager;
@Bean
public RedisManager redisManager()
RedisManager redisManager = new RedisManager();
redisManager.setHost("192.168.20.201:6379");
return redisManager;
/**
* 注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
* user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe 说白了,以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean()
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
LinkedHashMap<String, String> chain = new LinkedHashMap<String, String>();
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
//filters.put("pems",new ZaFilter());
filters.put("controllerFilter", new ControllerFilter());
chain.put("/login", "anon");
chain.put("/oauth/login/**", "anon");
chain.put("/oauth/loginOut", "anon");
chain.put("/css/**", "anon");
chain.put("/img/**", "anon");
chain.put("/js/**", "anon");
chain.put("/lib/**", "anon");
chain.put("/favicon.ico", "anon");
//禁用session,一般采用token验证时开启
//chain.put("/**", "authc,noSessionCreation");
chain.put("/**", "authc");
bean.setFilterChainDefinitionMap(chain);
//session失效、没有登录都会跳转到此页面
bean.setLoginUrl("/login");
bean.setSecurityManager(securityManager());
bean.setFilters(filters);
return bean;
@Bean
public SecurityManager securityManager()
DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(loginRealm());
//realm被shiroCacheManager管理,本质也是被redis管理
securityManager.setCacheManager(shiroCacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
@Bean
public SessionManager sessionManager()
DefaultWebSessionManager sessionManager = new WebSessionManager();
//session的有效时长为10秒,每隔5秒去扫描session的状态,扫描到超时的session时,会清除redis中的session缓存
//一般将session的过期时间与cookie的过期时间保持一致
sessionManager.setGlobalSessionTimeout(60 * 1000);
sessionManager.setSessionValidationInterval(5 * 1000);
sessionManager.setSessionIdUrlRewritingEnabled(false);
//session交给sesionDao管理
sessionManager.setSessionDAO(sessionDao());
return sessionManager;
@Bean
public SessionDAO sessionDao()
//sessionDao被shiroCacheManager操控,shiroCacheManager被redis操控
ShiroSessionDao shiroSessionDao = new ShiroSessionDao(shiroCacheManager());
return shiroSessionDao;
/**
* 开启认证、授权的缓存,且指定缓存的名字
*/
@Bean
public LoginRealm loginRealm()
String[] za = AUTHORIZATIONCACHEPREFIX.split("%");
String[] ca = AUTHENTICATIONCACHEPREFIX.split("%");
LoginRealm shiroRealm = new LoginRealm(AUTHENTICATIONCACHEPREFIX);
shiroRealm.setCachingEnabled(true);
shiroRealm.setAuthenticationCachingEnabled(true);
shiroRealm.setAuthenticationCacheName(ca[1]);
shiroRealm.setAuthorizationCachingEnabled(true);
shiroRealm.setAuthorizationCacheName(za[1]);
return shiroRealm;
/**
* 开启后端shiro标签使用
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager)
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor
= new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor()
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
shiro实现Cookie、Token双兼容
重写 getSessionId 方法如果前端携带了Token则从Token中获取 sessionId、反之从Cookie中获取 sessionId,如果都没有那么只能创建Session了,第一次登录的时候既没有Token也没有Cookie,那么我们如何将 SessonId 保存在Token || Cookie中然后返回给前端呢?在session创建完成之后会进行调用 onStart 方法,我们对他进行重写将 Token、Cookie填充到Reponse中的 Header 中就好了。
@Slf4j
public class WebSessionManager extends DefaultWebSessionManager
public final String TOKEN_NAME = "SessionToken";
public final String COOKIE_NAME = "SessionIdCookie";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response)
String sessionId = WebUtils.toHttp(request).getHeader(TOKEN_NAME);
if (StringUtils.isEmpty(sessionId))
Cookie[] cookies = WebUtils.toHttp(request).getCookies();
if (null != cookies)
for (Cookie cookie : cookies)
if (COOKIE_NAME.equals(cookie.getName()))
return cookie.getValue();
continue;
return sessionId;
/**
* 开启cookie机制
*/
@Override
public void setSessionIdCookieEnabled(boolean sessionIdCookieEnabled)
super.setSessionIdCookieEnabled(true);
/**
* 这段代码没啥好研究的 copy 父类的代码,增加了返回Cookie、Token的逻辑
*/
@Override
protected void onStart(Session session, SessionContext context)
System.out.println("执行onStart");
if (!WebUtils.isHttp(context))
log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response pair. No session ID cookie will be set.");
else
HttpServletRequest request = WebUtils.getHttpRequest(context);
HttpServletResponse response = WebUtils.getHttpResponse(context);
Serializable sessionId = session.getId();
this.storeSessionId(sessionId, request, response);
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
/**
* 假设一个用户没有登录过系统,但是却调用了需要权限的接口,会跳转到登录页面,此时shiro会为此用户分配一个session会话(会话一)
* 如果跳转到登录页面的用户此时立即登录,那么shiro又会为此用户分配一个session会话(会话二),但是如果此时的用户在请求头设置 SessionToken=会话一
* 那么系统就能够通过 token机制来获取到shiro内置的session对象了,从而避免了重复创建会话二的目的
*/
private void storeSessionId(Serializable sessionId, HttpServletRequest request, HttpServletResponse response)
if (sessionId == null)
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
else
String sId = sessionId.toString();
//返回给前端的token[SessionToken=sessionId]
response.setHeader(this.TOKEN_NAME, sId);
//返回给前端的cookie[SessionIdCookie=sessionId]
SimpleCookie sessionCookie = new SimpleCookie("SessionIdCookie");
sessionCookie.setValue(sId);
sessionCookie.setMaxAge(10);
sessionCookie.saveTo(request, response);
如何做到登出后清除认证、授权、Session缓存?
在用户登出的时候将会执行 Subject.LoginOut()的操作,观察其调用栈发现最终会执行一个叫做 clearCache 的方法,最终会自动清除认证缓存,但是涉及到Redis缓存的东西就定会遇到叫做缓存一致性的问题,如果在生产环境某个用户的权限被修改了,切记一定要考虑更新redis中的数据,以下代码我是直接用户一登出认证、授权缓存全给干掉了
@Slf4j
public class LoginRealm extends AuthorizingRealm implements Serializable
private Jedis jedis = new Jedis("192.168.20.201", 6379);
private String authenticationCachePrefix;
public LoginRealm(String authenticationcacheprefix)
this.authenticationCachePrefix = authenticationcacheprefix;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
log.info("授权~");
LoginUser user = (LoginUser) principals.getPrimaryPrincipal();
System.err.println(user);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
ArrayList<String> perms = new ArrayList<>();
perms.add("add");
perms.add("save");
simpleAuthorizationInfo.addRole(user.getUserName());
simpleAuthorizationInfo.addStringPermissions(perms);
log.info("授权完成~");
return simpleAuthorizationInfo;
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException
log.info("认证~");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
LoginUser loginUser = new LoginUser();
loginUser.setUserName(token.getUsername());
loginUser.setPassword(String.valueOf(token.getPassword()));
SimpleAuthenticationInfo info =
new SimpleAuthenticationInfo(loginUser, token.getPassword(), getName());
log.info("认证完成~");
return info;
/**
* 清除当前用户的的 授权缓存
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals)
super.clearCachedAuthorizationInfo(principals);
/**
* 清除当前用户的 认证缓存
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals)
super.clearCachedAuthenticationInfo(principals);
/**
* 触发时机:subject.LoginOut()
* 清除当前用户缓存,经过测试发现清除认证缓存的时候key居然是LoginUser对象,但是redis中的key是userName
* 因此这里做了手动清除认证缓存的处理
*/
@Override
public void clearCache(PrincipalCollection principals)
LoginUser user = (LoginUser) principals.getPrimaryPrincipal();
String replace = authenticationCachePrefix.replace("%", "");
jedis.del(replace + user.getUserName());
clearCachedAuthorizationInfo(principals);
/**
* 清除所有人的授权缓存
*/
public void clearAllCachedAuthorizationInfo()
getAuthorizationCache().clear();
/**
* 清除所有人的认证缓存
*/
public void clearAllCachedAuthenticationInfo()
getAuthenticationCache().clear();
/**
* 清除所有人的认证缓存、授权缓存
*/
public void clearAllCache()
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
如何对Session进行CRUD
public class ShiroSessionDao extends EnterpriseCacheSessionDAO
private String activeSessionName = "session";
private static Serializable sessionId = null;
public ShiroSessionDao(CacheManager cacheManager)
super.setActiveSessionsCacheName(activeSessionName);
super.setCacheManager(cacheManager);
@Override
protected void doUpdate(Session session)
super.doUpdate(session);
@Override
protected void doDelete(Session session)
System.err.println("删除session:" + session.getId());
super.doDelete(session);
@Override
protected Serializable doCreate(Session session)
IdWorker idWorker = new IdWorker();
sessionId = idWorker.nextId();
System.out.println(("创建session: " + sessionId));
super.assignSessionId(session, sessionId);
return sessionId;
@Override
protected Session doReadSession(Serializable sessionId)
System.err.println(sessionId);
return super.doReadSession(sessionId);
测试开启Cookie
打开俩个浏览器(都支持Cookie)正常登录一波系统然后调用接口,权限、认证、Session都存入了 Redis
zzz用户退出登录,清除相关的所有信息
打开一个浏览器,直接访问需要权限的接口,由于没有登录直接跳转到登录页面,但是可以看到已经创建了会话Session了
紧接着进行登录操作,发现并没有重复创建Session,原因就是前端携带了装有SessionId的Cookie给我门进行检索
测试禁用Cookie
浏览器缓存看这一篇就够了