博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
shiro remembeMe 原理分析
阅读量:7001 次
发布时间:2019-06-27

本文共 6844 字,大约阅读时间需要 22 分钟。

前言:

项目需要用户重启浏览器后,还能记录用户登录状态。项目鉴权使用了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
    (); 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;    }}复制代码

转载于:https://juejin.im/post/5cbd28c2e51d456e2c24852a

你可能感兴趣的文章
十大励志电影
查看>>
在Sql语句中使用正则表达式来查找你所要的字符
查看>>
18种最实用的网站推广方法大全
查看>>
浅谈C/C++中的typedef和#define
查看>>
浅谈C/C++中的指针和数组(一)
查看>>
这该死的数字化生活
查看>>
matlab练习程序(圆柱投影)
查看>>
需要谨记的产品设计原则
查看>>
checkbox实现单选多选
查看>>
billing是如何的拆分的?
查看>>
Lua 迭代器与closure
查看>>
mybatis_helloworld(2)_源码
查看>>
完整部署CentOS7.2+OpenStack+kvm 云平台环境(3)--为虚拟机指定固定ip
查看>>
BLE 广播数据解析
查看>>
Oracle用户密码过期和用户被锁解决方法【转】
查看>>
Android 解决Android的TextView和EditText换行问题
查看>>
CSS效果集锦(持续更新中)
查看>>
通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[中]:管道如何处理请求...
查看>>
Eigen教程(9)
查看>>
单元测试
查看>>