由于是第一次接触拦截器,所以对拦截器中的配置也不是很懂,导致在这个过程中踩了很多坑,最让我印象深刻的是,在配置拦截器的时候,由于我在项目中使用到了请求前缀,而我的拦截器又是从其他博主的博客中直接拿来用的,导致只要我一打开@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 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");
}
}
创建拦截器,在拦截器中添加放行路径以及登录校验
public class LoginInterceptor implements HandlerInterceptor {
/**
* 请求头
*/
private static final String HEADER_AUTH = "Authorization";
/**
* 安全的url,不需要令牌
*/
private static final List SAFE_URL_LIST = Arrays.asList("/wx/user/login", "/wx/user/register");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
response.setContentType("application/json; charset=utf-8");
String url = request.getRequestURI().substring(request.getContextPath().length());
System.out.println(url);
// 登录和注册等请求不需要令牌
if (SAFE_URL_LIST.contains(url)) {
return true;
}
// 从请求头里面读取token
String token = request.getHeader(HEADER_AUTH);
if (token == null) {
throw new RuntimeException("请求失败,令牌为空");
}
// 解析令牌
Map map = JwtUtil.resolveToken(token);
Long userId = Long.parseLong(map.get("userId").toString());
ContextHolder.setUserId(userId);
return true;
}
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
public LoginInterceptor loginInterceptor(){
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册TestInterceptor拦截器
registry.addInterceptor(loginInterceptor()); //添加需要拦截的路径(由于设置了请求前缀,所以必须将请求前缀加上)
}
}
针对第二种方式封装JWT工具
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 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 resolveToken(String token) {
System.out.println(token);
if (token == null) {
throw new RuntimeException("令牌为空");
}
try {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replaceFirst(TOKEN_PREFIX + "_", ""))
.getBody();
} catch (ExpiredJwtException e) {
throw new RuntimeException("令牌已过期");
} catch (Exception e) {
throw new RuntimeException("令牌解析异常");
}
}
}
public abstract class ContextHolder {
public static ThreadLocal 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();
}
}