1. 程式人生 > >【SpringBoot】整合JWT實現使用者認證

【SpringBoot】整合JWT實現使用者認證

初始JWT

1.什麼是JWT

JSON Web Token (JWT) 是一個開放標準 (RFC 7519),它定義了一種緊湊且獨立的方式,可以在客戶端與伺服器之間作為JSON物件安全地傳輸資訊。

2.JWT使用場景

  • 身份驗證: 使用者在登入以後,後續的每個請求都將包含JWT,允許使用者訪問該令牌允許的路由,服務和資源等。Session同樣也可以實現這個功能,但是在使用Session的同時也會相應的增加伺服器的壓力;而JWT的開銷則相對較小,因為其將儲存的壓力分佈到各個客戶端中,從而減輕了伺服器的壓力,並且能夠在不同域的系統當中輕鬆的使用。單點登入(SSO)就廣泛使用了JWT的功能。
  • 資訊交換: JWT能夠在客戶端與伺服器之間安全地傳輸資訊,因為其可以簽名,通過簽名可以驗證傳輸資訊是否被修改。

3.JWT組成

JWT就是一個字串,經過加密處理與校驗處理的字串,由 . 分割的三個部分組成,分別是頭(Header)、有效荷載(Playload)、簽名(Signature),因此JWT的格式通常也是這樣: header.playload.signature(header由JWT的表頭資訊經過加密後得到;playload由JWT用到的身份驗證資訊JSON資料加密得到;signature是由header和playload加密得到,這一部分作為校驗部分)。

  • Header

    通常是由兩部分組成的:一是令牌的型別,即JWT;二是雜湊演算法,比如SHA256

例如:

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

然後這個JSON通過Base64加密形成JWT的第一個部分即header

  • Playload
    JWT的第二個部分是有效荷載,其中包含了宣告(Claim)。JWT提供了一組預定義的宣告,這些宣告都是可選的,並不是強制性的。當然你也可以自定義宣告傳輸所需資訊,比如系統使用者ID。出於安全考慮,一般不會將使用者的敏感資訊存放在聲明當中。
宣告屬性 說明
iss 發行人,JWT由誰簽發
iat JWT建立時間,unix時間戳格式
exp JWT過期時間,unix時間戳格式
sub JWT所面向的使用者
aud 接收方,接收JWT的一方
nbf 當前時間在nbf之前,JWT不能被接收處理
jti JWT唯一ID

例如:

{
	"iss": "Hilox",
	"sub": "HiloxApiUser",
	"iat": "1542337107",
	"exp": "1542340707",
	"userId": "5"
}

將上述宣告(Claim)通過Base64加密後得到payload

  • Signature
    將表頭經過Base64加密得到的headerClaim經過Base64加密得到的playload進行組合,形成一個新字串header.playload,對新形成的字串使用標頭當中指定的演算法(例如:上述Header例子中使用HS256演算法)和自定義的金鑰(例如:Hilox)進行加密得到signature

最後,將字串組合 header.playload.signature就是生成的token了。

圖1 JWT生成流程圖

JWT應用

1.JWT如何使用

博主為移動端app搭建伺服器,所採用的方式是將token放到http請求的請求頭部當中,通常使用的是Authorization屬性欄位。
移動端app使用cookie不太方便,所以暫不做考慮。

2.應用流程

圖2 初次登入生成JWT流程圖
圖3 使用者訪問資源流程圖

JWT應用程式碼實現

下面通過程式碼來實現使用者認證的功能,博主這裡主要採用Spring Boot與JWT整合的方式實現。
關於Spring Boot專案如何搭建與使用本章不做詳細介紹。
程式碼當中針對異常自行做處理,我這裡偷點懶直接用日誌在控制檯列印。

1.新增JWT依賴

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

2.新增JWT相關配置

Base64線上加密

jwt:
 # 發行者
 name: Hilox
 # 金鑰, 經過Base64加密, 可自行替換
 base64Secret: SGlsb3g=
 #jwt中過期時間設定(分)
 jwtExpires: 120

3.JWT配置實體類

/**
 * jwt 相關引數
 * Created by Hilox on 2018/11/16 0016.
 */
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtParam {

    /**
     * 發行者名
     */
    private String name;

    /**
     * base64加密金鑰
     */
    private String base64Secret;

    /**
     * jwt中過期時間設定(分)
     */
    private int jwtExpires;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBase64Secret() {
        return base64Secret;
    }

    public void setBase64Secret(String base64Secret) {
        this.base64Secret = base64Secret;
    }

    public int getJwtExpires() {
        return jwtExpires;
    }

    public void setJwtExpires(int jwtExpires) {
        this.jwtExpires = jwtExpires;
    }
}

4.配置JWT攔截器

/**
 * jwt 攔截器
 * Created by Hilox on 2018/11/16 0016.
 */
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtParam jwtParam;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        final String authHeader = request.getHeader(JwtConstant.AUTH_HEADER_KEY);

        if (HttpMethod.OPTIONS.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // 校驗頭格式校驗
        if (!JwtUtils.validate(authHeader)) {
            // TODO 這裡自行丟擲異常
            log.info("===== 無校驗頭或校驗頭格式異常 =====");
            return false;
        }

        // token解析
        final String authToken = JwtUtils.getRawToken(authHeader);
        Claims claims = JwtUtils.parseToken(authToken, jwtParam.getBase64Secret());
        if (claims == null) {
            log.info("===== token解析異常 =====");
            return false;
        }
        // request.setAttribute("CLAIMS", claims);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

5.配置MVC攔截器

/**
 * mvc 配置
 * Created by Hilox on 2018/11/15 0015.
 */
@Configuration
public class MyWebConfigurer extends WebMvcConfigurerAdapter {

    // 這裡這麼做是為了提前載入, 防止過濾器中@AutoWired注入為空
    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }

    // 自定義過濾規則
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}

6.JWT工具類

/**
 * JWT工具類
 * Created by Hilox on 2018/11/16 0016.
 */
@Slf4j
public class JwtUtils {

    private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";

    // 構造私有
    private JwtUtils() {}

    /**
     * 獲取原始token資訊
     * @param authorizationHeader 授權頭部資訊
     * @return
     */
    public static String getRawToken(String authorizationHeader) {
        return authorizationHeader.substring(AUTHORIZATION_HEADER_PREFIX.length());
    }

    /**
     * 獲取授權頭部資訊
     * @param rawToken token資訊
     * @return
     */
    public static String getAuthorizationHeader(String rawToken) {
        return AUTHORIZATION_HEADER_PREFIX + rawToken;
    }

    /**
     * 校驗授權頭部資訊格式合法性
     * @param authorizationHeader 授權頭部資訊
     * @return
     */
    public static boolean validate(String authorizationHeader) {
        return StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith(AUTHORIZATION_HEADER_PREFIX);
    }

    /**
     * 生成token, 只在使用者登入成功以後呼叫
     * @param userId 使用者id
     * @param jwtParam JWT加密所需資訊
     * @return
     */
    public static String createToken(String userId, JwtParam jwtParam) {
        return createToken(userId, null, jwtParam);
    }

    /**
     * 生成token, 只在使用者登入成功以後呼叫
     * @param userId 使用者id
     * @param claim 宣告
     * @param jwtParam JWT加密所需資訊
     * @return
     */
    public static String createToken(String userId, Map<String, Object> claim, JwtParam jwtParam) {
        try {
            // 使用HS256加密演算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            // 生成簽名金鑰
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtParam.getBase64Secret());
            SecretKeySpec signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            // 新增構成JWT的引數
            JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
                    .claim(JwtConstant.USER_ID_KEY, userId)
                    .addClaims(claim)
                    .setIssuer(jwtParam.getName())
                    .setIssuedAt(now)
                    .signWith(signatureAlgorithm, signingKey);

            // 新增token過期時間
            long TTLMillis = jwtParam.getJwtExpires() * 60 * 1000;
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                jwtBuilder.setExpiration(exp).setNotBefore(now);
            }

            return jwtBuilder.compact();
        } catch (Exception e) {
            // TODO 這裡自行丟擲異常
            log.error("簽名失敗", e);
            return null;
        }
    }

    /**
     * 解析token
     * @param authToken 授權頭部資訊
     * @param base64Secret base64加密金鑰
     * @return
     */
    public static Claims parseToken(String authToken, String base64Secret) {
        try{
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Secret))
                    .parseClaimsJws(authToken).getBody();
            return claims;
        } catch (SignatureException se) {
            // TODO 這裡自行丟擲異常
            log.error("===== 金鑰不匹配 =====", se);
        } catch (ExpiredJwtException ejw) {
            // TODO 這裡自行丟擲異常
            log.error("===== token過期 =====", ejw);
        } catch (Exception e){
            // TODO 這裡自行丟擲異常
            log.error("===== token解析異常 =====", e);
        }
        return null;
    }
}

7.編寫登入驗證Controller

/**
 * 登入驗證Controller
 * Created by Hilox on 2018/11/16 0016.
 */
@Slf4j
@RestController
public class LoginController {

    @Autowired
    private JwtParam jwtParam;

    // 登入
    @PostMapping("/login")
    public String login() {
        // 1.使用者密碼驗證我這裡忽略, 假設使用者驗證成功, 取得使用者id為5
        Integer userId = 5;
        // 2.驗證通過生成token
        String token = JwtUtils.createToken(userId + "", jwtParam);
        if (token == null) {
            log.error("===== 使用者簽名失敗 =====");
            return null;
        }
        log.info("===== 使用者{}生成簽名{} =====", userId, token);
        return JwtUtils.getAuthorizationHeader(token);
    }

    // 測試
    @PostMapping("/hilox")
    public String hilox() {
        return "Hello World!";
    }
}

原始碼傳送門springboot-jwt

JWT程式碼測試效果

啟動以上專案,博主這裡使用工具Postman來模擬http請求。

1.未登入情況請求測試介面/hilox

圖4 未登入情況請求測試介面效果圖

2.請求登入介面/login

圖5 請求登入介面效果圖

3.登入情況請求測試介面/hilox

這裡我們需要將請求登入介面時返回的token放入請求頭的Authorization當中。

圖6 登入情況請求測試介面效果圖