diff --git a/pom.xml b/pom.xml
index 3c08628..822dca3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -126,6 +126,17 @@
${sverlet.version}
provided
+
+ cn.dev33
+ sa-token-spring-boot3-starter
+ ${sa-token.version}
+
+
+
+ cn.dev33
+ sa-token-redis-jackson
+ ${sa-token.version}
+
diff --git a/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java b/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java
index 0bcc700..5351862 100644
--- a/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java
+++ b/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java
@@ -23,6 +23,39 @@ import jakarta.servlet.http.HttpServletRequest;
@RestControllerAdvice
public class GlobalExceptionHandler {
+ /**
+ * Sa-Token: not login exception
+ */
+ @ExceptionHandler(cn.dev33.satoken.exception.NotLoginException.class)
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public GenericResponse handleNotLoginException(cn.dev33.satoken.exception.NotLoginException e,
+ HttpServletRequest request) {
+ log.warn("Not logged in [URL:{}]: {}", request.getRequestURI(), e.getMessage());
+ return new GenericResponse<>(401, null, "Please login first");
+ }
+
+ /**
+ * Sa-Token: not permission exception
+ */
+ @ExceptionHandler(cn.dev33.satoken.exception.NotPermissionException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public GenericResponse handleNotPermissionException(cn.dev33.satoken.exception.NotPermissionException e,
+ HttpServletRequest request) {
+ log.warn("No permission [URL:{}]: {}", request.getRequestURI(), e.getMessage());
+ return new GenericResponse<>(403, null, "No permission to access this resource");
+ }
+
+ /**
+ * Sa-Token: not role exception
+ */
+ @ExceptionHandler(cn.dev33.satoken.exception.NotRoleException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public GenericResponse handleNotRoleException(cn.dev33.satoken.exception.NotRoleException e,
+ HttpServletRequest request) {
+ log.warn("No role [URL:{}]: {}", request.getRequestURI(), e.getMessage());
+ return new GenericResponse<>(403, null, "Insufficient role privileges");
+ }
+
/**
* 处理登录/认证异常
*/
diff --git a/src/main/java/cn/xf/basedemo/config/SaTokenConfigure.java b/src/main/java/cn/xf/basedemo/config/SaTokenConfigure.java
index fa44f18..256c585 100644
--- a/src/main/java/cn/xf/basedemo/config/SaTokenConfigure.java
+++ b/src/main/java/cn/xf/basedemo/config/SaTokenConfigure.java
@@ -1,6 +1,8 @@
package cn.xf.basedemo.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.xf.basedemo.interceptor.SaTokenContextInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -14,10 +16,16 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
+
+ @Autowired
+ private SaTokenContextInterceptor saTokenContextInterceptor;
+
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
+ // 注册上下文注入拦截器,兼容旧业务代码
+ registry.addInterceptor(saTokenContextInterceptor).addPathPatterns("/**");
}
}
diff --git a/src/main/java/cn/xf/basedemo/controller/business/UserController.java b/src/main/java/cn/xf/basedemo/controller/business/UserController.java
index 271c008..a2f088a 100644
--- a/src/main/java/cn/xf/basedemo/controller/business/UserController.java
+++ b/src/main/java/cn/xf/basedemo/controller/business/UserController.java
@@ -2,7 +2,8 @@ package cn.xf.basedemo.controller.business;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
-import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.annotation.SaMode;
import cn.xf.basedemo.common.model.LoginUser;
import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.interceptor.SessionContext;
@@ -38,10 +39,38 @@ public class UserController {
@Operation(summary = "用户信息", description = "用户信息")
@PostMapping("/info")
- @SaCheckPermission("user:info") //权限校验
- public RetObj info(){
+ @SaCheckPermission("user:info") // 权限校验
+ public RetObj info() {
LoginUser loginUser = SessionContext.getInstance().get();
return RetObj.success(loginUser);
}
+ @Operation(summary = "注解示例-角色校验", description = "必须具有 'super-admin' 角色才能访问")
+ @PostMapping("/check-role")
+ @SaCheckRole("super-admin")
+ public RetObj checkRole() {
+ return RetObj.success("您拥有 super-admin 角色,验证通过");
+ }
+
+ @Operation(summary = "注解示例-权限组合(OR)", description = "只要拥有 user:add 或 user:update 其中一个权限即可")
+ @PostMapping("/check-permission-or")
+ @SaCheckPermission(value = { "user:add", "user:update" }, mode = SaMode.OR)
+ public RetObj checkPermissionOr() {
+ return RetObj.success("您拥有 user:add 或 user:update 权限,验证通过");
+ }
+
+ @Operation(summary = "注解示例-权限组合(AND)", description = "必须同时拥有 user:delete 和 user:export 权限")
+ @PostMapping("/check-permission-and")
+ @SaCheckPermission(value = { "user:delete", "user:export" }, mode = SaMode.AND)
+ public RetObj checkPermissionAnd() {
+ return RetObj.success("您同时拥有 user:delete 和 user:export 权限,验证通过");
+ }
+
+ @Operation(summary = "注解示例-忽略鉴权", description = "无需登录即可访问(常用于注册、验证码等公开接口)")
+ @PostMapping("/public-api")
+ @SaIgnore
+ public RetObj publicApi() {
+ return RetObj.success("这是一个公开接口,@SaIgnore 生效");
+ }
+
}
diff --git a/src/main/java/cn/xf/basedemo/interceptor/InterceptorConfig.java b/src/main/java/cn/xf/basedemo/interceptor/InterceptorConfig.java
index 2c0b13b..08a9899 100644
--- a/src/main/java/cn/xf/basedemo/interceptor/InterceptorConfig.java
+++ b/src/main/java/cn/xf/basedemo/interceptor/InterceptorConfig.java
@@ -1,7 +1,6 @@
package cn.xf.basedemo.interceptor;
import cn.dev33.satoken.interceptor.SaInterceptor;
-import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -17,18 +16,26 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
- @Bean
- public TokenInterceptor tokenInterceptor() {
- return new TokenInterceptor();
- }
+ @org.springframework.beans.factory.annotation.Autowired
+ private SaTokenContextInterceptor saTokenContextInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(tokenInterceptor()) //登录逻辑拦截类
- .addPathPatterns("/**") //需要拦截的请求(设置的全部拦截)
- .excludePathPatterns("/user/login", "/web/**"); //忽略的请求
- }
+ // 注册 Sa-Token 拦截器,定义详细认证规则
+ registry.addInterceptor(new SaInterceptor(handler -> {
+ // 指定一条 match 规则
+ cn.dev33.satoken.stp.StpUtil.checkLogin();
+ }))
+ .addPathPatterns("/**")
+ .excludePathPatterns("/user/login", "/web/**", "/swagger-resources/**", "/webjars/**", "/v3/**",
+ "/doc.html");
+ // 注册 Context 拦截器,用于注入 SessionContext
+ registry.addInterceptor(saTokenContextInterceptor)
+ .addPathPatterns("/**")
+ .excludePathPatterns("/user/login", "/web/**", "/swagger-resources/**", "/webjars/**", "/v3/**",
+ "/doc.html");
+ }
/**
* 放行Knife4j请求
diff --git a/src/main/java/cn/xf/basedemo/interceptor/SaTokenContextInterceptor.java b/src/main/java/cn/xf/basedemo/interceptor/SaTokenContextInterceptor.java
new file mode 100644
index 0000000..f2a4ec6
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/interceptor/SaTokenContextInterceptor.java
@@ -0,0 +1,41 @@
+package cn.xf.basedemo.interceptor;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.xf.basedemo.common.model.LoginUser;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * @Description: Sa-Token 上下文拦截器
+ * 用于将 Sa-Token Session 中的用户信息注入到 SessionContext (ThreadLocal)
+ * 以兼容旧的业务代码 (SessionContext.getInstance().get())
+ * @Author: xiongfeng
+ */
+@Component
+public class SaTokenContextInterceptor implements HandlerInterceptor {
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+ throws Exception {
+ // 如果已登录,尝试从 Session 中获取用户信息并注入 ThreadLocal
+ if (StpUtil.isLogin()) {
+ // 从 Sa-Token Session 中读取 loginUser (需确保登录时已存入)
+ LoginUser loginUser = (LoginUser) StpUtil.getSession().get("loginUser");
+ if (loginUser != null) {
+ SessionContext.getInstance().set(loginUser);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
+ ModelAndView modelAndView) throws Exception {
+ // 请求结束后清理 ThreadLocal,防止内存泄漏
+ SessionContext.getInstance().clear();
+ }
+}
diff --git a/src/main/java/cn/xf/basedemo/interceptor/StpInterfaceImpl.java b/src/main/java/cn/xf/basedemo/interceptor/StpInterfaceImpl.java
index a2098d4..1460268 100644
--- a/src/main/java/cn/xf/basedemo/interceptor/StpInterfaceImpl.java
+++ b/src/main/java/cn/xf/basedemo/interceptor/StpInterfaceImpl.java
@@ -1,9 +1,9 @@
package cn.xf.basedemo.interceptor;
import cn.dev33.satoken.stp.StpInterface;
-import cn.xf.basedemo.common.utils.ApplicationContextUtils;
import cn.xf.basedemo.mappers.SysPermissionMapper;
import cn.xf.basedemo.mappers.SysRoleMapper;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -18,19 +18,23 @@ import java.util.List;
@Component
public class StpInterfaceImpl implements StpInterface {
- private SysPermissionMapper sysPermissionMapper = ApplicationContextUtils.getBean(SysPermissionMapper.class);
- private SysRoleMapper sysRoleMapper = ApplicationContextUtils.getBean(SysRoleMapper.class);
+ @Autowired
+ private SysPermissionMapper sysPermissionMapper;
+
+ @Autowired
+ private SysRoleMapper sysRoleMapper;
+
@Override
public List getPermissionList(Object userId, String s) {
- //获取登录用户权限数据
- Long aLong = Long.valueOf(userId.toString());
- List permissionList = sysPermissionMapper.getPermissionListByRoleId(aLong);
+ // 获取登录用户权限数据
+ Long uId = Long.valueOf(userId.toString());
+ List permissionList = sysPermissionMapper.getPermissionListByUserId(uId);
return permissionList;
}
@Override
public List getRoleList(Object userId, String s) {
- //获取用户角色数据
+ // 获取用户角色数据
return sysRoleMapper.getRoleListByUserId((Long) userId);
}
}
diff --git a/src/main/java/cn/xf/basedemo/mappers/SysPermissionMapper.java b/src/main/java/cn/xf/basedemo/mappers/SysPermissionMapper.java
index cfecd13..0cf4589 100644
--- a/src/main/java/cn/xf/basedemo/mappers/SysPermissionMapper.java
+++ b/src/main/java/cn/xf/basedemo/mappers/SysPermissionMapper.java
@@ -7,14 +7,14 @@ import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
-* @author xiongfeng
-* @description 针对表【sys_permission(系统权限表 sys_permission)】的数据库操作Mapper
-* @createDate 2025-08-19 21:22:03
-* @Entity cn.xf.basedemo.model.domain.SysPermission
-*/
+ * @author xiongfeng
+ * @description 针对表【sys_permission(系统权限表 sys_permission)】的数据库操作Mapper
+ * @createDate 2025-08-19 21:22:03
+ * @Entity cn.xf.basedemo.model.domain.SysPermission
+ */
@Mapper
public interface SysPermissionMapper extends BaseMapper {
- List getPermissionListByRoleId(Long useId);
+ List getPermissionListByUserId(Long userId);
}
diff --git a/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java b/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java
index 65412be..33aee7e 100644
--- a/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java
+++ b/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java
@@ -1,7 +1,6 @@
package cn.xf.basedemo.service.impl;
import cn.dev33.satoken.stp.StpUtil;
-import cn.xf.basedemo.common.model.EsBaseModel;
import cn.xf.basedemo.common.model.LoginInfo;
import cn.xf.basedemo.common.model.LoginUser;
import cn.xf.basedemo.common.model.RetObj;
@@ -56,7 +55,8 @@ public class UserServiceImpl implements UserService {
}
String loginJson = "";
try {
- loginJson = RSAUtils.privateDecryption(res.getEncryptedData(), RSAUtils.getPrivateKey(globalConfig.getRsaPrivateKey()));
+ loginJson = RSAUtils.privateDecryption(res.getEncryptedData(),
+ RSAUtils.getPrivateKey(globalConfig.getRsaPrivateKey()));
} catch (Exception e) {
log.error("解密失败------", e);
}
@@ -70,7 +70,7 @@ public class UserServiceImpl implements UserService {
if (!StringUtils.isEmpty(loginInfo.check())) {
return RetObj.error(loginInfo.check());
}
- //校验登录账号密码
+ // 校验登录账号密码
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("account", loginInfo.getAccount());
queryWrapper.eq("password", loginInfo.getPwd());
@@ -84,13 +84,16 @@ public class UserServiceImpl implements UserService {
loginUser.setName(user.getName());
loginUser.setPhone(user.getPhone());
- String token = JwtTokenUtils.createToken(user.getId());
- loginUser.setToken(token);
-
- redisTemplate.opsForValue().set("token:" + token, JSONObject.toJSONString(loginUser), 3600, TimeUnit.SECONDS);
- redisTemplate.opsForValue().set("user_login_token:" + user.getId(), token, 3600, TimeUnit.SECONDS);
- //登录成功 写入sa-token中
+ // 登录成功 写入sa-token中
StpUtil.login(user.getId());
+
+ // 将用户信息缓存到 Session 中,以便后续获取
+ StpUtil.getSession().set("loginUser", loginUser);
+
+ // 获取 Sa-Token 生成的 token 值
+ cn.dev33.satoken.stp.SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
+ loginUser.setToken(tokenInfo.tokenValue);
+
return RetObj.success(loginUser);
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index ef49c74..d13cce4 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -24,3 +24,26 @@ spring:
import:
- nacos:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
+# Sa-Token Configuration
+sa-token:
+ # token name (frontend needs to use this name, e.g., Authorization: Bearer xxxx, or just satoken: xxxx)
+ token-name: Authorization
+ # token validity period (seconds), -1 means never expire
+ timeout: 2592000
+ # token temporary validity (seconds), -1 means never expire
+ activity-timeout: -1
+ # allow concurrent login
+ is-concurrent: true
+ # share token api
+ is-share: true
+ # token style
+ token-style: uuid
+ # log
+ is-log: false
+ # read from cookie
+ is-read-cookie: false
+ # read from header
+ is-read-header: true
+ # read from body
+ is-read-body: false
+
diff --git a/src/main/resources/mapper/SysPermissionMapper.xml b/src/main/resources/mapper/SysPermissionMapper.xml
index d2bc047..bc68c48 100644
--- a/src/main/resources/mapper/SysPermissionMapper.xml
+++ b/src/main/resources/mapper/SysPermissionMapper.xml
@@ -21,7 +21,7 @@
update_time,update_by
-