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字符串進行截取, 可以得到時間字符串, 之后對時間進行判斷, 就能知道, 是否過期. 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM