JWT 全名 JSON WEB Token 主要作用爲用戶身份驗證, 廣泛應用與前後端分離項目當中.
JWT 的優缺點 : https://www.jianshu.com/p/af8360b83a9f
一、pom.xml 引入jar文件
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
二、添加jwt自定義字段 application.yml
# jwt 配置
custom:
jwt:
# header:憑證(校驗的變量名)
header: token
# 有效期1天(單位:s)
expire: 5184000
# secret: 祕鑰(普通字符串)
secret: aHR0cHM6Ly9teS5vc2NoaW5hLm5ldC91LzM2ODE4Njg=
# 簽發者
issuer: test-kou
三、添加加密解密方法
import io.jsonwebtoken.*;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT 工具類
* <p>
* jwt含有三部分:頭部(header)、載荷(payload)、簽證(signature)
* (1)頭部一般有兩部分信息:聲明類型、聲明加密的算法(通常使用HMAC SHA256)
* (2)載荷該部分一般存放一些有效的信息。jwt的標準定義包含五個字段:
* - iss:該JWT的簽發者
* - sub: 該JWT所面向的用戶
* - aud: 接收該JWT的一方
* - exp(expires): 什麼時候過期,這裏是一個Unix時間戳
* - iat(issued at): 在什麼時候簽發的
* (3)簽證(signature) JWT最後一個部分。該部分是使用了HS256加密後的數據;包含三個部分
*
* @author kou
*/
@ConfigurationProperties(prefix = "custom.jwt")
@Data
@Component
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
// 祕鑰
private String secret;
// 有效時間
private Long expire;
// 用戶憑證
private String header;
// 簽發者
private String issuer;
/**
* 生成token簽名
*
* @param subject
* @return
*/
public String generateToken(String subject) {
final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
Date now = new Date();
// 過期時間
Date expireDate = new Date(now.getTime() + expire * 1000);
//Create the Signature SecretKey
final byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(Base64.getEncoder().encodeToString(getSecret().getBytes()));
final Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
final Map<String, Object> headerMap = new HashMap<>();
headerMap.put("alg", "HS256");
headerMap.put("typ", "JWT");
//add JWT Parameters
final JwtBuilder builder = Jwts.builder()
.setHeaderParams(headerMap)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expireDate)
.setIssuer(getIssuer())
.signWith(signatureAlgorithm, signingKey);
logger.info("JWT[" + builder.compact() + "]");
return builder.compact();
}
/**
* 解析token
*
* @param token token
* @return
*/
public Claims parseToken(String token) {
Claims claims = null;
try {
final byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(Base64.getEncoder().encodeToString(getSecret().getBytes()));
claims = Jwts.parser().setSigningKey(apiKeySecretBytes).parseClaimsJws(token).getBody();
logger.info("Parse JWT token by: ID: {}, Subject: {}, Issuer: {}, Expiration: {}", claims.getId(), claims.getSubject(), claims.getIssuer(), claims.getExpiration());
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException
| IllegalArgumentException e) {
logger.info("Parse JWT errror " + e.getMessage());
return null;
}
return claims;
}
/**
* 判斷token是否過期
*
* @param expiration
* @return
*/
public boolean isExpired(Date expiration) {
return expiration.before(new Date());
}
}
四、添加攔截方法,攔截token處理,我這邊使用的是filter攔截處理,根據自己需求自定
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* 如果請求中(請求頭或者Cookie)中存在JWT,則:
* 1、解析JWT並查找對應的用戶信息,然後加入request attribute中
* 2、更新Cookie時間、更新JWT失效時間放入Header
* <p>
* OncePerRequestFilter 一次請求只進入一次filter
*/
@Component
@Slf4j
public class JWTFilter extends OncePerRequestFilter {
public static final String SECURITY_USER = "SECURITY_USER";
// 設置不需要校驗的路徑
private static final String[] NOT_CHECK_URL = {"/login", "/registered"};
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 判斷是否需要對token處理
if (!isNotCheck(request.getRequestURI())) {
// 獲取token
String token = getToken(request);
if (StringUtils.isBlank(token)) {
log.info("請求無效,原因:{} 爲空!", jwtUtil.getHeader());
request.setAttribute("exceptionCode", SystemParameters.TOKEN_IS_NULL.getIndex());
request.setAttribute("exceptionMessage", "請求無效,原因:" + jwtUtil.getHeader() + " 爲空!");
request.getRequestDispatcher("/exception/authentication").forward(request, response);
// throw new AuthenticationException(SystemParameters.TOKEN_IS_NULL.getIndex(), "請求無效,原因:" + jwtUtil.getHeader() + " 爲空!");
return;
}
Claims claims = jwtUtil.parseToken(token);
// 判斷簽名信息
if (null != claims && !claims.isEmpty() && !jwtUtil.isExpired(claims.getExpiration())) {
// 獲取簽名用戶信息
String userId = claims.getSubject();
//獲取相應的用戶信息,可以在過濾器中先行獲取,也可以先保存用戶ID,在需要時進行獲取
// User user = usersService.findById(Long.valueOf(userId));
request.setAttribute(SECURITY_USER, userId);
Cookie jwtCookie = new Cookie(jwtUtil.getHeader(), URLEncoder.encode(token, "UTF-8"));
jwtCookie.setHttpOnly(true);
jwtCookie.setPath("/");
response.addCookie(jwtCookie);
response.addHeader(jwtUtil.getHeader(), token);
} else {
log.info("{} 無效,請重新登錄!", jwtUtil.getHeader());
request.setAttribute("exceptionCode", SystemParameters.AUTHENTICATION_FAILED.getIndex());
request.setAttribute("exceptionMessage", jwtUtil.getHeader() + " 無效,請重新登錄!");
request.getRequestDispatcher("/exception/authentication").forward(request, response);
// throw new AuthenticationException(SystemParameters.AUTHENTICATION_FAILED.getIndex(), jwtUtil.getHeader() + " 無效,請重新登錄!");
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 獲取token
*
* @param request
* @return 返回token
*/
private String getToken(HttpServletRequest request) {
//先從header中獲取token
String token = request.getHeader(jwtUtil.getHeader());
//再從cookie中獲取
if (StringUtils.isBlank(token)) {
try {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (jwtUtil.getHeader().equals(cookie.getName())) {
token = URLDecoder.decode(cookie.getValue(), "UTF-8");
}
}
}
} catch (Exception e) {
log.error("Can NOT get jwt from cookie -> Message: {}", e);
}
}
return token;
}
/**
* 根據url判斷是否需要校驗,false需要校驗
*
* @param url
* @return 是否需要校驗
*/
private boolean isNotCheck(String url) {
// 處理路徑以"/" 結尾的"/"
url = url.endsWith("/") ? url.substring(0, url.lastIndexOf("/")) : url;
for (String path : NOT_CHECK_URL) {
// 判斷是否以 "/**" 結尾
if (path.endsWith("/**")) {
return url.startsWith(path.substring(0, path.lastIndexOf("/") + 1))
|| url.equals(path.substring(0, path.lastIndexOf("/")));
}
// 判斷url == path
if (url.equals(path)) {
return true;
}
}
return false;
}
}
五、配置過濾器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 過濾器配置
*
* @author kou
*/
@Configuration
public class FilterConfig {
@Autowired
private JWTFilter jwtFilter;
@Bean
public FilterRegistrationBean registrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
// 設置過濾器
registration.setFilter(jwtFilter);
// 設置攔截路徑
registration.addUrlPatterns("/rest/*");
// 設置過濾器名稱
registration.setName("JWTFilter");
// 設置過濾器執行順序
registration.setOrder(1);
return registration;
}
}
六、添加異常處理類
/**
* 認證異常類
*
* @author kou
*/
public class AuthenticationException extends RuntimeException {
private static final long serialVersionUID = 1L;
//自定義錯誤碼
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
/**
* Creates a new AuthenticationException.
*/
public AuthenticationException() {
super();
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
*/
public AuthenticationException(String message) {
super(message);
}
/**
* Constructs a new AuthenticationException.
*
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(Throwable cause) {
super(cause);
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
*/
public AuthenticationException(int code, String message) {
super(message);
this.code = code;
}
/**
* Constructs a new AuthenticationException.
*
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(int code, Throwable cause) {
super(cause);
this.code = code;
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
七、添加全局異常處理類
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 全局異常處理
*
* @author kou
*/
@ControllerAdvice
public class GlobalException {
/**
* 授權登錄異常
*/
@ResponseBody
@ExceptionHandler(AuthenticationException.class)
public BaseResult authenticationException(AuthenticationException e) {
return new BaseResult(SystemParameters.FAIL.getIndex(), e.getMessage(), e.getCode());
}
@ExceptionHandler(Exception.class)
@ResponseBody
BaseResult exception(Exception e) {
return new BaseResult(SystemParameters.FAIL.getIndex(), e.getMessage(), SystemParameters.ERROR.getIndex());
}
}
注意:
由於使用filter過濾器,springmvc 不能進行filter攔截異常,故通過請求轉發來實現統一異常攔截
八、錯誤異常處理,用來處理filter轉發出來的請求攔截異常,進而讓springmvc統一處理異常
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 錯誤控制器
*
* @author kou
*/
@RestController
public class ErrorController {
@RequestMapping("/exception/authentication")
public BaseResult authenticationException(HttpServletRequest request) {
throw new AuthenticationException(Integer.valueOf(request.getAttribute("exceptionCode").toString()), request.getAttribute("exceptionMessage").toString());
}
}
九、返回統一格式
public class BaseResult {
// 結果狀態碼
private int ret;
// 結果說明
private String msg;
private int err;
public BaseResult() {
}
public BaseResult(int ret, String msg, int err) {
this.ret = ret;
this.msg = msg;
this.err = err;
}
public int getRet() {
return ret;
}
public void setRet(int ret) {
this.ret = ret;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getErr() {
return err;
}
public void setErr(int err) {
this.err = err;
}
}