Sa-Token 整合用户认证授权(完整实战指南)
·
Sa-Token 是一款轻量级 Java 权限认证框架,核心解决「登录认证、权限授权、单点登录、Token 管理」等问题,相比 Shiro/Spring Security 更简洁易用。
一、核心概念(先理清)
| 核心功能 | 作用 |
|---|---|
| 登录认证 | 验证用户身份,生成 Token 并维护会话(支持 Cookie、Header 传参) |
| 权限授权 | 验证用户是否拥有指定权限/角色(如 admin 角色、user:add 权限) |
| Token 管理 | 支持 Token 过期、刷新、踢人下线、注销登录等 |
| 路径拦截 | 拦截指定接口,仅允许认证/授权通过的用户访问 |
| 注解鉴权 | 通过 @SaCheckLogin/@SaCheckRole/@SaCheckPermission 注解鉴权 |
二、环境准备(Spring Boot 整合)
1. 引入依赖
<!-- Sa-Token 核心依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- 如需 Redis 存储 Token(分布式场景),引入以下依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 核心配置(application.yml)
# Sa-Token 配置
sa-token:
# Token 名称(默认:satoken)
token-name: satoken
# Token 有效期(秒),默认30天,-1 永久有效
timeout: 2592000
# Token 临时有效期(秒),默认30分钟(用于自动刷新)
activity-timeout: 1800
# 是否允许同一账号多地登录(默认false,即单端登录)
is-concurrent: false
# 在多人登录同一账号时,是否共用一个Token(默认false)
is-share: false
# Token 风格(默认uuid,可选:simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志(默认false)
is-log: true
# Redis 配置(分布式场景必填)
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
3. 自定义 Sa-Token 配置类(可选)
如需自定义 Token 生成规则、存储方式等,可编写配置类:
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.dao.impl.SaTokenDaoRedisJackson;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpLogic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaTokenConfig {
// 自定义 StpLogic(如整合 JWT)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
// 配置 Redis 存储(分布式场景)
@Bean
public SaTokenDao saTokenDao() {
return new SaTokenDaoRedisJackson();
}
}
三、核心功能实现(认证+授权)
1. 登录认证(核心)
步骤1:用户登录接口
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 登录认证控制器
*/
@RestController
public class LoginController {
/**
* 用户登录接口
* POST 请求示例:
* URL: /login
* Body: {"username":"admin","password":"123456"}
*/
@PostMapping("/login")
public SaResult login(@RequestBody Map<String, String> params) {
// 1. 获取前端参数
String username = params.get("username");
String password = params.get("password");
// 2. 模拟数据库校验用户(真实场景需查数据库)
if (!"admin".equals(username) || !"123456".equals(password)) {
return SaResult.error("用户名或密码错误");
}
// 3. 登录认证:传入用户ID(唯一标识),生成 Token
Long userId = 1001L; // 模拟用户ID
StpUtil.login(userId);
// 4. 获取 Token 信息
String token = StpUtil.getTokenValue();
long timeout = StpUtil.getTokenTimeout();
// 5. 返回结果
return SaResult.ok("登录成功")
.set("token", token)
.set("userId", userId)
.set("timeout", timeout);
}
/**
* 退出登录
*/
@PostMapping("/logout")
public SaResult logout() {
// 退出当前用户登录(销毁 Token)
StpUtil.logout();
return SaResult.ok("退出登录成功");
}
/**
* 获取当前登录用户信息
*/
@PostMapping("/getLoginUser")
public SaResult getLoginUser() {
// 1. 检查是否登录(未登录会抛出 NotLoginException 异常)
StpUtil.checkLogin();
// 2. 获取当前登录用户ID
Long userId = StpUtil.getLoginIdAsLong();
// 3. 模拟获取用户信息(真实场景查数据库)
return SaResult.ok("获取成功")
.set("userId", userId)
.set("username", "admin")
.set("role", "admin")
.set("permissions", new String[]{"user:add", "user:delete", "user:query"});
}
}
步骤2:全局异常处理(捕获未登录异常)
Sa-Token 未登录/未授权时会抛出异常,需统一处理:
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* Sa-Token 全局异常处理
*/
@RestControllerAdvice
public class SaTokenExceptionHandler {
// 处理未登录异常
@ExceptionHandler(NotLoginException.class)
public SaResult handleNotLoginException(NotLoginException e) {
// 返回未登录提示
return SaResult.error("未登录:" + e.getMessage()).setCode(401);
}
// 处理未授权异常(角色)
@ExceptionHandler(NotRoleException.class)
public SaResult handleNotRoleException(NotRoleException e) {
return SaResult.error("无指定角色:" + e.getRole()).setCode(403);
}
// 处理未授权异常(权限)
@ExceptionHandler(NotPermissionException.class)
public SaResult handleNotPermissionException(NotPermissionException e) {
return SaResult.error("无指定权限:" + e.getPermission()).setCode(403);
}
}
2. 权限授权(两种方式:注解+拦截器)
方式1:注解鉴权(推荐,灵活)
通过 @SaCheckLogin/@SaCheckRole/@SaCheckPermission 注解实现细粒度鉴权:
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 权限授权控制器(注解式)
*/
@RestController
public class PermissionController {
/**
* 示例1:仅登录用户可访问
*/
@SaCheckLogin
@GetMapping("/user/info")
public SaResult userInfo() {
return SaResult.ok("获取用户信息成功").set("data", "用户信息");
}
/**
* 示例2:仅拥有 admin 角色的用户可访问
* orRole = true:多个角色满足其一即可(默认false,需全部满足)
*/
@SaCheckRole(value = {"admin", "super_admin"}, orRole = true)
@GetMapping("/admin/dashboard")
public SaResult adminDashboard() {
return SaResult.ok("管理员控制台").set("data", "管理员数据");
}
/**
* 示例3:仅拥有 user:add 权限的用户可访问
* orPermission = true:多个权限满足其一即可
*/
@SaCheckPermission(value = {"user:add", "user:edit"}, orPermission = true)
@PostMapping("/user/add")
public SaResult addUser() {
return SaResult.ok("添加用户成功");
}
/**
* 示例4:登录且拥有指定角色+权限
*/
@SaCheckLogin
@SaCheckRole("admin")
@SaCheckPermission("user:delete")
@PostMapping("/user/delete")
public SaResult deleteUser() {
return SaResult.ok("删除用户成功");
}
}
方式2:拦截器鉴权(全局路径控制)
适合对整站路径进行统一鉴权(如 /admin/** 仅管理员可访问):
步骤1:编写 Sa-Token 拦截器
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Sa-Token 拦截器(路径鉴权)
*/
public class SaTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 配置不需要鉴权的路径
String path = request.getRequestURI();
if (path.contains("/login") || path.contains("/register") || path.contains("/static")) {
return true; // 放行
}
// 2. 登录校验:拦截所有请求,必须登录后才能访问
StpUtil.checkLogin();
// 3. 角色/权限校验:/admin/** 路径需 admin 角色
if (path.startsWith("/admin/")) {
StpUtil.checkRole("admin");
}
// 4. /user/** 路径需 user:query 权限
if (path.startsWith("/user/")) {
StpUtil.checkPermission("user:query");
}
// 放行
return true;
}
}
步骤2:注册拦截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web 配置(注册拦截器)
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器
registry.addInterceptor(new SaTokenInterceptor())
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/login", "/register", "/static/**"); // 排除无需鉴权的路径
}
}
3. 自定义权限加载(从数据库获取角色/权限)
真实项目中,角色和权限需从数据库加载,需实现 StpInterface 接口:
步骤1:实现 StpInterface 接口
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义权限加载接口(从数据库获取用户角色/权限)
*/
@Component
public class MyStpInterface implements StpInterface {
/**
* 返回指定用户拥有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 真实场景:根据 loginId(用户ID)查数据库获取角色
List<String> roleList = new ArrayList<>();
// 模拟:admin 用户拥有 admin 角色,普通用户拥有 user 角色
if ("1001".equals(loginId.toString())) {
roleList.add("admin");
} else {
roleList.add("user");
}
return roleList;
}
/**
* 返回指定用户拥有的权限标识集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 真实场景:根据 loginId 查数据库获取权限
List<String> permissionList = new ArrayList<>();
// 模拟:admin 用户拥有所有权限,普通用户仅拥有查询权限
if ("1001".equals(loginId.toString())) {
permissionList.add("user:add");
permissionList.add("user:delete");
permissionList.add("user:edit");
permissionList.add("user:query");
} else {
permissionList.add("user:query");
}
return permissionList;
}
}
步骤2:使用自定义权限
实现 StpInterface 后,@SaCheckRole/@SaCheckPermission 会自动从该接口获取角色/权限,无需手动绑定。
4. 高级功能:Token 管理
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Token 管理控制器
*/
@RestController
public class TokenController {
/**
* 刷新 Token(延长有效期)
*/
@PostMapping("/refreshToken")
public SaResult refreshToken() {
StpUtil.refreshToken();
return SaResult.ok("Token 刷新成功").set("token", StpUtil.getTokenValue());
}
/**
* 踢人下线(强制注销指定用户)
*/
@PostMapping("/kickout")
public SaResult kickout(Long userId) {
StpUtil.kickout(userId);
return SaResult.ok("已强制用户 " + userId + " 下线");
}
/**
* 检查 Token 是否有效
*/
@PostMapping("/checkToken")
public SaResult checkToken(String token) {
boolean isValid = StpUtil.checkToken(token);
return SaResult.ok("Token 有效性:" + isValid);
}
/**
* 设置 Token 有效期(单独指定某个用户的 Token 有效期)
*/
@PostMapping("/setTokenTimeout")
public SaResult setTokenTimeout(Long userId, long timeout) {
StpUtil.setTokenTimeout(userId, timeout);
return SaResult.ok("设置 Token 有效期成功");
}
}
四、实战场景整合(前后端分离)
1. 前端传递 Token 方式
前后端分离场景下,前端需在请求头中携带 Token(默认名称:satoken):
// Axios 请求示例
axios({
url: '/user/info',
method: 'get',
headers: {
'satoken': localStorage.getItem('satoken') // 从本地存储获取 Token
}
}).then(res => {
console.log(res.data);
});
2. 跨域配置(解决前端跨域携带 Token)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 跨域配置
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许所有域名跨域(生产环境需指定具体域名)
config.addAllowedOriginPattern("*");
// 允许携带 Cookie/Token
config.setAllowCredentials(true);
// 允许所有请求头
config.addAllowedHeader("*");
// 允许所有请求方法
config.addAllowedMethod("*");
// 暴露响应头中的 satoken
config.addExposedHeader("satoken");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
3. 常见场景:登录状态保持
- Sa-Token 会自动维护 Token 有效期,当用户操作时(如访问接口),Token 临时有效期(activity-timeout)会自动刷新;
- 若 Token 即将过期,前端可调用
/refreshToken接口刷新 Token,避免用户重新登录。
五、避坑要点(高频问题)
1. 未登录异常:NotLoginException
- 现象:访问需登录的接口时抛出该异常;
- 原因:
- 未携带 Token;
- Token 已过期;
- Token 无效/被注销;
- 解决:
- 前端检查 Token 是否正确携带;
- 后端检查 Token 有效期配置;
- 重新登录生成新 Token。
2. 跨域时 Token 无法携带
- 现象:前端携带 Token 但后端获取不到;
- 原因:跨域配置未开启
AllowCredentials; - 解决:跨域配置中设置
config.setAllowCredentials(true)。
3. 分布式场景 Token 不共享
- 现象:多服务部署时,登录后访问另一服务提示未登录;
- 原因:未配置 Redis 存储 Token;
- 解决:引入 Redis 依赖,配置 Redis 连接信息,让 Token 存储在 Redis 中。
4. 注解鉴权不生效
- 现象:加了
@SaCheckRole但无权限用户仍可访问; - 原因:
- 未实现
StpInterface接口,角色/权限加载失败; - 注解参数配置错误(如 orRole=false 但用户仅满足一个角色);
- 未实现
- 解决:
- 检查
StpInterface实现类是否加@Component; - 核对注解参数(如 orRole=true 表示满足其一即可)。
- 检查
5. Token 重复登录被挤下线
- 现象:同一账号登录多个端,后登录的挤掉先登录的;
- 原因:
sa-token.is-concurrent=false(禁止同一账号多地登录); - 解决:
- 设置
is-concurrent=true允许多地登录; - 或设置
is-share=true共用一个 Token。
- 设置
六、核心总结
- 认证核心:
- 登录:
StpUtil.login(userId)生成 Token; - 校验登录:
StpUtil.checkLogin()或@SaCheckLogin; - 退出:
StpUtil.logout()销毁 Token;
- 登录:
- 授权核心:
- 角色鉴权:
@SaCheckRole("admin"); - 权限鉴权:
@SaCheckPermission("user:add"); - 自定义权限:实现
StpInterface从数据库加载角色/权限;
- 角色鉴权:
- 分布式场景:
- 引入 Redis 依赖,配置 Redis 连接,实现 Token 跨服务共享;
- 避坑关键:
- 跨域配置需开启
AllowCredentials,否则 Token 无法携带; - 注解鉴权需确保
StpInterface实现类被 Spring 扫描; - 未登录/未授权异常需全局捕获,统一返回格式。
- 跨域配置需开启
Sa-Token 整合用户认证授权的核心是「简单、轻量、无侵入」,通过少量代码即可实现企业级的权限控制,相比传统框架大幅降低开发成本,适合中小项目、前后端分离项目快速集成。
更多推荐



所有评论(0)