SpringBoot集成JWT實現權限驗證
技術概述
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON
的開放標准((RFC 7519).定義了一種簡潔的,自包含的方法用於通信雙方之間以JSON
對象的形式安全的傳遞信息。由於完成的項目需要進行不同身份用戶的訪問權限驗證,因此采用的JWT實現驗證。
技術詳述
總體思路
在項目中的設計的思路如下圖所示,首先是在登錄驗證的函數接口不設置權限(具體權限怎么設置下文會講),經過驗證后將數據返回前端(本項目業務需要這里返回的是用戶視圖對象-UserVO,數據類型采用JSON數據類型,在賬戶數據中添加了token字段保存),前端在獲取數據后將token進行保存,在后續的訪問中將token加入訪問的請求頭中,經過解析token驗證用戶訪問是否滿足訪問函數要求的權限,決定是否拒絕訪問請求。
public class UserVO{
private User user;
private AccountData accountData;
private String token;
}
TokenService是根據用戶信息(這里是通過id和password)動態生成token,加密算法是HMAC256,這樣可以用於后續如果兩個用戶同時異地登錄但是一端在使用過程中修改密碼,另一端也被迫中止訪問的業務需求。讀者可以根據具體情況設計自己需要的token生成方法。
@Service("TokenService")
public class TokenService {
public String getToken(User user) {
String token="";
token= JWT.create().withAudience(user.getId().toString())//將user.id保存到token里面
.sign(Algorithm.HMAC256(user.getPassword()));//以password作為 token 的密鑰
return token;
}
}
TokenService中的JWT是通過導入com.auth0.jwt.JWT;包引入的類,maven依賴如下
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
為訪問接口添加權限
這里通過自定義注解的方式為訪問類/方法添加權限,下面這個是我自定義的一個管理員權限的注解,@Target的內容可以修改,修改后可以更改權限添加的位置(是類還是方法等等)
/**
* 需要管理員權限的注釋
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminLimit {
boolean required() default true;
}
我這里的是權限設置的是方法,我把注釋放在了controller的訪問接口上面,這里添加了兩個注解分別對應了后面注解的權限,在前端訪問該接口的時候會經過過濾器驗證,過濾器在下面會給出。
//管理員界面獲取獎勵申請記錄列表(具體實現)
@LoginToken//需要登錄
@AdminLimit//管理員權限
@GetMapping("/rewards")
public @ResponseBody List<RewardVO> getRewards(){
return rewardService.getRewardList();
}
@Target:注解的作用目標
@Target(ElementType.TYPE)——接口、類、枚舉、注解
@Target(ElementType.FIELD)——字段、枚舉的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法參數
@Target(ElementType.CONSTRUCTOR) ——構造函數
@Target(ElementType.LOCAL_VARIABLE)——局部變量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
@Retention:注解的保留位置
RetentionPolicy.SOURCE:這種類型的Annotations只在源代碼級別保留,編譯時就會被忽略,在class字節碼文件中不包含。
RetentionPolicy.CLASS:這種類型的Annotations編譯時被保留,默認的保留策略,在class文件中存在,但JVM將會忽略,運行時無法獲得。
RetentionPolicy.RUNTIME:這種類型的Annotations將被JVM保留,所以他們能在運行時被JVM或其他使用反射機制的代碼所讀取和使用。
@Document:說明該注解將被包含在javadoc中
@Inherited:說明子類可以繼承父類中的該注解
權限驗證過濾器
下面是我在項目中設置的權限過濾器,通過request.getHeader方法獲取前端在請求頭中設置的token變量,以為token是根據用戶id和password生成的(忘記的朋友可以到上面翻一下,通過TokenService生成),通過JWT.decode(token).getAudience().get(0)獲取用戶的id,通過userService用id從數據庫中獲取用戶信息,若通過驗證返回true會進行后續的訪問,否則返回false終止訪問,根據錯誤情況不同通過response.setStatus設置錯誤狀態碼,方便前端根據不同的狀態碼作出不同的反應。
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
String token = request.getHeader("token");// 從 http 請求頭中取出 token
User user = null;//登錄用戶
// 如果不是映射到方法直接通過
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//檢查是否有passToken注釋,有則跳過認證
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//判斷是否需要登錄權限
if(method.isAnnotationPresent(LoginToken.class)){
LoginToken userLoginToken = method.getAnnotation(LoginToken.class);
if (userLoginToken.required()) {
//執行認證
if(token == null){
//未登錄用戶
//設置響應狀態碼
response.setStatus(ErrorStatus.NOT_LOGGED_IN);
//停止后續訪問
return false;
}else{//有token
String userId;
try {//根據token獲取uid
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
//錯誤的token
response.setStatus(ErrorStatus.BAD_TOKEN);
return false;
}
user = userService.getUserById(Integer.parseInt(userId));
if(user == null){
//用戶不存在
//設置響應狀態碼
response.setStatus(ErrorStatus.ACCOUNT_NOT_EXIT);
//終止后續訪問
return false;
}else{
String token2 = tokenService.getToken(user);
if(!token2.equals((token))){
response.setStatus(ErrorStatus.PASSWORD_ERROR);
return false;
}
}
}
}
}
//檢查是否需要管理員權限
if(method.isAnnotationPresent(AdminLimit.class)){
if(!user.getIdentity().equals(UserIdentity.admin)){//沒有管理員權限
//設置響應狀態碼
response.setStatus(ErrorStatus.BEYOND_IDENTITY_LIMIT);
//終止后續訪問
return false;
}
}
if(method.isAnnotationPresent(UserLimit.class)){//需要普通用戶權限
if(!user.getIdentity().equals(UserIdentity.student)
&& !user.getIdentity().equals(UserIdentity.teacher)){
//既不是老師也不是學生身份
//設置響應狀態碼
response.setStatus(ErrorStatus.BEYOND_IDENTITY_LIMIT);
//終止后續訪問
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
HandlerInterceptor接口主要定義了三個方法
1.boolean preHandle ():
預處理回調方法,實現處理器的預處理,第三個參數為響應的處理器,自定義Controller,返回值為true表示繼續流程(如調用下一個攔截器或處理器)或者接着執行
postHandle()和afterCompletion();false表示流程中斷,不會繼續調用其他的攔截器或處理器,中斷執行。
2.void postHandle():
后處理回調方法,實現處理器的后處理(DispatcherServlet進行視圖返回渲染之前進行調用),此時我們可以通過modelAndView(模型和視圖對象)對模型數據進行處理或對視圖進行處理,modelAndView也可能為null。
3.void afterCompletion():
整個請求處理完畢回調方法,該方法也是需要當前對應的Interceptor的preHandle()的返回值為true時才會執行,也就是在DispatcherServlet渲染了對應的視圖之后執行。用於進行資源清理。整個請求處理完畢回調方法。如性能監控中我們可以在此記錄結束時間並輸出消耗時間,還可以進行一些資源清理,類似於try-catch-finally中的finally,但僅調用處理器執行鏈中
配置攔截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 攔截所有請求,通過判斷是否有 @LoginRequired 注解 決定是否需要登錄
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
感謝您的瀏覽,如果有說明不當之處歡迎指出,謝謝