后端技术选型 sa-token校验学习 下 结合项目学习 后端鉴权

目录

后端注册拦截器

[实现对 WebMvcConfigurer 接口的类实现](#实现对 WebMvcConfigurer 接口的类实现)

静态变量

[方法重写 注册 Spring Framework拦截器](#方法重写 注册 Spring Framework拦截器)

Sa-Token中SaServletFilter拦截器

[思考 为什么使用两个拦截器](#思考 为什么使用两个拦截器)

[1. Spring Framework 拦截器](#1. Spring Framework 拦截器)

[2. SaServletFilter](#2. SaServletFilter)

为什么要注册两个拦截器?

总结

完整代码

后端注册权限验证接口扩展

[实现 Satoken 的 StpInterface 接口](#实现 Satoken 的 StpInterface 接口)

获取用户和权限码

[在 Dao 层实现](#在 Dao 层实现)

[第一张表是 菜单表](#第一张表是 菜单表)

[第二张表是 用户表](#第二张表是 用户表)

[多表联查 SQL 语句](#多表联查 SQL 语句)

[遍历获取权限 getPermissionList 方法](#遍历获取权限 getPermissionList 方法)

[获取角色 getRoleList 方法](#获取角色 getRoleList 方法)

主要逻辑

权限与角色的关系

数据库查询

[SaSession 和缓存](#SaSession 和缓存)

完整代码

后端自定义侦听器

[doLogin --- 用户登录时触发](#doLogin — 用户登录时触发)

总结

完整代码


后端注册拦截器

实现对 WebMvcConfigurer 接口的类实现

静态变量

一个是不需要鉴权的网址

一个是超时过期时间

方法重写 注册 Spring Framework拦截器

这段代码的目的是注册多个拦截器,在 Spring 应用中统一处理:

  1. 分页:处理分页相关的逻辑。
  2. 限流:通过 Redis 实现请求频率控制,防止滥用。
  3. 权限鉴权:使用 Sa-Token 框架进行请求的权限验证。

所有这些拦截器会在请求到达控制器之前执行,确保全局的功能逻辑一致性(如分页、限流和权限检查)。

复制代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 注册分页拦截器
    registry.addInterceptor(new PageableInterceptor());
    // 注册Redis限流器
    registry.addInterceptor(accessLimitInterceptor);
    // 注册 Sa-Token 的注解拦截器,打开注解式鉴权功能
    registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}

Sa-TokenSaServletFilter拦截器

根据文档里面的写法

复制代码
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                // 拦截路径
                .addInclude("/**")
                // 放开路径
                .addExclude(EXCLUDE_PATH_PATTERNS)
                // 前置函数:在每次认证函数之前执行
                .setBeforeAuth(obj -> {
                    SaHolder.getResponse()
                            // 允许指定域访问跨域资源
                            .setHeader("Access-Control-Allow-Origin", "*")
                            // 允许所有请求方式
                            .setHeader("Access-Control-Allow-Methods", "*")
                            // 有效时间
                            .setHeader("Access-Control-Max-Age", "3600")
                            // 允许的header参数
                            .setHeader("Access-Control-Allow-Headers", "*");
                    // 如果是预检请求,则立即返回到前端
                    SaRouter.match(SaHttpMethod.OPTIONS)
                            .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
                            .back();
                })
                // 认证函数: 每次请求执行
                .setAuth(obj -> {
                    // 检查是否登录
                    SaRouter.match("/admin/**").check(r -> StpUtil.checkLogin());
                    // 刷新token有效期
                    if (StpUtil.getTokenTimeout() < timeout) {
                        StpUtil.renewTimeout(1800);
                    }
                    // 输出 API 请求日志,方便调试代码
                    SaManager.getLog().debug("请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
                })
                //  异常处理函数:每次认证函数发生异常时执行此函数
                .setError(e -> {
                    // 设置响应头
                    SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
                    if (e instanceof NotLoginException) {
                        // todo 确实是这边有问题
                        e.printStackTrace();
                        return JSONUtil.toJsonStr(Result.fail(UNAUTHORIZED.getCode(), UNAUTHORIZED.getMsg()));
                    }
                    // TODO 服务器后端在这里无法捕获异常,仅仅将异常信息传给了前端
                    e.printStackTrace();
                    return SaResult.error(e.getMessage());
                });
    }

思考 为什么使用两个拦截器

1. Spring Framework 拦截器

Spring 框架中的拦截器通常是通过实现 HandlerInterceptor 接口,或通过继承 WebMvcConfigurer 类中的 addInterceptors 方法来注册的。这类拦截器一般用于以下目的:

  • 日志记录:记录请求的日志。
  • 权限检查:用于访问控制,判断用户是否有权限访问某些资源。
  • 性能监控:统计接口响应时间等。
  • 请求处理:在请求进入控制器前对请求进行预处理,或在请求完成后进行后处理。

Spring 的拦截器是基于 HandlerInterceptor 接口的,它是为 Spring MVC 控制器定制的,具有以下生命周期:

  1. preHandle:在请求到达控制器之前执行。
  2. postHandle:在控制器方法执行后,渲染视图之前执行。
  3. afterCompletion:视图渲染完毕后执行,通常用于清理资源。

在这段代码中,Spring 的拦截器被注册为:

  • 分页拦截器 :用于处理分页参数(如 pagesize),确保分页逻辑的一致性。
  • 限流拦截器:用于限制 API 请求的频率,防止过于频繁的请求对服务器造成压力。
  • 权限拦截器:用于进行权限检查,确保用户请求的资源需要相应的权限。

2. SaServletFilter

SaServletFilter 是 Sa-Token 框架提供的一个过滤器,用于处理认证和权限管理。它作为一个 Servlet Filter 被引入到 Spring Web 应用中,并在请求进入控制器之前执行。SaServletFilter 的职责通常包括:

  • 用户认证:检查请求是否携带有效的 token,判断用户是否登录。
  • 跨域处理:配置跨域请求的响应头,确保前端能够正常访问接口。
  • 权限控制:根据不同的 URL 路径,检查用户是否具有访问权限。
  • 异常处理:在认证或权限检查失败时,返回统一的错误信息。

SaServletFilter 是一个全局过滤器,会拦截所有请求,处理权限相关的逻辑。它的作用是在用户访问接口时进行认证、权限检查、跨域处理等。

为什么要注册两个拦截器?

  1. 职责分离
    • Spring 拦截器 主要用于通用功能,如分页、限流、日志等,这些是应用中与业务逻辑和请求处理相关的通用功能。
    • SaServletFilter 主要负责安全相关的功能,如认证和权限检查。它是 Sa-Token 提供的专用过滤器,能够帮助应用实现基于 token 的权限控制。
  1. 功能互补
    • SaServletFilter 是为了处理与用户认证、权限相关的安全需求,而 Spring 拦截器通常用于通用功能(如分页、限流)。这两者的作用并不冲突,反而可以互补,Spring 拦截器可以集中处理一些公共逻辑,SaServletFilter 则专注于用户认证和权限控制。
  1. 实现细粒度控制
    • 在复杂的应用中,你可能需要对某些路径进行分页和限流控制,但对于其他路径,你需要确保严格的权限检查。SaServletFilter 提供了灵活的权限认证机制,而 Spring 的拦截器可以细分不同的逻辑(如分页、限流等),实现更精细的控制。
  1. 统一认证与权限管理
    • SaServletFilter 通过拦截所有请求并统一处理认证、跨域、权限等,确保每个请求都能经过安全检查。Spring 拦截器负责处理请求中的其他逻辑(如日志记录、分页等),这样可以将应用中的各个功能模块进行解耦,并且保证权限检查与认证操作的统一性。

总结

  • Spring Framework 拦截器:主要负责处理与请求相关的公共逻辑,如分页、限流、日志记录等。
  • SaServletFilter:主要负责安全相关的功能,执行认证、权限控制、跨域处理等操作。

通过同时注册这两个拦截器,应用能够既保持业务逻辑的清晰和分离,又能确保安全性(认证与权限管理)得到充分保障。两者各司其职,共同为应用提供完整的功能支持。

完整代码

复制代码
package com.ican.satoken;

import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import cn.hutool.json.JSONUtil;
import com.ican.interceptor.AccessLimitInterceptor;
import com.ican.interceptor.PageableInterceptor;
import com.ican.model.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import static com.ican.enums.StatusCodeEnum.UNAUTHORIZED;

/**
 * SaToken配置
 *
 * @author Dduo
 * @date 2024/11/28 22:12
 **/
@Slf4j
@Component
public class SaTokenConfig implements WebMvcConfigurer {

    @Autowired
    private AccessLimitInterceptor accessLimitInterceptor;

    private final String[] EXCLUDE_PATH_PATTERNS = {
            "/swagger-resources",
            "/webjars/**",
            "/v2/api-docs",
            "/doc.html",
            "/favicon.ico",
            "/login",
            "/oauth/*",
    };

    private final long timeout = 600;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册分页拦截器
        registry.addInterceptor(new PageableInterceptor());
        // 注册Redis限流器
        registry.addInterceptor(accessLimitInterceptor);
        // 注册 Sa-Token 的注解拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                // 拦截路径
                .addInclude("/**")
                // 放开路径
                .addExclude(EXCLUDE_PATH_PATTERNS)
                // 前置函数:在每次认证函数之前执行
                .setBeforeAuth(obj -> {
                    SaHolder.getResponse()
                            // 允许指定域访问跨域资源
                            .setHeader("Access-Control-Allow-Origin", "*")
                            // 允许所有请求方式
                            .setHeader("Access-Control-Allow-Methods", "*")
                            // 有效时间
                            .setHeader("Access-Control-Max-Age", "3600")
                            // 允许的header参数
                            .setHeader("Access-Control-Allow-Headers", "*");
                    // 如果是预检请求,则立即返回到前端
                    SaRouter.match(SaHttpMethod.OPTIONS)
                            .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
                            .back();
                })
                // 认证函数: 每次请求执行
                .setAuth(obj -> {
                    // 检查是否登录
                    SaRouter.match("/admin/**").check(r -> StpUtil.checkLogin());
                    // 刷新token有效期
                    if (StpUtil.getTokenTimeout() < timeout) {
                        StpUtil.renewTimeout(1800);
                    }
                    // 输出 API 请求日志,方便调试代码
                    SaManager.getLog().debug("请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
                })
                //  异常处理函数:每次认证函数发生异常时执行此函数
                .setError(e -> {
                    // 设置响应头
                    SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
                    if (e instanceof NotLoginException) {
                        // todo 确实是这边有问题
                        e.printStackTrace();
                        return JSONUtil.toJsonStr(Result.fail(UNAUTHORIZED.getCode(), UNAUTHORIZED.getMsg()));
                    }
                    // TODO 服务器后端在这里无法捕获异常,仅仅将异常信息传给了前端
                    e.printStackTrace();
                    return SaResult.error(e.getMessage());
                });
    }

}

后端注册权限验证接口扩展

实现 Satoken 的 StpInterface 接口

获取用户和权限码

根据文档里面的内容

每个用户 id 都对应一系列的权限码

每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。

我们将权限码存到数据库里面

在 Dao 层实现

第一张表是 菜单表

第二张表是 用户表

多表联查 SQL 语句

复制代码
<select id="selectPermissionByRoleId" resultType="java.lang.String">
    SELECT DISTINCT m.perms
    FROM t_menu m
             INNER JOIN t_role_menu rm ON m.id = rm.menu_id
    WHERE rm.role_id = #{roleId}
      AND m.is_disable = 0
</select>

遍历获取权限 getPermissionList 方法

复制代码
/**
 * 返回一个账号所拥有的权限码集合
 *
 * @param loginId   登录用户id
 * @param loginType 登录账号类型
 * @return 权限集合
 */
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
    // 声明权限码集合
    List<String> permissionList = new ArrayList<>();
    // 遍历角色列表,查询拥有的权限码
    for (String roleId : getRoleList(loginId, loginType)) {
        SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId);
        List<String> list = roleSession.get("Permission_List", () -> menuMapper.selectPermissionByRoleId(roleId));
        permissionList.addAll(list);
    }
    // 返回权限码集合
    return permissionList;
}
  • 角色与权限的查询 :通过 getRoleList 获取用户角色,再通过 getPermissionList 获取每个角色的权限。
  • 会话缓存 :使用 SaSession 缓存角色和权限信息,减少对数据库的查询。
  • 自定义权限与角色管理 :通过扩展 StpInterface 接口,灵活地实现了用户角色和权限的管理,适应应用中的业务需求。

获取角色 getRoleList 方法

复制代码
@Override
public List<String> getRoleList(Object loginId, String loginType) {
    SaSession session = StpUtil.getSessionByLoginId(loginId);
    return session.get("Role_List", () -> roleMapper.selectRoleListByUserId(loginId));
}
  • 功能 :返回一个账号(loginId)所拥有的所有角色。
  • 实现步骤
    1. 通过 StpUtil.getSessionByLoginId(loginId) 获取当前登录用户的会话。
    2. 从会话中获取角色列表。如果会话中没有角色信息,则通过 roleMapper.selectRoleListByUserId(loginId) 查询数据库获取。
    3. 返回角色列表。

主要逻辑

  • getRoleList 方法用于获取用户的角色列表。每个用户可以有多个角色,角色是权限的载体。
  • getPermissionList 方法根据用户的角色列表,查询每个角色对应的权限。权限是基于角色的,角色是用户的身份标识。权限是用户访问特定资源的授权标识。

权限与角色的关系

  • 在这个实现中,用户是通过角色来管理权限的。每个用户有一组角色,而每个角色又有一组权限。这种关系是典型的 角色权限控制(RBAC) 模式。
  • getRoleList 获取用户的角色列表,而 getPermissionList 则是基于角色来获取相应的权限。

数据库查询

  • menuMapper.selectPermissionByRoleId(roleId):根据角色 ID 查询该角色所拥有的权限。权限通常与菜单或 API 请求相关。
  • roleMapper.selectRoleListByUserId(loginId):根据用户 ID 查询该用户所拥有的角色。

SaSession 和缓存

  • SaSession 在这个实现中被用于缓存角色和权限信息。这样,避免每次请求都进行数据库查询。使用会话可以提高性能,避免重复查询数据库。

完整代码

复制代码
package com.ican.satoken;

import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.session.SaSessionCustomUtil;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import com.ican.mapper.MenuMapper;
import com.ican.mapper.RoleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义权限验证接口扩展
 *
 * @author Dduo
 */
@Component
public class StpInterfaceImpl implements StpInterface {

    @Autowired
    private MenuMapper menuMapper;

    @Autowired
    private RoleMapper roleMapper;

    /**
     * 返回一个账号所拥有的权限码集合
     *
     * @param loginId   登录用户id
     * @param loginType 登录账号类型
     * @return 权限集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 声明权限码集合
        List<String> permissionList = new ArrayList<>();
        // 遍历角色列表,查询拥有的权限码
        for (String roleId : getRoleList(loginId, loginType)) {
            SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId);
            List<String> list = roleSession.get("Permission_List", () -> menuMapper.selectPermissionByRoleId(roleId));
            permissionList.addAll(list);
        }
        // 返回权限码集合
        return permissionList;
    }

    /**
     * 返回一个账号所拥有的可用角色标识集合
     *
     * @param loginId   登录用户id
     * @param loginType 登录账号类型
     * @return 角色集合
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        SaSession session = StpUtil.getSessionByLoginId(loginId);
        return session.get("Role_List", () -> roleMapper.selectRoleListByUserId(loginId));
    }

}

后端自定义侦听器

doLogin --- 用户登录时触发

  • 功能:每当用户成功登录时触发此方法。
  • 主要步骤
  1. 查询用户信息,包括头像和昵称。
  2. 解析用户的浏览器和操作系统信息(通过 UserAgentUtils.parseOsAndBrowser)。
  3. 获取登录时的 IP 地址,并查询该 IP 的来源地(如城市名)。
  4. 获取当前的登录时间。
  5. 创建一个 OnlineUserResp 对象,包含用户的基本信息、登录 IP、操作系统和浏览器等信息,并将其保存到 SaSession 中。
  6. 更新数据库中的用户信息(如 IP 地址、登录时间等)。
  • 作用:此方法不仅处理了用户登录,还将用户的登录信息(如 IP 地址、设备信息等)存储到会话中,供后续使用。同时更新了数据库中的用户信息,以便后续管理。

总结

  • 功能MySaTokenListener 实现了 SaTokenListener 接口,并提供了用户登录、注销、踢下线等事件的自定义处理。
    • 在登录时,记录用户的登录信息,包括设备、IP 地址、地理位置等,并将这些信息存储到 token 会话中。
    • 在注销时,清除 token 会话中的用户信息。
  • 扩展性:其他事件(如被踢下线、二级认证等)目前没有具体实现,但可以根据业务需求添加逻辑,比如发送通知、更新状态等。
  • 依赖 :该实现依赖于 UserMapper(用于查询和更新用户信息)、IpUtils(用于获取 IP 地址的来源)、UserAgentUtils(用于解析用户的操作系统和浏览器信息)等工具类。

完整代码

复制代码
 /**
     * 每次登录时触发
     */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        // 查询用户昵称
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
                .select(User::getAvatar, User::getNickname)
                .eq(User::getId, loginId));
        // 解析browser和os
        Map<String, String> userAgentMap = UserAgentUtils.parseOsAndBrowser(request.getHeader("User-Agent"));
        // 获取登录ip地址
        String ipAddress = ServletUtil.getClientIP(request);
        // 获取登录地址
        String ipSource = IpUtils.getIpSource(ipAddress);
        // 获取登录时间
        LocalDateTime loginTime = LocalDateTime.now(ZoneId.of(SHANGHAI.getZone()));
        OnlineUserResp onlineUserResp = OnlineUserResp.builder()
                .id((Integer) loginId)
                .token(tokenValue)
                .avatar(user.getAvatar())
                .nickname(user.getNickname())
                .ipAddress(ipAddress)
                .ipSource(ipSource)
                .os(userAgentMap.get("os"))
                .browser(userAgentMap.get("browser"))
                .loginTime(loginTime)
                .build();
        // 更新用户登录信息
        User newUser = User.builder()
                .id((Integer) loginId)
                .ipAddress(ipAddress)
                .ipSource(ipSource)
                .loginTime(loginTime)
                .build();
        userMapper.updateById(newUser);
        // 用户在线信息存入tokenSession
        SaSession tokenSession = StpUtil.getTokenSessionByToken(tokenValue);
        tokenSession.set(ONLINE_USER, onlineUserResp);
    }
相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
mghio7 小时前
Dubbo 中的集群容错
java·微服务·dubbo
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github