文章目录
1. 登录认证
1.1 介绍
在现在的前后端项目中,在不使用框架的情况下,登录成功之后,会生产Token发送到前端,每次请求通过cookie或者请求头携带到后台,后台在执行业务代码之前,先校验用户是否登录,根据登录状态获取是否有该接口的权限。这个操作希望是跟业务代码分离的,实现非侵入式的登录拦截和权限控制。
1.2 方式
spring提供下面三种方式实现非侵入式的登录和权限校验,下面一一说明
- Java Web中提供的Filter
- SpringMvc中提供的拦截器Interceptor
- Spring提供的AOP技术+自定义注解
1.3 扩展
在使用上述三种方式实现登录登录拦截之后,为登录会直接响应JSON的错误数据。但是如果在方法中要使用到登录用户存储的登录信息,那么就得重新获取了。推荐两种比较简单的方式
- 在拦截器中判断登录状态之后,存储到线程池对象ThreadLocal对象中。但是如果不是在一个线程中,比较麻烦。
- 使用SpringMvc提供的自定义参数解析器,结合自定义参数注解,完成对标注注解的参数进行自动注入。比较简单,推荐使用
2. 实现
本文对应源码地址: 01-spring-boot-auth-filter · master · csdn / spring-boot-csdn · GitLab (sea-clouds.cn)
pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- 其他工具包 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.0</version>
</dependency>
</dependencies>
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<String, Object> redisTemplate;
private final LoginProperties loginProperties;
public LoginFilter(RedisTemplate<String, Object> 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<String> result = Result.<String>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<String, Object> redisTemplate;
/**
* 添加登录过滤器
*/
@Bean
public FilterRegistrationBean<Filter> loginFilterRegistration() {
// 注册LoginFilter
FilterRegistrationBean<Filter> 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<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配置拦截器
@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<String, Object> 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<String> result = Result.<String>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都有的话,顺序如下
- Filter
- Interceptor
- AOP
3. 扩展
3.1 ThreadLocal存放登录用户
LoginUserThread线程对象
public class LoginUserThread {
/** 线程池变量 */
private static final ThreadLocal<User> 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<String, Object> 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<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserResolver);
}
测试
controller方法改造
@GetMapping("/test")
public String test(@LoginUser User user) {
System.out.println(user);
return "测试编码";
}
访问查看控制台结果
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68319.html