内容简介:项目需要用户重启浏览器后,还能记录用户登录状态。项目鉴权使用了shiro框架,发现rememberMe功能刚好可以实现需求。按照教程把功能实现后,顺带阅读了一下源码,在这里做下阅读记录。众所周知,前端访问后端接口后,后端会向前端cookie写个sessionid作为会话标记。session有效期为这次关闭浏览器,所以只要重启时,保存下来,就能实现记录状态的功能了。在shiro提供的SecurityManager中,网站开发,我们常用DefaultWebSecurityManager,它继承于DefaultS
项目需要用户重启浏览器后,还能记录用户登录状态。项目鉴权使用了shiro框架,发现rememberMe功能刚好可以实现需求。按照教程把功能实现后,顺带阅读了一下源码,在这里做下阅读记录。
必要知识:
众所周知,前端访问后端接口后,后端会向前端cookie写个sessionid作为会话标记。session有效期为这次关闭浏览器,所以只要重启时,保存下来,就能实现记录状态的功能了。
在shiro提供的SecurityManager中,网站开发,我们常用DefaultWebSecurityManager,它继承于DefaultSecurityManager。DefaultSecurityManager是shiro自带实现的 最基础但已直接可用 的SecurityManager,它包含了shiro所有主要的鉴权流程。
shiro如何记录用户状态:
用户登陆:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { ... onSuccessfulLogin(token, info, loggedIn); return loggedIn; } 复制代码
在用户登录成功后,会有一个后置处理:
protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { rememberMeSuccessfulLogin(token, info, subject); } 复制代码
它的内部,就是来向前端cookie中记录当前登陆状态,
protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { RememberMeManager rmm = getRememberMeManager(); if (rmm != null) { try { rmm.onSuccessfulLogin(subject, token, info); ... } 复制代码
DefaultWebSecurityManager在构造时,默认会设置一个RememberMeManager
public DefaultWebSecurityManager() { super(); ... setRememberMeManager(new CookieRememberMeManager()); } 复制代码
具体执行cookie记录(看源码注释: 不管有没有,先删除一下,然后判断现在是否需要rememberMe)
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { //always clear any previous identity: forgetIdentity(subject); //now save the new identity: if (isRememberMe(token)) { rememberIdentity(subject, token, info); ... } 复制代码
-
删除cookie的操作,就是把当前key的cookie的maxAge设置为0,然后重新写回浏览器
public void removeFrom(HttpServletRequest request, HttpServletResponse response) { String name = getName(); String value = DELETED_COOKIE_VALUE; String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions String domain = getDomain(); String path = calculatePath(request); int maxAge = 0; //always zero for deletion int version = getVersion(); boolean secure = isSecure(); boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly); log.trace("Removed '{}' cookie by setting maxAge=0", name); } 复制代码
-
shiro默认是按token实现RememberMeAuthenticationToken这个接口,并设置isRememberMe为true来判断是否要记录状态的。
1.我们可以让自己的token实现这个接口
2.也可以自己写一个RememberMeManager的实现,重写isRememberMe,然后替换默认的。
protected boolean isRememberMe(AuthenticationToken token) { return token != null && (token instanceof RememberMeAuthenticationToken) && ((RememberMeAuthenticationToken) token).isRememberMe(); } 复制代码
-
前端最终记录的就是凭证组
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { PrincipalCollection principals = getIdentityToRemember(subject, authcInfo); rememberIdentity(subject, principals); } 复制代码
-
shiro会把凭证组序列化后,再加密
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); if (getCipherService() != null) { bytes = encrypt(bytes); } return bytes; } 复制代码
-
默认使用了AES加密
public AbstractRememberMeManager() { this.serializer = new DefaultSerializer<PrincipalCollection>(); AesCipherService cipherService = new AesCipherService(); this.cipherService = cipherService; setCipherKey(cipherService.generateNewKey().getEncoded()); } 复制代码
-
在最终写回前端时,shiro还会把加密后的值base64格式化一下,防止一些加密算法加密出奇怪的值来影响使用
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) { ... //base 64 encode it and store as a cookie: String base64 = Base64.encodeToString(serialized); Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); } 复制代码
以上,即使浏览器重启,也是会记录下用户前一次的登陆信息了,下次访问服务器时,cookie已经带上了用户信息
shiro如何重新读取用户状态
shiro默认会把subject存在当前线程中,如果没有,则会去创建建一个
public Subject createSubject(SubjectContext subjectContext) { ... //if possible before handing off to the SubjectFactory: context = resolvePrincipals(context); ... } 复制代码
默认会把subject保存在session中(也会有缓存或者自己写的存储机制等),如果没有,它就会去getRememberedIdentity()方法中获取
protected SubjectContext resolvePrincipals(SubjectContext context) { PrincipalCollection principals = context.resolvePrincipals(); if (CollectionUtils.isEmpty(principals)) { log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity."); principals = getRememberedIdentity(context); ... } 复制代码
最终就是从前端cookie中获取到上面步骤存储的内容,解密反序列化,得到用户凭证组信息(整个逻辑与上面同理相反,就不赘述了)
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) { RememberMeManager rmm = getRememberMeManager(); if (rmm != null) { try { return rmm.getRememberedPrincipals(subjectContext); ... } 复制代码
rememberMe与普通登陆的差别
使用rememberMe的功能时,路径拦截如果使用authc拦截器,还是会被拦截,需要使用user拦截器才能被通过。
这样的好处是,可以把重要的,比如说支付之类,需要每次登陆(防止陌生人使用你的电脑),而一些消息浏览的界面(不特别重要),可以让用户打开浏览器就能看到
区分拦截的原理:
为何rememberMe的用户无法访问authc拦截的内容,只能访问user拦截的呢!
前文提到,如果当前线程没有subject,shiro会去创建。
默认subject会存储在session中,并且会有一个标记值authenticated。
而rememberMe的用户信息是从cookie中解析出来的,session是刚新建的,里面没有登陆标记。
所以最终的subject与登陆后的subject都有凭证信息,但是登陆标记不一样。
public Subject createSubject(SubjectContext context) { ... //从session中获取登陆标记(获取不到则为false) boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletRequest request = wsc.resolveServletRequest(); ServletResponse response = wsc.resolveServletResponse(); return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); } 复制代码
shiro存储在session的登陆标记的默认key
/** * The session key that is used to store whether or not the user is authenticated. */ public static final String AUTHENTICATED_SESSION_KEY = DefaultSubjectContext.class.getName() + "_AUTHENTICATED_SESSION_KEY"; 复制代码
authc标记使用的FormAuthenticationFilter拦截器,用了默认的鉴权方法。如果isAuthenticated不是true,就认为没登陆,所以rememberMe的方式不能通过。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request, response); return subject.isAuthenticated(); } 复制代码
而user标记使用的UserFilter拦截器,重写了鉴权方法,它只是判断了subject中是否有用户凭证信息,所以rememberMe的方式才能被通过。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginRequest(request, response)) { return true; } else { Subject subject = getSubject(request, response); // If principal is not null, then the user is known and should be allowed access. return subject.getPrincipal() != null; } } 复制代码
以上所述就是小编给大家介绍的《shiro remembeMe 原理分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 深度分析ConcurrentHashMap原理分析
- Spring源码分析:@Autowired注解原理分析
- Struts2 源码分析-----工作原理分析
- KVO实现原理分析
- JavaScript运行原理分析
- LevelDB原理分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
TCP/IP网络编程
[韩] 尹圣雨 / 金国哲 / 人民邮电出版社 / 2014-7 / 79.00元
第一部分主要介绍网络编程基础知识。此部分主要论述Windows和Linux平台网络编程必备基础知识,未过多涉及不同操作系统特性。 第二部分和第三部分与操作系统有关。第二部分主要是Linux相关内容,而第三部分主要是Windows相关内容。从事Windows编程的朋友浏览第二部分内容后,同样可以提高技艺。 第四部分对全书内容进行总结,包含了作者在自身经验基础上总结的学习建议,还介绍了网络......一起来看看 《TCP/IP网络编程》 这本书的介绍吧!