前置
- 数据库设计:juejin.cn/post/737498…
- 实现基础登录: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可以为用户赋予角色集
实现代码如下:
@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