• shiro + jwt 实现 请求头中的 rememberMe 时间限制功能


    前言: 

      上一篇提出, 通过修改 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 尝试访问

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

    试验证明, 还是可行的.

    进行jwt处理之后, rememberMe字符串会比较长, 这里提供一种思路:

    1. 将过期时间转换成  yyyy-MM-dd HH:mm:ss 格式的时间字符串, 进行 base64编码, 会发现, 长度都是28位的. 假设定义为变量 timeBase64

    2. 在 rememberSerializedIdentity 方法中, 对 timeBase64 进行截取, 分为两段, 拼接到 上面 base64字符串中去, 这中方式就相当于是进行了简单的加密, 当然, 不进行此操作, 也完全可以, 直接拼接到 base64的开始处, 或者结尾处.

    3. 在  getRememberedSerializedIdentity 方法中, 对base64字符串进行截取, 可以得到时间字符串, 之后对时间进行判断, 就能知道, 是否过期. 

  • 相关阅读:
    Hash(学习笔记)
    [POI2012]OKR-A Horrible Poe(hash+线性筛素数)
    质数(学习笔记)
    [AHOI2014/JSOI2014]宅男计划(贪心+三分)
    [HEOI2015]定价(贪心+数学)
    Trie字典树(学习笔记)
    CF258D. Little Elephant and Broken Sorting(DP+概率期望)
    矩阵(01背包+滚动数组)
    [2015北大自招夏令营]产品排序(区间DP)
    HTML
  • 原文地址:https://www.cnblogs.com/elvinle/p/9295552.html
Copyright © 2020-2023  润新知