前言HttpSessionRedisSessionJwt+RedisSessionJWT+RedisSession+shiro的分布式Session权限控制方案
RedisSessionSubjectFactorySessionKey:SsoSecurityManager : git
前言前面对shiro的认证流程进行了分析:大致回顾总结下:
- 我们在ShiroFilterFactoryBean中设置对请求的拦截。请求到来后先通过请求路径匹配到对应的filter;以所有请求需要经过formAuthenticationFilter为例;请求先经过isAccessAllowed的验证,验证逻辑是:当前的subject是认证通过的,或当前请求不是登录请求且是被允许的。满足则放行;如果没有通过isAccessAllowed的验证,则会进入到onAccessDenied进行验证不通过的处理。如果是登录请求, formAuthenticationFilter中对于登录请求会通过配置的username、password、rememberMe创建一个UsernamePasswordToken然后进行登录逻辑。登录的时候回通过配置的Realm获取AuthenticationInfo,并且通过Realm中配置的凭证验证器进行凭证校验。如果通过Realm获取到了合法的AuthenticationInfo则登录请求完成。登录成功后会执行一系列回调 *** 作。包括把当前的subject的认证状态改成true和把当前的subject写到session中等 *** 作。如果在请求不是登录请求,那么就会进行重定向到配置的登录页面。
最简单的shiro控制场景下,浏览器发起认证请求后shiro会先创建Subject,然后进行认证,认证成功后会吧认证通过信息写到session中,session通过HttpServletRequest获取。初次访问的时候,HttpServeltRequest就会在服务端创建一个HttpSession存在内存中,然后与浏览器通过JSESSIONID来保持Session状态。
RedisSession但是在某些情况下,HttpSession失去了他的作用,比如后端服务采用了集群或分布式部署。这个时候就需要一个能够共享的Session,我们一般选择redis替换Tomcat的内存存储Session。然后通过JSESSIONID去取Session。
Jwt+RedisSession有些浏览器或者客户端在某些情况下会导致JSESSIONID失效,即每次发起的请求都跟是个新的请求一样,丢失Session状态。所以避免这种情况,我们可以用JWT来替换JSESSIONID。这个时候客户端和服务端Session的关系维持就可以交给我们自己维护了。
JWT+RedisSession+shiro的分布式Session权限控制方案首先基于Session接口定义RedisSession:
RedisSessionpublic class RedisSession implements Session,Serializable { private String id; private Date startTimestamp; private Date stopTimestamp; private Date lastAccessTime; private long timeout; private String host; private Map
Session的管理呢是Subject管理的,那我们需要定义一个自己的Subject吗?研究源码的时候我发现目前并不需要,在shiro中是交由WebDelegatingSubject进行代理的。所以我们只需要修改Subject的创建过程参考Request中的Token就好了。
所以我们需要重写SubjectFactory:
public class SsoSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { if (!(context instanceof WebSubjectContext)) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); ServletRequest request = wsc.resolveServletRequest(); String token=JwtUtil.getJwt((HttpServletRequest)request); //如果session是空的,那么从request中获取token,然后去redis中获取session if(session==null && token!=null){ RedisSessionKey sessionKey = new RedisSessionKey(token); session = securityManager.getSession(sessionKey); } //如果前面获取到了session,则把sessionId放到response中 if(session!=null){ JwtUtil.setAuthorizationToken((HttpServletResponse) wsc.resolveServletResponse(), (String) session.getId()); } boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletResponse response = wsc.resolveServletResponse(); return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); } }
我们在创建Subject的时候就给他把Session塞进去,这样shiro以后就不会再去创建session了。从源码分析也能看到Session可以通过org.apache.shiro.mgt.SessionsSecurityManager#getSession进行获取,而getSession的参数是SessionKey,所以根据我的设计方案,我自己搞了一个RedisSessionKey来替换原来的SessionKey,并且自己对DefaultWebSecurityManager进行复写来修改其Subject和Session的管理逻辑:
SessionKey:public class RedisSessionKey implements SessionKey { private String id; public RedisSessionKey(String id) { this.id = id; } public String getId() { return id; } public void setId(String id) { this.id = id; } public void setSessionId(Serializable sessionId){ this.id = (String) sessionId; } @Override public Serializable getSessionId() { return id; } }SsoSecurityManager :
public class SsoSecurityManager extends DefaultWebSecurityManager { RedisTemplate redisTemplate; public SsoSecurityManager(RedisTemplate redisTemplate) { super(); this.redisTemplate = redisTemplate; } @Override public boolean isHttpSessionMode() { return false; } @Override protected SessionKey getSessionKey(SubjectContext context) { if (WebUtils.isWeb(context)) { HttpServletRequest request = WebUtils.getHttpRequest(context); String authorization = JwtUtil.getJwt(request); return new RedisSessionKey(authorization); } return null; } @Override protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) { return super.createSubject(token, info, existing); } @Override public Session start(SessionContext context) throws AuthorizationException { RedisSession session = new RedisSession(); HttpServletRequest request = WebUtils.getHttpRequest(context); String host = context.getHost()==null?request.getRemoteHost():context.getHost(); String sessionId = JwtUtil.getJwt(request); //第一次访问,生成默认的token if(sessionId == null){ sessionId = JwtUtil.getUnauthorizedToken(host); HttpServletResponse response = WebUtils.getHttpResponse(context); JwtUtil.setAuthorizationToken(response,sessionId); } session.setId(sessionId); context.setSessionId(sessionId); //创建好的session缓存到redis后即表示启动了 cacheSession(session); return session; } public void cacheSession(RedisSession session) { redisTemplate.opsForValue().set( JwtConstans.getCacheSessionId(session.getId()) ,session ,JwtConstans.SESSION_EFFECTIVE_TIME ,JwtConstans.SESSION_EFFECTIVE_TIME_UNIT); } @Override public Session getSession(SessionKey key) throws SessionException { if(key==null || key.getSessionId()==null){ return null; } String cacheKey = JwtConstans.getCacheSessionId((String) key.getSessionId()); RedisSession session = (RedisSession) redisTemplate.opsForValue().get(cacheKey); return session; } public void stopSession(String sessionId) { redisTemplate.delete(JwtConstans.getCacheSessionId(sessionId)); } public void touchSession(RedisSession redisSession) { cacheSession(redisSession); } public void touchSessionId(String oldSessionId,String newSessionId){ //通过sessionId找到先前的session RedisSession session = (RedisSession) this.getSession(new RedisSessionKey(oldSessionId)); //移除旧的session this.stopSession(oldSessionId); //更新sessionid,并存入redis session.setId(newSessionId); this.cacheSession(session); } }
shiro中通常进行权限拦截用的是FormAuthenticationFilter进行拦截,但是我们这里要支持单点登录,所以登录方式不再是单纯的根据用户名密码登录,所以这里创建一个JwtFilter来替换。FormAuthenticationFilter
上面的基础准备好了后,开始进行shiro配置:
SsoClientShiroConfig:
@Configuration public class SsoClientShiroConfig { @Autowired SsoConfig ssoConfig; private static Mapfilters = new linkedHashMap<>(); @Bean @ConditionalOnMissingBean public ISSoServerUserService IssoServerUserService(){ if(ssoConfig.isSsoServer()){ Log.get().log(Level.WARN,"检测到当前服务是单点登录验证服务,但未找到{}的实现类,当前服务将被降级为单点登录客户端,可能无法提供登录相关 *** 作",ISSoServerUserService.class); Log.get().log(Level.WARN,"请检查sso.config.ssoServer配置:true->单点登录服务;false->单点登录客户端。若确定是单点登录服务请实现{}接口",ISSoServerUserService.class); ssoConfig.setSsoServer(false); } return new DefaultSSoServerUserServiceImpl(); } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ISSoServerUserService ssoServerUserService){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setFilters(filters); CaptchaFilter captchaFilter = new CaptchaFilter(); IndexFilter indexFilter = new IndexFilter(); SsoAuthenticationFilter authc = new SsoAuthenticationFilter(ssoConfig); filters.put("captcha",captchaFilter); filters.put("authc",authc); filters.put("index",indexFilter); //拦截器 Map filterChainDefinitionMap = new linkedHashMap<>(); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); filterChainDefinitionMap.put("/captcha.jpg","captcha"); //如果是单点登录服务端才配置登录相关请求,客户端的login请求会交给authc处理 if(ssoConfig.isSsoServer()){ filters.put("login",new LoginFilter(ssoServerUserService,ssoConfig)); filters.put("logout",new LogoutFilter()); //根据单点配置决定是否启用验证码校验 if(ssoConfig.isCaptcha()){ filterChainDefinitionMap.put("/login","captcha,login"); }else{ filterChainDefinitionMap.put("/login","login"); } } filterChainDefinitionMap.put("/logout","logout"); filterChainDefinitionMap.put("/index","index"); filterChainDefinitionMap.put(" @Bean public static HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new SsoCredentialsMatch(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher){ MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher); myShiroRealm.setCachingEnabled(false); return myShiroRealm; } @Bean public SessionsSecurityManager securityManager(MyShiroRealm shiroRealm,RedisTemplate redisTemplate){ SsoSecurityManager securityManager = new SsoSecurityManager(redisTemplate); securityManager.setRealm(shiroRealm); securityManager.setSubjectFactory(subjectFactory()); return securityManager; } @Bean public SubjectFactory subjectFactory(){ return new SsoSubjectFactory(); } }
通过上面的配置后,session就不再是HttpSession了,session的管理也交给redis管理了。下面再上几张测试图:
登录成功后在response的header中会有authorization信息,前端这个请求其他接口的时候带上这个authorization,那就可以无状态的访问服务端了,服务端通过authorization再从redis中取得session,从而实现无状态的session。
前面的描述可能不太完善,如果有兴趣可以看我开源的仓库:https://gitee.com/liu0829/redis-shiro-sso.git
也可以发邮件联系我:liuwanli_email@163.com
各位有什么要补充纠正的,请留下你的宝贵意见。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)