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 项目结构以及前置准备

image-20210807185738246

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

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

image-20210807185958557

  • 测试

    此处使用PostMan进行接口测试

login登录接口

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

image-20210807190603996

findAllUser查询接口

get /user 返回用户列表

image-20210807190812133

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

    image-20210807191702407

  • 登录之后正常访问

    image-20210807191731430

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

}

测试

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

    image-20210807192437682

  • 登录访问接口,正常通行

    image-20210807191731430

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标注注解

image-20210807193016813

测试

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

    image-20210807192437682

  • 登录访问接口,正常通行

    image-20210807191731430

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

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

image-20210807195052054

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

访问查看控制台结果

image-20210807200128831

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐