最近在做ERP系统的时候,遇到了一个挺头疼的问题:系统需要同时支持管理员后台登录和普通用户APP登录,但是SaToken默认只能处理一种用户类型。这就像是一个门禁系统,既要让保安能进,又要让住户能进,但是门禁卡只有一种类型,怎么办呢?
经过一番折腾,终于找到了解决方案。今天就来分享一下这个问题的解决思路和具体实现。
SaToken默认情况下,所有的登录、权限验证都是基于同一个StpUtil
来处理的。但是我们的系统有两个完全不同的用户体系:
如果都用同一个StpUtil
,就会出现以下问题:
刚开始的时候,我试过这样处理:
// ❌ 错误做法:混用同一个StpUtil
@SaCheckLogin
public void someMethod() {
// 这里无法区分是管理员还是普通用户
Integer userId = StpUtil.getLoginIdAsInt();
}
结果就是各种权限混乱,用户能访问管理员的接口,管理员登录后反而被拒绝访问。
SaToken提供了一个很强大的功能:多账号体系。通过创建不同的StpLogic
实例,可以为不同类型的用户创建独立的登录会话管理。
首先,我们需要创建一个门面类来管理不同类型的用户会话:
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"
:普通用户类型接下来在配置类中注册这些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());
}
}
为了方便使用,我们创建了两个自定义注解:
// 管理员登录校验注解
@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中就可以很清楚地知道这个接口需要什么类型的用户登录了。
在登录服务中,根据用户类型调用不同的登录方法:
@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());
}
}
}
为了方便操作,我们创建了一个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("获取用户信息异常");
}
}
}
现在在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);
}
}
对于需要权限验证的场景,我们实现了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();
}
}
管理员登录:
SecurityUtils.adminLogin()
admin
类型的tokenStpKit.ADMIN
中普通用户登录:
SecurityUtils.userLogin()
user
类型的tokenStpKit.USER
中接口访问控制:
@SaAdminCheckLogin
:只有管理员能访问@SaUserCheckLogin
:只有普通用户能访问用户信息获取:
SecurityUtils.getAdminLoginUser()
:获取管理员信息SecurityUtils.getAppLoginUser()
:获取普通用户信息每个用户类型都有独立的会话管理:
在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
确保拦截器能正确处理不同类型的请求:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/api/doc.html", "/api/swagger-ui/**", "/api/v3/api-docs/**");
}
在全局异常处理器中,要能区分不同类型的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的多账号体系,我们成功解决了多用户类型登录鉴权的问题。这种方案的优势在于:
虽然刚开始配置的时候有点复杂,但是一旦配置好了,用起来就非常方便了。就像搭积木一样,基础搭建好了,上面的功能就很容易实现了。
如果你也遇到了类似的问题,不妨试试这个方案。有什么问题或者更好的想法,欢迎在评论区交流!
本文基于实际项目经验总结,如有错误或遗漏,欢迎指正。