Spring Boot + Redis + Sa-Token
参考文献
Sa-Token实现分布式登录鉴权(Redis集成 前后端分离)-腾讯云开发者社区-腾讯云
介绍
StpInterface
是 Sa-Token 框架中的一个接口,属于 Sa-Token 身份认证与授权框架的一部分。该接口提供了一些方法来实现自定义的身份认证和授权管理功能,特别是针对自定义的权限验证。
StpInterface类的主要功能
StpInterface
用于定义 Sa-Token 中与用户身份相关的核心操作接口。通过实现这个接口,用户可以自定义如何获取用户信息、验证用户身份、判断是否有权限等。
主要方法
StpInterface
主要包括以下几个常用的方法:
-
getLoginId()
:获取当前登录用户的唯一标识(例如用户 ID)。String getLoginId();
-
isLogin()
:判断当前是否已登录。boolean isLogin();
-
login(Object loginId)
:登录方法,传入一个唯一标识来进行用户登录。void login(Object loginId);
-
logout()
:登出方法,清除用户的登录状态。void logout();
-
hasPermission(String permission)
:判断当前登录用户是否具有某个权限。boolean hasPermission(String permission);
-
hasRole(String role)
:判断当前登录用户是否拥有某个角色。boolean hasRole(String role);
使用场景
- 自定义身份认证:如果需要自定义登录逻辑或用户身份验证,可以实现
StpInterface
接口来替代 Sa-Token 默认的用户认证方式。 - 角色与权限管理:通过
hasRole
和hasPermission
等方法,进行角色与权限的验证,保证应用中的授权机制符合业务需求。
示例
以下是一个简单的实现例子,展示了如何实现 StpInterface
接口来定制认证与授权逻辑:
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
@Component
public class MyStpInterface implements StpInterface {
@Override
public String getLoginId() {
// 返回当前登录用户的ID
return "123"; // 假设返回用户ID为123
}
@Override
public boolean isLogin() {
// 判断当前用户是否登录
return true; // 假设用户已登录
}
@Override
public void login(Object loginId) {
// 实现用户登录逻辑
// 这里可以根据传入的loginId来设置用户的登录状态
}
@Override
public void logout() {
// 实现登出逻辑
// 清除用户的登录状态
}
@Override
public boolean hasPermission(String permission) {
// 判断用户是否有某个权限
return "admin".equals(permission); // 假设只有管理员有权限
}
@Override
public boolean hasRole(String role) {
// 判断用户是否有某个角色
return "admin".equals(role); // 假设只有管理员有该角色
}
}
通过实现 StpInterface
,你可以根据实际业务需求来定制用户认证、登录状态、权限验证等操作。
基于原有的基础 | 创建MySQL表单存储权限
-- 角色表
CREATE TABLE roles (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(50) NOT NULL
);
-- 权限表
CREATE TABLE permissions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_name VARCHAR(50) NOT NULL
);
-- 用户角色关联表
CREATE TABLE users_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
-- 角色权限关联表
CREATE TABLE roles_permissions (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id)
);
-- 用户表
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 用户唯一标识(主键)
username VARCHAR(50) NOT NULL UNIQUE, -- 用户名(唯一)
password VARCHAR(255) NOT NULL, -- 密码(加密存储)
email VARCHAR(100) DEFAULT NULL, -- 邮箱(可选)
phone VARCHAR(20) DEFAULT NULL, -- 手机号(可选)
status TINYINT DEFAULT 1, -- 用户状态(1=启用, 0=禁用)
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间
last_login_time TIMESTAMP NULL DEFAULT NULL -- 上次登录时间
);
INSERT INTO users (username, password, email, phone, status, create_time, last_login_time) VALUES
('zhangsan', '$2a$10$eC9yWZaMjMEbfBOAAsXHg.SUz3aHtYZJ/riMjHJ.TOu3NHsMFTm.a', 'zhangsan@example.com', '1234567890', 1, NOW(), NULL),
('lisi', '$2a$10$txB4zY7lqr9Kx.XHcGB5ruMiOBpFMHLF9rljN5iGtZ1o26g/.Agxe', 'lisi@example.com', '0987654321', 1, NOW(), NULL);
INSERT INTO roles (id, role_name) VALUES
(1, 'ADMIN'),
(2, 'USER');
INSERT INTO permissions (id, permission_name) VALUES
(1, 'user.add'),
(2, 'user.delete'),
(3, 'user.update'),
(4, 'user.view');
INSERT INTO users_roles (user_id, role_id) VALUES
(1, 1), -- zhangsan -> ADMIN
(2, 2); -- lisi -> USER
INSERT INTO roles_permissions (role_id, permission_id) VALUES
(1, 1), -- ADMIN 拥有 user.add 权限
(1, 2), -- ADMIN 拥有 user.delete 权限
(1, 3), -- ADMIN 拥有 user.update 权限
(1, 4), -- ADMIN 拥有 user.view 权限
(2, 4); -- USER 只拥有 user.view 权限
注意:密码已经使用 bcrypt 加密,明文分别为:
- zhangsan:
password123
- lisi:
mypassword
UserController
package com.example.satokendemo.controller;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.satokendemo.mapper.UserMapper;
import com.example.satokendemo.mapper.PermissionMapper;
import com.example.satokendemo.pojo.User;
import com.example.satokendemo.util.PasswordUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionMapper permissionMapper;
/**
* 用户登录
*/
@RequestMapping("/doLogin")
public SaResult doLogin(String username, String password) {
// 第1步:从数据库查询用户信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper); // 查询用户
if (user == null) {
System.out.println("用户不存在");
} else {
System.out.println("找到用户: " + user.getUsername());
}
if (!PasswordUtil.verify(password, user.getPassword())) {
System.out.println("用户输入密码: " + password);
System.out.println("数据库存储的加密密码: " + user.getPassword());
System.out.println("密码校验失败");
}
// 如果用户不存在或者密码不匹配,返回登录失败
if (user == null || !PasswordUtil.verify(password, user.getPassword())) { // 使用加密策略校验密码
return SaResult.error("用户名或密码错误");
}
// 第2步:登录
StpUtil.login(user.getId());
// 第3步:加载用户信息和权限信息
StpUtil.getSession().set("loginInfo", user);
// 加载用户权限
List<String> authList = permissionMapper.getPermissionsByUserId(user.getId());
StpUtil.getSession().set("authList", authList);
// 第4步:获取 Token 相关参数
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
// 第5步:返回给前端
return SaResult.data(tokenInfo);
}
/**
* 查询登录状态
*/
@RequestMapping("/isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
/**
* 获取当前登录用户信息
*/
@RequestMapping("/getUserInfo")
public User getUserInfo() {
return (User) StpUtil.getSession().get("loginInfo");
}
/**
* 测试方法:校验权限 - 添加操作
*/
@GetMapping("/add")
public String add() {
StpUtil.checkPermission("user.add");
return "ok";
}
/**
* 测试方法:校验权限 - 更新操作
*/
@GetMapping("/update")
public String update() {
StpUtil.checkPermission("user.update");
return "ok";
}
}
这里我把controller中的模拟数据改成了mysql数据库的数据并将密码进行加密,注册时也可以通过相应的方法对明文密码进行加密处理后存储至数据库中,使得数据更加安全。
完成后的图例应该如下图所示:
数据库的用户表单(角色,权限,具体信息,绑定关系…)
在原本代码的基础上和MySQL连接并完成权限认证与登录
其它的表单的实体类都可以直接使用mybatis-plus去完成增删改查,但permissions表单的实体类得在mapper中添加一个方法使用。
PermissionMapper
完成对权限的使用
public interface PermissionMapper extends BaseMapper<Permission> {
// 获取用户的所有权限名称
@Select("SELECT p.permission_name " +
"FROM permissions p " +
"JOIN roles_permissions rp ON p.id = rp.permission_id " +
"JOIN users_roles ur ON rp.role_id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<String> getPermissionsByUserId(@Param("userId") Long userId);
}
之后是我yml的配置
server:
# 端口
port: 8081
spring:
datasource:
username: root
password: 20050101
url: jdbc:mysql://localhost:3306/sa_token?serverTimezone=UTC&userUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
# redis配置
redis:
# Redis数据库索引(默认为0)
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
jpa:
open-in-view: false
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.satokendemo.pojo
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
这是spring boot项目的大体框架,可以按照我这个来。
之后启动项目
通过api工具postman进行登录测试,例如:http://localhost:8082/user/doLogin?username=lisi&password=mypassword
/**
* 示例代码:生成加密密码(可用于初始化数据库)
*/
public static void main(String[] args) {
String userPassword = "password123"; // 明文密码
String storedPassword = "$2a$10$eC9yWZaMjMEbfBOAAsXHg.SUz3aHtYZJ/riMjHJ.TOu3NHsMFTm.a"; // 从数据库获取的加密密码
boolean isValid = PasswordUtil.verify(userPassword, storedPassword);
System.out.println("密码验证结果: " + isValid);
String encryptedPassword = PasswordUtil.encrypt(userPassword);
System.out.println("加密后的密码: " + encryptedPassword);
System.out.println("加密后的密码与数据库中的密码是否匹配: " + PasswordUtil.verify(userPassword, encryptedPassword
)); // 再次验证加密后的密码
}
记得用这个工具类去改一下数据库的加密密码
使用postman测试后可以得到以下信息
在redis中也可以看到我们的数据已经传递成功了
无论是权限还是相关的用户信息都是已经成功传到redis缓存了,之后就是携带token去测试接口调用看是否符合我们的权限。
不携带token去访问接口 http://localhost:8081/user/add
携带token访问
没有相应的权限的用户携带token去访问