JWT,即JSON Web Token,是一種用於在網路上傳遞宣告的開放標準(RFC 7519)。JWT 可以在使用者和伺服器之間傳遞安全可靠的資訊,通常用於身份驗證和資訊交換。
攔截器(Interceptor)是一種用於處理請求的機制,可以讓你在請求的處理過程中進行預處理和後處理。攔截器類似於過濾器(Filter),但相比過濾器,攔截器更加專注於處理控制器層面的請求,可以對處理器的執行過程進行更加細粒度的控制。
使用場景:
- 執行順序:攔截器可以設定多個,它們的執行順序由設定時的順序決定。
- 非同步請求:攔截器也能處理非同步請求,需要實現
AsyncHandlerInterceptor
介面。
ThreadLocal
是 Java 中的一個類,主要用於提供了執行緒本地變數。 ThreadLocal
建立的變數只能被當前執行緒存取,其他執行緒無法直接存取或修改它。ThreadLocal
主要用於保持執行緒封閉性,即每個執行緒都擁有自己獨立的變數副本,不同執行緒之間不會相互干擾。
應用場景:
ThreadLocal
可以輕鬆地將資料在方法呼叫間傳遞,而無需將資料作為引數傳遞。ThreadLocal
來儲存每個執行緒的資料庫連線,確保每個執行緒使用的是自己的連線。ThreadLocal
可以用於事務管理,確保事務的一致性。注意:
- ThreadLocal可以在虛擬執行緒環境下使用
ThreadLocal
應該謹慎使用,避免濫用。過多的使用可能導致程式碼難以理解和維護。- 避免在
ThreadLocal
中儲存大物件,以防止記憶體漏失。- 在使用執行緒池時,需要注意清理
ThreadLocal
,以防止執行緒複用時出現資料汙染。
<!-- java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
推薦使用@Value
的方式從application.yml
中獲取金鑰和過期時間
// refresh-token金鑰
@Value("${refresh-token.secret}")
private String REFRESH_TOKEN_SECRET;
// refresh-token過期時間
@Value("${refresh-token.expire-time}")
private int REFRESH_TOKEN_EXPIRE_TIME;
// refresh-token金鑰
@Value("${access-token.secret}")
private String ACCESS_TOKEN_SECRET;
// refresh-token過期時間
@Value("${access-token.expire-time}")
private int ACCESS_TOKEN_EXPIRE_TIME;
/**
* 獲取access_token
* @param userId
* @param userType
* @return
*/
private String getAccessToken(int userId, int userType){
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, ACCESS_TOKEN_EXPIRE_TIME);
return JWT.create()
.withClaim("userId", userId)
.withClaim("userType", userType)
.withExpiresAt(calendar.getTime())
.sign(Algorithm.HMAC512(ACCESS_TOKEN_SECRET));
}
/**
* 獲取refresh_token
* @param userId
* @param userType
* @return
*/
private String getRefreshToken(int userId, int userType){
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, REFRESH_TOKEN_EXPIRE_TIME);
return JWT.create()
.withClaim("userId", userId)
.withClaim("userType", userType)
.withExpiresAt(calendar.getTime())
.sign(Algorithm.HMAC512(REFRESH_TOKEN_SECRET));
}
/**
* <p>
* 使用者SessionVO
* </p>
*
* @author jonil
* @since 2023/11/10 16:03
*/
@Data
public class UserSessionVO {
// 使用者id
Integer userId;
// 使用者型別
Integer userType;
}
/**
* <p>
* 使用者session上下文
* </p>
*
* @author jonil
* @since 2023/11/10 16:02
*/
public class UserSessionContext {
private static ThreadLocal<UserSessionVO> userSessionVOThreadLocal = new ThreadLocal<>();
public static void set(UserSessionVO userSessionVO) {
userSessionVOThreadLocal.set(userSessionVO);
}
public static UserSessionVO get() {
return userSessionVOThreadLocal.get();
}
public static void remove() {
userSessionVOThreadLocal.remove();
}
}
此處攔截器為處理HTTP請求前
中
後
各階段需要做的操作。HTTP請求進來的時候將JWT解析的資料放進ThreadLocal,出去的時候需要將資料從ThreadLocal移除,否則會造成記憶體漏失。
/**
* <p>
* JWT攔截器
* </p>
*
* @author jonil
* @since 2023/11/10 15:55
*/
public class JWTInterceptor implements HandlerInterceptor {
// refresh-token金鑰
@Value("${access-token.secret}")
private String ACCESS_TOKEN_SECRET;
/**
* 將使用者基本資訊新增到ThreadLocal
* @param request
* @param response
* @param handler
* @return
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String accessToken = request.getHeader("Authorization");
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512(ACCESS_TOKEN_SECRET)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(accessToken);
Integer userId = decodedJWT.getClaim("userId").asInt();
Integer userType = decodedJWT.getClaim("userType").asInt();
UserSessionVO userSessionVO = new UserSessionVO();
userSessionVO.setUserId(userId);
userSessionVO.setUserType(userType);
UserSessionContext.set(userSessionVO);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) {
}
/**
* 將使用者的基本資訊從ThreadLocal移除
* @param request
* @param response
* @param handler
* @param ex
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) {
UserSessionContext.remove();
}
}
在需要用到JWT內容的地方,使用以下程式碼即可獲取對應的內容
UserSessionVO userSessionVO = UserSessionContext.get();
Integer userId = userSessionVO.getUserId();
public enum UserType {
systemAdmin(0),
userAdmin(1),
maintenancePersonnel(2),
vipUser(3),
user(4),
none(5);
UserType(int code) {
this.code = code;
}
private int code;
public int getCode() {
return code;
}
}
/**
* <p>
* 自定義校驗註解
* </p>
*
* @author jonil
* @since 2023/11/13 20:05
*/
@Documented
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {PermissionValidator.class})
public @interface Permission {
UserType type() default UserType.none;
/**
* 是否強制校驗
* @return
*/
boolean required() default true;
/**
* 校驗不通過時的報錯資訊
* @return
*/
String message() default "許可權不足!";
/**
* 分組
* @return
*/
Class<?>[] groups() default {};
/**
* bean的負載
* @return
*/
Class<? extends Payload>[] payload() default {};
}
/**
* <p>
* 自定義註解實現
* </p>
*
* @author jonil
* @since 2023/11/13 20:05
*/
public class PermissionValidator implements ConstraintValidator<Permission, UserType> {
@Override
public void initialize(Permission constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
/**
* 判斷當前是否有許可權
* @param userType object to validate
* @param context context in which the constraint is evaluated
*
* @return
*/
@Override
public boolean isValid(UserType userType, ConstraintValidatorContext context) {
return UserSessionContext.get().getUserType() <= userType.getCode();
}
}
/**
* 獲取資訊介面
*/
@Permission(type = UserType.user)
@GetMapping("/info")
public R getInfo() {
return R.success(service.getInfo());
}
註解會從ThreadLocal獲取當前使用者的型別,然後進行比對,當然你的判斷邏輯可以比這個更加複雜,只需要符合業務實現就好了。
倘若你是在微服務環境或分散式環境下使用這一套邏輯,需要注意在閘道器(流量閘道器、業務閘道器)和OpenFeign中攜帶原生的Header,否則獲取不到該使用者的資訊。