springboot的拦截器无效的问题
springboot+jwt+token
1.扫描包的问题
导致拦截器失效的原因可能是没有扫描到配置拦截器的包
查看spring启动类的,添加@ComponentScan(basePackages = {"com.example.demo.config"})或@SpringBootApplication(scanBasePackages = {"com.example.demo.config"})
2.springboot版本问题
springboot2版本以上不支持WebMvcConfigurerAdapter,pom文件中可查看自己的版本。
将拦截器配置到Spring Boot环境中,有两种方法
// 方法一:实现WebMvcConfigurer接口
public class WebConfig implements WebMvcConfigurer{
// ...
}
// 方法二:继承WebMvcConfigurationSupport类
public class WebConfig extends WebMvcConfigurationSupport{
// ...
}
这两种方式有冲突,如果其他有可能的原因都排查了,可以看看是不是冲突问题,我的就是因为我写了一个实现方式的,结果有人在swagger里面写了一个继承的,排查了好久才看到,两边需要统一。
WebMvcConfigurer是自动配置,而WebMvcConfigurationSupport 是手动配置,当classpath中存在WebMvcConfigurationSupport 对象时,自动配置就不会生效,所以需要统一。
3.路径问题
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor).addPathPatterns("/**");
.excludePathPatterns("/","/user/login");
}
addPathPatterns()里面的路径是"/**",不是"/*"。也可以把全局路径加上,要注意自己的项目的路径,看看配置文件是否有。/**表示当前目录以及所有子目录,/*表示当前目录,不包括子目录。
excludePathPatterns()里面是不需要拦截的路径,是"/"。
4.注解问题
配置拦截器的类上要加@Configuration注解,这样才能统一管理拦截器实例。
拦截器的类上要加@Component注解,这样当前拦截器才能被扫描到。
5.简单的示例
- 引入依赖
<!--引入JWT依赖->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2.创建自定义注解,拦截器就可以通过注解来区分是否需要拦截,需要拦截的方法条件注解即可
/**
* require token.
*
* @author Mei
* @since 2020/8/20
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginToken {
boolean required() default true;
}
/**
* skip token.
*
* @author Mei
* @since 2020/8/20
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SkipToken {
boolean required() default true;
}
3.编写JwtUtil工具类
/**
* JWT工具类.
*
* @author Mei
* @since 2020/8/20
*/
@Slf4j
public class JwtUtil {
/**
* 密钥,不泄露,不更改
*/
private static final String SECRET_KEY = "设置自己的密钥";
/**
* token过期时间(单位:豪秒)
*/
public static final long TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000;
/**
* 签发人
*/
private static final String ISSUER = "issuer";
/**
* token生成器
*
* @param userId 用户Id
* @param role 角色
* @return token
*/
private static String generateToken(final long userId,
final Integer role) {
Date now = new Date();
// 算法
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
String token =
JWT.create()
.withIssuer(ISSUER)
.withIssuedAt(now)
.withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRE_TIME))
.withClaim("role", role)
.withClaim("userId", userId)
.sign(algorithm);
log.info("generateToken: 生成的token为 [{}]", token);
return token;
}
/**
* 生成token
*
* @param user 员工信息
* @return 生成后的token
*/
public static String generateToken(User user, int role) {
return generateToken(
user.getId(),
role);
}
/**
* 验证token
*
* @param token token
* @return true/false
*/
public static boolean verify(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
verifier.verify(token);
return true;
} catch (Exception e) {
log.warn("verify:[{}]", e.getMessage());
log.error(e.getMessage(), e);
return false;
}
}
/**
* 解析token(从token获取UserId)
*
* @param token token
* @return ID
*/
public static Long getUserId(String token) {
try {
return JWT.decode(token).getClaim("userId").asLong();
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
}
4.编写拦截器
/**
* 拦截器.
*
* @author Mei
* @since 2020/8/20
*/
@Slf4j
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Resource
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle:拦截 [request:{}, response:{}, handler:{}]", request, response, handler);
// 从 http 请求头中取出 token
String token = request.getHeader("Authorization");
log.info("preHandle:Authorization 为[token:{}]", token);
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//检查是否有SkipToken注释,有则跳过认证
if (method.isAnnotationPresent(SkipToken.class)) {
log.info("跳过认证");
SkipToken loginToken = method.getAnnotation(SkipToken.class);
if (loginToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken checkToken = method.getAnnotation(LoginToken.class);
if (checkToken.required()) {
// 执行认证
if (token == null) {
response.sendRedirect(request.getContextPath() + "/user/login");
return true;
}
// 获取 token 中的 userId
long userId;
try {
userId = JWTUtil.getUserId(token);
} catch (JWTDecodeException j) {
throw new RuntimeException("访问异常!");
}
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
Boolean verify = JWTUtil.verify(token);
if (!verify) {
throw new RuntimeException("非法访问!");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
5.配置拦截器
/**
* 配置拦截器.
*
* @author Mei
* @since 2020/8/21
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private AuthenticationInterceptor authenticationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有请求,通过判断是否有 @SkipToken注解 决定是否需要登录
registry.addInterceptor(authenticationInterceptor).addPathPatterns("/**");
}
}
6.Controller
@PostMapping(path = "/login")
@ApiOperation(value = "登录", notes = "根据用户名和密码登录,返回token")
@SkipToken
public R login(HttpServletRequest request,
@RequestParam(value = "name") String userName,
@RequestParam(value = "pwd") String passWord) {
String msg = "用户不存在";
User user1 = new User();
user1.setName(userName);
User user = userService.selectOne(user1);
if (user != null) {
if (user.getPwd().equals(passWord)) {
msg = "登录成功";
String token = JWTUtil.generateToken(user, 1);
return R.ok(msg).put("user", user).put("Authorization", token);
} else {
msg = "密码错误";
return R.ok(msg);
}
}
return R.error(msg);
}
@GetMapping(path = "/all")
@ApiOperation(value = "查询所有用户")
@LoginToken
public Object all() {
List<Map<String, Object>> list;
list = userService.selectAll();
return R.ok().putAll(list);
}
7.测试
测试登录,返回token
测试查询,带上token,返回结果
不带token、token带错、token过期,都会拿不到数据。
6.代码改进
示例的方式可能太多地方需要添加注解了,这样会比较麻烦,一般来说,除了登录不需要token之外,其他基本都需要token,当然还是根据具体业务来。这样就可以把示例代码的拦截器的内容稍微改一下,拦截器配置类是默认拦截所有请求的("/**"),那就只需要把不需要token的请求添加注解就可以了,这样就不用把所有需要token的请求添加注解了。也可以在拦截器配置类里面添加不需要token的路径,但是如果,不需要token的路径增多,这里也会增多,所以@SkipToken注解可以保留。
拦截器类代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle:拦截 [request:{}, response:{}, handler:{}]", request, response, handler);
// 从 http 请求头中取出 token
String token = request.getHeader("Authorization");
log.info("preHandle:TOKEN 为[token:{}]", token);
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//检查是否有PassToken注释,有则跳过认证
if (method.isAnnotationPresent(SkipToken.class)) {
log.info("跳过认证");
SkipToken loginToken = method.getAnnotation(SkipToken.class);
if (loginToken.required()) {
return true;
}
} else if (token == null) {
response.sendRedirect(request.getContextPath() + "/user/login");
return true;
} else {
// 获取 token 中的 userId
long userId = JwtUtil.getUserId(token);
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
Boolean verify = JwtUtil.verify(token);
if (!verify) {
throw new RuntimeException("非法访问!");
}
return true;
}
return true;
}
拦截器配置类代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有请求,通过判断是否有 @LoginToken注解 决定是否需要登录
registry.addInterceptor(interceptor).addPathPatterns("/**")
.excludePathPatterns("/**/user/login", "/error")
.excludePathPatterns("/**/swagger-ui.html",
"/**/webjars/springfox-swagger-ui/**",
"/**/swagger-resources/**");
}