文章目录

  • 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中提供的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;}@Overridepublic void init(FilterConfig filterConfig) {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) request;// 过滤路径String requestURI = httpServletRequest.getRequestURI();if (!loginProperties.getFilterExcludeUrl().contains(requestURI)) {// 获取tokenString token = httpServletRequest.getHeader(Constant.TOKEN_HEADER_NAME);if (StringUtils.isBlank(token)) {returnNoLogin(response);return;}// 从redis中拿token对应userUser 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));}@Overridepublic void destroy() {}}

WebMvcConfig配置拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Resourceprivate LoginProperties loginProperties;@Resourceprivate RedisTemplate<String, Object> redisTemplate;/*** 添加登录过滤器*/@Beanpublic FilterRegistrationBean<Filter> loginFilterRegistration() {// 注册LoginFilterFilterRegistrationBean<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 {@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取tokenString token = request.getHeader(Constant.TOKEN_HEADER_NAME);if (StringUtils.isBlank(token)) {returnNoLogin(response);return false;}// 从redis中拿token对应userUser 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 {@Resourceprivate LoginProperties loginProperties;@Resourceprivate LoginInterception loginInterception;@Overridepublic 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 {@Resourceprivate 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和responseServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes == null || requestAttributes.getResponse() == null) {// 如果不是从前段过来的,没有request,则直接放行return joinpoint.proceed(joinpoint.getArgs());}HttpServletRequest request = requestAttributes.getRequest();HttpServletResponse response = requestAttributes.getResponse();// 获取tokenString token = request.getHeader(Constant.TOKEN_HEADER_NAME);if (StringUtils.isBlank(token)) {returnNoLogin(response);return null;}// 从redis中拿token对应userUser 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 {// 获取tokenString token = request.getHeader(Constant.TOKEN_HEADER_NAME);if (StringUtils.isBlank(token)) {returnNoLogin(response);return false;}// 从redis中拿token对应userUser user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);if (user == null) {returnNoLogin(response);return false;}// 存放如ThreadLocalLoginUserThread.put(user);// 放行return true;
}@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 存放如ThreadLocalLoginUserThread.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 {@Resourceprivate RedisTemplate<String, Object> redisTemplate;/*** 是否进行拦截* @param parameter 参数对象* @return true,拦截。false,不拦截*/@Overridepublic boolean supportsParameter(MethodParameter parameter) {return parameter.hasParameterAnnotation(LoginUser.class);}/*** 拦截之后执行的方法*/@Overridepublic 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();// 获取tokenString token = request.getHeader(Constant.TOKEN_HEADER_NAME);if (StringUtils.isBlank(token)) {return null;}// 从redis中拿token对应userreturn (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 "测试编码";
}

访问查看控制台结果

Springboot实现登录拦截的三种方式相关推荐

  1. SpringBoot静态获取 bean的三种方式,你学会了吗?

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:blog.csdn.net/showchi/article/details/97005720 注意:调用者要被spring管理 ...

  2. springboot+mybatis实现数据分页(三种方式)

    项目准备 1.创建用户表 2.使用spring初始化向导快速创建项目,勾选mybatis,web,jdbc,driver 添加lombok插件 <?xml version="1.0&q ...

  3. 微信小程序之Github API用户登录认证的三种方式

    调用Github API时需要进行身份认证.Github建议并提供基于OAuth2的第三方认证. 一.使用github提供的第三方认证(最常用方法) 使用github提供的第三方认证,需要先注册0au ...

  4. Struts2--自定义拦截器三种方式(实现Interceptor接口、继承抽象类AbstractInterceptor、继承MethodFilterInterceptor)

    实现自定义拦截器 在实际的项目开发中,虽然 Struts2 的内建拦截器可以完成大部分的拦截任务,但是,一些与系统逻辑相关的通用功能(如权限的控制和用户登录控制等),则需要通过自定义拦截器实现.本节将 ...

  5. MySQL数据库修改用户登录密码的三种方式

    文章目录 一.更新 mysql.user 表 二.用 set password 命令 三.使用 mysqladmin 命令 提醒:MYSQL5.7 版本后不再支持password()函数和passwo ...

  6. 【小家Spring】SpringBoot中使用Servlet、Filter、Listener三大组件的三种方式以及原理剖析

    每篇一句 要么就安逸的穷,要么就拼命的干 前提概要 web开发使用Controller基本能解决大部分的需求,但是有时候我们也需要使用Servlet,因为相对于拦截和监听来说,有时候原生的还是比较好用 ...

  7. Springboot单元测试mysql_Springboot Mybatis-Plus数据库单元测试实战(三种方式)

    单元测试长久以来是热门话题,本文不会讨论需不需要写单测,可以看看参考资料1,我个人认为写好单测应该是每个优秀开发者必备的技能,关于写单测的好处在这里我就不展开讨论了,快速进入本文着重讨论的话题,如何写 ...

  8. springboot 多数据源配置的几种方式

    springboot多数据源配置的三种方式 application.yml配置 1.@Ds("配置数据源名称") 引入依赖 <dependency> <group ...

  9. Spring MVC 实战:三种方式获取登录用户信息

    前言 Web 项目中,维持用户登录状态的常用方式有三种,分别是 Cookie.Session.Token,不管哪种方案,都需要获取到用户信息供业务层使用. 由于获取用户信息与具体业务无关,因此在普通的 ...

最新文章

  1. [置顶]完美简版学生信息管理系统(附有源码)管理系统
  2. 349.两个数组的交集
  3. oracle 唯一递增列,在oracle中创建unique唯一约束(单列和多列)
  4. python整数类型-Python整数类型(int)详解
  5. 用API获得Internet Explorer_Server类的HTML
  6. Java SE之I/O流:知识框架
  7. rabbitmq-通配符模式
  8. 快递100接口的调用过程
  9. SpringBoot2.0整合Mybatis-Plus多数据源
  10. CS224N刷题——Assignment1.3_word2vec
  11. QImage使用说明
  12. 使用预计算实时全局光照优化照明-设置场景
  13. vue基础(三)——vue实例化对象
  14. informatic对表的增量抽取机制
  15. xlsxwriter进度条php,PHP_XLSXWriter
  16. Network Trimming: 数据指导的神经剪枝方法
  17. Qt在Win下调用系统的软键盘,区分win7\win8\win10
  18. 微型计算机的总线分为哪些,计算机总线的分类
  19. ByteBuf 读取字节数组数据
  20. 丁鹿学堂:前端http面试总结,状态码详解

热门文章

  1. 全新勒索病毒爆发:一样的套路这次没人上当了
  2. 配置vsftpd 服务器
  3. 让孩子赢在起跑线。。。
  4. 计算机网路络课设_学生宿舍网络规划与设计
  5. 把http升级到https——生命不息,折腾不止
  6. php程序主入口,主:(index.php)入口
  7. 51nod 1298 圆与三角形
  8. A - Heavy Transportation POJ - 1797
  9. vue 中echarts的使用
  10. 验证ssh免密登录_ssh无密码登录认证失败