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

ops/2025/1/15 15:58:51/

目录

后端注册拦截器

实现对 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/ops/150008.html

相关文章

Docker 部署 Typecho

1. 官网 https://typecho.org/插件 & 主题 https://github.com/typecho-fans/plugins https://typechx.com/ https://typecho.work/2. 通过 compose 文件安装 github官网&#xff1a; https://github.com/typecho/Dockerfile 新建一个目录&#xff0c;存放 typecho 的相…

(经过验证)在 Ubuntu 系统中为 VSCode、PyCharm 终端及 Jupyter Notebook 配置代理的完整方案

文章目录 1. 通过系统环境变量配置代理步骤一&#xff1a;打开终端步骤二&#xff1a;编辑 ~/.bashrc 文件步骤三&#xff1a;添加代理环境变量步骤四&#xff1a;保存并关闭文件步骤五&#xff1a;使配置生效步骤六&#xff1a;重启相关应用步骤七&#xff1a;使用代理函数 2.…

Zookeeper单机操作

个人博客地址&#xff1a;Zookeeper单机操作 | 一张假钞的真实世界 下载 从Apache下载镜像站点下载当前稳定发布版。 单机操作 安装单机模式的Zookeeper服务器是简单明了的。服务器包含在一个单独的JAR文件中&#xff0c;因此安装包含创建配置。 下载稳定的ZooKeeper发布版…

记录一次FFmpeg的安装过程

系统版本&#xff1a;CentOS 7 事情起因&#xff1a; 生产环境因为外网开放&#xff0c;密码强度为初始密码&#xff0c;造成挖矿病毒攻击&#xff0c;删除过程中发现&#xff0c;删除文件的同时&#xff0c;病毒会同时从外网下载&#xff0c;怎么也删除不干净&#xff0c;故决…

使用 Python 实现自动化办公(邮件、Excel)

目录 一、Python 自动化办公的准备工作 1.1 安装必要的库 1.2 设置邮件服务 二、邮件自动化处理 2.1 发送邮件 示例代码 注意事项 2.2 接收和读取邮件 示例代码 三、Excel 自动化处理 3.1 读取和写入 Excel 文件 示例代码 3.2 数据处理和分析 示例代码 四、综合…

泛目录和泛站有什么差别

啥是 SEO 泛目录&#xff1f; 咱先来说说 SEO 泛目录是啥。想象一下&#xff0c;你有一个巨大的图书馆&#xff0c;里面的书架上摆满了各种各样的书&#xff0c;每一本书都代表着一个网页。而 SEO 泛目录呢&#xff0c;就像是一个超级图书管理员&#xff0c;它的任务就是把这些…

CV(9)--迁移学习

前言 仅记录学习过程&#xff0c;有问题欢迎讨论 fine-tuning 在实践中&#xff0c;由于数据集不够大&#xff0c;很少有人从头开始训练网络。常见的做法是使用预训练的网络 &#xff08;例如在ImageNet上训练的分类1000类的网络&#xff09;来重新fine-tuning&#xff08;也…

浅谈云计算06 | 云管理系统架构

云管理系统架构 一、云管理系统架构&#xff08;一&#xff09;远程管理系统&#xff08;二&#xff09;资源管理系统&#xff08;三&#xff09;SLA 管理系统&#xff08;四&#xff09;计费管理系统 二、安全与可靠性保障&#xff08;一&#xff09;数据安全防线&#xff08;…