踩过的坑
由于是第一次接触拦截器,所以对拦截器中的配置也不是很懂,导致在这个过程中踩了很多坑,最让我印象深刻的是,在配置拦截器的时候,由于我在项目中使用到了请求前缀,而我的拦截器又是从其他博主的博客中直接拿来用的,导致只要我一打开@Configuration,就会报404错误(尽管放行了登录请求),其原因是在添加拦截请求时,使用的是 /** ,其会让我的请求前缀也会纳入进去,导致一进入登录请求,就会被拦截到,导致请求误拦截。
拦截器实现的方式
其实拦截器总的来说就是添加拦截请求和放行请求,但是在实现上也确实有不同的思路,以下是我总结的两种实现方式(本质上大差不差)
方式一 拦截路径和放行路径设置在一起
实现步骤
编写拦截器
import cn.hutool.jwt.Claims;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.example.demo.config.MyContext;
import com.example.demo.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
//认证拦截器
@Component //添加到容器
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtils jwtUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String login = request.getRequestURI();
if (login.contains("/login")) {
return true;
}
// //获取请求头中的token
final String token;
final String authHeader = request.getHeader("Authorization");
if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith("Bearer ")) {
// 截取token
token = authHeader.substring(7);
} else {
if (request.getHeader("token") != null) {
token = request.getHeader("token");
} else {
token = request.getParameter("token");
}
}
Map<String, Object> mp = new HashMap<>();
try {
if (token == null) {
System.out.println("token空");
}
jwtUtils.verify(token);//验证token
return true; //上一句无异常就 放行
} catch (SignatureVerificationException e) {
// e.printStackTrace();
mp.put("msg", "无效签名");
} catch (TokenExpiredException e) {
e.printStackTrace();
mp.put("msg", "token过期,请重新登录");
} catch (AlgorithmMismatchException e) {
// e.printStackTrace();
mp.put("msg", "算法不匹配");
} catch (NullPointerException e) {
// e.printStackTrace();
mp.put("msg", "token不能为空");
} catch(RuntimeException e){
e.printStackTrace();
mp.put("msg", "token不正确");
} catch (Exception e) {
// e.printStackTrace();
mp.put("msg", "其他异常");
}
mp.put("state", false);
//map转换为json
String json = new ObjectMapper().writeValueAsString(mp);
response.setContentType("application/json; charset=UTF-8");
//返回
response.getWriter().println(json);
return false;
}
}
添加拦截器进配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// 注入之前设置的拦截器(这里方式不止一种,之前听了一个实习生的描述,感觉他只知道Spring的这种注入。。。可别被限制了,这只是创建对象而已)
@Autowired
private LoginInterceptor loginInterceptor;
// 注入拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationFilter)
// 拦截所有请求(这里要十分注意,如果你在yml配置文件中添加了请求前缀,就设置为请求前缀下的路径。例如设置的请求前缀为 /admin ,则需要设置为 /admin/**)
.addPathPatterns("/**")
// 设置放行路径
.addPathPatterns("/workPlan/**")
.excludePathPatterns("/admin/login");
}
}
方式二 放行路径单独(或多个)的JWT设置
编写拦截器
创建拦截器,在拦截器中添加放行路径以及登录校验
import com.xinxun.wxsecondheadmark.util.ContextHolder;
import com.xinxun.wxsecondheadmark.util.JwtUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**
* 请求头
*/
private static final String HEADER_AUTH = "Authorization";
/**
* 安全的url,不需要令牌
*/
private static final List<String> SAFE_URL_LIST = Arrays.asList("/wx/admin/login", "/wx/admin/register");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
response.setContentType("application/json; charset=utf-8");
String prop = request.getRequestURI().substring(request.getContextPath().length());
String url = "/wx"+prop;
// 登录和注册等请求不需要令牌
// todo 由于上述集合中的安全URL是具体的,因为有新的需求,要求将以client开头的前缀给放行,因此使用了字符串的contains方法来模糊匹配
if (prop.contains("/client/")||prop.contains("/order/")||prop.contains("/goods/")||SAFE_URL_LIST.contains(url)) {
return true;
}
System.out.println("***********没有被放行的请求*************");
System.out.println(url);
// 从请求头里面读取token
String token = request.getHeader(HEADER_AUTH);
if (token == null) {
throw new RuntimeException("请求失败,令牌为空");
}
System.out.println(token);
// 解析令牌
Map<String, Object> map = JwtUtil.resolveToken(token);
Long userId = Long.parseLong(map.get("id").toString());
ContextHolder.setUserId(userId);
return true;
}
}
添加拦截器配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册LoginInterceptor拦截器
registry.addInterceptor(loginInterceptor);
}
}
封装的JWT工具
import cn.hutool.core.date.DateUtil;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.Map;
@Slf4j
public class JwtUtil {
/**
* 令牌密码 不少于32位
*/
private static final String SECRET = "token_secret";
/**
* 令牌前缀
*/
private static final String TOKEN_PREFIX = "Bearer";
/**
* 令牌过期时间
*/
private static final Integer EXPIRE_SECONDS = 60 * 60 * 24 * 7;
/**
* 生成令牌
*/
public static String generateToken(Map<String, Object> map) {
String jwt = Jwts.builder()
.setSubject("user info").setClaims(map)
.signWith(SignatureAlgorithm.HS512, SECRET)
.setExpiration(DateUtil.offsetSecond(new Date(), EXPIRE_SECONDS))
.compact();
return TOKEN_PREFIX + "_" + jwt;
}
/**
* 验证令牌
*/
public static Map<String, Object> resolveToken(String token) {
if (token == null) {
throw new RuntimeException("令牌为空");
}
try {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replaceFirst(TOKEN_PREFIX + "_", ""))
.getBody();
} catch (ExpiredJwtException e) {
log.error("JWT过期:", e);
throw new RuntimeException("JWT过期");
} catch (UnsupportedJwtException e) {
log.error("不支持的JWT:", e);
throw new RuntimeException("不支持的JWT");
} catch (MalformedJwtException e) {
log.error("JWT格式错误:", e);
throw new RuntimeException("JWT格式错误");
} catch (SignatureException e) {
log.error("签名异常:", e);
throw new RuntimeException("签名异常");
} catch (IllegalArgumentException e) {
log.error("非法请求:", e);
throw new RuntimeException("非法请求");
} catch (Exception e) {
log.error("解析异常:", e);
throw new RuntimeException("解析异常");
}
}
}
保存用户ID,方便以后食用(线程安全)
public abstract class ContextHolder {
public static ThreadLocal<Long> context = new ThreadLocal<>();
public static void setUserId(Long userId) {
context.set(userId);
}
public static Long getUserId() {
return context.get();
}
public static void shutdown() {
context.remove();
}
}