文章目录

1. 登录认证1.1 介绍1.2 方式1.3 扩展

2. 实现2.1 项目结构以及前置准备2.2 过滤器实现登录拦截2.3 拦截器实现登录拦截2.4 AOP+自定义注解实现2.5 顺序分析

3. 扩展3.1 ThreadLocal存放登录用户3.2 springMVC的参数解析器

1. 登录认证

1.1 介绍

在现在的前后端项目中,在不使用框架的情况下,登录成功之后,会生产Token发送到前端,每次请求通过cookie或者请求头携带到后台,后台在执行业务代码之前,先校验用户是否登录,根据登录状态获取是否有该接口的权限。这个操作希望是跟业务代码分离的,实现非侵入式的登录拦截和权限控制。

1.2 方式

spring提供下面三种方式实现非侵入式的登录和权限校验,下面一一说明

Java Web中提供的FilterSpringMvc中提供的拦截器InterceptorSpring提供的AOP技术+自定义注解

1.3 扩展

在使用上述三种方式实现登录登录拦截之后,为登录会直接响应JSON的错误数据。但是如果在方法中要使用到登录用户存储的登录信息,那么就得重新获取了。推荐两种比较简单的方式

在拦截器中判断登录状态之后,存储到线程池对象ThreadLocal对象中。但是如果不是在一个线程中,比较麻烦。使用SpringMvc提供的自定义参数解析器,结合自定义参数注解,完成对标注注解的参数进行自动注入。比较简单,推荐使用

2. 实现

本文对应源码地址: 01-spring-boot-auth-filter · master · csdn / spring-boot-csdn · GitLab (sea-clouds.cn)

pom.xml

org.springframework.boot

spring-boot-dependencies

2.5.2

pom

import

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-configuration-processor

org.springframework.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-starter-data-redis

org.apache.commons

commons-pool2

javax.servlet

javax.servlet-api

org.projectlombok

lombok

org.apache.commons

commons-lang3

cn.hutool

hutool-all

5.6.0

2.1 项目结构以及前置准备

前置实现,登录逻辑,这通过UserController中提供了三个接口,登录,查询用户,测试接口

登录接口登录成功之后,生成token,使用UUID,此处不使用加密算法。把token和登录信息对应关系存入redis,失效时间半个小时。

测试

此处使用PostMan进行接口测试

login登录接口

post /user/login 请求成功,返回token

findAllUser查询接口

get /user 返回用户列表

2.2 过滤器实现登录拦截

LoginFilter登录过滤器

public class LoginFilter implements Filter {

private final RedisTemplate redisTemplate;

private final LoginProperties loginProperties;

public LoginFilter(RedisTemplate redisTemplate, LoginProperties loginProperties) {

this.redisTemplate = redisTemplate;

this.loginProperties = loginProperties;

}

@Override

public void init(FilterConfig filterConfig) {

}

@Override

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

HttpServletRequest httpServletRequest = (HttpServletRequest) request;

// 过滤路径

String requestURI = httpServletRequest.getRequestURI();

if (!loginProperties.getFilterExcludeUrl().contains(requestURI)) {

// 获取token

String token = httpServletRequest.getHeader(Constant.TOKEN_HEADER_NAME);

if (StringUtils.isBlank(token)) {

returnNoLogin(response);

return;

}

// 从redis中拿token对应user

User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);

if (user == null) {

returnNoLogin(response);

return;

}

// token续期

redisTemplate.expire(Constant.REDIS_USER_PREFIX + token, 30, TimeUnit.MINUTES);

}

chain.doFilter(request, response);

}

/**

* 返回未登录的错误信息

* @param response ServletResponse

*/

private void returnNoLogin(ServletResponse response) throws IOException {

HttpServletResponse httpServletResponse = (HttpServletResponse) response;

ServletOutputStream outputStream = httpServletResponse.getOutputStream();

// 设置返回401 和响应编码

httpServletResponse.setStatus(401);

httpServletResponse.setContentType("Application/json;charset=utf-8");

// 构造返回响应体

Result result = Result.builder()

.code(HttpStatus.UNAUTHORIZED.value())

.errorMsg("未登陆,请先登陆")

.build();

String resultString = JSONUtil.toJsonStr(result);

outputStream.write(resultString.getBytes(StandardCharsets.UTF_8));

}

@Override

public void destroy() {

}

}

WebMvcConfig配置拦截器

@Configuration

public class WebMvcConfig implements WebMvcConfigurer {

@Resource

private LoginProperties loginProperties;

@Resource

private RedisTemplate redisTemplate;

/**

* 添加登录过滤器

*/

@Bean

public FilterRegistrationBean loginFilterRegistration() {

// 注册LoginFilter

FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();

registrationBean.setFilter(new LoginFilter(redisTemplate, loginProperties));

// 设置名称

registrationBean.setName("loginFilter");

// 设置拦截路径

registrationBean.addUrlPatterns(loginProperties.getFilterIncludeUrl().toArray(new String[0]));

// 指定顺序,数字越小越靠前

registrationBean.setOrder(-1);

return registrationBean;

}

}

测试

未登录访问查询接口,会报错401

登录之后正常访问

2.3 拦截器实现登录拦截

LoginInterception登录拦截器

@Component

public class LoginInterception implements HandlerInterceptor {

@Resource

private RedisTemplate 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 result = Result.builder()

.code(HttpStatus.UNAUTHORIZED.value())

.errorMsg("未登陆,请先登陆")

.build();

String resultString = JSONUtil.toJsonStr(result);

outputStream.write(resultString.getBytes(StandardCharsets.UTF_8));

}

}

WebMvcConfig配置拦截器

@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());

}

}

测试

未登录访问接口,正常拦截

登录访问接口,正常通行

2.4 AOP+自定义注解实现

LoginValidator自定义注解

/**

* @description 登录校验注解,用户aop校验

* @author HLH

* @email 17703595860@163.com

* @date Created in 2021/8/1 下午9:35

*/

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface LoginValidator {

boolean validated() default true;

}

LoginAspect登录AOP类

@Component

@Aspect

public class LoginAspect {

@Resource

private RedisTemplate redisTemplate;

/**

* 切点,方法上有注解或者类上有注解

* 拦截类或者是方法上标注注解的方法

*/

@Pointcut(value = "@annotation(xyz.hlh.annotition.LoginValidator) || @within(xyz.hlh.annotition.LoginValidator)")

public void pointCut() {}

@Around("pointCut()")

public Object before(ProceedingJoinPoint joinpoint) throws Throwable {

// 获取方法方法上的LoginValidator注解

MethodSignature methodSignature = (MethodSignature)joinpoint.getSignature();

Method method = methodSignature.getMethod();

LoginValidator loginValidator = method.getAnnotation(LoginValidator.class);

// 如果有,并且值为false,则不校验

if (loginValidator != null && !loginValidator.validated()) {

return joinpoint.proceed(joinpoint.getArgs());

}

// 正常校验 获取request和response

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

if (requestAttributes == null || requestAttributes.getResponse() == null) {

// 如果不是从前段过来的,没有request,则直接放行

return joinpoint.proceed(joinpoint.getArgs());

}

HttpServletRequest request = requestAttributes.getRequest();

HttpServletResponse response = requestAttributes.getResponse();

// 获取token

String token = request.getHeader(Constant.TOKEN_HEADER_NAME);

if (StringUtils.isBlank(token)) {

returnNoLogin(response);

return null;

}

// 从redis中拿token对应user

User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);

if (user == null) {

returnNoLogin(response);

return null;

}

// token续期

redisTemplate.expire(Constant.REDIS_USER_PREFIX + token, 30, TimeUnit.MINUTES);

// 放行

return joinpoint.proceed(joinpoint.getArgs());

}

/**

* 返回未登录的错误信息

* @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 result = Result.builder()

.code(HttpStatus.UNAUTHORIZED.value())

.errorMsg("未登陆,请先登陆")

.build();

String resultString = JSONUtil.toJsonStr(result);

outputStream.write(resultString.getBytes(StandardCharsets.UTF_8));

}

}

Controller标注注解

测试

未登录访问接口,正常拦截

登录访问接口,正常通行

2.5 顺序分析

如果Filter Interceptor AOP都有的话,顺序如下

FilterInterceptorAOP

3. 扩展

3.1 ThreadLocal存放登录用户

LoginUserThread线程对象

public class LoginUserThread {

/** 线程池变量 */

private static final ThreadLocal LOGIN_USER = new ThreadLocal<>();

private LoginUserThread() {}

public static User get() {

return LOGIN_USER.get();

}

public void put(User user) {

LOGIN_USER.set(user);

}

public void remove() {

LOGIN_USER.remove();

}

}

LoginInterceptor改造在前置方法中放入线程对象,在after中清空前置对象

@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;

}

// 存放如ThreadLocal

LoginUserThread.put(user);

// 放行

return true;

}

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

// 存放如ThreadLocal

LoginUserThread.remove();

}

测试

方法修改如下

@GetMapping

public ResponseEntity findAllUser() {

System.out.println(LoginUserThread.get());

return success(PRE_USER_LIST);

}

访问,查看控制台打印结果

3.2 springMVC的参数解析器

LoginUser自定义注解

/**

* @description 登录参数注解,通过spring参数解析器解析

* @author HLH

* @email 17703595860@163.com

* @date Created in 2021/8/1 下午9:35

*/

@Target(ElementType.PARAMETER) // 作用于参数

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface LoginUser {

}

LoginUserResolver参数解析器

/**

* @description 登录参数注入,通过spring参数解析器解析

* @author HLH

* @email 17703595860@163.com

* @date Created in 2021/8/1 下午9:35

*/

@Component

public class LoginUserResolver implements HandlerMethodArgumentResolver {

@Resource

private RedisTemplate redisTemplate;

/**

* 是否进行拦截

* @param parameter 参数对象

* @return true,拦截。false,不拦截

*/

@Override

public boolean supportsParameter(MethodParameter parameter) {

return parameter.hasParameterAnnotation(LoginUser.class);

}

/**

* 拦截之后执行的方法

*/

@Override

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

// 从request中获取token,此处只做参数解析,不做登录校验

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

if (requestAttributes == null) {

return null;

}

HttpServletRequest request = requestAttributes.getRequest();

// 获取token

String token = request.getHeader(Constant.TOKEN_HEADER_NAME);

if (StringUtils.isBlank(token)) {

return null;

}

// 从redis中拿token对应user

return (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);

}

}

WebMvcConfig添加参数解析器

@Resource

private LoginUserResolver loginUserResolver;

@Override

public void addArgumentResolvers(List resolvers) {

resolvers.add(loginUserResolver);

}

测试

controller方法改造

@GetMapping("/test")

public String test(@LoginUser User user) {

System.out.println(user);

return "测试编码";

}

访问查看控制台结果