使用 JWT 生成 token

JWT 簡介

  • JWT:Json Web Token

  • 官網://jwt.io

  • 優點:可生成安全性較高的 token 且可以完成時效性的檢驗(登陸過期檢查)

  • JWT 結構:(由官網獲取)

    image-20221116205641200

JWT 生成 token

添加依賴:

<!-- java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.2</version>
</dependency>
<!-- jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

生成 token 示例代碼:

//使用jwt生成token進行加密
JwtBuilder builder = Jwts.builder();

//此map可以存儲用戶角色權限信息
Map<String, Object> map = new HashMap<>();
map.put("k1", "v1");

//鏈式調用,設置相關加密信息
String token = builder.setSubject(name) //設置主題,也就是設置token中攜帶的數據
    .setIssuedAt(new Date()) //設置token生成時間
    .setId(users.get(0).getUserId() + "") //設置token的id(此處用的用戶id)
    .setClaims(map) //map中可存放用戶角色權限信息
    .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) //設置token過期時間為1天後
    .signWith(SignatureAlgorithm.HS256, "luis333") //設置加密方式和加密密碼
    .compact();

JWT 解析 token

//獲取token並校驗
if (token == null) {
    return new ResultVo(ResStatus.NO, "請先登陸!", null);
} else {

    //獲取jwt解析器
    JwtParser parser = Jwts.parser();
    parser.setSigningKey("luis333"); //之前加密時的加密密碼,一致才能解析成功

    try {
        //如果token正確(密碼正確,且在有效期內)則正常執行,否則拋出異常
        Jws<Claims> claimsJws = parser.parseClaimsJws(token); //解析token

        //獲取解析的token中相關數據
        Claims body = claimsJws.getBody(); //獲取token中用戶數據
        String subject = body.getSubject(); //獲取subject中數據
        String v1 = body.get("k1", String.class); //獲取claims中存儲的map中數據

        return new ResultVo(ResStatus.OK, "success", null);
    } catch (Exception e) {
        return new ResultVo(ResStatus.NO, "登陸過期,請重新登陸!", null);
    }
}

攔截器校驗 token

請求非常多,一個一個地進行校驗不現實,所以需要統一校驗 token,此時可以使用攔截器。

在攔截器的預處理方法中進行 token 統一校驗,校驗通過則放行請求,不通過則做出相關響應提示。

在攔截器統一校驗 token 後,在控制器中做過校驗的請求就不需要再次進行校驗了,直接寫業務邏輯即可。

攔截器類:

/**
 * 攔截器
 */
@Component
public class CheckTokenInterceptor implements HandlerInterceptor {

    /**
     * 預處理方法,攔截請求,決定是否放行。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //獲取token並校驗
        String token = request.getParameter("token");
        System.out.println("preHandle ========> token = " + token);
        if (token == null) {
            //沒有token
            ResultVo resultVo = new ResultVo(ResStatus.NO, "請先登錄!", null);
            doResponse(response, resultVo);
        } else {
            //獲取jwt解析器
            JwtParser parser = Jwts.parser();
            parser.setSigningKey("luis333"); //之前加密時的加密密碼,一致才能解析成功
            try {
                //如果token正確(密碼正確,且在有效期內)則正常執行,否則拋出異常
                Jws<Claims> claimsJws = parser.parseClaimsJws(token); //解析token
                return true;
            } catch (ExpiredJwtException e) {
                ResultVo resultVo = new ResultVo(ResStatus.NO, "token已過期!", null);
                doResponse(response, resultVo);
            } catch (UnsupportedJwtException e) {
                ResultVo resultVo = new ResultVo(ResStatus.NO, "異常token!", null);
                doResponse(response, resultVo);
            } catch (Exception e) {
                ResultVo resultVo = new ResultVo(ResStatus.NO, "請先登錄!", null);
                doResponse(response, resultVo);
            }
        }
        return false;
    }

    /**
     * 響應流程封裝
     */
    private void doResponse(HttpServletResponse response, ResultVo resultVo) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        String s = new ObjectMapper().writeValueAsString(resultVo);
        out.print(s);
        out.flush();
        out.close();
    }
}

攔截器配置類:

/**
 * 攔截器配置類(SpringBoot中配置攔截器)
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer { //注意此處需要實現WebMvcConfigurer接口

    @Autowired
    private CheckTokenInterceptor checkTokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(checkTokenInterceptor) //添加攔截器
                .addPathPatterns("/shopcart/**") //添加要攔截的請求
                .addPathPatterns("/orders/**")
                .excludePathPatterns("/user/**"); //放行的請求
    }
}

常用請求頭傳遞 token

前端但凡訪問受限資源,都必須攜帶 token 發起請求;token 可以通過請求行(params)、請求頭(headers)以及請求體(data)傳遞,但是習慣性使用 headers 傳遞!

在實際開發中,發送請求時,我們一般都是在請求頭中來傳遞 token,通過自定義請求頭數據,傳遞 token 到後端。

前端使用自定義請求頭數據傳遞 token 示例:(注意請求頭傳遞數據關鍵字 headers,帶 s)

<script type="text/javascript">
			
    var baseUrl = "//localhost:8080";
    var vm = new Vue({
        el: "#container",
        data: {
            token: ""
        },
        //鉤子函數,生命周期函數,直接定義!
        created: function() {
            //鉤子函數,data數據初始化之後自動調用
            console.log("========> created method execute !")

            var token = getCookieValue("token");
            this.token = token;
            console.log("token = " + token);

            axios({
                url: baseUrl + "/shopcart/list",
                method: "get",
                headers: {token: this.token} //自定義請求頭數據傳遞token
            }).then(function(res) {
                console.log(res.data);
            });
        }
    });
</script>

注意:使用自定義請求頭傳遞數據時,瀏覽器會自動發起一次預檢請求(method="OPTIONS"),來監測環境是否暢通,如果服務器正常響應,則後續請求才可正常發起。

image-20221117135445443

所以,當使用攔截器統一校驗 token ,並且使用自定義請求頭傳遞 token 時,需要在攔截器中放行瀏覽器自動發起的預檢請求,即放行 OPTIONS 請求方式的預檢請求。

主要注意點:

image-20221117144930253

攔截器代碼示例:

/**
 * 攔截器
 */
@Component
public class CheckTokenInterceptor implements HandlerInterceptor {

    /**
     * 預處理方法,攔截請求,決定是否放行。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //放行瀏覽器自動發起的預檢請求(請求方式為OPTIONS)
        String method = request.getMethod();
        System.out.println("method ========> " + method);
        if ("OPTIONS".equalsIgnoreCase(method)) {
            return true;
        }

        //從請求頭中獲取token並校驗
        String token = request.getHeader("token");
        System.out.println("preHandle ========> token = " + token);
        if (token == null) {
            //沒有token
            ResultVo resultVo = new ResultVo(ResStatus.NO, "請先登錄!", null);
            doResponse(response, resultVo);
        } else {
            //獲取jwt解析器
            JwtParser parser = Jwts.parser();
            parser.setSigningKey("luis333"); //之前加密時的加密密碼,一致才能解析成功
            try {
                //如果token正確(密碼正確,且在有效期內)則正常執行,否則拋出異常
                Jws<Claims> claimsJws = parser.parseClaimsJws(token); //解析token
                return true;
            } catch (ExpiredJwtException e) {
                ResultVo resultVo = new ResultVo(ResStatus.NO, "token已過期!", null);
                doResponse(response, resultVo);
            } catch (UnsupportedJwtException e) {
                ResultVo resultVo = new ResultVo(ResStatus.NO, "異常token!", null);
                doResponse(response, resultVo);
            } catch (Exception e) {
                ResultVo resultVo = new ResultVo(ResStatus.NO, "請先登錄!", null);
                doResponse(response, resultVo);
            }
        }
        return false;
    }

    /**
     * 響應流程封裝
     */
    private void doResponse(HttpServletResponse response, ResultVo resultVo) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        String s = new ObjectMapper().writeValueAsString(resultVo);
        out.print(s);
        out.flush();
        out.close();
    }
}
Tags: