Jwt 如何在 springboot 项目中进行接口访问鉴权
文章目录
结合以下文章:
jwt.io 官网详细介绍
SpringBoot项目使用JWT+拦截器实现token验证
spring-boot + JWT实现TOKEN登录接口验证
1 springboot 框架负责接口的拦截和放行
1.1 原理
使用 HandlerInterceptor (对于 springboot 框架不推荐使用 doFilter)
1.2 思路
白名单思路, 拦截所有接口目录/**
, 放行需要的接口
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
1.3 坑: Springboot 访问了错误处理路径 /error
接口程序中有未处理的异常, 报了 Null Pointer Exception
, Springboot 调用默认的错误处理接口 /error
企图调用错误处理程序, 第二次触发了 HandlerInterceptor, 由于/error
不带 token, 所以被拒绝,最终报 token 校验不通过的错误信息.
这里的解决方法:
1 处理程序中所有异常, 在最外层捕捉不可预见的异常, 返回统一错误信息,服务内部错误
2 自定义 springboot 的 error path 为符合自己程序的路径, 并用 controller 定义处理程序. 当springbot框架本身或者依赖包出现不可预知的错误时,转到这里, 可以返回统一错误信息
其它方法也可以尝试使用 @ControllerAdvice 自定义异常处理类, 处理程序自身不可预知的错误
2 jwt token 负责携带数据和签名的生成及校验
官方库
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.4</version>
</dependency>
2.1 初始化
JWTCreator.Builder builder = JWT.create();
2.2 设置 Header
Map<String,Object> headerMap = new HashMap<>();
builder.withHeader(headerMap);
2.3 携带数据 payload
自定义数据
for (Map.Entry<String,String> entry:data.entrySet()) {
builder.withClaim(entry.getKey(), entry.getValue());
}
设置过期时间
builder.withExpiresAt(expireDate);
Token 放在请求Header的Authorization字段里。Token 携带数据userId
Token 的格式:
header
{
"kid": "XXXXXXXXXXXXXXXXXX0MDVmLWIyMjEtMjQ1MWU3NWYxXXXXX5",
"typ": "JWT",
"alg": "RS256"
}
payload
{
"exp": 1684829637,
"userId": "xxxxxxxxxxxx==",
"iat": 1684829607
}
2.4 签名 sign 后, 生成 token
token = builder.sign(Algorithm.HMAC256(secretKey))
如果使用RSA非对称算法,使用私钥签名
token = builder.sign(Algorithm.RSA256(rsaPrivateKey))
2.5 校验
配置算法
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secretKey)).build();
如果使用RSA非对称算法,使用公钥校验
JWTVerifier verifier = JWT.require(Algorithm.RSA256(rsaPublicKey)).build();
校验
DecodedJWT decodedJWT = verifier.verify(token);
校验的方法是再生成一遍进行比较
2.6 获取信息
两种方法
- 第一种方法: 在 HandlerInterceptor 里的 PreHandle 校验通过后, 立即解析 token, 拿到数据. 把解析结果放入 threadlocal 变量, 这样在整个请求的主线程里, 可以使用该变量, 并且该变量对其它线程不可见, 在请求结束的 afterCompletion() 方法里把 threadlocal 变量注销释放.
- 第二种方法: 在需要获取信息的时候, 先获取该severlet请求的上下文 RequestContextHolder, 进而拿到请求Request中的 header, 进而拿到 token, 重新解析 token, 获取数据. 由于接口进来时, 已经通过校验, 可以不通过校验的方式获取解析后的token, 直接调用解析方法进行解析即可.
2.7 字段说明
3 拦截器代码
Springboot 建议使用Handler进行拦截
定义 annotation, 对这个annotation 修饰的接口进行拦截
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessWithoutToken {
boolean required() default true;
}
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("Request from {} to URI: {}, URL: {}", HttpClientUtil.getRemoteIp(request), request.getRequestURI(), request.getRequestURL().toString());
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
log.info("Method {}, {}", method.getName(), method.getDeclaredAnnotations());
if (method.isAnnotationPresent(AccessWithoutToken.class)) {
AccessWithoutToken accessWithoutToken = method.getAnnotation(AccessWithoutToken.class);
if (accessWithoutToken.required()) {
return true;
}
}
// Authorization: Bearer <token>
String authorization = request.getHeader("Authorization");
// TODO check token
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
if (!org.apache.commons.lang3.StringUtils.isBlank(authorization)) {
String[] authorizationStr= StringUtils.split(authorization, SPACE);
if (2 == authorizationStr.length) {
String authType = authorizationStr[0];
String token = authorizationStr[1];
if (authType.equals("Bearer") && !org.apache.commons.lang3.StringUtils.isBlank(token)) {
DecodedJWT decodedJWT = JwtUtil.verifyToken(token);
if (null != decodedJWT) {
// TODO 校验通过获取信息
log.info("token: {}......, 校验通过, 签发时间{}, userId{}", token.substring(0, 32), decodedJWT.getIssuedAt().getTime(), decodedJWT.getClaim("userId"));
return true;
}
} else {
log.error("Token 校验失败, auth prefix={}, token={}", authType, token);
}
} else {
log.error("Token 校验失败, http header 中解析 Authorization 字段错误, authorization={}", authorization);
}
} else {
log.error("Token 校验失败, http header 中没有 Authorization 字段, authorization={}", authorization);
}
try (PrintWriter writer = response.getWriter()) {
writer.print(RestResponse.fail(RestCode.USER_VALIDATE_FAIL_JWT_TOKEN));
} catch (Exception e) {
log.error("登录 JWT Token 校验失败 未知错误 error=", e);
}
return false;
}
}
扩展阅读
OWASP Top Ten 2021 : Related Cheat Sheets
okta What-is-the-lifetime-of-the-JWT-tokens
其它
关于springboot 默认 error path
customize springboot default error path
ErrorMvcAutoConfiguration.java
get-started-with-custom-error-handling-in-spring-boot-java/
spring-boot-custom-error-page
how-to-fix-spring-boot-customize-http-error-response-in-java
howto.actuator.customize-whitelabel-error-page
boot-features-error-handling
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!