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 - select code from sys_permission p left join sys_role_permission rp on p.id = rp.permission_id