天天看點

SpringBoot學習筆記(十三:JWT )一、JWT簡介二、SpringBoot整合JWT三、測試

文章目錄

SpringBoot學習筆記(十三:JWT )一、JWT簡介二、SpringBoot整合JWT三、測試

JSON Web Token(JWT)是一個開放的标準(RFC 7519),它定義了一個緊湊且自包含的方式,用于在各方之間作為JSON對象安全地傳輸資訊。由于此資訊是經過數字簽名的,是以可以被驗證和信任。

JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。

傳統的session認證一般是這樣的流程:

  • 1、使用者向伺服器發送使用者名和密碼。
  • 2、伺服器驗證通過後,在目前對話(session)裡面儲存相關資料,比如使用者角色、登入時間等等。
  • 3、伺服器向使用者傳回一個 session_id,寫入使用者的 Cookie。
  • 4、使用者随後的每一次請求,都會通過 Cookie,将 session_id 傳回伺服器。
  • 5、伺服器收到 session_id,找到前期儲存的資料,由此得知使用者的身份。

session認證流程

SpringBoot學習筆記(十三:JWT )一、JWT簡介二、SpringBoot整合JWT三、測試

這種模式的問題在于,擴充性(scaling)不好。單機當然沒有問題,如果是伺服器叢集,或者是跨域的服務導向架構,就要求 session 資料共享,每台伺服器都能夠讀取 session。

一種解決方案是 session共享,将session持久化或者存入緩存。各種服務收到請求後,都向持久層或緩存請求資料。這種方案的優點是架構清晰,缺點是工程量比較大。另外,持久層或者緩存萬一挂了,就會認證失敗。

另一種方案是伺服器索性不儲存 session 資料了,所有資料都儲存在用戶端,每次請求都發回伺服器。JWT 就是這種方案的一個代表。

JWT認證流程:

  • 1、 使用者使用賬号和密碼發出post請求;
  • 2、 伺服器使用私鑰建立一個jwt;
  • 3、 伺服器傳回這個jwt給浏覽器;
  • 4、 浏覽器将該jwt串在請求頭中像伺服器發送請求;
  • 5、 伺服器驗證該jwt;
  • 6、 傳回響應的資源給浏覽器。

jwt認證流程

JWT資訊

SpringBoot學習筆記(十三:JWT )一、JWT簡介二、SpringBoot整合JWT三、測試

從上圖可以看到,JWT含有三部分:頭部(header)、載荷(payload)、簽名(signature)。

JWT的頭部有兩部分資訊:

  • 聲明類型,這裡是JWT
  • 聲明加密的算法,通常直接使用HMAC SHA256

頭部示例如下:

{
  "alg": "HS256",
  "typ": "JWT"
}      

頭部一般使用base64加密(該加密是可以對稱解密的),構成了第一部分:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9      

這部分一般存放一些有效的資訊。JWT的标準定義包含五個字段:

  • iss:該JWT的簽發者
  • sub: 該JWT所面向的使用者
  • aud: 接收該JWT的一方
  • exp(expires): 什麼時候過期,這裡是一個Unix時間戳
  • iat(issued at): 在什麼時候簽發的

載荷示例如下:

{
    "iss": "kkjwt",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "kk"
}
      

Signature 部分是對前兩部分的簽名,防止資料篡改。

首先,需要指定一個密鑰(secret)。這個密鑰隻有伺服器才知道,不能洩露給使用者。然後,使用 Header 裡面指定的簽名算法(預設是 HMAC SHA256),按照下面的公式産生簽名。

HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)
      

算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字元串,每個部分之間用"點"(.)分隔,就可以傳回給使用者。

用戶端收到伺服器傳回的 JWT,可以儲存在 Cookie 裡面,也可以儲存在 localStorage。

此後,用戶端每次與伺服器通信,都要帶上這個 JWT。你可以把它放在 Cookie 裡面自動發送,但是這樣不能跨域,是以更好的做法是放在 HTTP 請求的頭資訊Authorization字段裡面。

Authorization: Bearer <token>      

另一種做法是,跨域的時候,JWT 就放在 POST 請求的資料體裡面。

引入jwt的依賴

<!--jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.2</version>
        </dependency>
      

Jwt工具類進行token的生成和認證:

/**
 * JWT工具類
 * 用于生成和校驗token
 */

public class JwtUtil {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
    /**
     * 秘鑰
     */
    private static final String SECRET = "my_secret";

    /**
     * 過期時間
     **/
    private static final long EXPIRATION = 1800L;//機關為秒

    /**
     * 生成使用者token,設定token逾時時間
     */
    public  static String createToken(User user){
        //過期時間
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");
        String token= JWT.create()
                .withHeader(map)                //添加頭部
                //可以把資料存在claim中
                .withClaim("id",user.getId())      //userId
                .withClaim("name",user.getName())
                .withClaim("userName",user.getUserName())
                .withExpiresAt(expireDate)          //逾時設定,設定過期的日期
                .withIssuedAt(new Date()) //簽發時間
                .sign(Algorithm.HMAC256(SECRET)); //SECRET加密
        return token;
    }

    /**
     * 檢驗token并解析token
     */
    public static Map<String, Claim> verifyToken(String token){
        DecodedJWT jwt=null;
        try {
            JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            jwt=verifier.verify(token);
        }catch (Exception e){
            logger.error(e.getMessage());
            logger.error("解析編碼異常");
        }

        return jwt.getClaims();
    }
}      

JWT過濾器中進行token的校驗和判斷,,token不合法直接傳回,合法則解密資料并把資料放到request中供後續使用。

為了使過濾器生效,需要在啟動類添加注解@ServletComponentScan(basePackages = “edu.hpu.filter”):

@WebFilter(filterName = "jwtFilter",urlPatterns = "/secure/*")
public class JwtFilter implements Filter{
    private final Logger logger = LoggerFactory.getLogger(JwtFilter.class);


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;

        response.setCharacterEncoding("UTF-8");
        //擷取header裡的token
        String token=request.getHeader("authorization");
        if ("OPTIONS".equals(request.getMethod())) {              //除了 OPTIONS請求以外, 其它請求應該被JWT檢查
            response.setStatus(HttpServletResponse.SC_OK);
            filterChain.doFilter(request, response);
        }else {
            if (token == null) {
                response.getWriter().write("沒有token!");
                return;
            }
        }

        Map<String, Claim> userData = JwtUtil.verifyToken(token);

        if (userData==null){
            response.getWriter().write("token不合法!");
            return;
        }
        Integer id = userData.get("id").asInt();
        String name = userData.get("name").asString();
        String userName = userData.get("userName").asString();
        //攔截器 拿到使用者資訊,放到request中
        request.setAttribute("id", id);
        request.setAttribute("name", name);
        request.setAttribute("userName", userName);
        filterChain.doFilter(servletRequest,servletResponse);

    }

    @Override
    public void destroy() {

    }
}      

  • 登入Controller進行登入操作,登入成功後生産token并傳回:
/**
 * 登入Controller
 */

@RestController

public class LoginController {
    private final Logger logger = LoggerFactory.getLogger(LoginController.class);

    //模拟資料庫
    static Map<Integer, User> userMap = new HashMap<>();
    static {
        User user1 = new User(1, "zhangsan", "張三", "123456");
        userMap.put(1, user1);
        User user2 = new User(2, "lisi", "李四", "123123");
        userMap.put(2, user2);
    }

    /**
     * 模拟使用者登入
     */

    @RequestMapping("/login")
    public String login(User user){
        for (User dbUser : userMap.values()) {
            if (dbUser.getName().equals(user.getName()) && dbUser.getPassword().equals(user.getPassword())) {
                logger.info("登入成功!生成token!");
                String token = JwtUtil.createToken(dbUser);
                return token;
            }
        }
        return "";
    }
}      
  • SecureController中的請求會被JWT過濾器攔截,合法後才能通路:
**
 * 需要登入後才可通路
 */
@RestController
public class SecureController {
    private final Logger logger = LoggerFactory.getLogger(SecureController.class);

    /**
     * 查詢使用者資訊,登入後才可通路
     */

    @RequestMapping("/secure/getUserInfo")
    public String getUserInfo(HttpServletRequest request){
        Integer id = (Integer) request.getAttribute("id");
        String name = request.getAttribute("name").toString();
        String userName = request.getAttribute("userName").toString();
        return "目前使用者資訊id=" + id + ",name=" + name + ",userName=" + userName;
    }
}      

測試時先通路登入接口,根據使用者名和密碼生成token,再将token放在請求頭裡,去通路需要登入才能通路的接口。

直接通路登入接口:http://localhost:8080/login?name=zhangsan&password=123456

登入成功則傳回token:

通路http://localhost:8080/secure/getUserInfo,請求頭裡需要攜帶token:

SpringBoot學習筆記(十三:JWT )一、JWT簡介二、SpringBoot整合JWT三、測試

成功擷取使用者資訊。