shiro + jwt 實現 請求頭中的 rememberMe 時間限制功能

前言: 

  上一篇提出, 經過修改 rememberMe 的編碼來實現 rememberMe的功能的設想, 過後我去嘗試實現了一番, 發現太麻煩, 仍是不要那麼作吧. 程序仍是要越簡單越好.java

  那功能老是要實現的啊, 總不能由於麻煩, 就把功能給砍了吧. web

  so, 換條路試試:spring

  在先後端項目中, app和後臺之間的登陸, 並不能經過cookie來維持, 有一種常使用的技術: jwt, 這個技術, 其實就是經過在請求頭或者參數中加入一個參數 token (這個token是通過jwt加密的)的方式, 來實現登陸認證的.具體的原理並不討論. jwt加密的時候, 是能夠加入時間限制的. apache

  在shiro裏面, 若是我將 rememberMe 也進行jwt加密, 而後再賦值回rememberMe , 給出去. 當我從請求頭中拿到rememberMe , 而後再解密成以前的數據, 不就能夠了麼.json

實現:

一. 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設置一個超時時間cookie

 

結果展現:

1. 登陸以後, 使用postmen 嘗試訪問

2. 耐心等待1分鐘, 而後再去訪問這個接口試試

試驗證實, 仍是可行的.app

進行jwt處理以後, rememberMe字符串會比較長, 這裏提供一種思路:ide

1. 將過時時間轉換成  yyyy-MM-dd HH:mm:ss 格式的時間字符串, 進行 base64編碼, 會發現, 長度都是28位的. 假設定義爲變量 timeBase64post

2. 在 rememberSerializedIdentity 方法中, 對 timeBase64 進行截取, 分爲兩段, 拼接到 上面 base64字符串中去, 這中方式就至關因而進行了簡單的加密, 固然, 不進行此操做, 也徹底能夠, 直接拼接到 base64的開始處, 或者結尾處.

3. 在  getRememberedSerializedIdentity 方法中, 對base64字符串進行截取, 能夠獲得時間字符串, 以後對時間進行判斷, 就能知道, 是否過時. 

相關文章
相關標籤/搜索