集成sa-token实现登录和RBAC权限控制

集成sa-token实现登录和RBAC权限控制

文章目录

  • 1.sa-token是什么?
    • 1.1简介
    • 1.2官网
    • 1.3 Sa-Token 功能一览
    • 1.4 功能结构图
  • 2.集成sa-token及配置
    • 2.1 pom依赖
    • 2.2 yaml配置
    • 2.3 代码配置
  • 4.RBAC权限控制表设计
  • 5.菜单权限树构造实现
    • 5.1菜单权限数据sql查询
    • 5.2菜单权限树构建
  • 6.登录实现
  • 7.总结

1.sa-token是什么?

1.1简介

  Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

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依赖

  sa-token的依赖组件也很多,根据自己的需求去官方网站参考引入即可:

        <dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.37.0</version></dependency><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-jwt</artifactId><version>1.37.0</version></dependency><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-redis-jackson</artifactId><version>1.37.0</version></dependency><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-aop</artifactId><version>1.37.0</version></dependency>

2.2 yaml配置

sa-token:# token 名称(同时也是 cookie 名称)token-name: satokentoken-prefix: Bearer# token 有效期(单位:秒) 默认30天,-1 代表永久有效 这里设置为1天timeout: 86400# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结active-timeout: -1# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)is-concurrent: true# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)is-share: false# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)token-style: uuid# 是否输出操作日志is-log: true# jwt秘钥jwt-secret-key: adfdfdsdasdasifdfdffhueuiwyudfdfddfdfsfsdfrfewbfjsdafjk

  解决反向代理 uri 丢失的问题

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

2.3 代码配置

  SaTokenConfigure配置jwt简单模式、全局过滤器SaServletFilter,RestResponse该类是自定义响应前端的类,可以自己去定义写,下面的代码只是一个大概的雏形,项目使用前后端分离的方式所以需要使用SaServletFilter的方式配置全局过滤器(所以不使用拦截器的方式配置),下面的配置决绝了跨越问题,配合上面引入的sa-token-spring-aop注解权限校验,在任意地方可以使用sa-token的注解鉴权了(@SaIgnore:不拦截,直接放行;@SaCheckPermission(“xxx.xxxx”):有xxx.xxxx权限才可以访问,官方还支持很多注解权限校验注解的),注意:sa-token-spring-aop + 全局过滤器SaServletFilter这种配合使用是没有啥问题的,使用拦截器的方式就不用引入sa-token-spring-aop了,拦截器默认只是控制到controller层,而sa-token-spring-aop + 全局过滤器SaServletFilter的方式是可以在任意位置都可以使用注解权限校验。

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;@Slf4j
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {// Sa-Token 整合 jwt (Simple 简单模式)@Beanpublic StpLogic getStpLogicJwt() {return new StpLogicJwtForSimple();}/*** 注册 [Sa-Token 全局过滤器]*/@Beanpublic 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 的自定义权限验证扩展*/
@Component
public class StpInterfaceImpl implements StpInterface {/*** 返回一个账号所拥有的权限码集合*/@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {List<String> permissionList = new ArrayList<>();//TODO 根据登录的loginId(登录用户id)去查权限,可以存缓存中,从缓存中取,权限有变动更新缓存return permissionList;}/*** 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {List<String> roleList = new ArrayList<>();//TODO 根据登录的loginId(登录用户id)去查角色,可以存缓存,从缓存中取,角色变动更新缓存return roleList;}}

4.RBAC权限控制表设计

  RBAC:基于角色的访问控制(需要实现对用户、角色、资源的管理)

  用户-角色-资源之间的对应关系是多对多的一个关系

  角色表-role表:

role表

  资源表-resource表:

资源表-resource表:

  角色所拥有的资源权限表-role_resource_power表:

role_resource_power表

  角色用户表-role-admin(role-user)表:

role-admin(role-user)表

  资源表-resource表里面有一个父级id,顶级父类的父类id是0或者是null,子资源需要设置所属哪个父资源下,所以就需要设置子资源的父级id,这种关系就形成了一颗菜单权限树。

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 p
FROM menu_power_tree a
ORDER BY parent_id,id;

5.2菜单权限树构建

  基础接口RoleResourcePowerMapper:

public interface RoleResourcePowerMapper extends BaseMapper<RoleResourcePower> {/*** 角色对应的菜单权限用于获取权限和构建权限** @param sourceType* @param roleIds* @return*/List<MenuPowerTreeDto> menuPowerTree(@Param("sourceType") Integer sourceType, @Param("roleIds") List<Integer> 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 <![CDATA[<> ]]>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,id</select>

  MenuPowerTreeDto类:

package xxxx.dto;import lombok.Data;import java.io.Serializable;@Data
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;@Data
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 List<MenuPowerTreeVo> childrenMenuType;private List<MenuPowerTreeVo> childrenButtonType;}

  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;@Slf4j
@Service
public class RoleResourcePowerServiceImpl extends ServiceImpl<RoleResourcePowerMapper, RoleResourcePower> implements RoleResourcePowerService {@Overridepublic MenuPowerTreeVo queryMenuPowerTreeVo(Integer sourceType, Integer roleId) {if (Objects.nonNull(sourceType) && Objects.nonNull(roleId)) {List<MenuPowerTreeDto> menuPowerTrees = this.baseMapper.menuPowerTree(sourceType, Arrays.asList(roleId));if (CollectionUtil.isNotEmpty(menuPowerTrees)) {MenuPowerTreeVo menuPowerTreeVo = new MenuPowerTreeVo();List<MenuPowerTreeDto> oneMenuPowerTrees = menuPowerTrees.stream().filter(e -> e.getParentId() == 0).collect(Collectors.toList());if (CollectionUtil.isNotEmpty(oneMenuPowerTrees)) {this.buildeTree(oneMenuPowerTrees, menuPowerTrees, menuPowerTreeVo);if (oneMenuPowerTrees.size() == 1) {List<MenuPowerTreeVo> children = 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(List<MenuPowerTreeDto> oneMenuPowerTrees, List<MenuPowerTreeDto> menuPowerTrees, MenuPowerTreeVo menuPowerTreeVo) {if (oneMenuPowerTrees.size() > 1) {menuPowerTreeVo.setId(-1);menuPowerTreeVo.setName("父节点");menuPowerTreeVo.setParentId(-1);menuPowerTreeVo.setMenuSort(0);List<MenuPowerTreeDto> childrens = 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();List<MenuPowerTreeDto> childrens = menuPowerTrees.stream().filter(e -> Objects.nonNull(e.getParentId()) && e.getParentId() == id).collect(Collectors.toList());this.commonBuildTree(menuPowerTrees, menuPowerTreeVo, childrens);}}}private void commonBuildTree(List<MenuPowerTreeDto> menuPowerTrees, MenuPowerTreeVo menuPowerTreeVo, List<MenuPowerTreeDto> childrens) {if (CollectionUtil.isNotEmpty(childrens)) {List<MenuPowerTreeDto> childrenButtonTypeDto = childrens.stream().filter(e -> Objects.nonNull(e.getType()) && e.getType() == 2).collect(Collectors.toList());if (CollectionUtil.isNotEmpty(childrenButtonTypeDto)) {List<MenuPowerTreeVo> childrenButtonType = 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);}List<MenuPowerTreeDto> childrenMenuTypeDto = childrens.stream().filter(e -> Objects.nonNull(e.getType()) && e.getType() == 1).collect(Collectors.toList());if (CollectionUtil.isNotEmpty(childrenMenuTypeDto)) {List<MenuPowerTreeVo> childrenMenuType = new ArrayList<>();for (MenuPowerTreeDto mp2 : childrenMenuTypeDto) {MenuPowerTreeVo menuPowerTreeVo1 = new MenuPowerTreeVo();List<MenuPowerTreeDto> oneMenuType = new ArrayList<>();oneMenuType.add(mp2);this.buildeTree(oneMenuType, menuPowerTrees, menuPowerTreeVo1);childrenMenuType.add(menuPowerTreeVo1);}Collections.sort(childrenMenuType, Comparator.comparing(MenuPowerTreeVo::getMenuSort));menuPowerTreeVo.setChildrenMenuType(childrenMenuType);}}}@Overridepublic List<String> queryMenuPower(Integer sourceType, List<Integer> roleIds) {List<String> powerList = new ArrayList<>();if (Objects.nonNull(sourceType) && CollectionUtil.isNotEmpty(roleIds)) {List<MenuPowerTreeDto> menuPowerTrees = this.baseMapper.menuPowerTree(sourceType, roleIds);if (CollectionUtil.isNotEmpty(menuPowerTrees)) {List<MenuPowerTreeDto> hasPower = 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}
}

  根据角色id可以构建一棵该角色所拥有的资源权限树,返回给前端遍历展示菜单权限树,用于重新给该角色勾选菜单权限,然后将勾选的资源id和角色id写入到角色所拥有的资源权限表-role_resource_power表中,勾选的资源id可以通过跟数据库里面求一个交并补集来实现重新设置新的权限,比如说roleId为1的角色,现在从数据库查出来的资源id是[1,2,3],后面重新勾选授权前端传给厚端的资源id集合是[3,4,5],两次操作3这个资源没有变,之前的拥有的1,2资源权限删除,4,5新给的权限插入即可,这种一操作就达到了给加色授权的目的,其它的管理操作都是CRUD了。

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;@Slf4j
@RestController
@RequestMapping("/user")
public class AdminController {//TODO 系统登录和登出//TODO 改为POST请求//登录@RequestMapping("/login")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);}//加入权限和角色List<String> roleList = StpUtil.getRoleList(admin.getId());saLoginModel.setExtra("roles", roleList);List<String> permissionList = 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 XXXXXXX@RequestMapping("/isLogin")public RestResponse isLogin() {return RestResponse.success("是否登录:" + StpUtil.isLogin());}//TODO 改为POST请求//注销 请求头带上login的satoken的值 Bearer XXXXXXX@RequestMapping("/logout")public RestResponse logout() {StpUtil.logout();return RestResponse.success();}}

  登录的雏形基本已经实现了,如果你对访问接口的安全性有要求还可以使用sa-token的一个很好的功能:API 接口参数签名

https://sa-token.cc/v/v1.36.0/doc.html#/plugin/api-sign

  使用该功能让你写的系统安全性更高。

7.总结

  由于最近在写一个项目,涉及到后台管理登录管理等功能,所以就构思了基于RBAC角色资源访问控制设计实现了菜单权限的控制,控制权限可以精确到按钮级别,然后接触了sa-token的这个国产开源框架,加入了社区交流群和参看官方文档(仔细看才不会遗漏任何一句有用的话),将项目源码拉下来大概的翻了一下,实现的还是挺优雅的,在项目中集成使用让你的登录功能、菜单权限功能的实现更加优雅,代码量更少,只需要按需引入依赖,简单配置即可优雅实现功能,让开发人员只需要去关注解决业务问题即可,相比于Spring Security+OAuth2来实现登录认证来说,代码量更少、更简单,还有一个开源项目值得更大家分享,JustAuth开箱即用的整合第三方登录的开源组件

https://www.justauth.cn/

有兴趣的可以去看一看,在集成使用sa-token的时候会遇到的问题:

  1.跨越问题:上面有解决方法。

  2.项目中集成了fegin方式的接口调用,fegin接口调用说白了其实本质还是一个http请求,所以会被sa-token拦截根据uri校验,所以只需要将fegin的接口的顶级uil路径写入到SaServletFilter().addExclude(“/xxxx/xxxx”)或者在SaServletFilter().setAuth(obj -> {SaRouter.notMatch(“/xxxxx/xxxx”)})里面即可放行。

3.集成了sa-token-spring-aop使用@SaIgnore注解不生效,这个问题正常集成是没有啥问题的,不生效估计是项目依赖有冲突导致不成效,所以可以使用如下办法:

image-20240416105243542

将加了@SaIgnore的请求方法路径解析为一个List 设置在SaServletFilter().addExclude(“/xxxx/xxxx”)或者在SaServletFilter().setAuth(obj -> {SaRouter.notMatch(“/xxxxx/xxxx”)})里面即可放行,上面是一个sa-token的群友写的,我觉得写的还是可以的,拿来分享下。

  3.解决反向代理 uri 丢失的问题

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

  4.其它问题:参看官方的常见问题排查

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

  到此我分享就结束了,希望能对你有所启发和帮助,请一键三连,么么么哒!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/823099.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

solidity(10)

&#x1f52e; Solidity变量指南&#xff1a;探寻状态、局部和全局三界之秘&#x1f30c; 在Solidity的神秘大陆上&#xff0c;变量就像是散落在时间和空间中的宝石&#x1f48e;&#xff0c;每颗都有它独特的魔力和作用域。掌握这三种变量&#xff0c;是成为合约法师的重要一…

Vue项目学习(一)-SQL闯关

Hello , 我是小恒不会java。今天来阅读一个Vue纯前端项目--SQL在线闯关 进步的方法除了文档书籍视频&#xff0c;学会阅读源代码&#xff0c;从代码中学会解决需求的方法也是必要的 已部署完成&#xff0c;在线体验&#xff1a;http://sql.yunduanjianzhan.cn 背景 简介 闯…

《黑神话:悟空》现已正式上架PS商城,今晚或有大动作

关于《黑神话&#xff1a;悟空》的消息可谓是喜闻乐见&#xff01;今天晚上19:10可能会有相关游戏内容放出&#xff0c;让人非常期待。而海信电视推出的《黑神话&#xff1a;悟空》专属画质模式&#xff0c;让玩家可以享受到更加细腻的游戏画面。 此外&#xff0c;海信和《黑神…

高级感拉满的个人UI网页

效果图 PC端 移动端 部分代码 index.html <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>Zboy的主页</title><link rel"stylesheet" href"css/normalize.css" /><link rel&qu…

SAP项目任务一览表

根据SAP Activate项目管理方法论的主要精神&#xff0c;浓缩到一些主要的团队和任务。 主要的团队有&#xff1a; 项目管理(办公室)Project Management(office)&#xff1a;项目经理团队&#xff0c;包括项目办公室。负责项目整体运行和监控&#xff0c;项目办公室负责项目的…

李沐-19 卷积层【动手学深度学习v2】

记录下关于权重下标变换的理解&#xff1a; 从原来的Wi,j到Wi,j,k,l是从二维到四维的过程&#xff0c;如下图所示 对全连接层使用平移不变性(如&#xff1a;卷积核在移动过程是不变的)和局部性&#xff08;如&#xff1a;卷积核有一定大小&#xff09;得到卷积层&#xff0c;这…

【leetcode】双指针算法技巧——滑动窗口

标题&#xff1a;【leetcode】双指针算法技巧——滑动窗口 水墨不写bug 正文开始&#xff1a; 滑动窗口介绍 滑动窗口是一种常用的算法技巧&#xff0c;用于解决一些涉及 连续子数组或子串 的问题。它的基本思想是 维护一个窗口&#xff0c;通过 在窗口内移动 来寻找满…

如何查询RGB图像的三维numpy数组中有多少个不同的RGB点,并打印具体数值?

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

xgplayer插件的使用-西瓜播放器 ---- Vue3中使用

xgplayer 视频 一、xgplayer官网 xgplayer官网 - 点我进入 一、xgplayer简单介绍 西瓜播放器是字节跳动推出的一款播放器。 稳定性好&#xff08;大厂&#xff08;字节跳动&#xff09;出品&#xff09;&#xff1b;插件方便实用且简洁高雅&#xff1b;官网文档清晰&#xff1b…

[Linux - C] 自主Shell

[Linux - C] 自主Shell [Linux - C语言] 自主Shell逻辑策划 main()打印命令行 void MakeCommandLineAndPrint()用户名 USER主机名 HOSTNAME当前目录 PWDSkipPath 切割目录打印命令行 获取用户字符串 int GetUserCommand()检查重定向 void CheckRedir()切割字符 void SplitComma…

数据加密、文档加密为什么都选择安企神软件

数据加密、文档加密为什么都选择安企神软件 免费试用安企神 在数据加密和文件加密领域&#xff0c;有众多优秀的软件&#xff0c;他们功能各异、价格不同、效果也大相径庭&#xff0c;经过对比使用、用户口碑和技术网站评判&#xff0c;安企神在各方面都稳坐第一把交易。其原…

新闻媒体行业邮件推广:精准推送,创造价值

在当今信息爆炸的时代&#xff0c;新闻行业如何在竞争激烈的市场中脱颖而出&#xff0c;吸引读者的目光&#xff0c;成为了每个新闻机构都需要认真思考的问题。许可式邮件营销成为了一种强大的工具&#xff0c;不仅能够向订阅者发送新闻期刊&#xff0c;还能够向广告商发送宣传…

【基础物理实验】【AFM虚拟实验】基于AFM的物质表面微观结构及力学性质表征仿真实验(下)【北京航空航天大学】

本次实验&#xff08;上&#xff09;见博客&#xff1a;【基础物理实验】【AFM虚拟实验】基于AFM的物质表面微观结构及力学性质表征仿真实验&#xff08;上&#xff09;【北京航空航天大学】 本次实验&#xff08;中&#xff09;见博客&#xff1a;【基础物理实验】【AFM虚拟实…

LLamaSharp加载llama.cpp转化好的模型

新建.net8控制台项目 安装依赖包 LLamaSharp和LLamaSharp.Backend.Cpu 准备好转化好的模型 没有的话参考这篇文章https://blog.csdn.net/qq_36437991/article/details/137248622 编写代码 using LLama; using LLama.Common; using LLama.Native;namespace llamasharpstu…

N皇后问题(DFS解决)

文章目录 一、题目分析二、对角线判断&#xff08;分两种&#xff09;三、代码演示 先赞后看&#xff0c;养成习惯&#xff01;&#xff01;&#xff01;^ _ ^<3 ❤️ ❤️ ❤️ 码字不易&#xff0c;大家的支持就是我坚持下去的动力。点赞后不要忘了关注我哦&#xff01; 一…

全球7大指纹浏览器排行榜:哪个最适合你?

在数字时代&#xff0c;我们每一次上网都会留下独特的数字足迹&#xff0c;被称为“浏览器指纹”。为了保护这些私人信息不被滥用&#xff0c;指纹浏览器成为了一个重要工具。但是&#xff0c;并非所有的指纹浏览器都是一样的&#xff0c;它们各有特点&#xff0c;适用于不同的…

数字乡村创新实践探索农业现代化路径:科技赋能农业产业升级、提升乡村治理效能与农民幸福感

随着信息技术的快速发展和数字化时代的到来&#xff0c;数字乡村建设正成为推动农业现代化、提升农业产业竞争力、优化乡村治理以及提高农民幸福感的重要途径。本文将围绕数字乡村创新实践&#xff0c;探讨其在农业现代化路径中的积极作用&#xff0c;以及如何通过科技赋能实现…

28. 找出字符串中第一个匹配项的下标(KMP)

class Solution {public int[] getNext(int[] next,String s){//j有两层含义&#xff1a;&#xff08;1&#xff09;最长公共前后缀的长度&#xff08;2&#xff09;前缀的末尾&#xff0c;是即将匹配的那个位置int j 0;//i含义&#xff1a;后缀的末尾&#xff0c;是即将匹配的…

Python疑难杂症(20)---介绍Python的pandas模块中将数据导入内存和导出数据的方法,以及一些参数的用法。

Python的pandas模块中数据框这种数据类型&#xff0c;可以通过文件导入函数&#xff0c;将磁盘上的csv、execl等类型的文件装入内存&#xff0c;并生成数据框的格式&#xff0c;以方便后续使用pandas的专有方法进行处理。 6、DataFrame数据输导入导出方法 Pandas常用的读取数…

安装ps提示MSVCP140.dll丢失怎么办,推荐几种有效的解决方法

在成功完成Adobe Photoshop&#xff08;简称PS&#xff09;软件的安装过程之后&#xff0c;当用户试图启动并运行该程序时&#xff0c;系统却弹出了一个令人困扰的错误提示信息&#xff0c;明确指出&#xff1a;“无法找到MSVCP140.dll”这一关键文件。这意味着尽管PS软件已经成…