Springboot实现登录拦截的三种方式 | 您所在的位置:网站首页 › Java实现登录 › Springboot实现登录拦截的三种方式 |
文章目录
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 返回用户列表 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 登录之后正常访问 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()); } }测试 未登录访问接口,正常拦截 登录访问接口,正常通行 LoginValidator自定义注解 /** * @description 登录校验注解,用户aop校验 * @author HLH * @email [email protected] * @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标注注解 测试 未登录访问接口,正常拦截 登录访问接口,正常通行 如果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); }访问,查看控制台打印结果 LoginUser自定义注解 /** * @description 登录参数注解,通过spring参数解析器解析 * @author HLH * @email [email protected] * @date Created in 2021/8/1 下午9:35 */ @Target(ElementType.PARAMETER) // 作用于参数 @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LoginUser { }LoginUserResolver参数解析器 /** * @description 登录参数注入,通过spring参数解析器解析 * @author HLH * @email [email protected] * @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 "测试编码"; }访问查看控制台结果 |
CopyRight 2018-2019 实验室设备网 版权所有 |