RuoYi-Vue-Plus深度解析:Sa-Token权限认证实战指南
引言:权限认证的痛点与解决方案
在企业级应用开发中,权限认证一直是开发者的痛点之一。传统的权限认证方案往往存在配置复杂、扩展性差、安全性不足等问题。RuoYi-Vue-Plus作为一款优秀的多租户后台管理系统,深度集成了Sa-Token权限认证框架,为开发者提供了一套完整的权限认证解决方案。
通过本文,你将掌握:
- ✅ Sa-Token在RuoYi-Vue-Plus中的核心配置
- ✅ 多租户环境下的权限认证实战
- ✅ 自定义权限验证的最佳实践
- ✅ 异常处理和安全防护机制
- ✅ 实际业务场景中的权限控制技巧
一、Sa-Token核心架构解析
1.1 整体架构设计
RuoYi-Vue-Plus的Sa-Token集成采用了分层架构设计:
1.2 核心配置类详解
@AutoConfiguration
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
public class SaTokenConfig {
@Bean
public StpLogic getStpLogicJwt() {
// Sa-Token 整合 jwt (简单模式)
return new StpLogicJwtForSimple();
}
@Bean
public StpInterface stpInterface() {
return new SaPermissionImpl();
}
@Bean
public SaTokenDao saTokenDao() {
return new PlusSaTokenDao();
}
@Bean
public SaTokenExceptionHandler saTokenExceptionHandler() {
return new SaTokenExceptionHandler();
}
}
二、多租户权限认证实战
2.1 登录认证流程
RuoYi-Vue-Plus支持多种登录方式,以下是密码登录的核心代码:
public class PasswordAuthStrategy implements IAuthStrategy {
@Override
public LoginVo authenticate(String... args) {
// 验证用户名密码
SysUser user = userService.selectUserByUserName(username);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new ServiceException("用户不存在/密码错误");
}
// 创建登录用户信息
LoginUser loginUser = buildLoginUser(user);
// Sa-Token登录
LoginHelper.login(loginUser, new SaLoginParameter()
.setDevice(deviceType)
.setTimeout(expireTime));
// 返回登录结果
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
return loginVo;
}
}
2.2 权限验证实现
核心权限验证类SaPermissionImpl实现了Sa-Token的权限接口:
public class SaPermissionImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) {
// 从数据库查询权限
PermissionService permissionService = getPermissionService();
List<String> list = StringUtils.splitList(loginId.toString(), ":");
return new ArrayList<>(permissionService.getMenuPermission(Long.parseLong(list.get(1))));
}
return new ArrayList<>(loginUser.getMenuPermission());
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 类似的角色权限获取逻辑
return new ArrayList<>(loginUser.getRolePermission());
}
}
三、登录助手工具类详解
3.1 LoginHelper核心方法
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoginHelper {
// 关键常量定义
public static final String LOGIN_USER_KEY = "loginUser";
public static final String TENANT_KEY = "tenantId";
public static final String USER_KEY = "userId";
// 登录方法
public static void login(LoginUser loginUser, SaLoginParameter model) {
model = ObjectUtil.defaultIfNull(model, new SaLoginParameter());
StpUtil.login(loginUser.getLoginId(),
model.setExtra(TENANT_KEY, loginUser.getTenantId())
.setExtra(USER_KEY, loginUser.getUserId())
.setExtra(USER_NAME_KEY, loginUser.getUsername())
);
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
// 获取当前用户信息
public static <T extends LoginUser> T getLoginUser() {
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
// 获取用户ID
public static Long getUserId() {
return Convert.toLong(StpUtil.getExtra(USER_KEY));
}
// 检查登录状态
public static boolean isLogin() {
try {
return getLoginUser() != null;
} catch (Exception e) {
return false;
}
}
}
3.2 业务场景中的使用示例
// 在Controller中获取当前用户信息
@RestController
@RequestMapping("/system/user")
public class UserController {
@GetMapping("/profile")
public R<LoginUser> getProfile() {
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser == null) {
throw new ServiceException("用户未登录");
}
return R.ok(loginUser);
}
@PostMapping("/update")
@SaCheckLogin // 需要登录验证
public R<Void> updateUser(@RequestBody UserUpdateDTO dto) {
Long currentUserId = LoginHelper.getUserId();
// 业务逻辑处理
userService.updateUser(currentUserId, dto);
return R.ok();
}
}
四、多设备类型支持
4.1 设备类型管理
RuoYi-Vue-Plus支持多种设备类型的权限控制:
public enum DeviceType {
PC("pc", "电脑端"),
APP("app", "移动端"),
WECHAT("wechat", "微信端"),
XCX("xcx", "小程序");
private final String code;
private final String info;
DeviceType(String code, String info) {
this.code = code;
this.info = info;
}
public String getCode() {
return code;
}
}
4.2 多设备登录实现
// 根据不同设备类型进行登录
public void loginByDevice(LoginUser loginUser, String deviceType) {
SaLoginParameter parameter = new SaLoginParameter()
.setDevice(deviceType)
.setTimeout(3600); // 1小时有效期
LoginHelper.login(loginUser, parameter);
}
// 检查特定设备的登录状态
public boolean isLoginOnDevice(String deviceType) {
return StpUtil.isLogin() &&
deviceType.equals(StpUtil.getLoginDevice());
}
五、权限控制最佳实践
5.1 接口权限控制
在Controller层使用注解进行权限控制:
@RestController
@RequestMapping("/system")
public class SystemController {
// 需要用户登录
@SaCheckLogin
@GetMapping("/info")
public R<SystemInfo> getSystemInfo() {
return R.ok(systemService.getSystemInfo());
}
// 需要特定权限
@SaCheckPermission("system:user:list")
@GetMapping("/users")
public R<List<UserVO>> getUserList() {
return R.ok(userService.getUserList());
}
// 需要管理员角色
@SaCheckRole("admin")
@PostMapping("/config")
public R<Void> updateConfig(@RequestBody ConfigDTO config) {
systemService.updateConfig(config);
return R.ok();
}
}
5.2 业务层权限验证
@Service
public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUser(Long userId, UserUpdateDTO dto) {
// 获取当前登录用户
LoginUser currentUser = LoginHelper.getLoginUser();
// 权限验证:只能修改自己的信息或拥有管理员权限
if (!userId.equals(currentUser.getUserId()) &&
!currentUser.getRolePermission().contains("admin")) {
throw new ServiceException("无权限修改其他用户信息");
}
// 业务逻辑处理
SysUser user = userMapper.selectById(userId);
if (user == null) {
throw new ServiceException("用户不存在");
}
// 更新操作
user.setEmail(dto.getEmail());
user.setPhone(dto.getPhone());
userMapper.updateById(user);
}
}
六、异常处理与安全防护
6.1 统一异常处理
public class SaTokenExceptionHandler {
@ExceptionHandler(NotLoginException.class)
public R<Void> handleNotLoginException(NotLoginException e) {
String message;
switch (e.getType()) {
case NotLoginException.NOT_TOKEN:
message = "未提供token";
break;
case NotLoginException.INVALID_TOKEN:
message = "token无效";
break;
case NotLoginException.TOKEN_TIMEOUT:
message = "token已过期";
break;
case NotLoginException.BE_REPLACED:
message = "token已被顶下线";
break;
case NotLoginException.KICK_OUT:
message = "token已被踢下线";
break;
default:
message = "当前会话未登录";
}
return R.fail(HttpStatus.UNAUTHORIZED, message);
}
@ExceptionHandler(NotPermissionException.class)
public R<Void> handleNotPermissionException(NotPermissionException e) {
return R.fail(HttpStatus.FORBIDDEN, "无权限访问:" + e.getPermission());
}
@ExceptionHandler(NotRoleException.class)
public R<Void> handleNotRoleException(NotRoleException e) {
return R.fail(HttpStatus.FORBIDDEN, "无角色权限:" + e.getRole());
}
}
6.2 安全防护措施
// 并发登录控制
public class ConcurrentLoginControl {
private static final int MAX_CONCURRENT_LOGINS = 3;
public void checkConcurrentLogin(String username) {
int loginCount = StpUtil.getLoginCount(username);
if (loginCount >= MAX_CONCURRENT_LOGINS) {
// 踢掉最早的登录会话
StpUtil.kickout(StpUtil.getLoginId(username, 0));
}
}
}
// 敏感操作验证
public class SecurityValidator {
public void validateSensitiveOperation(Long userId, String operation) {
LoginUser currentUser = LoginHelper.getLoginUser();
// 验证操作权限
if (!hasPermission(currentUser, operation)) {
throw new ServiceException("无权限执行此操作");
}
// 记录操作日志
auditLogService.logSensitiveOperation(userId, operation);
}
private boolean hasPermission(LoginUser user, String operation) {
return user.getMenuPermission().contains(operation) ||
user.getRolePermission().contains("admin");
}
}
七、实战案例:多租户权限系统
7.1 租户隔离实现
// 租户权限过滤器
@Component
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String tenantId = httpRequest.getHeader("X-Tenant-Id");
if (StringUtils.isNotBlank(tenantId)) {
// 设置当前租户上下文
TenantContext.setCurrentTenant(tenantId);
// 验证租户权限
if (!tenantService.isValidTenant(tenantId)) {
throw new ServiceException("租户不存在或已禁用");
}
}
try {
chain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
}
// 租户上下文管理
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenantId) {
currentTenant.set(tenantId);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
7.2 跨租户数据权限控制
// 数据权限拦截器
@Interceptor
public class DataPermissionInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 获取当前用户和租户信息
LoginUser loginUser = LoginHelper.getLoginUser();
String currentTenant = TenantContext.getCurrentTenant();
if (loginUser != null && currentTenant != null) {
// 构建数据权限SQL
String dataPermissionSql = buildDataPermissionSql(loginUser, currentTenant);
// 修改原始SQL,添加数据权限条件
String originalSql = boundSql.getSql();
String newSql = originalSql + " AND " + dataPermissionSql;
// 反射修改BoundSql中的SQL
ReflectUtil.setFieldValue(boundSql, "sql", newSql);
}
}
private String buildDataPermissionSql(LoginUser user, String tenantId) {
StringBuilder sqlBuilder = new StringBuilder();
// 根据用户角色构建不同的数据权限
if (user.getRolePermission().contains("admin")) {
// 管理员可以看到所有数据
sqlBuilder.append("tenant_id = '").append(tenantId).append("'");
} else if (user.getRolePermission().contains("dept_manager")) {
// 部门经理只能看到本部门数据
sqlBuilder.append("tenant_id = '").append(tenantId)
.append("' AND dept_id = ").append(user.getDeptId());
} else {
// 普通员工只能看到自己的数据
sqlBuilder.append("tenant_id = '").append(tenantId)
.append("' AND create_by = ").append(user.getUserId());
}
return sqlBuilder.toString();
}
}
八、性能优化与最佳实践
8.1 权限缓存策略
// 基于Redis的权限缓存
@Component
public class PermissionCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PERMISSION_KEY_PREFIX = "permission:";
private static final String ROLE_KEY_PREFIX = "role:";
private static final long CACHE_TIMEOUT = 30 * 60; // 30分钟
public List<String> getCachedPermissions(Long userId) {
String key = PERMISSION_KEY_PREFIX + userId;
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return (List<String>) cached;
}
// 从数据库查询并缓存
List<String> permissions = permissionMapper.selectPermissionsByUserId(userId);
redisTemplate.opsForValue().set(key, permissions, CACHE_TIMEOUT, TimeUnit.SECONDS);
return permissions;
}
public void evictPermissionCache(Long userId) {
String key = PERMISSION_KEY_PREFIX + userId;
redisTemplate.delete(key);
}
}
8.2 批量权限验证优化
// 批量权限验证服务
@Service
public class BatchPermissionService {
@Autowired
private PermissionService permissionService;
/**
* 批量验证权限,减少数据库查询次数
*/
public Map<Long, Boolean> batchCheckPermissions(List<Long> userIds, String permission) {
Map<Long, Boolean> result = new HashMap<>();
// 批量获取用户权限
Map<Long, List<String>> userPermissions = permissionService.batchGetPermissions(userIds);
for (Long userId : userIds) {
List<String> permissions = userPermissions.get(userId);
result.put(userId, permissions != null && permissions.contains(permission));
}
return result;
}
/**
* 预加载权限数据到缓存
*/
@Async
public void preloadPermissions(List<Long> userIds) {
permissionService.batchGetPermissions(userIds);
}
}
总结
RuoYi-Vue-Plus通过深度集成Sa-Token,提供了一套完整、灵活、高效的权限认证解决方案。本文从核心架构、多租户支持、权限控制、异常处理、性能优化等多个维度进行了详细解析,并提供了丰富的实战案例。
关键要点总结:
- 架构清晰:采用分层设计,职责分明,易于扩展和维护
- 多租户支持:完善的租户隔离和数据权限控制机制
- 灵活配置:支持多种设备类型和登录方式的权限控制
- 安全可靠:完善的异常处理和安全防护机制
- 性能优化:合理的缓存策略和批量处理优化
通过掌握这些技术要点,开发者可以快速构建安全可靠的企业级权限系统,满足各种复杂的业务场景需求。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



