天天看點

java實作三天免登陸_JWT過期重新整理問題,實作十五天免登陸

java實作三天免登陸_JWT過期重新整理問題,實作十五天免登陸
違背的青春
  • Spring Security(一):整合JWT實作登入功能
  • Spring Security(二):擷取使用者權限菜單樹
  • Spring Security(三):與Vue.js整合
  • Spring Security(四):更新前端路由擷取方式
  • Spring Security(五):前後端權限控制詳解
  • Spring Security(六):前端菜單,角色權限頁面的搭建
  • Spring Security(七):自定義攔截器實作對權限異常的處理
  • Spring Security(八):Vue.js使用 CryptoJS加密密碼以及BCryptPasswordEncoder的使用

當使用

jwt

進行校驗使用者資訊,由于

jwt

是無狀态的,是以最重要的就是解決重新整理

token

帶來問題,一個簡單的需求就是15天内免登入。當然最簡單粗暴的方式是你可以設

token

有效期為半個月嘛,這确實是一個解決方案,但是對于資訊比較重要的還是不要采用這種方案,最好把

token

的有效期時間設定為短一些使用

refresh

重新整理

token

就ok。

大概流程如下圖

java實作三天免登陸_JWT過期重新整理問題,實作十五天免登陸

簡單解釋下:現在我采取的方案是設定

token

的有效期為兩小時,大概流程

  1. 使用者通過登入,後端會存儲這個

    token

    Redis

    ,key設定為

    userid

    ,value設定為

    token

    ,有效期設定為15天(如果一周免登陸,那

    Redis

    的有效期就要設定為1周)
  2. 如果使用者在兩個小時後請求資料,這個

    token

    是失效的,但是我們可以通過這個失效的

    token

    擷取

    payload

    中的使用者資訊(我把使用者id存儲到了

    token

    payload

    中),通過解析

    token

    擷取使用者id,根據這個id去查到

    Redis

    這個key值是否存在,如果還在

    Redis

    有效期内我們就可以重新簽發

    token

    傳回給前端,前端把之前存儲的

    token

    替換為現在得,周而複始,
  3. 如果時間過了規定的免登入時間那

    Redis

    中,

    token

    會失效,我們隻需要傳回失效資訊給前端處理就完事了。

    關鍵代碼:

@Slf4j
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {



    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private RedisUtil redisUtil;

    @Autowired
    private UserMapper userMapper;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException {
        String authHeader = request.getHeader(jwtTokenUtil.getHeader());
        try {
            if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
                String username = jwtTokenUtil.getUsernameFromToken(authHeader);
                jwtTokenUtil.validateToken(authHeader);//驗證令牌
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                    if (jwtTokenUtil.validateToken(authHeader)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
            chain.doFilter(request, response);
        }
        catch (ExpiredJwtException e){
            e.printStackTrace();
            Map<String,Object> map = jwtTokenUtil.parseJwtPayload(authHeader);
            String userid = (String)map.get("userid");
            //這裡的方案是如果令牌過期了,先去判斷redis中存儲的令牌是否過期,如果過期就重新登入,如果redis中存儲的沒有過期就可以
            //繼續生成token傳回給前端存儲方式key:userid,value:令牌
            String redisResult = redisUtil.get(userid);
            String username= (String) map.get("sub");
            if(StringUtils.isNoneEmpty(redisResult)){
                JwtUser jwtUser = new JwtUser();
                jwtUser.setUserid(userid);
                jwtUser.setUsername(username);
                Map<String, Object> claims = new HashMap<>(2);
                claims.put("sub", jwtUser.getUsername());
                claims.put("userid", jwtUser.getUserid());
                claims.put("created", new Date());
                String token = jwtTokenUtil.generateToken(jwtUser);
                //更新redis中的token
                //首先擷取key的有效期,把新的token的有效期設為舊的token剩餘的有效期
                redisUtil.setAndTime(userid,token,redisUtil.getExpireTime(userid));
                if (token != null && StringUtils.isNotEmpty(token)) {
                    jwtTokenUtil.validateToken(token);//驗證令牌
                    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                        if (jwtTokenUtil.validateToken(token)) {
                            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                        }
                    }
                }
                response.setHeader("newToken",token);
                response.addHeader("Access-Control-Expose-Headers","newToken");
                response.setContentType("application/json;charset=utf-8");
                response.setCharacterEncoding("UTF-8");
                try {
                    chain.doFilter(request, response);
                } catch (IOException e1) {
                    e1.printStackTrace();
                } catch (ServletException e1) {
                    e1.printStackTrace();
                }

            } else {
                response.addHeader("Access-Control-Allow-origin","http://localhost:9528");
                RetResult retResult = new RetResult(RetCode.EXPIRED.getCode(),"抱歉,您的登入資訊已過期,請重新登入");
                response.setContentType("application/json;charset=utf-8");
                response.setCharacterEncoding("UTF-8");
                response.getWriter().write(JSON.toJSONString(retResult));
                System.out.println("redis過期");
            }
        } catch (ServletException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           

主要修改了

JwtAuthenticationTokenFilter

的邏輯代碼,其實很簡單就是首先判斷

jwt

是否過期,然後再判斷

Redis

是否過期,根絕結果傳回資訊罷了。

幾個問題

1.重新整理

token

如何傳回前端,這裡隻介紹

Spring Boot

的傳回方式,很簡單就是通過

response

傳回

token

給前端,注意一點就是需要添加一個自定義頭資訊我設為

newToken

,這樣前端才能擷取到,如果不添加

response.addHeader("Access-Control-Expose-Headers","newToken");

這段代碼,前端是擷取不到我們新簽發的

token

response.setHeader("newToken",token);
    response.addHeader("Access-Control-Expose-Headers","newToken");
    response.setContentType("application/json;charset=utf-8");
    response.setCharacterEncoding("UTF-8");
           

效果大概如下:

java實作三天免登陸_JWT過期重新整理問題,實作十五天免登陸

而且以後的請求,都是使用了這個重新整理的

token

通路的

java實作三天免登陸_JWT過期重新整理問題,實作十五天免登陸

2.

token

改變了之後那我們後續的請求肯定是不行了,如何解決?

很好解決,拿着這個token,繼續走

JwtAuthenticationTokenFilter

中的驗權代碼即可

redisUtil.setAndTime(userid,token,redisUtil.getExpireTime(userid));
                if (token != null && StringUtils.isNotEmpty(token)) {
                    jwtTokenUtil.validateToken(token);//驗證令牌
                    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                        if (jwtTokenUtil.validateToken(token)) {
                            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                        }
                    }
                }
           

3.前端如何擷取新簽發的

token

?

隻需要在

srcutilsrequest.js

中修改相關代碼,判斷我們是上面傳回的

newToken

,如果這個

response

作用域中有該字段,就證明後端更新token,我們前端需要修改

token

了,然後調用

setToken

即可,後續的請求中就會使用了該

token

去請求接口。

if(response.headers.newtoken){
      setToken(response.headers.newtoken)
    }
           

4.如何以一定的格式傳回給前端錯誤資訊,這裡提供一個例子,第一行的代碼是為了解決跨域問題,然後傳回一個

Json

類型的字元串就好。

response.addHeader("Access-Control-Allow-origin","http://localhost:9528");
    RetResult retResult = new RetResult(RetCode.EXPIRED.getCode(),"抱歉,您的登入資訊已過期,請重新登入");
    response.setContentType("application/json;charset=utf-8");
    response.getWriter().write(JSON.toJSONString(retResult));
           

5.如何解析過期的

token

?由于

jwt

payload

就是

key-value

格式,是以這裡我傳回了一個

map

格式

private static final ObjectMapper MAPPER = new ObjectMapper();

    private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver();
    public static Map parseJwtPayload(String jwt) {
        Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
        String base64UrlEncodedHeader = null;
        String base64UrlEncodedPayload = null;
        String base64UrlEncodedDigest = null;
        int delimiterCount = 0;
        StringBuilder sb = new StringBuilder(128);
        for (char c : jwt.toCharArray()) {
            if (c == '.') {
                CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb);
                String token = tokenSeq != null ? tokenSeq.toString() : null;

                if (delimiterCount == 0) {
                    base64UrlEncodedHeader = token;
                } else if (delimiterCount == 1) {
                    base64UrlEncodedPayload = token;
                }

                delimiterCount++;
                sb.setLength(0);
            } else {
                sb.append(c);
            }
        }
        if (delimiterCount != 2) {
            String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
            throw new MalformedJwtException(msg);
        }
        if (sb.length() > 0) {
            base64UrlEncodedDigest = sb.toString();
        }
        if (base64UrlEncodedPayload == null) {
            throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
        }
        // =============== Header =================
        Header header = null;
        CompressionCodec compressionCodec = null;
        if (base64UrlEncodedHeader != null) {
            String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
            Map<String, Object> m = readValue(origValue);
            if (base64UrlEncodedDigest != null) {
                header = new DefaultJwsHeader(m);
            } else {
                header = new DefaultHeader(m);
            }
            compressionCodec = codecResolver.resolveCompressionCodec(header);
        }
        // =============== Body =================
        String payload;
        if (compressionCodec != null) {
            byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
            payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8);
        } else {
            payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
        }
        JSONObject jsonObject = JSONObject.parseObject(payload);
        Map<String, Object> map = jsonObject;
        return map;
    }

     /**
     * @Description
     * @Param [val] 從json資料中讀取格式化map
     * @Return java.util.Map<java.lang.String,java.lang.Object>
     */
    public static Map<String, Object> readValue(String val) {
        try {
            return MAPPER.readValue(val, Map.class);
        } catch (IOException e) {
            throw new MalformedJwtException("Unable to read JSON value: " + val, e);
        }
    }
           

歡迎關注我的個人公衆号~github:

ywbjja/springSecurity-jwt-vue-temple​github.com

java實作三天免登陸_JWT過期重新整理問題,實作十五天免登陸

個人微信公衆号~

java實作三天免登陸_JWT過期重新整理問題,實作十五天免登陸

繼續閱讀