概要
记录一下项目里用到的拦截器
拦截器应用
1.在访问需要token的页面时,在拦截器会对token做校验
2.做限流(10s只能生成两次短链,一分钟内验证码只能发送一次)
拦截器
实现WebMvcConfigurer接口
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
@Autowired
private TraceIdInterceptor traceIdInterceptor;
@Autowired
private TenantAuthenticeInterceptor tenantAuthenticeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置需要拦截上下文列表(除了配置的这些,剩下的都不拦)
List<String> tenantIncludePaths = new ArrayList<>();
tenantIncludePaths.add("/tenant/url/**"); // 租户url短链管理
tenantIncludePaths.add("/tenant/log/**"); // 租户短链日志管理
tenantIncludePaths.add("/tenant/dashboard/**"); // 租户仪表盘
tenantIncludePaths.add("/tenant/statistic/**"); // 租户数据统计管理
// 注册租户会话拦截器
registry.addInterceptor(tenantAuthenticeInterceptor)
.addPathPatterns(tenantIncludePaths);
// 注册限流拦截器
registry.addInterceptor(accessLimitInterceptor)
.addPathPatterns("/**");
}
登录
登录成功后,将token存到redis和cookie
// 数据写入redis,登录成功
String token = UUID.randomUUID().toString().replaceAll("-", "");
redisTemplate.opsForValue().set(SystemConst.SYSTEM_TENANT_KEY + ":" + token, JsonUtils.writeValueAsString(tenantInfo), 1800, TimeUnit.SECONDS);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put(SystemConst.SYSTEM_TENANT_TOKEN, token);
// 将token放在cookie中
CookieUtils.setCookie(response, SystemConst.SYSTEM_TENANT_TOKEN, token);
拦截器先判断请求方式,再判断token是否为空-》是否存在-》value是否合法(key【token】-value【用户信息】),合法刷新缓存时长,不合法跳回登录页
@Component
public class TenantAuthenticeInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
/*
* 进入controller层之前拦截请求
* 返回值:表示是否将当前的请求拦截下来 false:拦截请求,请求别终止。true:请求不被拦截,继续执行
* Object obj:表示被拦的请求的目标对象(controller中方法)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 判断请求类型,如果是OPTIONS,直接返回
String options = HttpMethod.OPTIONS.toString();
if (options.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
// 先判断token是否为空
String token = TenantAuthenticeUtil.getToken(request);
if (logger.isInfoEnabled()) {
logger.info("TenantAuthenticeInterceptor -- preHandle -- token = {}", token);
}
if (StringUtils.isBlank(token)) {
responseError(request, response);
return false;
}
// 再判断token是否存在
String tenantInfoString = redisTemplate.opsForValue().get(SystemConst.SYSTEM_TENANT_KEY + ":" + token);
//判断获取到的字符串是否为空或者字符串为""
if (StringUtils.isBlank(tenantInfoString)) {
responseError(request, response);
return false;
}
// 再判断token是否合法
//将上述获取到的字符串转成Map的格式
Map<String, Object> tenantInfo = (Map<String,Object>) JsonUtils.readValue(tenantInfoString, Map.class);
if (logger.isInfoEnabled()) {
logger.info("TenantAuthenticeInterceptor -- preHandle -- tenantInfo = {}", tenantInfo);
}
//判断Map是否是空或者""
if (tenantInfo == null || tenantInfo.isEmpty()) {
responseError(request, response);
return false;
}
//可以直接将字符串转成Map格式而不用先判断字符串是否为空,但是先判断字符串是否空的代价更小
// 刷新会话缓存时长
redisTemplate.expire(SystemConst.SYSTEM_TENANT_KEY + ":" + token, 1800, TimeUnit.SECONDS);
TenantHolder.setTenant(tenantInfo);
// 合格不需要拦截,放行
return true;
}
/*
* 处理请求完成后视图渲染之前的处理操作
* 通过ModelAndView参数改变显示的视图,或发往视图的方法
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
//logger.info("TenantAuthenticeInterceptor -- postHandle -- 执行了");
}
/*
* 视图渲染之后的操作
*/
@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception {
//logger.info("TenantAuthenticeInterceptor -- afterCompletion -- 执行了");
TenantHolder.clearTenant();
}
/**
* 无权限时的返回
*
* @param request
* @param response
* @throws IOException
*/
private void responseError(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 拦截后跳转至登录页
response.sendRedirect("/tenant/login");
}
}
1.options请求我理解的是出现了跨域,需要先确认是否支持跨域(即发送options请求),确认支持以后再发送正式请求(GET、POST...)
2.在判断token是否为空之后,会到redis里面判断token是否存在,存在后是先判断获取到的字符串是否为空,再对字符串转Map格式进一步判断Map里面是否为空,而不是直接转成Map格式再判断Map里面是否为空。先做前置判断的话代价要更小
限流
这里使用了注解做限流标注
/**
* @description seconds 秒内只能执行 maxCount 次
* 自定义注解包含的常见的三个注解
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimit {
/**
* 限制周期(秒)
*/
int seconds();
/**
* 规定周期内最大次数
*/
int maxCount();
/**
* 触发限制时的消息提示
*/
String msg() default "操作频率过高,请稍后再试!";
}
自定义注解的使用
限流拦截器
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
// 判断请求类型,如果是OPTIONS,直接返回(OPTIONS是预检,判断是否支持跨域,支持才会正式发送请求)
String options = HttpMethod.OPTIONS.toString();
if (options.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
// 接口上没有注解,说明这个接口不做限制
if (accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
String ip = IpAddressUtils.getIpAddress(request);
String method = request.getMethod();
String requestURI = request.getRequestURI().replace("/", "");
//这里URI可以用URL代替(这里需要加的原因是这个注解好几个地方用到了(门户生成短链和邮箱验证码生成),都是同一个ip和post,但又不能相互影响,所以加URI)
String redisKey = ip + ":" + method + ":" + requestURI;
Object redisResult = redisTemplate.opsForValue().get(redisKey);
// 获取当前访问次数
Integer count = JsonUtils.convertValue(redisResult, Integer.class);
if (count == null) {
// 在规定周期内第一次访问,存入redis,次数+1
redisTemplate.opsForValue().increment(redisKey, 1);
redisTemplate.expire(redisKey, seconds, TimeUnit.SECONDS);
} else {
if (count >= maxCount) {
// 超出访问限制次数
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Result result = Result.create(403, accessLimit.msg());
out.write(JsonUtils.writeValueAsString(result));
out.flush();
out.close();
return false;
} else {
// 没超出访问限制次数,则继续次数+1
redisTemplate.opsForValue().increment(redisKey, 1);
}
}
}
return true;
}
}
小结
拦截器我觉得是AOP的一种应用,在请求调用前后会经过拦截器
拦截器的处理顺序是按照注册的先后顺序来
preHandle():处理方法前
postHandle():处理方法后,视图渲染前
afterCompletion():视图渲染后