天天看點

Jwt-應用實踐

Jwt應用實踐

    • Jwt應用實踐
      • 1、概述(what)
        • 1.1 定義
        • 1.2 如何構造
          • 1.2.1 header
          • 1.2.2 payload
        • 1.2.3 signature
      • 2、工作機理(why)
        • 2.1 特點
      • 3、應用場景(who\where\when)
      • 4、實戰(how)
        • 4.1、引入依賴
        • 4.2、給application.yml 添加配置
        • 4.3、添加配置類 JwtUtils
        • 4.4、工具包
          • 4.4.1、ResultCode
          • 4.4.2、BusinessException
        • 4.5、自定義攔截器
        • 4.6、注冊攔截器
        • 4.7、源碼位址
      • 5、參考

Jwt應用實踐

1、概述(what)

1.1 定義

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs  can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JSON Web令牌(JWT)是一個開放标準(RFC 7519),它定義了一種緊湊且獨立的方法,用于在各方之間安全地将資訊作為JSON對象傳輸。 由于此資訊是經過數字簽名的,是以可以被驗證和信任。 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公用/專用密鑰對對JWT進行簽名。
           

1.2 如何構造

JWT包含了三部分,分别使用'.'分割:
	Header(頭資訊)
	Payload(載荷,或者說資料)
	Signature(簽名,附屬資訊)
JWT的标準樣式:
	xxxxx.yyyyy.zzzzz
           
1.2.1 header
The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

頭資訊包含兩部分:
	(1) token,也就是 JWT,是Base64編碼的Url	
	(2) 加密算法(如 HMAC SHA256 or RSA)
舉例:
	{
      "alg": "HS256",
      "typ": "JWT"
	}
           
1.2.2 payload
The second part of the token is the payload, which contains theclaims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.

這部分主要是聲明,内容如下:
	标準的實體資訊,如使用者
	附加資訊
聲明分為三種:
	注冊式聲明
	公開聲明
	私有聲明
	
示例:
	{
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
 Base64 編碼
 注意:
	  請注意,對于已簽名的令牌,此資訊盡管可以防止篡改,但任何人都可以讀取。除非将其加密,否則請勿将機密資訊放入JWT的有效負載或報頭元素中。輸出是三個由點分隔的Base64-URL字元串,可以在HTML和HTTP環境中輕松傳遞這些字元串,與基于XML的标準(例如SAML)相比,它更緊湊。
           
  • Registered claims
These are a set of predefined claims which are not mandatory but recommended, to provide a set of useful, interoperable claims. Some of them are: iss (issuer), exp (expiration time), sub (subject), aud (audience), and others.

	這些是一組非強制性的但建議使用的預定義要求,以提供一組有用的,可互操作的要求。 其中一些是:iss(發行者),exp(到期時間),sub(主題),aud(閱聽人)等。
           
  • Public claims
These can be defined at will by those using JWTs. But to avoid collisions they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision resistant namespace.
	JWT使用者可以随意定義聲明。 但是為避免沖突,應在IANA JSON Web令牌系統資料庫中定義它們,或将其定義為包含抗沖突名稱空間的URI。
           
  • Private claims
These are the custom claims created to share information between parties that agree on using them and are neither registered or public claims.
這些是自定義聲明,旨在使用者之間共享資訊,既不是注冊聲明也不是公共聲明。
           

1.2.3 signature

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.
	要建立簽名部分,您必須擷取編碼的标頭,編碼的有效載荷,機密,标頭中指定
的算法,并對其進行簽名。
如,如果要使用HMAC SHA256算法,則将通過以下方式建立簽名:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

	簽名用于驗證消息在此過程中沒有更改,并且對于使用私鑰進行簽名的令牌,它還可以驗證JWT的發送者是它所說的真實身份。
           

2、工作機理(why)

在身份驗證中,當使用者使用其憑據成功登入時,将傳回JSON Web令牌。 由于令牌是憑據,是以必須格外小心以防止安全問題。 通常,令牌的保留時間不應超過要求的時間。
	由于缺乏安全性,您也不應該将敏感的會話資料存儲在浏覽器中。
	每當使用者想要通路受保護的路由或資源時,使用者代理通常應使用承載模式在授權标頭
中發送JWT。 标頭的内容應如下所示:
Authorization: Bearer <token>
           

2.1 特點

- 優勢:

(1)讓我們談談與簡單Web令牌(SWT)和安全性聲明标記語言令牌(SAML)相比,JSON Web令牌(JWT)的好處。	
(2)由于JSON不如XML冗長,是以在編碼時JSON的大小也較小,進而使JWT比SAML更為緊湊。 這使得JWT是在HTML和HTTP環境中傳遞的不錯的選擇。
(3)在安全方面,隻能使用HMAC算法由共享機密對SWT進行對稱簽名。 但是,JWT和SAML令牌可以使用X.509證書形式的公用/專用密鑰對進行簽名。 與簽名JSON的簡單性相比,使用XML數字簽名對XML進行簽名而不引入模糊的安全漏洞是非常困難的。
(4)JSON解析器在大多數程式設計語言中都很常見,因為它們直接映射到對象。 相反,XML沒有自然的文檔到對象映射。 與SAML斷言相比,這使使用JWT更加容易。
(5)關于用法,JWT是在Internet規模上使用的。 這強調了在多個平台(尤其是移動平台)上對JSON Web令牌進行用戶端處理的簡便性。
           

3、應用場景(who\where\when)

JWT的使用場景

 	授權:這是使用JWT的最常見方案。 一旦使用者登入,每個後續請求将包括JWT,進而允許使用者通路該令牌允許的路由,服務和資源。 單一登入是當今廣泛使用JWT的一項功能,因為它的開銷很小并且可以在不同的域中輕松使用。

	資訊交換:JSON Web令牌是在各方之間安全傳輸資訊的一種好方法。 因為可以對JWT
	進行簽名(例如,使用公鑰/私鑰對),是以您可以确定發件人是他們所說的人。 此外,由于簽名是使用标頭和有效負載計算的,是以您還可以驗證内容是否遭到篡改。
           

4、實戰(how)

4.1、引入依賴

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
           

4.2、給application.yml 添加配置

jwt:
  config:
    key: saas_hrm
    ttl: 3600000
    secret-key: secretKey	
           

4.3、添加配置類 JwtUtils

import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.Date;
    import java.util.Map;
    
    /**
     * @author 三多
     * @Time 2020/3/24
     */
    @Getter
    @Setter
    @ConfigurationProperties(prefix = "jwt.config")
    @Configuration
    public class JwtUtils {
        /**簽名私鑰*/
        private String key;
        /**簽名的失效時間*/
        private Long ttl;
        /**簽名的失效時間*/
        private String secretKey;
    
        /**
         * 解析token擷取claims
         */
        public  Claims parseJwt(String token) {
            Jws<Claims> parseClaimsJws = Jwts.parser()
                    //私鑰
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token);
            Claims claims = parseClaimsJws.getBody();
            return claims;
        }
    
        /**
         * 建立token
         *      id:登入使用者id
         *      subject:登入使用者名
         *  步驟:
         *      1. 設定失效時間
         *      2. 建立jwtBuilder
         *      3. 根據map 設定claims
         *      4.建立token
         */
        public String createJwt(String id,String name,Map<String,Object> params) {
            //1. 設定失效時間
            Long  now  = System.currentTimeMillis();
            long  exp = now + ttl;
            //4.建立token
            return Jwts.builder().setId(id)
                    //2. 建立jwtBuilder
                    .setSubject(name)
                    //簽發時間
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(exp))
                    .signWith(SignatureAlgorithm.HS512, secretKey)
                    //3. 根據map 設定claims
                    .addClaims(params)
                    .compact();
        }
    
    
    }
           

4.4、工具包

4.4.1、ResultCode
import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    /**
     * 公共傳回碼
     *
     * @author 三多
     * @Time 2020/3/10
     */
    @Getter
    @AllArgsConstructor
    public enum ResultCode {
        SUCCESS(true, 10000, "操作成功!"),
        /************************系統錯誤傳回碼:1XXX***************************/
        FAIL(false, 10001, "操作失敗!"),
        UN_AUTHENTICATED(false, 10002, "您還未登入!"),
        UN_AUTHORISE(false, 10003, "沒有通路權限!"),
        SERVER_ERROR(false, 99999, "抱歉系統繁忙,請稍後重試!"),
        /************************使用者操作傳回碼:2XXX***************************/
        MOBILE_ERROR_OR_PASSWORD_ERROR(false, 20001, "使用者名或者密碼錯誤!");
        /************************企業操作傳回碼:3XXX***************************/
        /************************權限操作傳回碼: 4XXX***************************/
        /************************其他操作傳回碼: 5XXX***************************/
    
        /**
         * 操作是否成功
         */
        boolean success;
        /**
         * 操作代碼
         */
        int code;
        /**
         * 提示資訊
         */
        String message;
    
    }
    
           
4.4.2、BusinessException
import com.hrm.common.entity.ResultCode;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.http.HttpStatus;
    
    import java.io.Serializable;
    
    /**
     * @author 三多
     * @Time 2020/3/13
     */
    public class BusinessException extends RuntimeException implements Serializable {
    
        @Setter
        @Getter
        private int code=HttpStatus.INTERNAL_SERVER_ERROR.value();
    
        @Setter
        @Getter
        private String message;
        public BusinessException(String message) {
            super(message);
            this.message = message;
        }
        public BusinessException(ResultCode resultCode) {
            super(resultCode.getMessage());
            this.message = resultCode.getMessage();
        }
        public BusinessException(String message,Throwable throwable) {
            super(message,throwable);
            this.message = message;
        }
    
        /**
         *
         * @param message 消息
         * @param code 狀态碼
         */
        public BusinessException(String message, int code) {
            super(message);
            this.code = code;
            this.message = message;
        }
        public  BusinessException(int code,String message ,Throwable throwable){
            super(message,throwable);
            this.code = code;
        }
    }
    
           

4.5、自定義攔截器

import com.hrm.common.entity.ResultCode;
    import com.hrm.common.exception.BusinessException;
    import com.hrm.common.utils.JwtUtils;
    import io.jsonwebtoken.Claims;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Objects;
    
    /**
     * 自定義攔截器:
     *     繼承:
     *     HandlerInterceptorAdapter
     *      preHandle:進入到控制器之前執行的内容
     *          boolean:
     *              true:可以繼續執行控制器方法
     *              false:攔截控制器方法
     *      postHandle:執行控制器方法之後執行的内容
     *      afterCompletion:響應結束之後執行的内容
     * @author 三多
     * @Time 2020/3/26
     */
     @Component
    public class JwtIntercept extends HandlerInterceptorAdapter {
    
        @Autowired
        private JwtUtils jwtUtils;
    
        /**
         * 1、通過攔截器擷取token
         *      統一的使用者權限校驗(是否登入)
         *         a. 通過request擷取token資訊
         *         b. 從token中擷取claims
         *         c. 将claims 綁定到request域中
         * 2、判斷目前使用者是否具有目前通路接口的權限
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //a. 通過request擷取token資訊
            String authorization = request.getHeader("Authorization");
            //判斷請求頭是否為空,或者以Bearer開頭
            if(! StringUtils.isEmpty(authorization)&& authorization.startsWith("Bearer")){
                //擷取token
                String token = authorization.replace("Bearer ", "");
                //解析token 擷取 claims
                Claims claims = jwtUtils.parseJwt(token);
                if(Objects.nonNull(claims)){
                    /**
                     * 擷取目前使用者可通路的API權限字元串,
                     *      a. 擷取handlerMethod
                     *      b. 擷取注解
                     *      c. 擷取注解的名稱
                     *      d. 判斷是否包含
                     *          包含就賦權
                     *          否則提示沒有權限
                     */
                    //擷取目前使用者可通路的API權限字元串
                    String apis = String.valueOf(claims.get("apis"));
                    if(handler instanceof HandlerMethod) {
                        HandlerMethod handlerMethod = (HandlerMethod) handler;
                        DeleteMapping methodAnnotation = handlerMethod.getMethodAnnotation(DeleteMapping.class);
                        String name = methodAnnotation.name();
                        if(apis.contains(name)){
                            request.setAttribute("user_claims",claims);
                            return true;
                        }else{
                            throw new BusinessException(ResultCode.UN_AUTHORISE);
                        }
                    }
                }
            }
            throw new BusinessException(ResultCode.UN_AUTHENTICATED);
    
        }
    
    }
           

4.6、注冊攔截器

package com.hrm.system.config;

import com.hrm.common.intercept.JwtIntercept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 三多
 * @Time 2020/3/27
 */
@Configuration
public class SysConfig implements WebMvcConfigurer {
    @Autowired
    private JwtIntercept jwtIntercept;

    /**
     * 添加攔截器配置
     *      1. 添加自定義攔截器
     *      2. 指定攔截器的URL位址
     *      3. 指定不攔截器的URL位址
     *  造成問題:
     *      延遲加載no session
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //1. 添加自定義攔截器
        registry.addInterceptor(jwtIntercept)
                //2. 指定攔截器的URL位址
                .addPathPatterns("/**")
                //3. 指定不攔截器的URL位址
                .excludePathPatterns("/system/login","/frame/register/**");
    }

    /**
     * jpa EntityManager 懶加載:解決no session
     * 防止 session失效
     *
     * 延遲關閉session到 view 層
     * @return
     */
    @Bean
    public OpenEntityManagerInViewFilter openEntityManagerInViewFilter(){
        return new OpenEntityManagerInViewFilter();
    }
}
           

4.7、源碼位址

github源碼位址

5、參考

  1. JWT官網參考文檔
  2. 10分鐘了解JSON Web令牌(JWT)
  3. RFC 定義