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

server/2025/1/12 7:30:11/

目录

后端注册拦截器

实现对 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/server/157698.html

相关文章

嵌入式系统Linux实时化(二)Xenomai技术框架分析

Xenomai 是 Linux 内核的一个实时开发框架。它希望通过无缝地集成到Linux 环境中来给用户空间应用程序提供全面的、与接口无关的硬实时性能。Xenomai 项目始于2001年8月&#xff0c;作为一个自由软件项目&#xff0c;完全遵守GNU/Linux自由软件协议。2003 年它和RTAI项目合并推…

Mybatis中 Dao接口和XML文件的SQL如何建立关联

建立连接工作&#xff08;准备&#xff09;: 首先会加载Myatis的配置文件&#xff0c;因为这些配置文件里面包括了数据源的各种配置&#xff0c;然后通过SqlSessionFactory创建一个SqlSession&#xff0c;然后可以通过SqlSession操作数据库。 一、解析XML: 首先&#xff0c;…

Android车机DIY开发之学习篇(二)编译Kernel以正点原子为例

Android车机DIY开发之学习篇(二)编译Kernel以正点原子为例 1.代码在/kernel-5.10文件夹下 2.在kernel-5.10目录下执行如下命令编译 &#xff1a; 编译之前&#xff0c;需要将 clang 导出到 PATH 环境变量&#xff1a; 如果是 Android12 执行下面这条命令 export PATH../pr…

doris:手动分区

分区列​ 分区列可以指定一列或多列&#xff0c;分区列必须为 KEY 列。PARTITION 列默认必须为 NOT NULL 列&#xff0c;如果需要使用 NULL 列&#xff0c;应设置 session variable allow_partition_column_nullable true。对于 LIST PARTITION&#xff0c;支持真正的 NULL 分…

Java SE 与 Java EE 简介

诸神缄默不语-个人CSDN博文目录 在学习和使用Java语言时&#xff0c;我们常常会遇到两个常见的术语&#xff1a;Java SE 和 Java EE。它们分别代表了Java的不同应用层次和开发领域。本文将深入介绍Java SE和Java EE的区别、特点以及适用场景&#xff0c;帮助大家更好地理解这两…

js代理模式

允许在不改变原始对象的情况下&#xff0c;通过代理对象来访问原始对象。代理对象可以在访问原始对象之前或之后&#xff0c;添加一些额外的逻辑或功能。 科学上网过程 一般情况下,在访问国外的网站,会显示无法访问 因为在dns解析过程,这些ip被禁止解析,所以显示无法访问 引…

7 分布式定时任务调度框架

先简单介绍下分布式定时任务调度框架的使用场景和功能和架构&#xff0c;然后再介绍世面上常见的产品 我们在大型的复杂的系统下&#xff0c;会有大量的跑批&#xff0c;定时任务的功能&#xff0c;如果在独立的子项目中单独去处理这些任务&#xff0c;随着业务的复杂度的提高…

B+树在MySQL中的应用价值

为什么MySQL选择B树存储数据 在数据库管理系统中&#xff0c;存储和查询数据的效率直接影响系统的性能。MySQL 作为最常用的关系型数据库之一&#xff0c;其存储引擎&#xff08;例如 InnoDB&#xff09;选择了 B 树作为索引的数据结构。这种选择并非偶然&#xff0c;而是经过…