前言
1. 什麼是JWT?
JWT,全稱是Json Web Token, 是一種JSON風格的輕量級的授權和身份認證規範,可實現無狀態、分佈式的Web應用授權。它是基於 RFC 7519 標準定義的一種可以安全傳輸的 小巧 和 自包含 的JSON對象。由於數據是使用數字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。
2. JWT的工作流程
- 用戶進入登錄頁,輸入用戶名、密碼,進行登錄。
- 服務器驗證登錄鑑權,如果改用戶合法,根據用戶的信息和服務器的規則生成 JWT Token。
- 服務器將該 token 以 json 形式返回(不一定要json形式,這裏說的是一種常見的做法)。
- 用戶得到 token,存在 localStorage、cookie 或其它數據存儲形式中。以後用戶請求 /protected 中的 API 時,在請求的 header 中加入 Authorization: Bearer xxxx(token)。此處注意token之前有一個7字符長度的 Bearer。
- 服務器端對此 token 進行檢驗,如果合法就解析其中內容,根據其擁有的權限和自己的業務邏輯給出對應的響應結果。
- 用戶取得結果。
來看一下 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
token 分成了三部分,頭部(header),荷載(Payload) 和 簽名(Signature),每部分用 . 分隔,其中頭部和荷載使用了base64編碼,分別解碼之後得到兩個JSON串:
第一部分-頭部:
{
"alg": "HS256",
"typ": "JWT"
}
alg字段爲加密算法,這是告訴我們 HMAC 採用 HS512 算法對 JWT 進行的簽名。
第二部分-荷載:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
荷載的字段及含義:
- iss: 該JWT的簽發者
- sub: 該JWT所面向的用戶
- aud: 接收該JWT的一方
- exp(expires): 什麼時候過期,這裏是一個Unix時間戳
- iat(issued at): 在什麼時候簽發的
這段告訴我們這個Token中含有的數據聲明(Claim),這個例子裏面有三個聲明:sub, name 和 iat。在我們這個例子中,分別代表着所面向的用戶、用戶名、創建時間,當然你可以把任意數據聲明在這裏。
第三部分-簽名:
第三部分簽名則不能使用base64解碼出來,該部分用於驗證頭部和荷載數據的完整性。
一、pom中引入JWT的依賴
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
同時在配置文件properties中加入:
# 密鑰
jwt.secret=mySecret
jwt.header=Authorization
# token 過期時間 2個小時
jwt.expiration=7200000
二、新增工具類JwtTokenUtil,定義關於JWT的一些方法
/**
* @program sweet-dream
* @description: JWT工具類
* @author: zhangchao
* @date: 2020/03/21 00:29
* @since: 1.0.0
*/
@Slf4j
@Component
public class JwtTokenUtil implements Serializable {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.header}")
private String header;
@Value("${jwt.expiration}")
private long expiration;
/**
* 獲取token中的信息
* @param token 生成的token
* @return 信息
*/
public String getInfoFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 獲取token的生成時間
* @param token 生成的token
* @return token的生成時間
*/
public Date getIssuedAtDateFromToken(String token) {
return getClaimFromToken(token, Claims::getIssuedAt);
}
/**
* 獲取token的過期時間
* @param token 生成的token
* @return token的過期時間
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* 判斷token是否過期
* @param token 生成的token
* @return true:過期,false:失效
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(Date.from(Instant.now()));
}
public <T> T getClaimFromToken(String token, Function<Claims,T> claimsResolver){
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
private Boolean ignoreTokenExpiration(String token) {
// here you specify tokens, for that the expiration is ignored
return false;
}
/**
* 生成令牌
* @param userDetails
* @return
*/
public String makeToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
/**
* 真正進行創建token的方法
* @param claims
* @param subject
* @return
*/
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = Date.from(Instant.now());
final Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder()
.setClaims(claims) /* 自定義屬性 */
.setSubject(subject) /* 該JWT所面向的用戶 */
.setIssuedAt(createdDate) /* 設置發放的時間,類型爲: Date*/
.setExpiration(expirationDate) /* 設置過期時間 類型爲:Date */
.signWith(SignatureAlgorithm.HS512, secret) /* jwt簽名算法和密鑰 */
.compact(); /* 返回一個URL安全JWT字符串 */
}
/**
* 刷新token
* @param token
* @return
*/
public String refreshToken(String token) {
final Date createdDate = Date.from(Instant.now());
final Date expirationDate = calculateExpirationDate(createdDate);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
SecurityUser user = (SecurityUser) userDetails;
final Date created = getIssuedAtDateFromToken(token);
/* final Date expiration = getExpirationDateFromToken(token);
如果token存在,且token創建日期 > 最後修改密碼的日期 則代表token有效*/
return (!isTokenExpired(token)
/*&& !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())*/
);
}
/**
* 生成過期時間
* @param createdDate 當前時間
* @return 返回到期時間
*/
private Date calculateExpirationDate(Date createdDate) {
return Date.from(Instant.ofEpochMilli(createdDate.toInstant().toEpochMilli()+expiration));
}
}
三、修改以前定義的登錄成功處理器LoginSuccessHandler,在登錄成功後生成和返回一個token給客戶端
/**
* @author ZHANGCHAO
* @date 2020/3/13 9:35
* @since 1.0.0
*/
@Slf4j
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
log.info("登錄成功!");
/* 默認:會幫我們跳轉到上一次請求的頁面上 */
//super.onAuthenticationSuccess(request, response, authentication);
//生成token
String token = jwtTokenUtil.makeToken((UserDetails) authentication.getPrincipal());
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"ok\",\"msg\":\"登錄成功\",\"token\":\""+token+"\"}");
writer.flush();
writer.close();
}
}
四、自定義一個JWT的過濾器
/**
* @program sweet-dream
* @description: JWT過濾器
* @author: zhangchao
* @date: 2020/03/21 00:48
* @since: 1.0.0
*/
@Slf4j
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Value("${jwt.header}")
private String header;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String header = request.getHeader(this.header);
String authToken = null;
String username = null;
if (null != header && header.startsWith("Bearer")){
authToken = header.replace("Bearer ","");
log.info("獲取token: {}",authToken);
username = jwtTokenUtil.getInfoFromToken(authToken);
}
if (null != username && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.customUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken,userDetails)){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); /* 增加額外數據 */
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request,response);
}
}
五、將自定義JWT過濾器加入Security配置文件SecurityConfig的過濾器鏈中
// 禁用CSRF防護
http.csrf().disable(); http.addFilterBefore(jwtAuthorizationTokenFilter,UsernamePasswordAuthenticationFilter.class);
六、啓動項目測試
輸入用戶名密碼和驗證碼,登錄成功後可以看到已經返回token了,以後每次請求都要在header裏帶上token。比如訪問/test接口:
以上!