3.Sa-Token授权

196 阅读7分钟

前置

  1. 数据库设计:juejin.cn/post/737498…
  2. 实现基础登录:juejin.cn/post/737507…

个人说明: 用户 -> 角色用户 -> 权限个人觉得实际开发大部分情况下实现一种即可,也可以两种都做,我这里看了下其实两种基本上没啥区别,两个方式揉到一块反而极大提高了代码的复杂度。

我这里由于学习必要,所以两种都做了,实际测试就只拿用户 -> 权限验证了结果的正确性

实体类

用户 -> 角色

 package com.ayo.entity;
 ​
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 ​
 import java.time.LocalDateTime;
 ​
 /**
  * 角色表
  * */
 ​
 ​
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 @Schema(name = "数据库角色对象➱Role")
 @TableName("sa_role")
 public class Role {
 ​
     @Schema(name = "角色id")
     @TableId(value = "id" , type = IdType.AUTO)
     private int id;
 ​
     @Schema(name = "角色名")
     @TableField("`name`")
     private String name;
 ​
     @Schema(name = "角色描述")
     @TableField("description")
     private String description;
 ​
     @Schema(name = "排序字段")
     @TableField("order_num")
     private int orderNum;
 ​
     @Schema(name = "创建时间")
     @TableField("`create`")
     private LocalDateTime  create;
 ​
     @Schema(name = "更新时间")
     @TableField("`update`")
     private LocalDateTime update;
 ​
 ​
 }
 ​

用户 -> 权限

 package com.ayo.entity;
 ​
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 ​
 import java.time.LocalDateTime;
 ​
 /**
  * 权限表
  */
 ​
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
 @Schema(name = "数据库权限对象➱Permission")
 @TableName("sa_permission")
 public class Permission {
 ​
     @Schema(name = "权限ID")
     @TableId(value = "id" , type = IdType.AUTO)
     private int id;
 ​
     @Schema(name = "父菜单id",description = "paren_id为0 => menu_type为M是一级菜单")
     @TableField("`parent_id`")
     private int parentId;
 ​
     @Schema(name = "权限名称")
     @TableField("`name`")
     private String name;
 ​
     @Schema(name = "权限类型",description = "M目录 C菜单")
     @TableField("menu_type")
     private char menuType;
 ​
     @Schema(name = "权限标识",description = "M目录 C菜单")
     @TableField("perms")
     private String perms;
 ​
     @Schema(name = "权限描述")
     @TableField("description")
     private String description;
 ​
     @Schema(name = "排序")
     @TableField("order_num")
     private int orderNum;
 ​
     @Schema(name = "创建时间")
     @TableField("`create`")
     private LocalDateTime create;
 ​
     @Schema(name = "更新时间")
     @TableField("`update`")
     private LocalDateTime update;
 ​
 }
 ​

DTO层

这里由于用户权限角色需要通过用户ID来从数据库查询获得,基本上不和前端做数据交互,所以这里我使用了DTO封装数据格式,用于服务器内部的传输处理

DTO的数据格式为SQL查询语句返回的格式

用户 -> 角色

 package com.ayo.model.dto;
 ​
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 ​
 /**
  * 联表查询获取到的用户角色类
  * */
 ​
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 public class UserRoleDto {
 ​
     @Schema(name = "用户id")
     private String id;
 ​
     @Schema(name = "用户名")
     private String username;
 ​
     @Schema(name = "角色id")
     private int roleId;
 ​
     @Schema(name = "角色名")
     private String roleName;
 ​
     @Schema(name = "角色描述")
     private String roleDescription;
 ​
 }

用户 -> 权限

 package com.ayo.model.dto;
 ​
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 ​
 /**
  * 联表查询获取到的用户权限类
  * */
 ​
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 public class UserPermissionDto {
     @Schema(name = "用户id")
     private String id;
 ​
     @Schema(name = "用户名")
     private String username;
 ​
     @Schema(name = "权限id")
     private int permissionId;
 ​
     @Schema(name = "权限名")
     private String permissionName;
 ​
     @Schema(name = "权限描述")
     private String permissionDescription;
 ​
     @Schema(name = "权限类型")
     private char permissionType;
 ​
     @Schema(name = "权限标识")
     private String permissionPerms;
 ​
 }

Dao层

用户 -> 角色

 /**
  * 角色Mapper
  * */
 ​
 @Repository
 @Mapper
 public interface RoleMapper extends BaseMapper<Role> {
 ​
     /* 根据用户Id获取用户角色 */
     List<UserRoleDto> getUserRolesById(@Param("userId") String userId);
 }

用户 -> 权限

 @Repository
 @Mapper
 public interface PermissionMapper extends BaseMapper<Permission> {
 ​
      /* 根据用户Id获取用户权限 */
      List<UserPermissionDto> getUserPermissionById(@Param("userId") String userId);
 ​
 }

XML文件的SQL实现

用户 -> 角色

 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ayo.dao.RoleMapper">
     <!-- 根据用户Id获取用户角色 -->
     <select id="getUserRolesById" parameterType="String" resultType="com.ayo.model.dto.UserRoleDto">
         SELECT
             u.id , u.username ,r.id AS roleId, r.`name` AS roleName, r.description AS roleDescription
         FROM
             sa_user u,sa_role r,user_role ur
         WHERE
             u.id=#{userId} AND u.id=ur.user_id AND ur.role_id=r.id;
     </select>
 </mapper>

用户 -> 权限

 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ayo.dao.PermissionMapper">
     <!-- 根据用户Id获取用户权限 -->
     <select id="getUserPermissionById" parameterType="String" resultType="com.ayo.model.dto.UserPermissionDto">
         SELECT
             res.id ,
             res.`username`,
             p.id AS permissionId,
             p.`name` AS permissionName,
             p.description AS permissionDescription,
             p.menu_type AS permissionType,
             p.perms AS  permissionPerms
         FROM
             sa_permission p
                 JOIN
             (
                 SELECT se_ur.id,se_ur.`username`,rp.permission_id
                 FROM
                     role_permission rp
                         JOIN
                     (
                         SELECT u.id , u.`username`,role_id
                         FROM (
                                  SELECT * FROM sa_user WHERE `id`=#{userId}
                              ) u
                                  JOIN user_role ur ON ur.user_id = u.id
                     )   se_ur
                     ON
                         se_ur.role_id = rp.role_id
             ) res
             ON p.id = res.permission_id # OR res.permission_id = p.parent_id
         GROUP BY p.id;
     </select>
 </mapper>

Service层

用户 -> 角色

 public interface RoleService extends IService<Role> {
 ​
     /* 根据用户Id获取用户角色 */
     List<UserRoleDto> getUserRolesById(String userId);
 ​
 }
Impl
 @Service
 public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
 ​
     /* 根据用户Id获取用户角色 */
     @Override
     public List<UserRoleDto> getUserRolesById(String userId) {
         return baseMapper.getUserRolesById(userId);
     }
 }

用户 -> 权限

 public interface PermissionService extends IService<Permission> {
 ​
     /* 根据用户Id获取用户权限 */
     List<UserPermissionDto> getUserPermissionById(@Param("userId") String userId);
 ​
 }
Impl
 @Service
 public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {
     @Override
     public List<UserPermissionDto> getUserPermissionById(String userId) {
         return baseMapper.getUserPermissionById(userId);
     }
 }

用户 -> 登录

由于SaToken没法获取到登录时的用户名,只能拿到用户ID,后续自定义权限加载接口实现类需要通过用户名去判断是不是root用户,如果是root用户就不去数据库查角色权限,直接为其赋予所有的权限"*",所以就得在登录服务里,将数据存起来,方便后期拿,当然你也可以通过用户ID去数据库查用户信息来获得用户名

我这里就使用session存了

  // 存储用户信息
 HttpSession session = request.getSession();
 session.setAttribute(user.getId(),loginRes);

源文件如下:

 
 @Service
 @RequiredArgsConstructor
 public class LoginServiceImpl extends ServiceImpl<UserMapper, User> implements LoginService {
     /* LoginServiceImpl类继承的ServiceImpl类导入了UserMapper,变量名为baseMapper,所以不再重复注入 */
     //@Autowired
     //private UserMapper userMapper;
 ​
     private final UserConvert userConvert;
 ​
     //请求对象
     private final HttpServletRequest request;
 ​
     /* 用户登录请求 */
     @Override
     public LoginRes login(LoginReq login) {
 ​
         User user = baseMapper.selectOne(
                 new LambdaQueryWrapper<User>()
                         .select(User::getId, User::getUsername, User::getPassword, User::getCreate, User::getUpdate)
                         .eq(User::getUsername, login.getUsername())
         );
 ​
         /**
          * assert(条件语句),如果条件语句为真,继续往下执行,如果为假就会报错
          * */
         Assert.notNull(user, "用户名不存在");
         Assert.isTrue(BCrypt.checkpw(login.getPassword(), user.getPassword()), "密码错误");
 ​
 ​
         // 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
         StpUtil.checkDisable(user.getId());
         // 通过校验后,再进行登录
         StpUtil.login(user.getId());
 ​
         // entity数据转为vo响应的数据
         LoginRes loginRes = userConvert.toLoginRes(user);
         loginRes.setToken(StpUtil.getTokenValue());
 ​
         // 在登录时缓存 user 对象
         //HttpSession session = request.getSession();
         //session.setAttribute(user.getId(),loginRes);
         StpUtil.getSession().set(user.getId(), loginRes);
 ​
         return loginRes;
     }
 }

自定义权限加载接口实现类

写一个StpInterface接口的实现类,实现getPermissionList方法可以为用户赋予权限集,实现getRoleList可以为用户赋予角色集

官方文档:sa-token.cc/doc.html#/u…

实现代码如下:

 
 @Component    // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
 @RequiredArgsConstructor
 public class StpInterfaceImpl implements StpInterface {
 ​
     /* 角色服务 */
     private final RoleService roleService;
 ​
     /* 权限服务 */
     private final PermissionService permissionService;
     //请求对象
     private final HttpServletRequest request;
 ​
     /* 顶级用户名 */
     private String ROOT_NAME = "root";
 ​
     /* 用户ID */
     private String getUserId() {
         return (String) StpUtil.getLoginId();
     }
 ​
     /* 用户名 => 后续可以从Redis取 */
     private String getUserName(String userId) {
         //HttpSession session = request.getSession();
         //LoginRes loginRes = (LoginRes) session.getAttribute(getUserId());
         LoginRes loginRes = (LoginRes) StpUtil.getSession().get(getUserId());
         return loginRes.getUsername();
     }
 ​
     /**
      * 返回一个账号所拥有的权限码集合
      */
     @Override
     public List<String> getPermissionList(Object o, String s) {
         // 账号所拥有的角色标识集合
         List<String> permissionList = new ArrayList<String>();
 ​
         // 验证是否为root用户
         if (getUserName(getUserId()).equals(ROOT_NAME)) {
             permissionList.add("*");
         } else {
             /* 根据ID查出用户权限信息集合 */
             List<UserPermissionDto> userPermissions = permissionService.getUserPermissionById(getUserId());
             userPermissions.forEach(item -> {
                 permissionList.add(item.getPermissionPerms());
             });
         }
         return permissionList;
     }
 ​
     /**
      * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
      */
     @Override
     public List<String> getRoleList(Object o, String s) {
         // 账号所拥有的角色标识集合
         List<String> roleList = new ArrayList<String>();
 ​
         if (getUserName(getUserId()).equals(ROOT_NAME)) {
             roleList.add("*");
         } else {
             /* 根据ID查出用户角色信息集合 */
             List<UserRoleDto> userRoles = roleService.getUserRolesById(getUserId());
             userRoles.forEach(item -> {
                 roleList.add(item.getRoleName());
             });
             //for (UserRoleDto userRole : userRoles){
             //    roleList.add(userRole.getRoleName());
             //}
         }
         return roleList;
     }
 }

Controller

用户 -> 角色

 @ApiSort(3)
 @Tag(name = "权限_角色相关", description = "权限_角色相关")
 @RestController
 @RequiredArgsConstructor
 public class RoleController {
 ​
     @ApiOperationSupport(order = 1) //Knife4j => order用于接口的显示排序
     @Operation(summary = "获取用户角色列表")
     @PostMapping("getRoleList")
     public Result getRoleList() {
         //StpUtil.getRoleList().forEach(System.out::println);
         //等效于
         //StpUtil.getRoleList().forEach((item)->{
         //    System.out.println(item);
         //});
         // 获取:当前账号所拥有的角色集合
         return Result.success(StpUtil.getRoleList());
     }
 ​
 }

用户 -> 权限

 @ApiSort(4)
 @Tag(name = "权限_权限相关", description = "权限_权限相关")
 @RestController
 @RequiredArgsConstructor
 public class PermissionController {
 ​
     @ApiOperationSupport(order = 1) //Knife4j => order用于接口的显示排序
     @Operation(summary = "获取用户权限列表")
     @PostMapping("getPermissionList")
     public Result getRoleList() {
         // 获取:当前账号所拥有的权限集合
         //StpUtil.getPermissionList().forEach(System.out::println);
         //等效于
         //StpUtil.getRoleList().forEach((item)->{
         //    System.out.println(item);
         //});
         // 获取:当前账号所拥有的角色集合
         return Result.success(StpUtil.getPermissionList());
     }
 }

用户测试接口

 @ApiSort(2)
 @Tag(name = "用户操作", description = "用户操作相关的接口")
 @RestController
 @RequestMapping("/user/")
 // 登录校验:只有登录之后才能进入该方法
 @SaCheckLogin
 public class UserController {
 ​
     @SaCheckPermission("user:email:update")
     @Operation(summary = "说你好")
     @PostMapping("/sayHello")
     public Result login() {
         return Result.success("你好啊");
     }
 }
 ​

这里我使用的是注解(@SaCheckPermission)的方式来为接口添加权限,每一个接口都是单独设置的,具体参考官方文档:sa-token.cc/doc.html#/u…

更新SaTokenConfig类,打开注解式鉴权功能

 @Configuration
 public class SaTokenConfig  implements WebMvcConfigurer {
 ​
     // 放行路径
     private final String[] EXCLUDE_PATH_PATTERNS = {
             //"/css/**",
             //"/js/**",
             "/doc.html",
             "/webjars/**",
             //"/swagger-resources/**",
             "/swagger-resources",
             //http://127.0.0.1:8081/v3/api-docs/swagger-config接口为swagger主要的接口
             "/v3/api-docs/**",
             //
             "favicon.ico",
             "/login",
             "/logout",
     };
 ​
     private final long timeout = 600;
 ​
     // 注册 Sa-Token 拦截器,打开注解式鉴权功能
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         // 注册 Sa-Token 拦截器,打开注解式鉴权功能
         registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
     }
 ​
     /**
      * 注册 [Sa-Token全局过滤器]
      */
     @Bean
     public SaServletFilter getSaServletFilter() {
         return new SaServletFilter()
                 // 拦截路径
                 .addInclude("/**")
                 // 放行路由(开放地址)
                 .addExclude(EXCLUDE_PATH_PATTERNS)
                 // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
                 .setBeforeAuth(new BeforeAuthFilter())
                 // 认证函数: 每次请求执行
                 .setAuth(new AuthFilter(timeout, EXCLUDE_PATH_PATTERNS))
                 //  异常处理函数:每次认证函数发生异常时执行此函数
                 .setError(new ErrorFilter());
     }
 ​
 ​
 }

如果要统一设置的话,参考官方文档:sa-token.cc/doc.html#/u…

本地测试地址

http://127.0.0.1:8081/doc.html

项目Gitee地址

gitee.com/CnAyo/code-…