【SpringBoot】整合JWT實現使用者認證
阿新 • • 發佈:2018-11-17
初始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
加密得到的header
和Claim
經過Base64
加密得到的playload
進行組合,形成一個新字串header.playload
,對新形成的字串使用標頭當中指定的演算法(例如:上述Header例子中使用HS256
演算法)和自定義的金鑰(例如:Hilox
)進行加密得到signature
。
最後,將字串組合 header.playload.signature
就是生成的token了。
JWT應用
1.JWT如何使用
博主為移動端app搭建伺服器,所採用的方式是將token放到http請求的請求頭部當中,通常使用的是Authorization
屬性欄位。
移動端app使用cookie不太方便,所以暫不做考慮。
2.應用流程
JWT應用程式碼實現
下面通過程式碼來實現使用者認證的功能,博主這裡主要採用Spring Boot與JWT整合的方式實現。
關於Spring Boot專案如何搭建與使用本章不做詳細介紹。
程式碼當中針對異常自行做處理,我這裡偷點懶直接用日誌在控制檯列印。
1.新增JWT依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2.新增JWT相關配置
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
2.請求登入介面/login
3.登入情況請求測試介面/hilox
這裡我們需要將請求登入介面時返回的token放入請求頭的Authorization
當中。