SpringBoot实现登录拦截的三种方式

发布于 2023-02-13 | 作者: HLH_2021 | 来源: CSDN博客 | 转载于: CSDN博客

1. 登录认证

1.1 介绍

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

1.2 方式

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

1.3 扩展

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

2. 实现

本文对应源码地址: 01-spring-boot-auth-filter·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 项目结构以及前置准备

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

}	

测试

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 +自定义注解实现

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都有的话,顺序如下

3. 扩展

3.1 存放登录用户

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 的参数解析器

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 "测试编码";
}	

访问查看控制台结果