一文搞定Spring Boot权限认证:登录、权限、踢人、JWT、签名校验全覆盖

前言

Sa-Token 是一个轻量级、功能强大的 Java 权限认证框架,与 Spring Boot 完美集成,主打登录认证、权限验证、Session会话、单点登录、OAuth2.0、微服务网关鉴权等功能。该框架具有零配置启动、API极简、高扩展性等特点,能够以简单优雅的方式解决系统权限认证问题。

Sa-Token 目前最新版本为 v1.44.0,GitHub 关注量已超过 18k+ Star,是主要竞争框架 Spring Security 的 1.97 倍,Apache Shiro 的 4.18 倍,曾获 GVP - Gitee 最有价值开源项目、OSCHINA 2021 人气指数 TOP 30 开源项目等多项荣誉。

一、Spring Boot项目快速整合

1.1 创建Spring Boot项目

使用 Spring Initializr 创建项目,或在现有 Spring Boot 项目中添加依赖。

项目结构示例:

src/main/java
├── com/example/satoken
│   ├── SaTokenApplication.java          # 启动类
│   ├── config
│   │   ├── SaTokenConfigure.java        # Sa-Token配置类
│   │   └── StpInterfaceImpl.java        # 权限接口实现
│   ├── controller
│   │   ├── LoginController.java         # 登录控制器
│   │   ├── UserController.java          # 用户控制器
│   │   ├── AdminController.java         # 管理员控制器
│   │   └── ApiController.java           # API接口控制器
│   └── model
│       └── User.java                    # 用户实体类
src/main/resources
└── application.yml                      # 配置文件

1.2 添加 Maven 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>sa-token-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>1.8</java.version>
        <sa-token.version>1.44.0</sa-token.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Sa-Token 核心依赖 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>${sa-token.version}</version>
        </dependency>

        <!-- Sa-Token 整合 Redis(使用jackson序列化) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-redis-jackson</artifactId>
            <version>${sa-token.version}</version>
        </dependency>

        <!-- Redis 连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

1.2 创建Spring Boot启动类

package com.example.satoken;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Sa-Token Spring Boot 启动类
 */
@SpringBootApplication
public class SaTokenApplication {
    public static void main(String[] args) {
        SpringApplication.run(SaTokenApplication.class, args);
        System.out.println("Sa-Token Spring Boot项目启动成功!");
    }
}

1.3 基础配置 (application.yml)

server:
  port: 8080
  servlet:
    context-path: /api

spring:
  application:
    name: sa-token-demo
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

# Sa-Token 配置
sa-token:
  # token 名称(默认 satoken)
  token-name: Authorization
  # token 有效期(秒),默认 7 天
  timeout: 2592000
  # token 最低活跃时间(秒),默认 0(不刷新)
  active-timeout: 1800
  # 是否允许同一账号多地登录(true=允许,false=踢掉前一个)
  is-concurrent: true
  # 是否启用二级认证(如短信验证码)
  is-share: false
  # token 风格(默认为:uuid)
  token-style: uuid
  # 是否输出日志
  is-log: true

💡 小贴士:初学者可以先不配,全部用默认值!

⚠️ 重要:只要引入 sa-token-redis-jackson 依赖并配置Redis,Sa-Token 会自动使用 Redis!

1.4 实现登录接口

package com.example.satoken.controller;

import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 登录控制器
 */
@RestController
@RequestMapping("/auth")
public class LoginController {

    // 模拟数据库用户(实际应查数据库)
    private static final Map<String, String> USER_DB = new HashMap<>();
    static {
        USER_DB.put("admin", "123456");
        USER_DB.put("user", "123456");
    }

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public Map<String, Object> login(@RequestParam String username, @RequestParam String password) {
        Map<String, Object> result = new HashMap<>();

        // 1. 校验账号密码
        if (!USER_DB.containsKey(username) || !USER_DB.get(username).equals(password)) {
            result.put("code", 401);
            result.put("msg", "账号或密码错误");
            return result;
        }

        // 2. 写入当前登录用户 ID(Sa-Token 自动创建 Token 并绑定会话)
        Long userId = "admin".equals(username) ? 10001L : 10002L;
        StpUtil.login(userId);

        // 3. 返回Token和用户信息
        result.put("code", 200);
        result.put("msg", "登录成功");
        result.put("token", StpUtil.getTokenValue());
        result.put("tokenName", StpUtil.getTokenName());
        result.put("userId", userId);
        result.put("username", username);

        return result;
    }

    /**
     * 用户退出
     */
    @PostMapping("/logout")
    public Map<String, Object> logout() {
        StpUtil.logout();
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "已退出登录");
        return result;
    }

    /**
     * 检查是否登录
     */
    @GetMapping("/isLogin")
    public Map<String, Object> isLogin() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("isLogin", StpUtil.isLogin());
        return result;
    }

    /**
     * 获取当前登录用户信息
     */
    @GetMapping("/userInfo")
    public Map<String, Object> getUserInfo() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "获取成功");
        result.put("userId", StpUtil.getLoginId());
        result.put("tokenValue", StpUtil.getTokenValue());
        result.put("loginDevice", StpUtil.getLoginDevice());
        return result;
    }
}

💡 核心API:调用 StpUtil.login(userId) 后,Sa-Token 会自动生成 Token 并关联用户会话!

1.5 配置Sa-Token拦截器

package com.example.satoken.config;

import cn.dev33.satoken.interceptor.SaRouteInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Sa-Token 配置类
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

    /**
     * 注册 Sa-Token 拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaRouteInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/auth/login",        // 登录接口
                        "/auth/logout",       // 退出接口
                        "/auth/isLogin",      // 检查登录状态
                        "/error",             // 错误页面
                        "/doc.html",          // Swagger文档
                        "/webjars/**",        // Swagger静态资源
                        "/swagger-resources/**" // Swagger资源
                );
    }
}

这样,所有未登录的请求访问受保护接口都会返回401!

二、登录认证深度实践

2.1 单端登录

单端登录指同一账号在一个设备登录后,其他设备登录会踢掉前一个登录。

sa-token:
  # 是否允许同一账号多地登录(false=新登录踢掉旧会话)
  is-concurrent: false

实现代码示例:

package com.example.satoken.controller;

import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 单端登录控制器
 */
@RestController
@RequestMapping("/login/single")
public class SingleLoginController {

    private static final Map<String, String> USER_DB = new HashMap<>();
    static {
        USER_DB.put("admin", "123456");
    }

    /**
     * 单端登录(同一账号只能在一个设备登录)
     */
    @PostMapping("/login")
    public Map<String, Object> login(@RequestParam String username, @RequestParam String password,
                                       @RequestParam(defaultValue = "PC") String device) {
        Map<String, Object> result = new HashMap<>();

        if (!USER_DB.containsKey(username) || !USER_DB.get(username).equals(password)) {
            result.put("code", 401);
            result.put("msg", "账号或密码错误");
            return result;
        }

        // 单端登录:新登录会踢掉旧会话
        StpUtil.login(10001L, device);

        result.put("code", 200);
        result.put("msg", "登录成功,其他设备已下线");
        result.put("token", StpUtil.getTokenValue());
        result.put("device", device);

        return result;
    }
}

💡 应用场景:大多数后台管理系统应设为 false,提升安全性!

2.2 多端登录

多端登录允许同一账号在多个不同类型设备同时登录,如 PC 端和手机端可以同时在线。

sa-token:
  # 允许多地同时登录
  is-concurrent: true
  # 每个登录创建独立Token
  is-share: false

实现代码示例:

package com.example.satoken.controller;

import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 多端登录控制器
 */
@RestController
@RequestMapping("/login/multi")
public class MultiLoginController {

    private static final Map<String, String> USER_DB = new HashMap<>();
    static {
        USER_DB.put("admin", "123456");
    }

    /**
     * 多端登录(同一账号可在不同设备同时登录)
     */
    @PostMapping("/login")
    public Map<String, Object> login(@RequestParam String username, @RequestParam String password,
                                       @RequestParam(defaultValue = "PC") String device) {
        Map<String, Object> result = new HashMap<>();

        if (!USER_DB.containsKey(username) || !USER_DB.get(username).equals(password)) {
            result.put("code", 401);
            result.put("msg", "账号或密码错误");
            return result;
        }

        // 多端登录:不同设备可以同时在线
        StpUtil.login(10001L, device);

        result.put("code", 200);
        result.put("msg", "登录成功");
        result.put("token", StpUtil.getTokenValue());
        result.put("device", device);

        return result;
    }

    /**
     * 获取当前账号的所有在线会话
     */
    @GetMapping("/sessions")
    public Map<String, Object> getSessions() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "获取成功");
        result.put("sessions", StpUtil.getTokenSessionListByLoginId(10001L));
        return result;
    }
}

2.3 同端互斥登录

同端互斥登录允许同一账号在不同类型设备同时登录,但在同一类型设备下只能单端登录,如两个手机不能同时登录,但手机和电脑可以同时登录。

sa-token:
  is-concurrent: false
  replaced-range: CURR_DEVICE_TYPE  # 仅同设备类型互斥

2.4 七天内免登录

通过设置合理的 timeout 和 active-timeout 配置实现免登录功能:

sa-token:
  # Token有效期设为7天
  timeout: 604800
  # 30分钟内无操作则过期
  active-timeout: 1800

💡 推荐配置

  • 普通系统:timeout=86400(1天),active-timeout=1800(30分钟)
  • 金融/支付系统:timeout=1800,active-timeout=300

三、权限认证体系

3.1 实现权限接口

首先实现 StpInterface 接口,提供权限和角色信息:

@Component
public class StpInterfaceImpl implements StpInterface {

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 根据loginId查询用户权限,实际项目中从数据库获取
        if (loginId.equals(10001L)) {
            return Arrays.asList("user:add", "user:delete", "user:update");
        }
        return Arrays.asList("user:query");
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 根据loginId查询用户角色,实际项目中从数据库获取
        if (loginId.equals(10001L)) {
            return Arrays.asList("admin");
        }
        return Arrays.asList("user");
    }
}

3.2 代码方式鉴权

@RestController
public class UserController {

    @GetMapping("/user/info")
    public Object getUserInfo() {
        // 检查是否登录,未登录会抛出NotLoginException异常
        StpUtil.checkLogin();

        // 获取当前登录用户 ID
        Long userId = StpUtil.getLoginIdAsLong();

        // 检查是否具有admin角色
        if (StpUtil.hasRole("admin")) {
            // 管理员操作
        }

        // 检查是否具有user:delete权限
        if (StpUtil.hasPermission("user:delete")) {
            // 有权限操作
        }

        return Map.of("userId", userId, "username", "admin");
    }
}

3.3 注解式鉴权(推荐)

首先注册注解拦截器:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

    // 注册 Sa-Token 拦截器,打开注解式鉴权功能
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
    }
}

然后使用注解进行权限控制:

@RestController
public class UserController {

    // 必须登录才能访问
    @SaCheckLogin
    @GetMapping("/user/info")
    public Object getUserInfo() {
        Long userId = StpUtil.getLoginIdAsLong();
        return Map.of("userId", userId, "username", "admin");
    }

    // 必须具有 admin 角色
    @SaCheckRole("admin")
    @GetMapping("/admin/setting")
    public String adminSetting() {
        return "管理员设置页面";
    }

    // 必须具有 user:delete 权限
    @SaCheckPermission("user:delete")
    @GetMapping("/user/delete")
    public String deleteUser() {
        return "删除用户成功";
    }

    // 必须具有多个权限(AND关系)
    @SaCheckPermission(value = {"user:add", "user:edit"}, mode = SaMode.AND)
    @PostMapping("/user/saveOrUpdate")
    public String saveOrUpdate() {
        return "保存或更新用户成功";
    }

    // 具有任一权限即可(OR关系)
    @SaCheckPermission(value = {"user:view", "user:list"}, mode = SaMode.OR)
    @GetMapping("/user/list")
    public String list() {
        return "用户列表";
    }
}

3.4 会话二级认证

二级认证适用于敏感操作,如支付、修改密码等场景:

@RestController
public class PayController {

    // 发起二级认证
    @GetMapping("/pay/openSafe")
    public String openSafe() {
        // 开启二级认证,有效期5分钟
        StpUtil.openSafe(300);
        return "二级认证已开启";
    }

    // 需要二级认证的敏感操作
    @SaCheckSafe
    @PostMapping("/pay/transfer")
    public String transferMoney() {
        return "转账成功";
    }
}

四、踢人下线功能

4.1 根据账号ID踢人下线

@RestController
@RequestMapping("/admin")
public class AdminController {

    // 将指定账号踢下线
    @PostMapping("/kickout")
    public String kickout(@RequestParam Long userId) {
        StpUtil.kickout(userId);
        return "用户已被踢下线";
    }

    // 将指定账号的指定端踢下线
    @PostMapping("/kickoutByDevice")
    public String kickoutByDevice(@RequestParam Long userId, @RequestParam String device) {
        StpUtil.kickout(userId, device);
        return "用户" + userId + "的" + device + "端已被踢下线";
    }
}

4.2 根据Token值踢人下线

@PostMapping("/kickoutByToken")
public String kickoutByToken(@RequestParam String token) {
    StpUtil.kickoutByTokenValue(token);
    return "Token对应的用户已被踢下线";
}

4.3 强制注销与踢人下线的区别

功能 API 说明
强制注销 StpUtil.logout(10001) 等价于对方主动调用了注销方法,再次访问会提示:Token无效
踢人下线 StpUtil.kickout(10001) 不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线

五、前后端分离支持

对于APP、小程序等不支持Cookie的终端,Sa-Token提供了完善的支持。

5.1 前端携带Token

前端需要在请求头中携带Token,默认Header名为satoken(可配置):

// 前端请求示例
axios.get("/user/info", {
    headers: {
        "Authorization": "xxx-xxx-xxx"  // 如果token-name改为Authorization
    }
});

5.2 配置Token名称

sa-token:
  # token 名称(前端请求头中携带的字段名)
  token-name: Authorization
  # 不从Cookie读取Token
  is-read-cookie: false

⚠️ 安全建议:不要在URL传Token,容易泄露;敏感操作建议加二次验证(如支付前输密码)

六、Token风格定制

6.1 内置Token风格

Sa-Token内置六种Token风格:

sa-token:
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tiktok-uuid)
  token-style: uuid

💡 生产推荐:tiktok-uuid,既保证唯一性,又便于日志追踪和数据库索引优化。

6.2 自定义Token生成策略

实现TokenGenerator接口:

@Component
public class MyTokenGenerator implements TokenGenerator {
    @Override
    public String generate(Object loginId, String loginType) {
        // 自定义Token生成逻辑
        return UUID.randomUUID().toString().replace("-", "") + System.currentTimeMillis();
    }
}

七、JWT集成实战

7.1 添加JWT依赖

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-jwt</artifactId>
    <version>1.44.0</version>
</dependency>

7.2 配置JWT密钥

sa-token:
  # jwt秘钥
  jwt-secret-key: your-jwt-secret-key

7.3 注册JWT逻辑实现

@Configuration
public class SaTokenConfig {
    @Bean
    public StpLogic getStpLogicJwt() {
        // Sa-Token 整合 jwt (简单模式)
        return new StpLogicJwtForSimple();
    }
}

7.4 JWT模式登录

@PostMapping("/jwt/login")
public String jwtLogin(@RequestParam String username, @RequestParam String password) {
    // 校验账号密码...

    // JWT模式登录
    StpUtil.login(10001);
    return "JWT登录成功,Token: " + StpUtil.getTokenValue();
}

Sa-Token提供三种模式的JWT集成方案,支持Token扩展参数能力。

八、参数签名安全

Sa-Token提供跨系统API调用签名校验模块,防参数篡改,防请求重放。

8.1 配置签名密钥

sa-token:
  sign:
    # API 接口签名秘钥
    secret-key: your-sign-secret-key

8.2 请求发起端生成签名

@GetMapping("/sendRequest")
public String sendRequest() {
    Map<String, Object> params = new HashMap<>();
    params.put("userId", 10001);
    params.put("money", 100);

    // 添加签名参数并拼接成参数字符串
    String paramStr = SaSignUtil.addSignParamsAndJoin(params);

    // 发送请求
    String url = "http://localhost:8080/api/testSign";
    return HttpUtil.post(url, paramStr);
}

8.3 请求接收端校验签名

@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/testSign")
    public String testSign() {
        // 校验请求签名
        SaSignUtil.checkRequest(SaHolder.getRequest());

        // 处理业务逻辑
        return "签名校验通过";
    }

    // 使用注解方式校验签名
    @SaCheckSign
    @PostMapping("/testSignByAnnotation")
    public String testSignByAnnotation() {
        // 处理业务逻辑
        return "注解式签名校验通过";
    }
}

九、自动续签机制

Sa-Token提供两种Token过期策略,灵活搭配使用,还可自动续签。

9.1 配置自动续签

sa-token:
  # token 有效期(秒)
  timeout: 86400          # 1天
  # token 最低活跃时间(秒)
  active-timeout: 1800    # 30分钟无操作则过期

9.2 自动续签原理

  • timeout:Token的长久有效期,例如配置为86400(1天),代表1天后Token必定过期
  • active-timeout:最低活跃频率,例如配置为1800(30分钟),代表用户如果30分钟无操作,则Token过期

当用户在active-timeout时间内有操作,Token会自动刷新有效期,避免用户频繁登录。

9.3 手动续签

// 检查当前Token是否已经被冻结
StpUtil.checkActiveTimeout();
// 续签当前Token:将[最后操作时间]更新为当前时间戳
StpUtil.updateLastActiveToNow();

// 为指定Token续签
StpUtil.stpLogic.updateLastActiveToNow(tokenValue);

十、生产环境最佳实践

10.1 推荐配置

sa-token:
  token-name: Authorization
  timeout: 86400          # 1天
  active-timeout: 1800    # 30分钟无操作则过期
  is-concurrent: false    # 单点登录
  is-share: false
  token-style: tiktok-uuid
  is-log: false           # 生产关闭日志
  is-read-cookie: false   # 前后端分离不读取Cookie

10.2 安全建议

  1. 生产环境务必使用Redis,避免服务重启丢失会话
  2. 敏感操作建议加二次验证StpUtil.openSafe()
  3. 不要在URL中传递Token,使用请求头
  4. 合理设置Token超时时间,平衡安全性和用户体验
  5. 定期轮换JWT密钥和签名密钥

10.3 常用API速查表

功能 代码示例
用户登录 StpUtil.login(10001);
获取当前登录用户ID StpUtil.getLoginId();
检查是否登录 StpUtil.isLogin();
注销登录 StpUtil.logout();
踢人下线 StpUtil.kickout(10001);
检查角色 StpUtil.hasRole("admin");
检查权限 StpUtil.hasPermission("user:add");
获取Session StpUtil.getSession();
指定设备登录 StpUtil.login(10001, "PC");
指定设备注销 StpUtil.logoutByLoginId(10001, "PC");
临时身份切换 StpUtil.switchTo(10044);

十一、总结

通过以上配置和代码示例,你可以基于Sa-Token实现完整的权限认证系统,包括登录认证、权限管理、会话控制等核心功能。Sa-Token的API设计简洁直观,极大降低了权限系统的开发难度,让开发者可以更专注于业务逻辑实现。

核心优势总结:

  • API极简:一行代码搞定登录、鉴权
  • 功能全面:覆盖登录、权限、会话、单点登录、OAuth2.0等
  • 易于集成:Spring Boot零配置启动
  • 扩展性强:支持自定义Token、JWT、签名校验等
  • 社区活跃:18k+ Star,持续更新维护

🎯 推荐理由:相比Spring Security的复杂性,Sa-Token提供了更直观的API设计;相比Shiro的轻量级,Sa-Token功能更加丰富完善。如果你正在寻找一款简单易用、功能强大的Java权限认证框架,Sa-Token绝对是首选!


参考文档:

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐