KnowEngine 项目采用 sa-token + Redis 实现双轨登录体系,分别服务于两类用户群体: - 网页端客户:通过手机号+密码登录,用于车辆售前/售后咨询 - 钉钉员工(客服):不通过网页登录,直接通过钉钉机器人提问,系统自动识别身份 两套体系共享同一套认证服务接口,但通过不同的登录入口、不同的 loginId 标识进行隔离。
核心架构
┌─────────────────────────────────────────────┐
│ 前端/客户端 │
│ login.html / staff-login.html / 钉钉 │
└──────────────┬──────────────────┬────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ AuthController │ │ DingTalk 回调拦截器 │
│ /auth/login │ │ /dingtalk/** │
│ /auth/staffLogin │ │ (排除 sa-token 校验) │
└──────────┬───────────┘ └──────────┬───────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────┐
│ AuthServiceImpl │
│ - login(LoginDTO) → 客户登录 │
│ - staffLogin(StaffLoginDTO) → 员工登录 │
│ - getCurrentUserId() → 从 sa-token 获取 │
│ - isStaffLogin() → 判断是否为员工 │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ sa-token (StpUtil) │
│ StpUtil.login(loginId, device) │
│ StpUtil.getLoginIdAsString() │
│ StpUtil.checkLogin() │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Redis │
│ 存储 session、token 映射关系 │
└──────────────────────────────────────────────┘
sa-token接入
增加相关依赖
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency>
<!-- Sa-Token 整合 Redis(使用 Jackson 序列化) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.39.0</version>
</dependency>
<!-- 连接池(sa-token-redis-jackson 依赖 commons-pool2) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
redis用于登录状态管理。 在application.yml中增加配置:
## Sa-Token 配置
sa-token:
# token 名称(同时也是 Cookie 名称、Header 名称)
token-name: satoken
# token 有效期(秒),默认 30 天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(秒),如果 token 超过这个时间没有访问系统就会被冻结,默认-1 代表不限制
active-timeout: -1
# 是否允许同一账号并发登录(为 true 时允许一起登录,为 false 时新登录会挤下旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token(为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token)
is-share: true
# token 风格(uuid)
token-style: uuid
# 是否输出操作日志
is-log: false
# 是否从 cookie 中读取 token
is-read-cookie: true
# 是否从 header 中读取 token
is-read-header: true
全局拦截配置:
package cn.hollis.llm.mentor.know.engine.auth.config;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Set;
/**
* Sa-Token 配置:注册全局拦截器,对除登录、静态资源外的所有接口进行登录校验
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
private static final Set<String> STATIC_EXTENSIONS = Set.of(
"html", "js", "css", "png", "jpg", "jpeg", "gif", "svg",
"ico", "woff", "woff2", "ttf"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
// 全局登录校验(排除无需登录的路径和静态资源)
SaRouter.match("/**")
.notMatch("/auth/**")
.notMatch("/dingtalk/**")
.notMatch("/druid/**")
.check(r -> {
if (isStaticResource()) return;
StpUtil.checkLogin();
});
// 文档管理路由需要员工登录(通过 loginId 前缀识别员工身份)
SaRouter.match("/api/document/**", "/api/segment/**")
.check(r -> {
StpUtil.checkLogin();
String loginId = StpUtil.getLoginIdAsString();
if (!loginId.startsWith("staff_")) {
throw new NotLoginException("文档管理需要员工登录", NotLoginException.NOT_TOKEN, null);
}
});
})).addPathPatterns("/**");
}
private boolean isStaticResource() {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) return false;
HttpServletRequest request = attrs.getRequest();
String uri = request.getRequestURI();
int dotIndex = uri.lastIndexOf('.');
if (dotIndex == -1) return false;
String ext = uri.substring(dotIndex + 1).toLowerCase();
return STATIC_EXTENSIONS.contains(ext);
}
}
客户登录流程
登录入口
文件:AuthController.java
@PostMapping
public
}
认证逻辑
文件:AuthServiceImpl.java → login(LoginDTO)
public
log
userInfo
}
前端交互
文件:login.html
const
}
const
if
window
}
说明:sa-token 默认启用 Cookie 模式(is-read-cookie: true),登录成功后自动将 token 写入浏览器 Cookie,后续请求自动携带。
员工登录流程
网页端员工登录
员工可通过网页端手动登录(用于测试或后台管理):
@PostMapping
public
}
认证逻辑与客户登录类似,区别在于:
// 员工登录:使用 staff_ 前缀 + id 作为登录主键,设备标识为 "staff"
StpUtil
// 示例:loginId = "staff_12345", device = "staff"
身份识别机制
获取当前用户信息
@Override
public
}
@Override
public
}
在controller中,不再接受前端传入的userId,而是用以上方法,从session中读取。前端会传入登录后保存的token,通过这个token就能去session中查询已经保存过的用户id了。 没登录的用户进来会无法获取用户id。也无法获取其他人的id,这样做会更加安全。
路由权限控制
全局登录校验
所有接口默认需要登录,除了以下白名单:
.
.
.
员工专属路由
文档管理相关接口要求必须是员工登录:
// 文档管理路由需要员工登录(通过 loginId 前缀识别员工身份)
SaRouter
业务场景中的应用
查询对话列表
做了权限隔离,用户只能查自己的对话列表。
@GetMapping
public
}