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

news/2025/1/15 7:54:09/

目录

后端注册拦截器

实现对 WebMvcConfigurer 接口的类实现

静态变量

方法重写 注册 Spring Framework拦截器

Sa-Token中SaServletFilter拦截器

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

1. Spring Framework 拦截器

2. SaServletFilter

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

总结

完整代码

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

实现 Satoken 的 StpInterface 接口

获取用户和权限码

在 Dao 层实现

第一张表是 菜单表

第二张表是 用户表

多表联查 SQL 语句

遍历获取权限 getPermissionList 方法

获取角色 getRoleList 方法

主要逻辑

权限与角色的关系

数据库查询

SaSession 和缓存

完整代码

后端自定义侦听器

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拦截器

根据文档里面的写法

    @Beanpublic 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 {@Autowiredprivate 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;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册分页拦截器registry.addInterceptor(new PageableInterceptor());// 注册Redis限流器registry.addInterceptor(accessLimitInterceptor);// 注册 Sa-Token 的注解拦截器,打开注解式鉴权功能registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");}@Beanpublic 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.permsFROM t_menu mINNER JOIN t_role_menu rm ON m.id = rm.menu_idWHERE 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 {@Autowiredprivate MenuMapper menuMapper;@Autowiredprivate RoleMapper roleMapper;/*** 返回一个账号所拥有的权限码集合** @param loginId   登录用户id* @param loginType 登录账号类型* @return 权限集合*/@Overridepublic 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 角色集合*/@Overridepublic 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(用于解析用户的操作系统和浏览器信息)等工具类。

完整代码

 /*** 每次登录时触发*/@Overridepublic 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和osMap<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);// 用户在线信息存入tokenSessionSaSession tokenSession = StpUtil.getTokenSessionByToken(tokenValue);tokenSession.set(ONLINE_USER, onlineUserResp);}

http://www.ppmy.cn/news/1562950.html

相关文章

Linux内核学习——数据结构

文章目录 链表双向链表哈希链表 红黑树无锁环形缓冲区映射参考 内核版本&#xff1a;linux-6.6.69 longterm 链表 双向链表 Linux内核实现了一套纯链表的封装&#xff0c;链表节点数据结构只有指针区而没有数据区&#xff0c;另外还封装了各种操作函数&#xff0c;如创建节点…

Linux一键安装Docker和Docker Compose

Centos docker安装脚本 #!/bin/bash# docker_manager.sh # 用于管理 Docker 和 Docker Compose 的交互脚本# # 配置变量 # # Docker 仓库地址 DOCKER_REPO"https://download.docker.com/linux/centos/docker-ce.repo"# # 函数定义 # # 安装 Docker 和 Docker Comp…

Android 判断RecyclerView是否滚动到底部

需求&#xff1a;列表滚动后&#xff0c;判断是否滚动到底部&#xff0c;展示加载更多的Loading文案和动画 binding.watchMoreRv.addOnScrollListener(object : RecyclerView.OnScrollListener() {override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {sup…

vue 打包部署后 无法找到 app.js 文件

资料 Vue报错&#xff1a;This dependency was not found_vue this dependency was not found:-CSDN博客 过程 大概原因和上面的一样, 在打包好的 文件目录里面出现了这样的 格式: 实际上不需要 最前面的斜杠 解决 加上,重新打包&#xff0c;部署就好了 publicPath: "…

mysql本地安装和pycharm链接数据库操作

MySQL本地安装和相关操作 Python相关&#xff1a;基础、函数、数据类型、面向、模块。 前端开发&#xff1a;HTML、CSS、JavaScript、jQuery。【静态页面】 Java前端&#xff1b; Python前端&#xff1b; Go前端 -> 【动态页面】直观&#xff1a; 静态&#xff0c;写死了…

【LevelDB 和 Sqlite】

关于 LevelDB的小总结&#xff1a; LevelDB 和 SQLite 是两种不同类型的数据库: 1. LevelDB: 特点:NoSQL 数据库键值(Key-Value)存储由 Google 开发数据按键排序存储支持批量操作简单的 Get/Put/Delete 接口 示例操作: key1 -> value1 key2 -> value2 2. SQLite: 特点…

ASP.NET Core - 日志记录系统(一)

ASP.NET Core - 日志记录系统&#xff08;一&#xff09; 一、日志记录二、ASP.Net Core 的日志记录2.1. 日志记录系统的接入2.2 记录日志2.3 基本配置2.3.1 日志级别2.3.2 全局输出配置2.3.3 针对特定日志提供程序的配置2.3.6 显式设置2.3.4 配置筛选原理2.3.5 日志作用域 一、…

QT中引入OpenCV库总结(qmake方式和cmake方式)

文章目录 前言opencv环境配置一、opencv库获取的两种方式二、qmake和cmake配置2.1、 qmake2.2、cmake2.2.1、引入opencv示例 三、qt与opencv对应关系四、问题 前言 我的软件环境&#xff0c;写在前面 Windows10QT5.12.12VS2017OpenCV4.5.4 opencv环境配置 一、opencv库获取…