
1.sa-token是什么?
1.1简介
1.2官网
https://sa-token.cc/v/v1.36.0/doc.html#/
1.3 Sa-Token 功能一览
Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。
登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录 权限认证 —— 权限认证、角色认证、会话二级认证 Session会话 —— 全端共享Session、单端独享Session、自定义Session 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线 账号封禁 —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定 OAuth2.0认证 —— 轻松搭建 OAuth2.0 服务,支持openid模式 二级认证 —— 在已登录的基础上再次认证,保证安全性 Basic认证 —— 一行代码接入 Http Basic 认证 独立Redis —— 将权限缓存与业务缓存分离 临时Token认证 —— 解决短时间的Token授权问题 模拟他人账号 —— 实时操作任意用户状态数据 临时身份切换 —— 将会话身份临时切换为其它账号 前后端分离 —— APP、小程序等不支持Cookie的终端 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权 Token风格定制 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀 注解式鉴权 —— 优雅的将鉴权与业务代码分离 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签 会话治理 —— 提供方便灵活的会话查询接口 记住我模式 —— 适配[记住我]模式,重启浏览器免验证 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作 开箱即用 —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用
1.4 功能结构图

2.集成sa-token及配置
2.1 pom依赖
<dependency><groupId>cn.dev33groupId><artifactId>sa-token-spring-boot-starterartifactId><version>1.37.0version>dependency><dependency><groupId>cn.dev33groupId><artifactId>sa-token-jwtartifactId><version>1.37.0version>dependency><dependency><groupId>cn.dev33groupId><artifactId>sa-token-redis-jacksonartifactId><version>1.37.0version>dependency><dependency><groupId>cn.dev33groupId><artifactId>sa-token-spring-aopartifactId><version>1.37.0version>dependency>
2.2 yaml配置
sa-token:token-name: satokentoken-prefix: Bearertimeout: 86400active-timeout: -1is-concurrent: trueis-share: falsetoken-style: uuidis-log: truejwt-secret-key: adfdfdsdasdasifdfdffhueuiwyudfdfddfdfsfsdfrfewbfjsdafjk
https://sa-token.cc/v/v1.36.0/doc.html#/fun/curr-domain2.3 代码配置
https://sa-token.cc/v/v1.36.0/doc.html#/plugin/aop-at
https://sa-token.cc/v/v1.36.0/doc.html#/use/at-check
#使用 Sa-Token 的全局过滤器解决跨域问题(三种方式全版)
https://juejin.cn/post/7247376558367981627
SaTokenConfigure配置
package xxxxx.config;import cn.dev33.satoken.SaManager;import cn.dev33.satoken.context.SaHolder;import cn.dev33.satoken.exception.SaTokenException;import cn.dev33.satoken.filter.SaServletFilter;import cn.dev33.satoken.jwt.StpLogicJwtForSimple;import cn.dev33.satoken.router.SaHttpMethod;import cn.dev33.satoken.router.SaRouter;import cn.dev33.satoken.stp.StpLogic;import cn.dev33.satoken.stp.StpUtil;import xxxxx.RestResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;public class SaTokenConfigure implements WebMvcConfigurer {// Sa-Token 整合 jwt (Simple 简单模式)public StpLogic getStpLogicJwt() {return new StpLogicJwtForSimple();}/*** 注册 [Sa-Token 全局过滤器]*/public SaServletFilter getSaServletFilter() {return new SaServletFilter()// 指定 [拦截路由] 与 [放行路由].addInclude("/**")// 登录认证 -- 拦截所有路由,并排除/user/login 用于开放登录.addExclude("/user/**").addExclude("/favicon.ico").addExclude("*.js").addExclude("*.css")// 认证函数: 每次请求执行.setAuth(obj -> {SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());// ...SaRouter.match("/**") // 拦截的 path 列表,可以写多个.check(r -> StpUtil.checkLogin());// 要执行的校验动作,可以写完整的 lambda 表达式// 根据路由划分模块,不同模块不同鉴权SaRouter.match("/xxx/xxxx/**", r -> StpUtil.checkPermission("xxxx.xxx"));,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,// 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */})// 异常处理函数:每次认证函数发生异常时执行此函数.setError(e1 -> {log.error("sa-token异常:{}", e1.getMessage());// 设置响应头SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");/*** sa-token登录相关异常处理* https://sa-token.cc/v/v1.36.0/doc.html#/fun/exception-code*/if (e1 instanceof SaTokenException) {SaTokenException e = (SaTokenException) e1;// 根据不同异常细分状态码返回不同的提示if (e.getCode() == 11001) {return RestResponse.fail("未能读取到有效Token");}if (e.getCode() == 11002) {return RestResponse.fail("登录时的账号为空");}if (e.getCode() == 11011) {return RestResponse.fail("未能读取到有效Token");}if (e.getCode() == 11012) {return RestResponse.fail("Token无效");}if (e.getCode() == 11013) {return RestResponse.fail("Token已过期");}if (e.getCode() == 11014) {return RestResponse.fail("Token已被顶下线");}if (e.getCode() == 11015) {return RestResponse.fail("Token已被踢下线");}if (e.getCode() == 11016) {return RestResponse.fail("Token已被冻结");}if (e.getCode() == 11017) {return RestResponse.fail("未按照指定前缀提交token");}if (e.getCode() == 11041) {return RestResponse.fail("缺少指定的角色");}if (e.getCode() == 11051) {return RestResponse.fail("缺少指定的权限");}if (e.getCode() == 11061) {return RestResponse.fail("当前账号未通过服务封禁校验");}if (e.getCode() == 11062) {return RestResponse.fail("提供要解禁的账号无效");}if (e.getCode() == 12001) {return RestResponse.fail("请求中缺少指定的参数");}if (e.getCode() == 12111) {return RestResponse.fail("密码md5加密异常");}if (e.getCode() == 30201) {return RestResponse.fail("对jwt字符串解析失败");}if (e.getCode() == 30202) {return RestResponse.fail("此jwt的签名无效");}if (e.getCode() == 30203) {return RestResponse.fail("此jwt的loginType字段不符合预期");}if (e.getCode() == 30204) {return RestResponse.fail("此jwt已超时");}if (e.getCode() == 30205) {return RestResponse.fail("没有配置jwt秘钥");}if (e.getCode() == 30206) {return RestResponse.fail("登录时提供的账号为空");}// 更多 code 码判断 ...// 默认的提示return RestResponse.fail("登录异常,请联系管理员处理...");}return RestResponse.fail(e1.getMessage());})// 前置函数:在每次认证函数之前执行.setBeforeAuth(obj -> {// ---------- 设置一些安全响应头 ----------SaHolder.getResponse()// 服务器名称//.setServer("sa-server")// 是否可以在iframe显示视图:DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以.setHeader("X-Frame-Options", "SAMEORIGIN")// 是否启用浏览器默认XSS防护:0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面.setHeader("X-XSS-Protection", "1; mode=block")// 禁用浏览器内容嗅探.setHeader("X-Content-Type-Options", "nosniff")// ---------- 设置跨域响应头 ----------// 允许指定域访问跨域资源.setHeader("Access-Control-Allow-Origin", "*")// 允许所有请求方式.setHeader("Access-Control-Allow-Methods", "*")// 允许的header参数.setHeader("Access-Control-Allow-Headers", "*")// 有效时间.setHeader("Access-Control-Max-Age", "3600");// 如果是预检请求,则立即返回到前端SaRouter.match(SaHttpMethod.OPTIONS).free(r -> log.info("--------OPTIONS预检请求,不做处理")).back();});}}
自定义权限加载接口实现类:
package xxxxx.config;import cn.dev33.satoken.stp.StpInterface;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.Serializable;import java.util.ArrayList;import java.util.List;import java.util.Objects;/*** 自定义权限加载接口实现类* 保证此类被 SpringBoot 扫描,完* 成 Sa-Token 的自定义权限验证扩展*/public class StpInterfaceImpl implements StpInterface {/*** 返回一个账号所拥有的权限码集合*/public List<String> getPermissionList(Object loginId, String loginType) {List<String> permissionList = new ArrayList<>();//TODO 根据登录的loginId(登录用户id)去查权限,可以存缓存中,从缓存中取,权限有变动更新缓存return permissionList;}/*** 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)*/public List<String> getRoleList(Object loginId, String loginType) {List<String> roleList = new ArrayList<>();//TODO 根据登录的loginId(登录用户id)去查角色,可以存缓存,从缓存中取,角色变动更新缓存return roleList;}}
4.RBAC权限控制表设计
角色表-role表:




5.菜单权限树构造实现
5.1菜单权限数据sql查询
with recursive menu_power_tree(id, parent_id, type, name,remarks,source_type,menu_sort,menu_level) AS (-- 初始查询,选择所有没有父级别的分类(即根分类)SELECT id, parent_id, type, name,remarks,source_type,menu_sort,menu_levelFROM dyict_resourceWHERE parent_id = 0 and source_type = 1UNION ALL-- 递归查询,选择所有子分类SELECT c1.id, c1.parent_id, c1.type, c1.name, c1.remarks,c1.source_type,c1.menu_sort,c1.menu_level FROM(SELECT id, parent_id, type, name,remarks,source_type,menu_sort,menu_levelFROM dyict_resource c WHERE c.parent_id <> 0) c1INNER JOIN menu_power_tree ct ON ct.id = c1.parent_id)SELECT a.id, a.parent_id, a.type, a.name,a.remarks,a.source_type,a.menu_sort,a.menu_level,(SELECT count(*) FROM dyict_role_resource_power b WHERE a.id = b.resource_id and b.role_id in(1)) as pFROM menu_power_tree aORDER BY parent_id,id;
5.2菜单权限树构建
基础接口RoleResourcePowerMapper:
public interface RoleResourcePowerMapper extends BaseMapper<RoleResourcePower> {/*** 角色对应的菜单权限用于获取权限和构建权限** @param sourceType* @param roleIds* @return*/ListmenuPowerTree(@Param("sourceType") Integer sourceType, @Param("roleIds") List roleIds); }
RoleResourcePowerMapper.xml
<select id="menuPowerTree" resultType="xxxx.dto.MenuPowerTreeDto">with recursive menu_power_tree(id, parent_id, type, name,remarks,source_type,menu_sort,menu_level) AS (-- 初始查询,选择所有没有父级别的分类(即根分类)SELECT id, parent_id, type, name,remarks,source_type,menu_sort,menu_levelFROM dyict_resourceWHERE parent_id = 0<if test="sourceType != null">and source_type = #{sourceType}if>UNION ALL-- 递归查询,选择所有子分类SELECT c1.id, c1.parent_id, c1.type, c1.name, c1.remarks,c1.source_type,c1.menu_sort,c1.menu_level FROM(SELECT id, parent_id, type, name,remarks,source_type,menu_sort,menu_levelFROM dyict_resource c WHERE c.parent_id ]]>0) c1INNER JOIN menu_power_tree ct ON ct.id = c1.parent_id)SELECT a.id, a.parent_id, a.type, a.name,a.remarks,a.source_type,a.menu_sort,a.menu_level,(SELECT count(*) FROM dyict_role_resource_power b WHERE a.id = b.resource_id<if test="roleIds != null and roleIds.size() > 0 ">andb.role_id in<foreach collection="roleIds" item="id" index="index" open="(" close=")" separator=",">#{id}foreach>if>) as pFROM menu_power_tree aORDER BY parent_id,idselect>
MenuPowerTreeDto类:
package xxxx.dto;import lombok.Data;import java.io.Serializable;public class MenuPowerTreeDto implements Serializable {private static final long serialVersionUID = -8644290706362470684L;private Integer id;private Integer parentId;private Integer type;private String name;private String remarks;private Integer sourceType;private Integer menuSort;private Integer menuLevel;private Integer p;}
MenuPowerTreeVo类:
package xxxx.vo;import lombok.Data;import java.io.Serializable;import java.util.List;public class MenuPowerTreeVo implements Serializable {private static final long serialVersionUID = 3214808951975328795L;private Integer id;private Integer parentId;private Integer type;private String name;private String remarks;private Integer sourceType;private Integer menuSort;private Integer menuLevel;private Boolean hasPower;private ListchildrenMenuType; private ListchildrenButtonType; }
RoleResourcePowerServiceImpl类:
package xxxxx.service.impl;import cn.hutool.core.collection.CollectionUtil;import com.alibaba.fastjson.JSON;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.BeanUtils;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.Comparator;import java.util.List;import java.util.Objects;import java.util.stream.Collectors;4jpublic class RoleResourcePowerServiceImpl extends ServiceImpl<RoleResourcePowerMapper, RoleResourcePower> implements RoleResourcePowerService {public MenuPowerTreeVo queryMenuPowerTreeVo(Integer sourceType, Integer roleId) {if (Objects.nonNull(sourceType) && Objects.nonNull(roleId)) {ListmenuPowerTrees = this.baseMapper.menuPowerTree(sourceType, Arrays.asList(roleId)); if (CollectionUtil.isNotEmpty(menuPowerTrees)) {MenuPowerTreeVo menuPowerTreeVo = new MenuPowerTreeVo();ListoneMenuPowerTrees = menuPowerTrees.stream().filter(e -> e.getParentId() == 0).collect(Collectors.toList()); if (CollectionUtil.isNotEmpty(oneMenuPowerTrees)) {this.buildeTree(oneMenuPowerTrees, menuPowerTrees, menuPowerTreeVo);if (oneMenuPowerTrees.size() == 1) {Listchildren = new ArrayList<>(); children.add(menuPowerTreeVo);MenuPowerTreeVo menuPowerTreeVo2 = new MenuPowerTreeVo();menuPowerTreeVo2.setId(-1);menuPowerTreeVo2.setName("父节点");menuPowerTreeVo2.setParentId(-1);menuPowerTreeVo2.setMenuSort(0);menuPowerTreeVo2.setChildrenMenuType(children);return menuPowerTreeVo2;}return menuPowerTreeVo;}}}return null;}private void buildeTree(ListoneMenuPowerTrees, List {menuPowerTrees, MenuPowerTreeVo menuPowerTreeVo) if (oneMenuPowerTrees.size() > 1) {menuPowerTreeVo.setId(-1);menuPowerTreeVo.setName("父节点");menuPowerTreeVo.setParentId(-1);menuPowerTreeVo.setMenuSort(0);Listchildrens = oneMenuPowerTrees; this.commonBuildTree(menuPowerTrees, menuPowerTreeVo, childrens);} else if (oneMenuPowerTrees.size() == 1) {for (MenuPowerTreeDto mp : oneMenuPowerTrees) {BeanUtils.copyProperties(mp, menuPowerTreeVo);if (Objects.nonNull(mp.getP()) && mp.getP() > 0) {menuPowerTreeVo.setHasPower(true);}Integer id = mp.getId();Listchildrens = menuPowerTrees.stream().filter(e -> Objects.nonNull(e.getParentId()) && e.getParentId() == id).collect(Collectors.toList()); this.commonBuildTree(menuPowerTrees, menuPowerTreeVo, childrens);}}}private void commonBuildTree(ListmenuPowerTrees, MenuPowerTreeVo menuPowerTreeVo, List {childrens) if (CollectionUtil.isNotEmpty(childrens)) {ListchildrenButtonTypeDto = childrens.stream().filter(e -> Objects.nonNull(e.getType()) && e.getType() == 2).collect(Collectors.toList()); if (CollectionUtil.isNotEmpty(childrenButtonTypeDto)) {ListchildrenButtonType = new ArrayList<>(); for (MenuPowerTreeDto mb : childrenButtonTypeDto) {MenuPowerTreeVo menuPowerTreeVo2 = new MenuPowerTreeVo();BeanUtils.copyProperties(mb, menuPowerTreeVo2);if (Objects.nonNull(mb.getP()) && mb.getP() > 0) {menuPowerTreeVo2.setHasPower(true);}childrenButtonType.add(menuPowerTreeVo2);}menuPowerTreeVo.setChildrenButtonType(childrenButtonType);}ListchildrenMenuTypeDto = childrens.stream().filter(e -> Objects.nonNull(e.getType()) && e.getType() == 1).collect(Collectors.toList()); if (CollectionUtil.isNotEmpty(childrenMenuTypeDto)) {ListchildrenMenuType = new ArrayList<>(); for (MenuPowerTreeDto mp2 : childrenMenuTypeDto) {MenuPowerTreeVo menuPowerTreeVo1 = new MenuPowerTreeVo();ListoneMenuType = new ArrayList<>(); oneMenuType.add(mp2);this.buildeTree(oneMenuType, menuPowerTrees, menuPowerTreeVo1);childrenMenuType.add(menuPowerTreeVo1);}Collections.sort(childrenMenuType, Comparator.comparing(MenuPowerTreeVo::getMenuSort));menuPowerTreeVo.setChildrenMenuType(childrenMenuType);}}}public ListqueryMenuPower(Integer sourceType, List {roleIds) ListpowerList = new ArrayList<>(); if (Objects.nonNull(sourceType) && CollectionUtil.isNotEmpty(roleIds)) {ListmenuPowerTrees = this.baseMapper.menuPowerTree(sourceType, roleIds); if (CollectionUtil.isNotEmpty(menuPowerTrees)) {ListhasPower = menuPowerTrees.stream().filter(e -> Objects.nonNull(e.getP()) && e.getP() > 0).collect(Collectors.toList()); if (CollectionUtil.isNotEmpty(hasPower)) {for (MenuPowerTreeDto p : hasPower) {powerList.add(p.getName());}}}}log.info("queryMenuPower.sourceType:{}.roleIds:{}.powerList:{}", sourceType, JSON.toJSONString(roleIds), JSON.toJSONString(powerList));return powerList;}}
实现效果:
{"code": "000000","msg": "success","timestamp": "2024-04-07 17:38:35","data": {"id": -1,"parentId": -1,"type": null,"name": "父节点","remarks": null,"sourceType": null,"menuSort": 0,"menuLevel": null,"hasPower": null,"childrenMenuType": [{"id": 3,"parentId": 0,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 1,"menuSort": 1,"menuLevel": 1,"hasPower": true,"childrenMenuType": [{"id": 4,"parentId": 3,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 1,"menuSort": 1,"menuLevel": 2,"hasPower": true,"childrenMenuType": null,"childrenButtonType": Array[2]},{"id": 5,"parentId": 3,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 1,"menuSort": 2,"menuLevel": 2,"hasPower": true,"childrenMenuType": null,"childrenButtonType": Array[1]},{"id": 6,"parentId": 3,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 1,"menuSort": 3,"menuLevel": 2,"hasPower": null,"childrenMenuType": null,"childrenButtonType": Array[9]},{"id": 7,"parentId": 3,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 1,"menuSort": 4,"menuLevel": 2,"hasPower": null,"childrenMenuType": null,"childrenButtonType": null},{"id": 22,"parentId": 3,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 1,"menuSort": 5,"menuLevel": 2,"hasPower": null,"childrenMenuType": null,"childrenButtonType": Array[3]},{"id": 23,"parentId": 3,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 1,"menuSort": 6,"menuLevel": 2,"hasPower": null,"childrenMenuType": null,"childrenButtonType": Array[1]}],"childrenButtonType": Array[2]}],"childrenButtonType": null}}=========================================={"code": "000000","msg": "success","timestamp": "2024-04-07 17:39:19","data": {"id": -1,"parentId": -1,"type": null,"name": "父节点","remarks": null,"sourceType": null,"menuSort": 0,"menuLevel": null,"hasPower": null,"childrenMenuType": [{"id": 28,"parentId": 0,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 1,"menuLevel": null,"hasPower": true,"childrenMenuType": null,"childrenButtonType": null},{"id": 29,"parentId": 0,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 2,"menuLevel": null,"hasPower": true,"childrenMenuType": null,"childrenButtonType": [{"id": 30,"parentId": 29,"type": 2,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 0,"menuLevel": null,"hasPower": null,"childrenMenuType": null,"childrenButtonType": null},{"id": 31,"parentId": 29,"type": 2,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 0,"menuLevel": null,"hasPower": null,"childrenMenuType": null,"childrenButtonType": null}]},{"id": 32,"parentId": 0,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 3,"menuLevel": null,"hasPower": null,"childrenMenuType": null,"childrenButtonType": null},{"id": 33,"parentId": 0,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 4,"menuLevel": null,"hasPower": null,"childrenMenuType": null,"childrenButtonType": [{"id": 35,"parentId": 33,"type": 2,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 0,"menuLevel": null,"hasPower": null,"childrenMenuType": null,"childrenButtonType": null}]},{"id": 34,"parentId": 0,"type": 1,"name": "xxx.xxx","remarks": "xxx.xxx","sourceType": 2,"menuSort": 5,"menuLevel": null,"hasPower": null,"childrenMenuType": null,"childrenButtonType": null}],"childrenButtonType": null}}
6.登录实现
package xxx.controller;import cn.dev33.satoken.stp.SaLoginConfig;import cn.dev33.satoken.stp.SaLoginModel;import cn.dev33.satoken.stp.SaTokenInfo;import cn.dev33.satoken.stp.StpUtil;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;import java.util.Objects;public class AdminController {//TODO 系统登录和登出//TODO 改为POST请求//登录public RestResponse login(String account, String pwdCipherText, Integer isRememberMe) {//TODO 可以加上登录失败次数校验,错误次数存redis中//TODO 密码加密,这里使用MD5加密,后台分配管理员设置账号时需要存储明文和加密密文,这里取的是加密密文对比//TODO 或者可以加入一个生成验证码验证,提供一个获取验证码的接口给前端,生成后输入验证码验证登录,可以防止接口被刷的风险if (StringUtils.isBlank(account) || StringUtils.isBlank(pwdCipherText)) {return RestResponse.fail("登录账号或密码不为空!");}try {//TODO 根据account、Md5加密密码pwdCipherText查询User/adminUser user = xxxxif (Objects.isNull(user)) {return RestResponse.fail("登录账号不存在!");}if ((StringUtils.isNotBlank(user.getAccount())&& !user.getAccount().equals(account))|| (StringUtils.isNotBlank(user.getCipherText()) && !user.getCipherText().equals(pwdCipherText))) {return RestResponse.fail("登录账号或密码不为正确!");}// 记住我--->`SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑SaLoginModel saLoginModel = SaLoginConfig.setDevice("PC") // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型.setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在).setIsWriteHeader(false); // 是否在登录后将 Token 写入到响应头if (Objects.nonNull(isRememberMe) && isRememberMe.intValue() == 1) {// 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值),全局的timeout设置的是1天,记住我设置的是7天saLoginModel.setTimeout(60 * 60 * 24 * 7);}//加入权限和角色ListroleList = StpUtil.getRoleList(admin.getId()); saLoginModel.setExtra("roles", roleList);ListpermissionList = StpUtil.getPermissionList(admin.getId()); saLoginModel.setExtra("permissions", permissionList);//这里的id是admin的id主键StpUtil.login(admin.getId(), saLoginModel);SaTokenInfo tokenInfo = StpUtil.getTokenInfo();return RestResponse.success(tokenInfo);} catch (Exception e) {log.error("企业会员外部系统登录异常:{}", e.getMessage());if (StringUtils.isBlank(e.getMessage())) {return RestResponse.fail("登录失败");}return RestResponse.fail("登录失败:{}", e.getMessage());}}// 查询登录状态 请求头带上login的satoken的值 Bearer XXXXXXXpublic RestResponse isLogin() {return RestResponse.success("是否登录:" + StpUtil.isLogin());}//TODO 改为POST请求//注销 请求头带上login的satoken的值 Bearer XXXXXXXpublic RestResponse logout() {StpUtil.logout();return RestResponse.success();}}
https://sa-token.cc/v/v1.36.0/doc.html#/plugin/api-sign
7.总结
https://www.justauth.cn/

https://sa-token.cc/v/v1.36.0/doc.html#/fun/curr-domain
https://sa-token.cc/v/v1.36.0/doc.html#/more/common-questions