Spring Boot + Redis + Sa-Token

参考文献

Sa-Token实现分布式登录鉴权(Redis集成 前后端分离)-腾讯云开发者社区-腾讯云

介绍

StpInterface 是 Sa-Token 框架中的一个接口,属于 Sa-Token 身份认证与授权框架的一部分。该接口提供了一些方法来实现自定义的身份认证和授权管理功能,特别是针对自定义的权限验证。

StpInterface类的主要功能

StpInterface 用于定义 Sa-Token 中与用户身份相关的核心操作接口。通过实现这个接口,用户可以自定义如何获取用户信息、验证用户身份、判断是否有权限等。

主要方法

StpInterface 主要包括以下几个常用的方法:

  1. getLoginId():获取当前登录用户的唯一标识(例如用户 ID)。

    String getLoginId();

  2. isLogin():判断当前是否已登录。

    boolean isLogin();

  3. login(Object loginId):登录方法,传入一个唯一标识来进行用户登录。

    void login(Object loginId);

  4. logout():登出方法,清除用户的登录状态。

    void logout();

  5. hasPermission(String permission):判断当前登录用户是否具有某个权限。

    boolean hasPermission(String permission);

  6. hasRole(String role):判断当前登录用户是否拥有某个角色。

    boolean hasRole(String role);

使用场景
  • 自定义身份认证 :如果需要自定义登录逻辑或用户身份验证,可以实现 StpInterface 接口来替代 Sa-Token 默认的用户认证方式。
  • 角色与权限管理 :通过 hasRolehasPermission 等方法,进行角色与权限的验证,保证应用中的授权机制符合业务需求。
示例

以下是一个简单的实现例子,展示了如何实现 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/doLoginusername=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去访问

相关推荐
uzong5 小时前
技术故障复盘模版
后端
GetcharZp5 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程6 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi6 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
一只爱撸猫的程序猿7 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
甄超锋7 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
阿华的代码王国7 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy7 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
鼠鼠我捏,要死了捏7 小时前
生产环境Redis缓存穿透与雪崩防护性能优化实战指南
redis·cache