上线我的 2.0

上线我的 2.0

马图图

岁月变迁何必不悔,尘世喧嚣怎能无愧。

3 文章数
0 评论数

SaToken多用户类型登录鉴权问题解决实战

Matuto
2025-08-20 / 0 评论 / 62 阅读 / 0 点赞

前言

最近在做ERP系统的时候,遇到了一个挺头疼的问题:系统需要同时支持管理员后台登录和普通用户APP登录,但是SaToken默认只能处理一种用户类型。这就像是一个门禁系统,既要让保安能进,又要让住户能进,但是门禁卡只有一种类型,怎么办呢?

经过一番折腾,终于找到了解决方案。今天就来分享一下这个问题的解决思路和具体实现。

问题分析

为什么会有这个问题?

SaToken默认情况下,所有的登录、权限验证都是基于同一个StpUtil来处理的。但是我们的系统有两个完全不同的用户体系:

  1. 管理员用户:后台管理系统的用户,有完整的权限管理
  2. 普通用户:APP端的用户,权限相对简单

如果都用同一个StpUtil,就会出现以下问题:

  • 用户类型混乱,无法区分
  • 权限验证逻辑复杂
  • 登录状态管理困难
  • 安全性无法保证

常见的错误做法

刚开始的时候,我试过这样处理:

// ❌ 错误做法:混用同一个StpUtil
@SaCheckLogin
public void someMethod() {
    // 这里无法区分是管理员还是普通用户
    Integer userId = StpUtil.getLoginIdAsInt();
}

结果就是各种权限混乱,用户能访问管理员的接口,管理员登录后反而被拒绝访问。

解决方案

核心思路

SaToken提供了一个很强大的功能:多账号体系。通过创建不同的StpLogic实例,可以为不同类型的用户创建独立的登录会话管理。

具体实现步骤

1. 创建StpKit门面类

首先,我们需要创建一个门面类来管理不同类型的用户会话:

package com.xscqc.erp.common.security;

import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpLogic;

/**
 * StpLogic门面类,管理不同类型的用户会话
 */
public class StpKit {
    /**
     * 管理员会话对象,管理Admin表所有账号的登录、权限认证
     */
    public static final StpLogic ADMIN = new StpLogicJwtForSimple("admin");

    /**
     * 普通用户会话对象,管理User表所有账号的登录、权限认证
     */
    public static final StpLogic USER = new StpLogicJwtForSimple("user");
}

这里的关键是给每个StpLogic指定不同的loginType

  • "admin":管理员类型
  • "user":普通用户类型

2. 配置SaToken

接下来在配置类中注册这些StpLogic:

@Configuration
@Slf4j
public class SaTokenConfig implements WebMvcConfigurer {
  
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/api/doc.html", "/api/swagger-ui/**", "/api/v3/api-docs/**");
    }
  
    @Bean
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForSimple();
    }

    @PostConstruct
    public void registerStp() {
        // 确保权限类型正确配置并输出日志
        StpLogic adminStp = StpKit.ADMIN;
        StpLogic userStp = StpKit.USER;
        log.info("Sa-Token StpLogic 注册完成 - Admin: {}, User: {}", 
                adminStp.getLoginType(), userStp.getLoginType());
    }
}

3. 创建自定义注解

为了方便使用,我们创建了两个自定义注解:

// 管理员登录校验注解
@SaCheckLogin(type = "admin")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaAdminCheckLogin {
}

// 普通用户登录校验注解
@SaCheckLogin(type = "user")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaUserCheckLogin {
}

这样在Controller中就可以很清楚地知道这个接口需要什么类型的用户登录了。

4. 实现登录逻辑

在登录服务中,根据用户类型调用不同的登录方法:

@Service
public class UserServiceImpl implements UserService {
  
    @Override
    public String login(UserLoginParam userLoginParam) {
        // 验证用户信息...
        User dbUser = validateUser(userLoginParam);
    
        // 根据用户类型调用不同的登录方法
        if (dbUser.getType().equals(SystemConstant.UserType.ADMIN)) {
            return SecurityUtils.adminLogin(dbUser.getId(), dbUser.getUsername(), 
                    dbUser.getMobile(), dbUser.getAuthName());
        } else {
            return SecurityUtils.userLogin(dbUser.getId(), dbUser.getUsername(), 
                    dbUser.getMobile(), dbUser.getAuthName());
        }
    }
}

5. 创建安全工具类

为了方便操作,我们创建了一个SecurityUtils工具类:

public class SecurityUtils {
  
    /**
     * 管理员登录
     */
    public static String adminLogin(Integer userId, String userName, String phone, String realName) {
        StpKit.ADMIN.login(userId);
        LoginUser loginUser = new LoginUser();
        loginUser.setId(userId);
        loginUser.setUsername(userName);
        loginUser.setPhone(phone);
        loginUser.setAuthName(realName);
        StpKit.ADMIN.getSession().set("loginUser", loginUser);
        return StpKit.ADMIN.getTokenValue();
    }

    /**
     * 普通用户登录
     */
    public static String userLogin(Integer userId, String userName, String phone, String realName) {
        StpKit.USER.login(userId);
        LoginUser loginUser = new LoginUser();
        loginUser.setId(userId);
        loginUser.setUsername(userName);
        loginUser.setPhone(phone);
        loginUser.setAuthName(realName);
        StpKit.USER.getSession().set("loginUser", loginUser);
        return StpKit.USER.getTokenValue();
    }
  
    /**
     * 判断是否为管理员登录
     */
    public static boolean isAdminLogin() {
        return StpKit.ADMIN.isLogin();
    }
  
    /**
     * 判断是否为普通用户登录
     */
    public static boolean isUserLogin() {
        return StpKit.USER.isLogin();
    }
  
    /**
     * 获取管理员登录用户信息
     */
    public static LoginUser getAdminLoginUser() {
        try {
            return (LoginUser) StpKit.ADMIN.getSession().get("loginUser");
        } catch (Exception e) {
            throw new ServiceException("获取用户信息异常");
        }
    }
  
    /**
     * 获取APP登录用户信息
     */
    public static LoginUser getAppLoginUser() {
        try {
            return (LoginUser) StpKit.USER.getSession().get("loginUser");
        } catch (Exception e) {
            throw new ServiceException("获取用户信息异常");
        }
    }
}

6. 在Controller中使用

现在在Controller中就可以很清楚地使用不同的注解了:

// 管理员接口
@Tag(name = "鉴权", description = "鉴权")
@SaAdminCheckLogin  // 只需要管理员登录
@RequestMapping("/auth")
@RestController
public class AuthController {
  
    @SaIgnore  // 登录接口不需要验证
    @PostMapping("/login")
    public ResultVo login(@RequestBody @Valid UserLoginParam userLoginParam) {
        return ResultVo.success(userService.login(userLoginParam));
    }
  
    @GetMapping("/permissions")
    public ResultVo permissions() {
        return ResultVo.success(userService.permissions());
    }
}

// 普通用户接口
@Tag(name = "用户", description = "用户相关")
@SaUserCheckLogin   // 只需要普通用户登录
@RequestMapping("/user")
@RestController
public class UserController {
  
    @GetMapping("/profile")
    public ResultVo getProfile() {
        LoginUser user = SecurityUtils.getAppLoginUser();
        return ResultVo.success(user);
    }
}

7. 权限验证实现

对于需要权限验证的场景,我们实现了StpInterface

@Component
@Slf4j
public class AdminUserPermission implements StpInterface {

    @Resource
    private MenuService menuService;

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        log.info("获取权限列表,用户Id:{}, 登录类型:{}", loginId, loginType);
    
        try {
            // 只有管理员类型才需要权限验证
            if ("admin".equals(loginType)) {
                List<String> permissions = menuService.selectMenuPermsByUserId(
                    Integer.parseInt(loginId.toString()));
                log.info("用户 {} 的权限列表: {}", loginId, permissions);
                return permissions;
            }
            return List.of();
        } catch (Exception e) {
            log.error("获取用户权限失败,用户ID: {}, 错误: {}", loginId, e.getMessage(), e);
            return List.of();
        }
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        log.info("获取角色列表,用户Id:{}, 登录类型:{}", loginId, loginType);
        // 暂时返回空列表,如果需要角色验证可以在这里实现
        return List.of();
    }
}

使用效果

登录流程

  1. 管理员登录

    • 调用SecurityUtils.adminLogin()
    • 生成admin类型的token
    • 存储在StpKit.ADMIN
  2. 普通用户登录

    • 调用SecurityUtils.userLogin()
    • 生成user类型的token
    • 存储在StpKit.USER

权限验证

  1. 接口访问控制

    • @SaAdminCheckLogin:只有管理员能访问
    • @SaUserCheckLogin:只有普通用户能访问
    • 两种用户类型完全隔离
  2. 用户信息获取

    • SecurityUtils.getAdminLoginUser():获取管理员信息
    • SecurityUtils.getAppLoginUser():获取普通用户信息

会话管理

每个用户类型都有独立的会话管理:

  • 登录状态独立
  • Token独立
  • 会话数据独立
  • 退出登录独立

注意事项

1. 配置文件中要注意

application.yml中,SaToken的配置要支持多账号体系:

# sa-token
sa-token:
  token-name: Authorization
  timeout: 2592000
  active-timeout: -1
  is-concurrent: false
  is-share: false
  token-style: jwt
  jwt-secret-key: your-secret-key
  is-log: false
  token-prefix: Bearer

2. 拦截器配置

确保拦截器能正确处理不同类型的请求:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor())
            .addPathPatterns("/**")
            .excludePathPatterns("/api/doc.html", "/api/swagger-ui/**", "/api/v3/api-docs/**");
}

3. 异常处理

在全局异常处理器中,要能区分不同类型的SaToken异常:

@RestControllerAdvice
public class GlobalExceptionHandler {
  
    @ExceptionHandler(NotLoginException.class)
    public ResultVo handleNotLoginException(NotLoginException e) {
        String message = "用户未登录";
        if (e.getType().equals(NotLoginException.INVALID_TOKEN)) {
            message = "Token无效";
        } else if (e.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
            message = "Token已过期";
        }
        return ResultVo.error(message);
    }
}

总结

通过SaToken的多账号体系,我们成功解决了多用户类型登录鉴权的问题。这种方案的优势在于:

  1. 清晰分离:管理员和普通用户完全隔离
  2. 易于维护:代码结构清晰,逻辑简单
  3. 扩展性好:如果以后需要增加其他用户类型,很容易扩展
  4. 安全性高:不同类型的用户无法互相访问

虽然刚开始配置的时候有点复杂,但是一旦配置好了,用起来就非常方便了。就像搭积木一样,基础搭建好了,上面的功能就很容易实现了。

如果你也遇到了类似的问题,不妨试试这个方案。有什么问题或者更好的想法,欢迎在评论区交流!


本文基于实际项目经验总结,如有错误或遗漏,欢迎指正。

下一篇
评论
来首音乐
最新回复
    暂无内容
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月
文章目录
每日一句