目录
第二小节,我们结合实战,看看在拦截器中如何校验jwt,又如何将jwt的载荷(payload)传递到controller控制器。
以下内容,我们用支付宝小程序场景来进行实战说明。
一、实战场景
支付宝小程序用户认证登录时序图
在支付宝小程序内,得到用户授权的authCode,经过支付宝授权平台认证通过后,由后端服务器生成对应用户的jwt,并返回给前端小程序,最终将其缓存起来,用于后续业务接口的调用。
对于能解密的密文,我们尽量的少暴露用户的信息,所以我们只会在jwt中保存用户的user_id和小程序的app_id。
//密钥secret
String JWT_USER_AUTH_SECRET = "qwerasdf123";
JWTCreator.Builder builder = JWT.create();
//添加claim
builder.withClaim("app_id", "20220606162783612");
builder.withClaim("open_id", "123456789");
//可根据实际情况,添加字段信息,数据以key-value方式保存
//设置JWT令牌的过期时间为一天
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 1 * 86400);
builder.withExpiresAt(instance.getTime());
//生成JWT
String token = builder.sign(Algorithm.HMAC256(JWT_USER_AUTH_SECRET));
System.out.println(token);
二、实战处理
一般用户鉴权可以写在过滤器或者是拦截器,从执行顺序来看,过滤器要比拦截器更靠前。
这两者的区别在于:
- Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
- Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理的方式来执行。
- Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便。
创建用户鉴权拦截器
自定义的拦截器实现HandlerInterceptor,并重写preHandle方法,对请求进行拦截,校验请求附带的jwt是否合法。
public class AuthenticationInterceptor implements HandlerInterceptor {
/***
* 在请求处理之前进行调用(Controller方法调用之前)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//如果不是映射到方法直接通过
if(!(handler instanceof HandlerMethod)) {
return true;
}
//判断是否为通行注解,这里通过注解的方式,能灵活放行一些不需要鉴权的方法
HandlerMethod handlerMethod = (HandlerMethod) handler;
//这个注解是加在方法上,所以从方法上获取这个注解
Method method = handlerMethod.getMethod();
if(method.isAnnotationPresent(PassAuthToken.class)) {
PassAuthToken passAuthToken = method.getAnnotation(PassAuthToken.class);
if (passAuthToken.required()) {
return true;
}
}
//从头部获取授权信息
String authorization = request.getHeader("Authorization");
if (StrUtil.isEmpty(authorization)) {//如果授权信息为空,则抛出异常,中断请求链
//抛出自定义异常后,由统一异常处理类返回对应错误信息
throw new AuthException("授权信息为空");
}
//此时的授权信息为'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMTIzNDU2IiwidXNlcl9uYW1lIjoi5byg5LiJIiwiZXhwIjoxNjU0NTI2MDU5fQ.MCuHhUOoRkMeabCk2SlfubDAL7ZgOopn7nti5QDaEhY'
String[] tokens = authorization.split(" ");
if (null == tokens || tokens.length != 2 || StrUtil.isBlank(tokens[1])) {
throw new AuthException("未找到授权信息");
}
//后面的部分才是我们生成的jwt
String token = tokens[1];
//获取jwt里面的用户信息
JwtUser jwtUser = this.getJwtUser(token);
//将用户信息通过request传递到下一层,然后通过解析器将用户信息加到方法参数上
request.setAttribute("CurrentUser", jwtUser);
//返回true,则代表放行
return true;
}
/**
* 获取jwt的用户信息
*
* @param token
* @return
*/
private JwtUser getJwtUser(String token) throws AuthException {
DecodedJWT jwt = this.getDecodedJWT(PublicConstants.JWT_USER_AUTH_SECRET, token);
if (ObjectUtil.isNotEmpty(jwt)) {
JSONObject userJson = this.convertClaimToJson(jwt);
if (ObjectUtil.isNotEmpty(userJson)) {
//jwt的用户信息bean
JwtUser jwtUser = new JwtUser();
//封装平台app_id
String appId = userJson.getString("app_id");
jwtUser.setAppId(appId);
//封装用户open_id
String openId = userJson.getString("open_id");
jwtUser.setOpenId(openId);
//这里可以通过openId查询数据库,封装一些其他的用户信息,让它带到后面的控制器参数上
return jwtUser;
}
}
return null;
}
/**
* 获取解密jwt信息
*
* @param secret
* @param token
* @return
*/
private DecodedJWT getDecodedJWT(String secret, String token) throws AuthException {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).build();
try {
//先使用常规接口的秘钥
DecodedJWT jwt = verifier.verify(token);
return jwt;
} catch (Exception e) {
throw new AuthException("授权信息错误");
}
}
}
要将jwt拿到的用户信息传递到controller控制器,这一行是重点。
request.setAttribute("CurrentUser", jwtUser);
注册拦截器
集成WebMvcConfigurationSupport,重写addInterceptors方法,加入我们自定义的拦截器。需要注意的是自定义的拦截器需要使用bean方式声明,这样才能够在拦截器中注入其他的bean。
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
//这里以bean的方式声明拦截器,那么在拦截器中才可以注入其他的bean,例如注入用户service,用于查询用户表信息等
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
//这里可以定义拦截器的路径
registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/api/**");
}
}
参数解析器
自定义的参数解析器,实现HandlerMethodArgumentResolver,重写supportsParameter方法,定义支持的参数;重写resolveArgument方法,分解实参。
public class JwtUserArgumentResolver implements HandlerMethodArgumentResolver {
//定义这个解析器要处理什么,当方法返回true,才会执行下面的resolveArgument方法
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Class<?> clazz = methodParameter.getParameterType();
return clazz == JwtUser.class;
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) {
//获取当前请求对象
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
//从拦截器传递的请求链中获取用户信息
JwtUser jwtUser = (JwtUser) request.getAttribute("CurrentUser");
return jwtUser;
}
}
这里有个地方需要注意,就是controller控制器的JwtUser参数必须要放在第一位,否则无法注入成功。
添加解析器
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
//这里以bean的方式声明拦截器,那么在拦截器中才可以注入其他的bean,例如注入用户service,用于查询用户表信息等
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
//添加参数解析器
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new JwtUserArgumentResolver());
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
//这里可以定义拦截器的路径
registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/api/**");
}
}
控制器
在控制器中,在方法上直接填入JwtUser,从参数解析器里面就可以实现自动注入jwt的信息。
@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
UserService userService;
@GetMapping("/info")
public Result userPlayTimes(JwtUser jwtUser) {
String openId = jwtUser.getOpenId();
UserInfo userInfo = userService.getByOpenId(openId);
return ResultUtil.success(userInfo);
}
}