SpringBoot集成JWT


    JWT(json web tokens)是目前比較流行的跨域認證解決方案;說通俗點就是比較流行的token生成和校驗的方案。碰巧公司有個app的項目的token采用了jwt方案,因此記錄下后端項目集成jwt的過程,方便后續查閱。

一、jwt的簡單介紹

    jwt生成的token是一種無狀態的token,服務端不需要對該token進行保存;它一般由客戶端保存。客戶端訪問請求服務時,服務端對token進行校驗,然后進行各種控制。下面直接拿一個生成好的token來講解
         
    通過上圖我們可以發現jwt生成的token是非常長的字符串,並且字符串中有2個小點("."),通過這2個小點我們可以把這token分成3部分。
      • header:頭部,是用來描述這個token是什么類型,采用了何種加密算法;token中header是經過base64編碼的
      • payload:荷載,用來存放需要傳遞的數據。官方提供的幾個標准字段,同時也可以自己往里面加自定義的字段和內容,用來存放一些不敏感的用戶信息。可以簡單的把它想像成一個Map集合;token中payload也是經過base64編碼的
      • signature:簽名,主要是將header和payload的base64編碼后內容用點拼接在一起然后進行加密生成簽名。服務端需要利用這簽名來校驗token是否被篡改(驗簽)
    所以通俗的來講,token = base64(header) + "." + base64(payload) + "." + 簽名
    網上很多博文對jwt的介紹都比較詳細,因此本文就不再詳細的介紹jwt相關細節,重點放在java代碼該怎么寫。jwt相關詳細介紹可以參考如下鏈接:
    下面直接上代碼

二、Maven依賴版本說明

    pom中部分重要jar包依賴版本如下:
    <!-- SpringBoot 版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

        <!--jwt 依賴-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

三、token生成和解析工具類及token認證攔截器的編寫

(1)token生成和解析工具類編寫

    該工具類需要具有如下功能
      • 生成jwt標准的token;生成token時支持把不敏感的用戶信息放在token里面,后續解析token后可以直接使用這些用戶信息
      • 解析token,校驗token是否過期和篡改
          直接看下面代碼
package com.psx.gqxy.web.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * JwtUtils
 * @author ZENG.XIAO.YAN
 * @blog https://www.cnblogs.com/zeng1994/
 * @version 1.0
 */
@Slf4j
public final class JwtUtils {

    /** 存放token的請求頭對應的key的名字 */
    private static String headerKey = "token";
    /** 加密的secret */
    private static String secret = "zxyTestSecret";
    /** 過期時間,單位為秒 */
    private static long expire = 1800L;

    static {
        // TODO 上面變量的值應該從配置文件中讀取,方便測試這里就不從配置文件中讀取
        // 利用配置文件中的值覆蓋靜態變量初始化的值
    }


    /**
     * 生成jwt token
     */
    public static String generateToken(Map<String, Object> userInfoMap) {
        if (Objects.isNull(userInfoMap)) {
            userInfoMap = new HashMap<>();
        }
        //  過期時間
        Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")   // 設置頭部信息
                .setClaims(userInfoMap)               // 裝入自定義的用戶信息
                .setExpiration(expireDate)            // token過期時間
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 校驗token並解析token
     * @param token
     * @return Claims:它繼承了Map,而且里面存放了生成token時放入的用戶信息
     */
    public static Claims verifyAndGetClaimsByToken(String token) {
        try {
            /* 如果過期或者是被篡改,則會拋異常.
                注意點:只有在生成token設置了過期時間(setExpiration(expireDate))才會校驗是否過期,
                可以參考源碼io.jsonwebtoken.impl.DefaultJwtParser的299行。
                拓展:利用不設置過期時間就不校驗token是否過期的這一特性,我們不設置Expiration;
                      而采用自定義的字段來存放過期時間放在Claims(可以簡單的理解為map)中;
                      通過token獲取到Claims后自己寫代碼校驗是否過期。
                      通過這思路,可以去實現對過期token的手動刷新
            */
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            log.debug("verify token error:[{}] ", ExceptionUtils.getStackTrace(e));
            return null;
        }
    }

    public static String getHeaderKey() {
        return headerKey;
    }


}

(2)token身份認證攔截器的編寫

    攔截器主要作用如下:
      • 1)攔截器攔截到請求后,拿請求頭中的token,如果不存在只直接response輸出token不能為空
      • 2)拿到token后,進行token的解析,校驗是否篡改或者過期。如果被篡改或者過期只直接response輸出token已失效
      • 3)如果校驗都通過了,則把token中解析出的用戶信息放在request請求域中,方便后續Controller方法取用戶信息
          直接看參考下面代碼
package com.psx.gqxy.web.jwt;
import com.alibaba.fastjson.JSON;
import com.psx.gqxy.common.base.CommonConstant;
import com.psx.gqxy.common.base.ModelResult;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * jwtToken校驗攔截器
 * @author ZENG.XIAO.YAN
 * @blog https://www.cnblogs.com/zeng1994/
 * @version 1.0
 */
public class JwtInterceptor extends HandlerInterceptorAdapter {

    public static final String USER_INFO_KEY = "user_info_key";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //  獲取用戶 token
        String token = request.getHeader(JwtUtils.getHeaderKey());
        if (StringUtils.isBlank(token)) {
            token = request.getParameter(JwtUtils.getHeaderKey());
        }
        //  token為空
        if(StringUtils.isBlank(token)) {
            this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
                    JwtUtils.getHeaderKey() + " can not be blank",
                    response);
            return false;
        }
        //  校驗並解析token,如果token過期或者篡改,則會返回null
        Claims claims = JwtUtils.verifyAndGetClaimsByToken(token);
        if(null == claims){
            this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
                    JwtUtils.getHeaderKey() + "失效,請重新登錄",
                    response);
            return false;
        }
        //  校驗通過后,設置用戶信息到request里,在Controller中從Request域中獲取用戶信息
        request.setAttribute(USER_INFO_KEY, claims);
        return true;
    }

    /**
     * 利用response直接輸出錯誤信息
     * @param code
     * @param msg
     * @param response
     * @throws IOException
     */
    private void writerErrorMsg (String code, String msg, HttpServletResponse response) throws IOException {
        ModelResult<Void> result = new ModelResult<>();
        result.setCode(code);
        result.setMsg(msg);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(result));
    }

}

四、攔截器的配置和功能測試

(1)編寫一個Controller

    寫一個Controller,里面包含一登錄方法和一個test方法
      • 登錄方法用來實現登錄,登錄成功后返回token
      • test方法,主要通過攔截器攔截該方法的請求,當用戶帶有效的token訪問時才允許訪問該方法
          代碼如下:
package com.psx.gqxy.web.controller;
import com.psx.gqxy.common.base.CommonConstant;
import com.psx.gqxy.common.base.ModelResult;
import com.psx.gqxy.domain.dto.UserLoginDTO;
import com.psx.gqxy.web.jwt.JwtInterceptor;
import com.psx.gqxy.web.jwt.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * TestJwtController
 * @author ZENG.XIAO.YAN
 * @blog https://www.cnblogs.com/zeng1994/
 * @Date 2019-07-14
 * @version 1.0
 */

@RestController
@RequestMapping("jwt")
public class TestJwtController {

    @PostMapping("login")
    public ModelResult<String> login(@RequestBody UserLoginDTO dto) {
        ModelResult<String> result = new ModelResult<>();
        // 這里登錄就簡單的模擬下
        if ("root".equals(dto.getUserName()) && "123456".equals(dto.getPassword())) {
            Map<String, Object> userInfoMap = new HashMap<>();
            userInfoMap.put("userName", "隔壁老王");
            String token = JwtUtils.generateToken(userInfoMap);
            result.setData(token);
        } else {
            result.setCode(CommonConstant.FAIL);
            result.setMsg("用戶名或密碼錯誤");
        }
        return result;
    }

    @GetMapping("test")
    public String test(HttpServletRequest request) {
        // 登錄成功后,從request中獲取用戶信息
        Claims claims = (Claims) request.getAttribute(JwtInterceptor.USER_INFO_KEY);
        if (null != claims) {
            return (String) claims.get("userName");
        } else {
            return "fail";
        }
    }

}

(2)攔截器的配置

    攔截器攔截需要身份認證的請求,同時放行登錄接口
    代碼如下:
/**
 * web相關的定制化配置
 * @author ZENG.XIAO.YAN
 * @version 1.0
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    // WebMvcConfigurerAdapter 這個類在SpringBoot2.0已過時,官方推薦直接實現WebMvcConfigurer 這個接口

    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration jwtInterceptorRegistration = registry.addInterceptor(jwtInterceptor());
        // 配置攔截器的攔截規則和放行規則
        jwtInterceptorRegistration.addPathPatterns("/jwt/**")
                .excludePathPatterns("/jwt/login");
    }
}    

(3)相關測試

    • 不帶token訪問 /jwt/test接口,被攔截器攔截;返回token不能為空; 效果如下圖
                   

    • 訪問登錄接口,進行登錄;登錄成功,同時返回生成的token;效果如下圖
                   

    • 帶上登錄成功返回的token訪問/jwt/test接口,攔截器放行了請求,成功請求到了test方法;效果如下圖
                   

    • 當token被篡改或者已過期時,訪問/jwt/test接口,攔截器攔截了該請求,返回token已失效;效果如下圖
                   

    進行完上述測試后,說明jwt的集成已經大功告成了。

五、小結

    jwt集成不麻煩,但是也有很多不完善的地方,后續再想辦法把它完善。
    在我的JwtUtils的verifyAndGetClaimsByToken方法里提到了相關擴展的思路,可以通過該思路來實現token的刷新及其他的騷操作。
    當然,jwt也有很多缺點,這里就不在贅述了。



免責聲明!

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



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