欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

shiro + jwt 实现 rememberMe 超时功能

程序员文章站 2023-01-19 13:47:03
前言: 上一篇提出, 通过修改 rememberMe 的编码来实现 rememberMe的功能的设想, 事后我去尝试实现了一番, 发现太麻烦, 还是不要那么做吧. 程序还是要越简单越好. 那功能总是要实现的啊, 总不能因为麻烦, 就把功能给砍了吧. so, 换条路试试: 在前后端项目中, app和后 ......

前言: 

  上一篇提出, 通过修改 rememberMe 的编码来实现 rememberMe的功能的设想, 事后我去尝试实现了一番, 发现太麻烦, 还是不要那么做吧. 程序还是要越简单越好.

  那功能总是要实现的啊, 总不能因为麻烦, 就把功能给砍了吧. 

  so, 换条路试试:

  在前后端项目中, app和后台之间的登录, 并不能通过cookie来维持, 有一种常使用的技术: jwt, 这个技术, 其实就是通过在请求头或者参数中加入一个参数 token (这个token是经过jwt加密的)的方式, 来实现登录认证的.具体的原理并不讨论. jwt加密的时候, 是可以加入时间限制的. 

  在shiro里面, 如果我将 rememberMe 也进行jwt加密, 然后再赋值回rememberMe , 给出去. 当我从请求头中拿到rememberMe , 然后再解密成之前的数据, 不就可以了么.

实现:

一. jwt帮助类

import ccdc.zykt.model.vo.UserExt;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClaims;
import org.apache.commons.lang.time.DateUtils;
import org.apache.shiro.codec.Base64;

import java.util.Date;

public class JWTUtil {
    private static final String KEY = Base64.encodeToString("jwt.key".getBytes());

    public static String createJWT(String token) {
        Date now = new Date();
        return Jwts.builder()
                .setSubject(token)
                .setIssuedAt(now)
                .setExpiration(DateUtils.addMinutes(now, 1))
                .signWith(SignatureAlgorithm.HS512, KEY).compact();
    }

    public static String createJWT(String token, int amount){
        Date now = new Date();
        return Jwts.builder().setSubject(token).setIssuedAt(now).setExpiration(DateUtils.addHours(now, amount)).signWith(SignatureAlgorithm.HS512, KEY).compact();
    }

    public static boolean validate(String jwt){
        try {
             Jwts.parser().setSigningKey(KEY).parse(jwt);
             return true;
        } catch (Throwable t) {
            return false;
        }
    }

    public static String validateJWT(String jwt) {
        try {
            Jwt parse = Jwts.parser().setSigningKey(KEY).parse(jwt);
            DefaultClaims body = (DefaultClaims) parse.getBody();
            String phone = body.getSubject();
            return phone;
        } catch (Throwable t) {
            return null;
        }
    }
}

此处我将过期时间设置为1分钟, 在实际使用中, 可以将这个参数变成可配置的,  到时候根据实际需要, 将时间改长一些就可以了.

 

二. 改写HeaderRememberMeManager类

package ccdc.zykt.web.shiro.headtoken;

import ccdc.zykt.web.util.JWTUtil;
import com.google.common.base.Strings;
import org.apache.commons.compress.utils.ByteUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.AbstractRememberMeManager;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.subject.WebSubjectContext;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

/**
 * 将remember放到响应头中去, 然后从请求头中解析
 * @author: elvin
 * @time: 2018-07-05 15:11
 * @desc:
 **/
public class HeaderRememberMeManager extends AbstractRememberMeManager {

    private static final transient Logger log = LoggerFactory.getLogger(HeaderRememberMeManager.class);

    // header 中 固定使用的 key
    public static final String DEFAULT_REMEMBER_ME_HEADER_NAME = "remember-me";

    @Override
    protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
        if (!WebUtils.isHttp(subject)) {
            if (log.isDebugEnabled()) {
                String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

        } else {
            HttpServletResponse response = WebUtils.getHttpResponse(subject);

            String base64 = Base64.encodeToString(serialized);

            base64 = JWTUtil.createJWT(base64);

            // 设置 rememberMe 信息到 response header 中
            response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, base64);
        }
    }

    private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
        ServletRequest request = subjectContext.resolveServletRequest();
        if (request == null) {
            return false;
        } else {
            Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
            return removed != null && removed;
        }
    }

    @Override
    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
        if (!WebUtils.isHttp(subjectContext)) {
            if (log.isDebugEnabled()) {
                String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

            return null;
        } else {
            WebSubjectContext wsc = (WebSubjectContext) subjectContext;
            if (this.isIdentityRemoved(wsc)) {
                return null;
            } else {
                HttpServletRequest request = WebUtils.getHttpRequest(wsc);
                // 在request header 中获取 rememberMe信息
                String base64 = request.getHeader(DEFAULT_REMEMBER_ME_HEADER_NAME);
                if ("deleteMe".equals(base64)) {
                    return null;
                } else if (base64 != null) {
                    base64 = JWTUtil.validateJWT(base64);
                    if(Strings.isNullOrEmpty(base64)){
                        return null;
                    }

                    base64 = this.ensurePadding(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Acquired Base64 encoded identity [" + base64 + "]");
                    }

                    byte[] decoded = Base64.decode(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
                    }

                    return decoded;
                } else {
                    return null;
                }
            }
        }
    }

    private String ensurePadding(String base64) {
        int length = base64.length();
        if (length % 4 != 0) {
            StringBuilder sb = new StringBuilder(base64);

            for (int i = 0; i < length % 4; ++i) {
                sb.append('=');
            }

            base64 = sb.toString();
        }

        return base64;
    }

    @Override
    protected void forgetIdentity(Subject subject) {
        if (WebUtils.isHttp(subject)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subject);
            HttpServletResponse response = WebUtils.getHttpResponse(subject);
            this.forgetIdentity(request, response);
        }

    }

    @Override
    public void forgetIdentity(SubjectContext subjectContext) {
        if (WebUtils.isHttp(subjectContext)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
            HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
            this.forgetIdentity(request, response);
        }
    }

    private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
        //设置删除标示
        response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, "deleteMe");
    }
}

rememberMe 在解析的时候, 就会将时间算进去, 如果超时了, 解析会返回false. 这样, 就可以为rememberMe设置一个超时时间

 

结果展示:

1. 登录之后, 使用postmen 尝试访问

shiro + jwt 实现 rememberMe 超时功能

2. 耐心等待1分钟, 然后再去访问这个接口试试

shiro + jwt 实现 rememberMe 超时功能

试验证明, 还是可行的.