使用 SpringBoot 提供 api 的時候,我更喜歡使用 jwt 的方式來做驗證。網上有會多 Spring Security 整合 jwt 的,也有 Shiro 整合 jwt 的,感覺有點複雜。這裡分享一下自己在項目中的簡單實現。 依賴包 除了 SpringBoot 基本的依賴,需要一個生成 ...
使用 SpringBoot 提供 api 的時候,我更喜歡使用 jwt 的方式來做驗證。網上有會多 Spring Security 整合 jwt 的,也有 Shiro 整合 jwt 的,感覺有點複雜。這裡分享一下自己在項目中的簡單實現。
依賴包
除了 SpringBoot 基本的依賴,需要一個生成 jwt 和序列化的包。生成 jwt 的包依賴很多,因為我項目里使用了 hutool 這個包,就只用用它了。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.9</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.9</version>
</dependency>
jwt用戶模型
定義一個 Jwt 的 sub 欄位模型,存儲用戶:
import lombok.Data;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Data
public class JwtUser {
/**
* 用戶編號
*/
private Integer id;
/**
* 用戶名
*/
private String name;
/**
* 角色
*/
private String role;
/**
* 獲取當前請求用戶
* @return
*/
public static JwtUser getCurrentUser() {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
return (JwtUser) request.getAttribute("user");
}
}
驗證註解
定義一個用於請求類和方法的註解
import java.lang.annotation.*;
@Inherited
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorize {
/**
* 是否匿名可以訪問
* @return
*/
boolean anonymous() default false;
/**
* 角色
* @return
*/
String[] roles() default {};
}
JWT 幫助類
用於生成 jwt 和 解析 JwtUser 對象。
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.google.gson.Gson;
import com.mpyf.xapi.security.JwtUser;
import lombok.var;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JwtTokenUtils {
public static final String SECRET = "your_secret";
public static final String ISS = "com.your.cn";
private static final int EXPIRATIONHOURS = 24; //過期時間24小時
//創建token
public static String createToken(JwtUser user) {
return createToken(user, EXPIRATIONHOURS);
}
public static String createToken(JwtUser user, int hours) {
String subJson = new Gson().toJson(user);
JWTSigner jwtSigner = JWTSignerUtil.hs512(SECRET.getBytes());
JWT jwt = JWT.create().setSigner(jwtSigner);
jwt
.setJWTId(UUID.randomUUID().toString().replace("-", ""))
.setSubject(subJson) //用戶信息
.setIssuer(ISS) //簽發者
//.setAudience("受眾")
//.setNotBefore(new Date())
.setIssuedAt(new Date())
.setExpiresAt(new Date(System.currentTimeMillis() + hours * 3600 * 1000));
return jwt.sign();
}
public static JwtUser getUser(String token) {
if (StringHelper.isNullOrEmpty(token)) return null;
var jwt = JWTUtil.parseToken(token);
JWTSigner jwtSigner = JWTSignerUtil.hs512(SECRET.getBytes());
jwt.setSigner(jwtSigner);
if (jwt.validate(10)) {
var subJson = jwt.getPayload("sub").toString();
JwtUser user = new Gson().fromJson(subJson, JwtUser.class);
return user;
} else {
return null;
}
}
}
驗證攔截器
定義jwt的驗證攔截器,從請求頭獲取 token 解析並驗證。
import com.mpyf.xapi.helper.JwtTokenUtils;
import com.mpyf.xapi.helper.StringHelper;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
/**
* jwt 驗證攔截器
*/
@Component
public class JwtAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//Authorization:Bearer+空格+token
String token = request.getHeader("Authorization");
if (token != null) {
token = token.replace("Bearer ", "");
}
//處理模擬登錄的jwt
if (StringHelper.isNullOrEmpty(token)) {
token = request.getParameter("jwt");
}
if (StringHelper.isNullOrEmpty(token)) {
//相容從請求參數傳token
Object jwt = request.getAttribute("jwt");
if (jwt != null) {
token = jwt.toString();
}
}
JwtUser user = JwtTokenUtils.getUser(token);
request.setAttribute("user", user);
if (handler instanceof HandlerMethod) {
HandlerMethod h = (HandlerMethod) handler;
Authorize authorize = h.getMethodAnnotation(Authorize.class);
if (authorize == null) {
authorize = h.getMethod().getDeclaringClass().getAnnotation(Authorize.class);
}
//如果沒有Authorize或者可以匿名訪問,直接返回
if (authorize != null && !authorize.anonymous()) {
{
if (user == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
} else if (authorize.roles() != null && authorize.roles().length > 0 &&
Arrays.stream(authorize.roles()).allMatch(s -> !s.equalsIgnoreCase(user.getRole()))) {
//沒許可權
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return false;
}
}
}
}
return true;
}
}
註冊攔截器
在 WebMvc 配置中註冊攔截器,並支持跨域請求
import com.mpyf.xapi.security.JwtAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
JwtAuthInterceptor jwtAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtAuthInterceptor).addPathPatterns("/api/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
//.maxAge(3600)
.allowCredentials(true);
WebMvcConfigurer.super.addCorsMappings(registry);
}
}
Controller中使用
@RestController
@RequestMapping("/api/test")
@Authorize(roles = {"admin", "user"})
public class TestController {
@GetMapping("admin_and_user")
public String admin_and_user(){
return "admin 和 user 角色都可以訪問";
}
@GetMapping("admin_only")
@Authorize(roles = "admin") //覆蓋Controller的設置
public String admin_only(){
return "只有 admin 角色可以訪問";
}
@GetMapping("public_all")
@Authorize(anonymous = true)
public String public_all(){
return "匿名可以訪問";
}
}
不用 Spring Security 和 Shiro ,是不是更簡單呢!