什么是分布式锁

前言

在系统中,我们经常需要在处理用户请求之前和之后执行一些行为,例如检测用户的权限,或者将请求的信息记录到日志中。当然不仅仅这些,所以需要一种机制,拦截用户的请求,在请求的前后添加处理逻辑。

本文就以登录拦截为主,介绍三种方式实现登录拦截。

方案一(过滤器)

过滤器(Filter)是对数据进行过滤,预处理。开发人员可以对客户端提交的数据进行过滤处理,比如敏感词,也可以对服务端返回的数据进行处理。还有就是可以验证用户的登录情况,权限验证,对静态资源进行访问控制,没有登录或者是没有权限时是不能让用户直接访问这些资源的。类似的过滤器还有很多的功能,比如说编码,压缩服务端给客户端返回的各种数据等等。

使用过滤器实现登录拦截要先实现一个过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class MyFilter implements Filter {

private static final String CURRENT_USER = "current_user";

//配置白名单
protected static List<Pattern> patterns = new ArrayList<Pattern>();

//静态代码块,在虚拟机加载类的时候就会加载执行,而且只执行一次
static {
patterns.add(Pattern.compile("/index"));
patterns.add(Pattern.compile("/login"));
patterns.add(Pattern.compile("/register"));
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(
ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(httpResponse);

String url = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
if (isInclude(url)) {
//在白名单中的url,放行访问
filterChain.doFilter(httpRequest, httpResponse);
return;
}
if (SessionUtils.getSessionAttribute(CURRENT_USER) != null) {
//若为登录状态 放行访问
filterChain.doFilter(httpRequest, httpResponse);
return;
} else {
//否则默认访问index接口
wrapper.sendRedirect("/index");
}

}

@Override
public void destroy() {

}

//判断当前请求是否在白名单
private boolean isInclude(String url) {
for (Pattern pattern : patterns) {
Matcher matcher = pattern.matcher(url);
if (matcher.matches()) {
return true;
}
}
return false;
}
}

然后注册过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
public class WebConfig {
/**
* 配置过滤器
* @return
*/
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(myFilter());
//拦截/*的访问 多级匹配(springboot 过滤器/*以及匹配 /**多级匹配)
registration.addUrlPatterns("/*");
registration.setName("myFilter");
return registration;
}

/**
* 创建一个bean
* @return
*/
@Bean(name = "myFilter")
public Filter myFilter() {
return new MyFilter();
}
}

方案二(拦截器)

拦截器(Interceptor)是SpringMVC提供的一种拦截机制,基于AOP动态代理实现,用于请求的预处理和后处理。在SpringMVC中定义一个拦截器有两种方法:第一种是实现HandlerInterceptor接口,或者继承实现了HandlerInterceptor接口的类(例如:HandlerInterceptorAdapter);第二种方法时实现Spring的WebRequestInterceptor接口,或者继承实现了WebRequestInterceptor接口的类。这些拦截器都是在Handler的执行周期内进行拦截操作的。下面主要介绍第一种方法。

首先我们要实现HandlerInterceptor接口定义登录校验的方法。在实现这个接口之前我们要先看看这个接口的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {
default boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return true;
}

default void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}

default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex) throws Exception {
}
}

可以看到HandlerInterceptor接口中有三个要实现的方法,分别是preHandlepostHandleafterCompletion

  • preHandle():这个方法将在请求处理之前进行调用。注意:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。

  • postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 有意思的是:postHandle()方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle()方法先执行,而postHandle()方法反而会后执行。

  • afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。

而我们如果要实现登录拦截,就需要重写preHandle()方法,然后在其中实现登录拦截的逻辑代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Component
public class LoginInterception implements HandlerInterceptor {

@Resource
private RedisTemplate<String, Object> redisTemplate;

@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 获取token
String token = request.getHeader(Constant.TOKEN_HEADER_NAME);
if (StringUtils.isBlank(token)) {
returnNoLogin(response);
return false;
}
// 从redis中拿token对应user
User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);
if (user == null) {
returnNoLogin(response);
return false;
}
// token续期
redisTemplate.expire(Constant.REDIS_USER_PREFIX + token, 30, TimeUnit.MINUTES);
// 放行
return true;
}

/**
* 返回未登录的错误信息
* @param response ServletResponse
*/
private void returnNoLogin(HttpServletResponse response) throws IOException {
ServletOutputStream outputStream = response.getOutputStream();
// 设置返回401 和响应编码
response.setStatus(401);
response.setContentType("Application/json;charset=utf-8");
// 构造返回响应体
Result<String> result = Result.<String>builder()
.code(HttpStatus.UNAUTHORIZED.value())
.errorMsg("未登陆,请先登陆")
.build();
String resultString = JSONUtil.toJsonStr(result);
outputStream.write(resultString.getBytes(StandardCharsets.UTF_8));
}

}

然后配置WebMvcConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Resource
private LoginProperties loginProperties;
@Resource
private LoginInterception loginInterception;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterception)
.addPathPatterns(loginProperties.getInterceptorIncludeUrl())
.excludePathPatterns(loginProperties.getInterceptorExcludeUrl());
}

}

方案三(AOP+自定义注解)

通过自定义注解和AOP也可以实现登录拦截的功能

首先自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 全局拦截器注解
*/

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface GlobalInterceptor {

/**
* 校验登录
*/
boolean checkLogin() default true;

/**
* 校验管理员
*/
boolean checkAdmin() default false;
}

然后编写自定义注解的切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 全局操作切面类
* 功能:实现登录拦截
*/
@Component("globalOperationAspect")
@Aspect //说明当前对象是一个切面
public class GlobalOperationAspect {

@Resource
private RedisUtils redisUtils;

private static final Logger logger = LoggerFactory.getLogger(GlobalOperationAspect.class);

//使用@Before注解定义了一个前置通知,这意味着在任何标有@GlobalInterceptor注解的方法执行前,都会先执行interceptorDo方法。
@Before("@annotation(com.easychat.annotation.GlobalInterceptor)")
public void interceptorDo(JoinPoint joinPoint) { //通过JoinPoint获取当前连接点的信息,进一步通过MethodSignature获取到目标方法。
try {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取目标方法上的GlobalInterceptor注解实例,如果不存在该注解,则直接返回,不进行后续的拦截逻辑。
GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
if (null == interceptor) { //没有@GlobalInterceptor注解,放行
return;
}
//校验登录 根据GlobalInterceptor注解的checkLogin和checkAdmin属性决定是否需要执行登录校验。
if (interceptor.checkLogin() || interceptor.checkAdmin()) {
checkLogin(interceptor.checkAdmin());
}
} catch (BusinessException e) {
logger.error("全局拦截器异常", e);
throw e;
} catch (Throwable e) {
logger.error("全局拦截器异常", e);
throw new BusinessException(ResponseCodeEnum.CODE_500);
}
}

//身份认证
private void checkLogin(Boolean checkAdmin) {
//通过RequestContextHolder.getRequestAttributes()获取当前线程的请求属性,然后强制转换为ServletRequestAttributes,
//进而得到HttpServletRequest request对象。这一步骤是为了从请求中获取HTTP头部信息。
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String token = request.getHeader("token");
TokenUserInfoDto tokenUserInfoDto = (TokenUserInfoDto) redisUtils.get(Constants.REDIS_KEY_WS_TOKEN + token);
if (tokenUserInfoDto == null) { //通过token没找到对应的tokenUserInfoDto
throw new BusinessException(ResponseCodeEnum.CODE_901);
}
if (checkAdmin && !tokenUserInfoDto.getAdmin()) {
throw new BusinessException(ResponseCodeEnum.CODE_404);
}
}
}

最后在需要登录拦截的Controller方法上使用@GlobalInterceptor注解即可实现登录拦截。

总结

以上就是本文关节登录拦截的三种实现方式,而这三种方式的执行顺序是 Filter > Interceptor > AOP 。而且拦截器和过滤器的区别可参见面试突击90:过滤器和拦截器有什么区别?-腾讯云开发者社区-腾讯云 (tencent.com)

Reference:

java过滤器实现登录拦截处理_java 服务端屏蔽某个接口-CSDN博客

SpringBoot自定义注解—AOP方式和拦截器方式实现_自定义注解aop拦截-CSDN博客

Springboot实现登录拦截的三种方式_springboot登录拦截-CSDN博客