shiro remembeMe 原理分析

栏目: Java · 发布时间: 7年前

内容简介:项目需要用户重启浏览器后,还能记录用户登录状态。项目鉴权使用了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 原理分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hackers

Hackers

Steven Levy / O'Reilly Media / 2010-5-30 / USD 21.99

This 25th anniversary edition of Steven Levy's classic book traces the exploits of the computer revolution's original hackers -- those brilliant and eccentric nerds from the late 1950s through the ear......一起来看看 《Hackers》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具