SpringBoot基于token的最简权限控制

75 阅读6分钟

1、整体流程

权限控制是一个系统的必备的功能。业界有很多成熟的框架比如Spring Security、Shiro等,功能都比较强大。本文讲解如何自己手动用最简的方式实现基于token的权限控制。

登录用户会有token,非登录客户没有token。有些页面/API只有登录用户才能访问,有些则不做限制。那如何实现这样的控制呢?

token是在用户登录时生成,并存储在前端,前端每次请求API时带上token ,后台系统需对前端请求的token进行效验,该效验逻辑使用SpringBoot全局的拦截器统一处理,流程如下:

graph LR
    A[用户登录] --> B{登录校验}
    B -->|通过| C[生成token]
    B -->|失败| G[流程结束]
    C --> E[返回前端]
    E -.-> E1[前端请求API]
    E1 --> F[后台全局token拦截效验]
    F -->|验证通过| H(执行后续业务流程)
    F -->|验证失败| G[结束]

2、登录时的token处理

登录时,验证用户名密码成功后,就会生成token,并返回给前端。登录代码如下:

    @PostMapping("/postPasswordLogin")
    public PageResult<LoginUserResponse>  postPasswordLogin(@RequestBody LoginUserReq user){
        try {
            User loginUser = userService.login(user);
            if(loginUser ==null) return PageResult.error(BusErrorCodeConstants.USERNAME_PASSWORD_ERROR);

            LoginUserResponse response = userService.loginSuccessProces(loginUser);
            return PageResult.success(response);
        }catch (Exception e){
            log.error(e.getMessage(),e);
            LoginUserResponse response = new LoginUserResponse();
            return PageResult.error(BusErrorCodeConstants.USERNAME_PASSWORD_ERROR);
        }
    }

登录成功后的处理逻辑是这段代码:

LoginUserResponse response = userService.loginSuccessProces(loginUser);

这个方法的具体处理逻辑如下:

    public LoginUserResponse loginSuccessProces(User loginUser) throws JsonProcessingException {
        loginUser.setPassword("*");//抹去真实的密码
        LoginUserResponse response = new LoginUserResponse();
        response.setLoginUser(loginUser);
        Map payload = new HashMap<>();
        payload.put("userId",loginUser.getUserId());
        response.setAccessToken(TokenUtil.createToken(payload));

        List<User> childList = getUserChildList(loginUser.getUserId());
        response.setChildList(childList);
        // 保存登录用户信息到redis,便于后续接口调用时校验token
        loginUserUtil.saveLoginUser(loginUser,response.getAccessToken());

        return response;
    }

生成token是这行代码:

response.setAccessToken(TokenUtil.createToken(payload));java

token和User对象也需要存储在redis中

loginUserUtil.saveLoginUser(loginUser,response.getAccessToken());

3、后台全局token拦截处理

前端拿到了token后,以后每次API请求都需要带上token ,后台API接口系统需要配置拦截器,对token进行效验。

3.1 HandlerInterceptor接口介绍

HandlerInterceptor 是 Spring MVC 框架中的一个接口,用于对请求进行拦截处理。通过实现这个接口,开发者可以在请求到达控制器之前或响应返回客户端之前对请求和响应进行预处理和后处理。

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

3.2 请求预处理-preHandle方法

在请求到达控制器之前,可以对请求进行预处理。例如:

  • 权限校验:检查用户是否有权限访问某个资源。

  • 日志记录:记录请求的详细信息,如请求时间、请求路径、请求参数等。

  • 请求参数修改:根据需要对请求参数进行修改或补充。

3.3 TokenInterceptor具体实现解析

 @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求头中获取token
        String token = request.getHeader("Authorization");
        if(token!=null)
            token = token.replace("Bearer ","");
        String url = request.getRequestURI();
        log.info("request url ="+url);
        log.info("token="+token);
        // 验证token(这里需要根据你的具体实现来验证token的有效性)
        User user = loginUserUtil.validateToken(token);
        if (user == null) {
            // token无效或未提供,返回未授权响应
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Unauthorized");
            return false;
        }
        request.getSession().setAttribute(Constant.SESSION_USER_INFO,user);
        request.getSession().setAttribute(Constant.SESSION_TOKEN,token);
        log.info("access success ,user is: "+user.getAccount()+","+user.getNickname());
        // token有效,继续处理请求
        return true;
    }

上面的代码主要是从请求头中获取token(该token在登录时生成,返回给前端),注意,前端请求的token的格式是:Bearer +空格 + token ,所以先要做下字符替换处理,即:

token = token.replace("Bearer ","");

获取到token后,就要验证token的有效性:

User user = loginUserUtil.validateToken(token);

该方法的具体实现是:先验证token,若通过了则从redis中取User对象

    public User validateToken(String token) {
        // token存储在Redis中,查询Redis来验证token的有效性
        try {
            if(TokenUtil.validateToken(token)==null){
                return null;
            }
            User user = getLoginUser(token);
            return user;
        } catch (JsonProcessingException e) {
            log.error(e.getMessage(),e);
            return null;
        }

    }

TokenUtil验证token和生成token要一起看:

public class TokenUtil {

    public static String createToken(Map payload){
        return JWTUtil.createToken(payload, Constant.TOKEN_SECRET.getBytes(StandardCharsets.UTF_8));
    }
    public static Map validateToken(String token){
        if(token==null) return null;
        if(JWTUtil.verify(token,Constant.TOKEN_SECRET.getBytes(StandardCharsets.UTF_8))){
            Map playload = JWTUtil.parseToken(token).getPayloads();
            return playload;
        }
        return null;
    }
}

生成token的方法createToken和验证token的方法validateToken都是使用cn.hutool.jwt 中的JWTUtil工具类。

其中Constant.TOKEN_SECRET定义了一个字符串变量,这个是生成token和验证token的密钥。

3.4 session处理

request.getSession().setAttribute(Constant.SESSION_USER_INFO,user);
request.getSession().setAttribute(Constant.SESSION_TOKEN,token);

在preHandle方法的最后会把User对象和token放到token中,这样后续业务模块中就能根据session获取到登录的用户及对应的token 。

从session中获取User对象可以封装起来,放在工具类LoginUserUtil中:

    // 从session中获取用户信息
    public User getLoginUser(HttpServletRequest request){
        // 从session中获取用户信息,TokenInterceptor已经将用户信息存入session
        User loginUser = (User) request.getSession().getAttribute(Constant.SESSION_USER_INFO);
        return loginUser;
    }

4、WebMvcConfigurer接口

WebMvcConfigurer 是 Spring MVC 框架中的一个接口,用于自定义 Spring MVC 的配置。通过实现这个接口,开发者可以灵活地配置请求映射、视图解析器、拦截器、消息转换器、静态资源处理等。通过实现 addInterceptors 方法,开发者可以向 Spring MVC 添加自定义的拦截器,用于对请求进行拦截处理。

package com.yunei.gut.common.interceptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,并指定需要拦截的路径
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/login/**")
                .excludePathPatterns("/api/wx/loginByCode","/api/wx/loginByPhone","/api/wx/registerUserPhoneByCode","/api/wx/pay/callback")
                .excludePathPatterns("/api/**/nologin/**")
                .excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html"); // 排除Swagger UI路径
    }
}

WebConfig类实现了WebMvcConfigurer的addInterceptors方法,把TokenInterceptor拦截器注入到了系统中,并且设置了拦截的URL的路径已经不拦截的路径。

4.1 @Configuration 注解

WebConfig类添加了Configuration注解。这就意味着这是一个配置类。Spring 容器会在启动时自动检测并调用这些配置类中的方法,以进行 Web MVC 的相关配置。

@Configuration 注解在 Spring 框架中起到了非常重要的作用,它使得开发者能够使用基于 Java 的配置方式来替代传统的 XML 配置文件,从而更加灵活地管理 Spring 容器中的 bean 和配置。通过定义配置类,开发者可以轻松地实现组件扫描、导入其他配置类、支持 Profile 等高级功能。

4.2 addPathPatterns

该方法配置哪些URL需要做token检测,这里配置的是所有以/api/开头的,都需要检查token。

4.3 excludePathPatterns

该方法可排除不需要做token检测的URL,如登录操作、swagger对应的操作、以及其它业务可以让非登录用户查看的数据,这些接口URL中需有 :/nologin/


系统还在开发中,后续会分享如何打通阿里云的百炼大模型,有兴趣可以加入交流群:

学习群