使用JWT、拦截器与ThreadLocal实现在任意位置获取Token中的信息,并结合自定义注解实现对方法的鉴权
1. 簡介
1.1 JWT
JWT,即JSON Web Token,是一種用于在網(wǎng)絡(luò)上傳遞聲明的開放標(biāo)準(zhǔn)(RFC 7519)。JWT 可以在用戶和服務(wù)器之間傳遞安全可靠的信息,通常用于身份驗(yàn)證和信息交換。
-
聲明(Claims): JWT 包含一組稱為聲明的信息,聲明描述了一些數(shù)據(jù)。有三種類型的聲明:
- 注冊聲明(Registered Claims):這是一些預(yù)定義的聲明,包括標(biāo)準(zhǔn)的聲明,例如"iss"(簽發(fā)者)、"sub"(主題)和"exp"(過期時(shí)間)等。
- 公共聲明(Public Claims):這些聲明是由使用 JWT 的雙方定義的,并且必須遵守一定的規(guī)定,以防止沖突。
- 私有聲明(Private Claims):這些是自定義的聲明,用于在雙方之間共享信息。
-
編碼結(jié)構(gòu): JWT 由三部分組成,使用點(diǎn)號(hào)(.)分隔開:
- Header(頭部):包含了令牌的元數(shù)據(jù),例如算法和令牌類型。
- Payload(載荷):包含了聲明,即實(shí)際傳輸?shù)臄?shù)據(jù)。
- Signature(簽名):用于驗(yàn)證發(fā)送方的身份以及確保消息的完整性。簽名由前兩部分的內(nèi)容和密鑰組成,以防篡改。
-
編碼方法: JWT 可以使用不同的編碼方法,包括:
- Base64 URL encoding:用于編碼頭部和載荷。
- HMACSHA256:用于生成簽名,以確保消息完整性。
-
使用場景:
- 身份驗(yàn)證:用戶登錄后,服務(wù)器生成一個(gè)包含用戶信息的JWT,并將其發(fā)送給客戶端。客戶端在后續(xù)請求中將JWT包含在請求頭中,服務(wù)器驗(yàn)證JWT以確保請求的合法性。
- 信息交換:JWT還可以包含其他信息,例如用戶的角色、訪問權(quán)限等,這些信息可以在不需要查詢數(shù)據(jù)庫的情況下進(jìn)行快速訪問。
1.2 攔截器
攔截器(Interceptor)是一種用于處理請求的機(jī)制,可以讓你在請求的處理過程中進(jìn)行預(yù)處理和后處理。攔截器類似于過濾器(Filter),但相比過濾器,攔截器更加專注于處理控制器層面的請求,可以對處理器的執(zhí)行過程進(jìn)行更加細(xì)粒度的控制。
使用場景:
- 身份驗(yàn)證和授權(quán):攔截器可以用于檢查用戶是否已經(jīng)登錄,是否具有足夠的權(quán)限訪問某個(gè)資源。
- 日志記錄:攔截器可以用于記錄請求和響應(yīng)的日志信息,方便調(diào)試和監(jiān)控。
- 性能監(jiān)控:通過攔截器,你可以記錄請求的處理時(shí)間,幫助進(jìn)行性能監(jiān)控。
- 執(zhí)行順序:攔截器可以配置多個(gè),它們的執(zhí)行順序由配置時(shí)的順序決定。
- 異步請求:攔截器也能處理異步請求,需要實(shí)現(xiàn)
AsyncHandlerInterceptor接口。
1.3 ThreadLocal
ThreadLocal 是 Java 中的一個(gè)類,主要用于提供了線程本地變量。 ThreadLocal 創(chuàng)建的變量只能被當(dāng)前線程訪問,其他線程無法直接訪問或修改它。ThreadLocal 主要用于保持線程封閉性,即每個(gè)線程都擁有自己獨(dú)立的變量副本,不同線程之間不會(huì)相互干擾。
應(yīng)用場景:
-
線程安全的數(shù)據(jù)傳遞:在多線程環(huán)境中,通過
ThreadLocal可以輕松地將數(shù)據(jù)在方法調(diào)用間傳遞,而無需將數(shù)據(jù)作為參數(shù)傳遞。 -
數(shù)據(jù)庫連接管理:在數(shù)據(jù)庫連接的管理中,可以使用
ThreadLocal來存儲(chǔ)每個(gè)線程的數(shù)據(jù)庫連接,確保每個(gè)線程使用的是自己的連接。 -
事務(wù)管理:
ThreadLocal可以用于事務(wù)管理,確保事務(wù)的一致性。
注意:
- ThreadLocal可以在虛擬線程環(huán)境下使用
ThreadLocal應(yīng)該謹(jǐn)慎使用,避免濫用。過多的使用可能導(dǎo)致代碼難以理解和維護(hù)。- 避免在
ThreadLocal中存儲(chǔ)大對象,以防止內(nèi)存泄漏。- 在使用線程池時(shí),需要注意清理
ThreadLocal,以防止線程復(fù)用時(shí)出現(xiàn)數(shù)據(jù)污染。
2. 代碼實(shí)戰(zhàn)
2.1 引入依賴
<!-- java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
2.2 獲取token的函數(shù)
推薦使用@Value的方式從application.yml中獲取密鑰和過期時(shí)間
// refresh-token密鑰
@Value("${refresh-token.secret}")
private String REFRESH_TOKEN_SECRET;
// refresh-token過期時(shí)間
@Value("${refresh-token.expire-time}")
private int REFRESH_TOKEN_EXPIRE_TIME;
// refresh-token密鑰
@Value("${access-token.secret}")
private String ACCESS_TOKEN_SECRET;
// refresh-token過期時(shí)間
@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));
}
2.3 ThreadLocal工具類
/**
* <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();
}
}
2.4 攔截器
此處攔截器為處理HTTP請求前 中 后各階段需要做的操作。HTTP請求進(jìn)來的時(shí)候?qū)WT解析的數(shù)據(jù)放進(jìn)ThreadLocal,出去的時(shí)候需要將數(shù)據(jù)從ThreadLocal移除,否則會(huì)造成內(nèi)存泄漏。
/**
* <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();
}
}
2.5 使用
在需要用到JWT內(nèi)容的地方,使用以下代碼即可獲取對應(yīng)的內(nèi)容
UserSessionVO userSessionVO = UserSessionContext.get();
Integer userId = userSessionVO.getUserId();
3. 進(jìn)階
3.1 結(jié)合自定義注解實(shí)現(xiàn)對方法的鑒權(quán)
角色枚舉類
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>
* 自定義校驗(yàn)注解
* </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;
/**
* 是否強(qiáng)制校驗(yàn)
* @return
*/
boolean required() default true;
/**
* 校驗(yàn)不通過時(shí)的報(bào)錯(cuò)信息
* @return
*/
String message() default "權(quán)限不足!";
/**
* 分組
* @return
*/
Class<?>[] groups() default {};
/**
* bean的負(fù)載
* @return
*/
Class<? extends Payload>[] payload() default {};
}
自定義注解實(shí)現(xiàn)類
/**
* <p>
* 自定義注解實(shí)現(xiàn)
* </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);
}
/**
* 判斷當(dāng)前是否有權(quán)限
* @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());
}
注解會(huì)從ThreadLocal獲取當(dāng)前用戶的類型,然后進(jìn)行比對,當(dāng)然你的判斷邏輯可以比這個(gè)更加復(fù)雜,只需要符合業(yè)務(wù)實(shí)現(xiàn)就好了。
4. 注意
倘若你是在微服務(wù)環(huán)境或分布式環(huán)境下使用這一套邏輯,需要注意在網(wǎng)關(guān)(流量網(wǎng)關(guān)、業(yè)務(wù)網(wǎng)關(guān))和OpenFeign中攜帶原生的Header,否則獲取不到該用戶的信息。
總結(jié)
以上是生活随笔為你收集整理的使用JWT、拦截器与ThreadLocal实现在任意位置获取Token中的信息,并结合自定义注解实现对方法的鉴权的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 原生JS实现视频截图
- 下一篇: 文心一言 VS 讯飞星火 VS chat