SpringBoot 集成 JWT 實現 token 鑑權
完整項目地址:https://gitee.com/aoxiaobao/JWT-study.git
pom依賴
- 首先當然是引入pom依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
一個簡單的小例子
- 創建 token
- 通過使用 Jwts.builder()
- 在 token 添加一些簡單的用戶信息
- 選擇一個加密算法
- 就可以創建出一個 token
- 解析 token
- 通過 Jwts.parser()
- 獲取其中加密的用戶信息
public class TestJwt {
public static void main(String[] args) {
System.out.println(createJwt());
parseJwt(createJwt());
}
public static String createJwt(){
JwtBuilder jwtBuilder = Jwts.builder()
.setId("888888") // 添加id
.setSubject("藏冰") // 添加用戶名
.setIssuedAt(new Date()) // 添加當前時間
.signWith(SignatureAlgorithm.HS256,"aowei"); // 設置加密算法,密鑰
String token = jwtBuilder.compact(); // 獲取token
return token;
}
public static void parseJwt(String token){
Claims claims = Jwts.parser()
.setSigningKey("aowei")
.parseClaimsJws(token).getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());
}
}
JwtUtils 工具類
- 將上面的簡單例子進一步完善,
- 把 私鑰 和 簽名的失效時間,單獨提取出來
- 還可以在 token 中添加自定義的 Map 格式數據
- 最後添加上自定義異常的異常處理
public class JwtUtils {
// 簽名私鑰
public static final String AUTH_HEADER_KEY = "Authorization";
// 簽名的失效時間
private static final long TTL = 10000 ;
private static Logger log = LoggerFactory.getLogger(JwtUtils.class);
/**
* 設置認證token
* id:登錄用戶id
* subject:登錄用戶名
*/
public static String createJwt(String id, String name, Map<String,Object> map) throws CustomException {
// 設置失效時間
long now = System.currentTimeMillis(); // 當前毫秒數
long exp = now + TTL;
// 創建jwtBuilder
String token = null;
try {
JwtBuilder jwtBuilder = Jwts.builder()
.setId(id) // 添加id
.setSubject(name) // 添加用戶名
.setIssuedAt(new Date()) // 添加當前時間
.signWith(SignatureAlgorithm.HS256,AUTH_HEADER_KEY); // 設置加密算法,密鑰
// 根據map設置claims
for (Map.Entry<String,Object> entry : map.entrySet()){
jwtBuilder.claim(entry.getKey(),entry.getValue());
}
// 指定失效時間
jwtBuilder.setExpiration(new Date(exp));
// 創建token
token = jwtBuilder.compact();
} catch (Exception e) {
log.error("簽名失敗", e);
throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
}
return token;
}
/**
* 解析token字符串,獲取clamis
*/
public static Claims parseJwt(String token) throws CustomException {
try {
Claims claims = Jwts.parser()
.setSigningKey(AUTH_HEADER_KEY)
.parseClaimsJws(token).getBody();
return claims;
} catch (ExpiredJwtException e) {
log.error("===== Token過期 =====", e);
throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED);
} catch (Exception e) {
log.error("===== token解析異常 =====", e);
throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
}
}
}
自定義JWT攔截器
-
創建一個 token 攔截器
-
繼承 HandlerInterceptorAdapter
-
有三個常用的方法,這裏只需要重寫 preHandle 方法即可
-
重寫 preHandle方法(進入到控制器方法之前執行的內容)
- 返回值是 boolean類型
- true: 可以繼續執行控制器方法
- false: 攔截請求
-
postHandler(執行控制器方法之後執行的內容)
-
afterHandler(響應結束之前執行的內容)
-
這裏攔截器要實現的功能有
- 簡化獲取token數據的代碼編寫
- 統一的用戶權限校驗(是否登錄)
- 判斷用戶是否具有當前訪問接口的權限
/**
* 自定義攔截器
* 繼承HandlerInterceptorAdapter
* preHandle: 進入到控制器方法之前執行的內容
* boolean:
* true: 可以繼續執行控制器方法
* false: 攔截
* postHandler: 執行控制器方法之後執行的內容
* afterHandler: 響應結束之前執行的內容
*
* 簡化獲取token數據的代碼編寫
* 統一的用戶權限校驗(是否登錄)
* 判斷用戶是否具有當前訪問接口的權限
* @author aowei
*/
@Component
@Slf4j
public class JwtInterceptor extends HandlerInterceptorAdapter {
/**
* 通過攔截器獲取token數據
* 從token中解析獲取claims
* 將claims綁定到request域中
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 忽略帶JwtIgnore註解的請求, 不做後續token認證校驗
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
if (jwtIgnore != null) {
return true;
}
}
// 通過request獲取請求token信息
String authorization = request.getHeader("Authorization");
// 判斷請求頭信息是否爲空,或是否以Bearer 開頭
if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")){
// 獲取token數據
String token = authorization.replace("Bearer","");
// 解析token獲取claims
Claims claims = JwtUtils.parseJwt(token);
if(claims != null){
// 通過claims獲取到當前用戶的可訪問api權限字符串
String apis = (String) claims.get("roles");
// 通過handler
String name = null;
if (handler instanceof HandlerMethod) {
HandlerMethod h = (HandlerMethod) handler;
// 獲取接口上的requestmapping註解
RequestMapping annotation = h.getMethodAnnotation(RequestMapping.class);
// 獲取當前請求接口中的name屬性
name = annotation.name();
}else {
throw new CustomException(ResultCode.INTERFACE_ADDRESS_INVALID);
}
if(apis.contains(name)){
request.setAttribute("userClaims",claims);
return true;
}else{
throw new CustomException(ResultCode.PERMISSION_UNAUTHORISE);
}
}
}
throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
}
}
配置攔截器
- 攔截器寫好之後,還不能真正起到作用
- 這裏需要配置一下 JWT 攔截器
- 實現 WebMvcConfigurer 接口
- 用 addPathPatterns() 方法攔截請求
- /**表示攔截所有的請求
- 可以在 excludePathPatterns() 方法中指明不需要攔截的路徑
@Configuration
public class JwtConfig implements WebMvcConfigurer {
/**
* 添加攔截器的配置
*/
@Override
public void addInterceptors(InterceptorRegistry registry){
// 添加自定義攔截器
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/**");
}
/**
* 跨域支持
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
.maxAge(3600 * 24);
}
}
自定義註解
- 通過自定義註解 @JwtIgnore
- 實現放行某些接口(不攔截請求)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtIgnore {
// String value() default "JWT";
}
代碼測試
- 將用戶的角色也添加到 token 中
- 在攔截器中解析 token 獲得用戶角色
- 再從 handler 中獲取請求的 RequestMapping註解的name屬性
- 可以在name屬性裏,寫上訪問該接口所需的權限名稱
- 比較 用戶角色和 name 屬性,就可以判斷出用戶是否有訪問該接口的權限
@Slf4j
@RestController
public class Login {
@Autowired
UserService userService;
@Autowired
RoleService roleService;
@RequestMapping(value = "/login",method = RequestMethod.POST)
@JwtIgnore
public Result login(String username, String password) throws CustomException {
User user = userService.findByUsername(username);
if(user == null || !user.getPassword().equals(password)){
return new Result(ResultCode.FAIL);
} else {
// 登錄成功
// 獲取到所有的可訪問api權限
// StringBuilder sb = new StringBuilder();
// for(Role role : user.getRole()){
// for(Permission perm : role.getPermissions()){
// sb.append(perm.getCode().append(","));
// }
// }
Role role = roleService.findByUsername(username);
Map<String,Object> map = new HashMap<>();
// 可訪問的角色
map.put("roles",role.getRoleName());
String token = JwtUtils.createJwt(username,username,map);
// Claims claims = JwtUtils.parseJwt(token);
// System.out.println("角色:"+claims.get("roles"));
// System.out.println("用戶名:"+claims.getSubject());
// System.out.println("用戶id:"+claims.getId());
Result result = new Result(ResultCode.SUCCESS);
result.setData(token);
return result;
}
}
// 測試需要權限的url(需要登錄且擁有all-admin的權限)
@RequestMapping(value = "/test3",method = RequestMethod.GET,name="all-admin")
public Result test3(){
return new Result(ResultCode.SUCCESS);
}
// 測試需要權限的url(需要登錄且擁有test2的權限)
@RequestMapping(value = "/test2",method = RequestMethod.GET,name="test2")
public Result test1(){
return new Result(ResultCode.SUCCESS);
}
// 測試被攔截的url(需要登錄)
@RequestMapping(value = "/test1",method = RequestMethod.GET)
public Result test2(){
return new Result(ResultCode.SUCCESS);
}
// 測試不攔截的url(不需要登錄)
@RequestMapping("/test")
@JwtIgnore
public Result test(){
return new Result(ResultCode.SUCCESS);
}
}
參考鏈接:https://www.jianshu.com/p/430cd44a2796