Sa-Token 核心原理与实战:从登录会话到微服务权限鉴权

Sa-Token 核心原理

1 Sa-Token 是什么

Sa-Token 是一个 Java 权限认证框架,常用能力包括登录认证、角色认证、权限认证、分布式 Session、多设备登录、踢人下线、单点登录 SSO、OAuth2、JWT 集成、微服务网关鉴权和多账号体系。它的价值不是替业务系统校验密码,而是把"登录成功后的状态维护、Token 管理和权限校验"统一封装起来。

能认证,能鉴权。官方对自己的介绍是 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!

java 复制代码
// 建立登录状态
StpUtil.login(10001L);

// 校验是否登录
StpUtil.checkLogin();

// 获取当前登录用户 ID
long userId = StpUtil.getLoginIdAsLong();

// 退出登录
StpUtil.logout();

必须记住:StpUtil.login(userId) 不会检查用户名和密码。账号密码、短信验证码、账号状态等由业务系统校验,校验成功后再调用 Sa-Token 建立登录状态。
#mermaid-svg-FcbN6l5koyEBCGEL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FcbN6l5koyEBCGEL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FcbN6l5koyEBCGEL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FcbN6l5koyEBCGEL .error-icon{fill:#552222;}#mermaid-svg-FcbN6l5koyEBCGEL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FcbN6l5koyEBCGEL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FcbN6l5koyEBCGEL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FcbN6l5koyEBCGEL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FcbN6l5koyEBCGEL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FcbN6l5koyEBCGEL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FcbN6l5koyEBCGEL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FcbN6l5koyEBCGEL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FcbN6l5koyEBCGEL .marker.cross{stroke:#333333;}#mermaid-svg-FcbN6l5koyEBCGEL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FcbN6l5koyEBCGEL p{margin:0;}#mermaid-svg-FcbN6l5koyEBCGEL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FcbN6l5koyEBCGEL .cluster-label text{fill:#333;}#mermaid-svg-FcbN6l5koyEBCGEL .cluster-label span{color:#333;}#mermaid-svg-FcbN6l5koyEBCGEL .cluster-label span p{background-color:transparent;}#mermaid-svg-FcbN6l5koyEBCGEL .label text,#mermaid-svg-FcbN6l5koyEBCGEL span{fill:#333;color:#333;}#mermaid-svg-FcbN6l5koyEBCGEL .node rect,#mermaid-svg-FcbN6l5koyEBCGEL .node circle,#mermaid-svg-FcbN6l5koyEBCGEL .node ellipse,#mermaid-svg-FcbN6l5koyEBCGEL .node polygon,#mermaid-svg-FcbN6l5koyEBCGEL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FcbN6l5koyEBCGEL .rough-node .label text,#mermaid-svg-FcbN6l5koyEBCGEL .node .label text,#mermaid-svg-FcbN6l5koyEBCGEL .image-shape .label,#mermaid-svg-FcbN6l5koyEBCGEL .icon-shape .label{text-anchor:middle;}#mermaid-svg-FcbN6l5koyEBCGEL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FcbN6l5koyEBCGEL .rough-node .label,#mermaid-svg-FcbN6l5koyEBCGEL .node .label,#mermaid-svg-FcbN6l5koyEBCGEL .image-shape .label,#mermaid-svg-FcbN6l5koyEBCGEL .icon-shape .label{text-align:center;}#mermaid-svg-FcbN6l5koyEBCGEL .node.clickable{cursor:pointer;}#mermaid-svg-FcbN6l5koyEBCGEL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FcbN6l5koyEBCGEL .arrowheadPath{fill:#333333;}#mermaid-svg-FcbN6l5koyEBCGEL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FcbN6l5koyEBCGEL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FcbN6l5koyEBCGEL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FcbN6l5koyEBCGEL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FcbN6l5koyEBCGEL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FcbN6l5koyEBCGEL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FcbN6l5koyEBCGEL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FcbN6l5koyEBCGEL .cluster text{fill:#333;}#mermaid-svg-FcbN6l5koyEBCGEL .cluster span{color:#333;}#mermaid-svg-FcbN6l5koyEBCGEL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FcbN6l5koyEBCGEL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FcbN6l5koyEBCGEL rect.text{fill:none;stroke-width:0;}#mermaid-svg-FcbN6l5koyEBCGEL .icon-shape,#mermaid-svg-FcbN6l5koyEBCGEL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FcbN6l5koyEBCGEL .icon-shape p,#mermaid-svg-FcbN6l5koyEBCGEL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FcbN6l5koyEBCGEL .icon-shape .label rect,#mermaid-svg-FcbN6l5koyEBCGEL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FcbN6l5koyEBCGEL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FcbN6l5koyEBCGEL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FcbN6l5koyEBCGEL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 校验失败
校验成功
用户提交账号密码
业务系统校验身份
返回登录失败
调用 StpUtil.login
生成 Token
保存登录状态
返回 Token 给客户端


2 认证、授权和会话管理

认证解决"你是谁",授权解决"你能做什么",会话管理解决"登录成功后如何持续识别你"。账号密码登录、短信验证码登录和扫码登录都属于认证方式;角色和权限校验属于授权;Token、Session 和 Refresh Token 属于会话管理手段。

概念 解决的问题 常见实现
认证 Authentication 当前用户是谁 账号密码、验证码、扫码登录
授权 Authorization 当前用户能访问什么 RBAC、角色、权限码
会话管理 Session Management 如何维持登录状态 Session、Token、JWT

完整流程如下:
#mermaid-svg-ZbcLZqVGdgYl8mf5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .error-icon{fill:#552222;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .marker.cross{stroke:#333333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 p{margin:0;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .cluster-label text{fill:#333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .cluster-label span{color:#333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .cluster-label span p{background-color:transparent;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .label text,#mermaid-svg-ZbcLZqVGdgYl8mf5 span{fill:#333;color:#333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .node rect,#mermaid-svg-ZbcLZqVGdgYl8mf5 .node circle,#mermaid-svg-ZbcLZqVGdgYl8mf5 .node ellipse,#mermaid-svg-ZbcLZqVGdgYl8mf5 .node polygon,#mermaid-svg-ZbcLZqVGdgYl8mf5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .rough-node .label text,#mermaid-svg-ZbcLZqVGdgYl8mf5 .node .label text,#mermaid-svg-ZbcLZqVGdgYl8mf5 .image-shape .label,#mermaid-svg-ZbcLZqVGdgYl8mf5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .rough-node .label,#mermaid-svg-ZbcLZqVGdgYl8mf5 .node .label,#mermaid-svg-ZbcLZqVGdgYl8mf5 .image-shape .label,#mermaid-svg-ZbcLZqVGdgYl8mf5 .icon-shape .label{text-align:center;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .node.clickable{cursor:pointer;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .arrowheadPath{fill:#333333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZbcLZqVGdgYl8mf5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZbcLZqVGdgYl8mf5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZbcLZqVGdgYl8mf5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .cluster text{fill:#333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .cluster span{color:#333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZbcLZqVGdgYl8mf5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .icon-shape,#mermaid-svg-ZbcLZqVGdgYl8mf5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .icon-shape p,#mermaid-svg-ZbcLZqVGdgYl8mf5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .icon-shape .label rect,#mermaid-svg-ZbcLZqVGdgYl8mf5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZbcLZqVGdgYl8mf5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZbcLZqVGdgYl8mf5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZbcLZqVGdgYl8mf5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交身份凭证
认证成功
创建登录会话
客户端保存 Token
后续请求携带 Token
服务端恢复用户身份
执行权限校验


3 Sa-Token 默认不是 JWT

Sa-Token 默认生成的是随机 Token,Token 本身通常不携带用户 ID、角色、权限和过期时间。服务端保存 Token → LoginId 映射,请求到达后通过 Token 查询当前登录用户,因此默认模式是有状态认证,本质上更接近分布式 Session,而不是无状态 JWT。
#mermaid-svg-J1tcnDnW2H2mL4gm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J1tcnDnW2H2mL4gm .error-icon{fill:#552222;}#mermaid-svg-J1tcnDnW2H2mL4gm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J1tcnDnW2H2mL4gm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J1tcnDnW2H2mL4gm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J1tcnDnW2H2mL4gm .marker.cross{stroke:#333333;}#mermaid-svg-J1tcnDnW2H2mL4gm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J1tcnDnW2H2mL4gm p{margin:0;}#mermaid-svg-J1tcnDnW2H2mL4gm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J1tcnDnW2H2mL4gm .cluster-label text{fill:#333;}#mermaid-svg-J1tcnDnW2H2mL4gm .cluster-label span{color:#333;}#mermaid-svg-J1tcnDnW2H2mL4gm .cluster-label span p{background-color:transparent;}#mermaid-svg-J1tcnDnW2H2mL4gm .label text,#mermaid-svg-J1tcnDnW2H2mL4gm span{fill:#333;color:#333;}#mermaid-svg-J1tcnDnW2H2mL4gm .node rect,#mermaid-svg-J1tcnDnW2H2mL4gm .node circle,#mermaid-svg-J1tcnDnW2H2mL4gm .node ellipse,#mermaid-svg-J1tcnDnW2H2mL4gm .node polygon,#mermaid-svg-J1tcnDnW2H2mL4gm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J1tcnDnW2H2mL4gm .rough-node .label text,#mermaid-svg-J1tcnDnW2H2mL4gm .node .label text,#mermaid-svg-J1tcnDnW2H2mL4gm .image-shape .label,#mermaid-svg-J1tcnDnW2H2mL4gm .icon-shape .label{text-anchor:middle;}#mermaid-svg-J1tcnDnW2H2mL4gm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J1tcnDnW2H2mL4gm .rough-node .label,#mermaid-svg-J1tcnDnW2H2mL4gm .node .label,#mermaid-svg-J1tcnDnW2H2mL4gm .image-shape .label,#mermaid-svg-J1tcnDnW2H2mL4gm .icon-shape .label{text-align:center;}#mermaid-svg-J1tcnDnW2H2mL4gm .node.clickable{cursor:pointer;}#mermaid-svg-J1tcnDnW2H2mL4gm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J1tcnDnW2H2mL4gm .arrowheadPath{fill:#333333;}#mermaid-svg-J1tcnDnW2H2mL4gm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J1tcnDnW2H2mL4gm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J1tcnDnW2H2mL4gm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J1tcnDnW2H2mL4gm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J1tcnDnW2H2mL4gm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J1tcnDnW2H2mL4gm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J1tcnDnW2H2mL4gm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J1tcnDnW2H2mL4gm .cluster text{fill:#333;}#mermaid-svg-J1tcnDnW2H2mL4gm .cluster span{color:#333;}#mermaid-svg-J1tcnDnW2H2mL4gm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J1tcnDnW2H2mL4gm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J1tcnDnW2H2mL4gm rect.text{fill:none;stroke-width:0;}#mermaid-svg-J1tcnDnW2H2mL4gm .icon-shape,#mermaid-svg-J1tcnDnW2H2mL4gm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J1tcnDnW2H2mL4gm .icon-shape p,#mermaid-svg-J1tcnDnW2H2mL4gm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J1tcnDnW2H2mL4gm .icon-shape .label rect,#mermaid-svg-J1tcnDnW2H2mL4gm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J1tcnDnW2H2mL4gm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J1tcnDnW2H2mL4gm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J1tcnDnW2H2mL4gm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 随机 Token
Redis 中查询
LoginId
Account-Session
角色与权限

传统 Session、Sa-Token 默认模式和 JWT 的区别如下:

对比项 Cookie + Session Sa-Token 默认模式 JWT
客户端凭证 SessionId 随机 Token 自包含 Token
服务端是否保存状态 通常否
分布式部署 共享 Session 共享 Redis 各服务本地验签
主动注销 容易 容易 较难
踢人下线 需要扩展 内置支持 需要黑名单
权限实时变更 较容易 较容易 旧 Token 可能继续有效
每次请求是否查询存储 通常需要 通常需要 可不查询

Sa-Token 也可以集成 JWT,但那是可选模式,不是默认行为。


4 Sa-Token 核心对象

4.1 LoginId

LoginId 是账号唯一标识,通常使用数据库用户主键,例如用户 ID、管理员 ID、商家 ID。不要优先使用用户名,因为用户名可能修改,而主键通常更稳定。

java 复制代码
StpUtil.login(10001L);

这里的 10001L 就是 LoginId。

4.2 TokenValue

TokenValue 是返回给客户端的登录凭证,客户端后续通过 Header 或 Cookie 携带它。

java 复制代码
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
String token = tokenInfo.getTokenValue();
http 复制代码
Authorization: Bearer e5f87e8d-3f19-4e55-9222-4ccac51a0d62

4.3 LoginType

LoginType 表示账号体系。系统中可以同时存在 useradminmerchant 等多个账号体系,每个体系拥有独立的登录状态、权限数据和会话配置。

java 复制代码
public static final StpLogic USER_LOGIC = new StpLogic("user");
public static final StpLogic ADMIN_LOGIC = new StpLogic("admin");

即使普通用户和管理员的 ID 都是 10001,只要 LoginType 不同,就不会冲突。

比如用户可以是普通用户登录小程序,也可以是管理员登录后台管理系统,身份不同,但是是同一个人

4.4 StpUtil 与 StpLogic

StpUtil 是默认账号体系的静态工具类,真正执行登录、注销、踢人下线、权限校验的是 StpLogic。多账号体系的本质,就是为不同 LoginType 创建不同的 StpLogic

java 复制代码
public class StpUtil {
    public static final String TYPE = "login";
    public static StpLogic stpLogic = new StpLogic("login");

    private StpUtil() {
    }
}

// 所以我们也可以自己定义一个,使用方法和 StpUtil 也一样了
public final class StpAdminUtil {

    public static final String LOGIN_TYPE = "admin";

    private static final StpLogic STP_LOGIC = new StpLogic(LOGIN_TYPE);

    private StpAdminUtil() {
    }

    public static void login(Long userId) {
        STP_LOGIC.login(userId);
    }

    public static String getTokenValue() {
        return STP_LOGIC.getTokenValue();
    }

    public static void logout() {
        STP_LOGIC.logout();
    }
}

这个 login 就是上面指定的 StpLogic 的参数

4.5 SaTokenDao

SaTokenDao 是 Sa-Token 的数据持久化接口,负责保存 Token 映射、Session、过期时间和活跃时间。单机环境可以使用本地内存,集群环境通常使用 Redis,否则用户在实例 A 登录后,请求落到实例 B 时,实例 B 无法识别登录状态。

基于本地内存是默认的实现,底层使用的 ConcurrentHashMap 数据结构

如果我们想使用 redis 来保存数据,我们需要导入 sa-token-redis-template,如果想让 Sa-Token 用 jackson 处理 JSON 序列化,就导入 sa-token-token-jackson,看上面的依赖看来是不需要,因为 sa-token-reactor-spring-boot3-starter 已经自动导入了。


5 Sa-Token 会话结构

理解 Sa-Token 的会话结构,可以先记住一句话:Sa-Token 会围绕"账号"和"Token"分别保存数据,这些数据不是层层嵌套,而是通过 LoginId 和 Token 相互关联。

当然 Sa-Token 本身有配置来配置是否多端登录,是否 Token 共享等,下面讲述的是这个配置下的情况

yaml 复制代码
sa-token:
	is-concurrent: true
 	is-share: false

假设用户 10001 分别在电脑和手机上登录:

  • PC 端获得 Token-A
  • APP 端获得 Token-B
  • 两个 Token 都属于账号 10001

Sa-Token 中的数据关系如下:
#mermaid-svg-shjCaFhUQI2O0pfW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-shjCaFhUQI2O0pfW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-shjCaFhUQI2O0pfW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-shjCaFhUQI2O0pfW .error-icon{fill:#552222;}#mermaid-svg-shjCaFhUQI2O0pfW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-shjCaFhUQI2O0pfW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-shjCaFhUQI2O0pfW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-shjCaFhUQI2O0pfW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-shjCaFhUQI2O0pfW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-shjCaFhUQI2O0pfW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-shjCaFhUQI2O0pfW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-shjCaFhUQI2O0pfW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-shjCaFhUQI2O0pfW .marker.cross{stroke:#333333;}#mermaid-svg-shjCaFhUQI2O0pfW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-shjCaFhUQI2O0pfW p{margin:0;}#mermaid-svg-shjCaFhUQI2O0pfW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-shjCaFhUQI2O0pfW .cluster-label text{fill:#333;}#mermaid-svg-shjCaFhUQI2O0pfW .cluster-label span{color:#333;}#mermaid-svg-shjCaFhUQI2O0pfW .cluster-label span p{background-color:transparent;}#mermaid-svg-shjCaFhUQI2O0pfW .label text,#mermaid-svg-shjCaFhUQI2O0pfW span{fill:#333;color:#333;}#mermaid-svg-shjCaFhUQI2O0pfW .node rect,#mermaid-svg-shjCaFhUQI2O0pfW .node circle,#mermaid-svg-shjCaFhUQI2O0pfW .node ellipse,#mermaid-svg-shjCaFhUQI2O0pfW .node polygon,#mermaid-svg-shjCaFhUQI2O0pfW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-shjCaFhUQI2O0pfW .rough-node .label text,#mermaid-svg-shjCaFhUQI2O0pfW .node .label text,#mermaid-svg-shjCaFhUQI2O0pfW .image-shape .label,#mermaid-svg-shjCaFhUQI2O0pfW .icon-shape .label{text-anchor:middle;}#mermaid-svg-shjCaFhUQI2O0pfW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-shjCaFhUQI2O0pfW .rough-node .label,#mermaid-svg-shjCaFhUQI2O0pfW .node .label,#mermaid-svg-shjCaFhUQI2O0pfW .image-shape .label,#mermaid-svg-shjCaFhUQI2O0pfW .icon-shape .label{text-align:center;}#mermaid-svg-shjCaFhUQI2O0pfW .node.clickable{cursor:pointer;}#mermaid-svg-shjCaFhUQI2O0pfW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-shjCaFhUQI2O0pfW .arrowheadPath{fill:#333333;}#mermaid-svg-shjCaFhUQI2O0pfW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-shjCaFhUQI2O0pfW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-shjCaFhUQI2O0pfW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-shjCaFhUQI2O0pfW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-shjCaFhUQI2O0pfW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-shjCaFhUQI2O0pfW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-shjCaFhUQI2O0pfW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-shjCaFhUQI2O0pfW .cluster text{fill:#333;}#mermaid-svg-shjCaFhUQI2O0pfW .cluster span{color:#333;}#mermaid-svg-shjCaFhUQI2O0pfW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-shjCaFhUQI2O0pfW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-shjCaFhUQI2O0pfW rect.text{fill:none;stroke-width:0;}#mermaid-svg-shjCaFhUQI2O0pfW .icon-shape,#mermaid-svg-shjCaFhUQI2O0pfW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-shjCaFhUQI2O0pfW .icon-shape p,#mermaid-svg-shjCaFhUQI2O0pfW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-shjCaFhUQI2O0pfW .icon-shape .label rect,#mermaid-svg-shjCaFhUQI2O0pfW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-shjCaFhUQI2O0pfW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-shjCaFhUQI2O0pfW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-shjCaFhUQI2O0pfW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PC 请求携带 Token-A
Token-A 映射
APP 请求携带 Token-B
Token-B 映射
LoginId 10001
Account-Session

账号 10001 共享
Token-Session A

仅 PC 端使用
Token-Session B

仅 APP 端使用
Token-A 最后活跃时间
Token-B 最后活跃时间

如果使用默认的 token-name=satokenlogin-type=login,Redis 中的数据可以大致理解为:

数据 示例 Key 保存的内容
Token 映射 satoken:login:token:Token-A 100000050
Account-Session satoken:login:session:100000050 账号共享数据、终端列表
Token-Session satoken:login:token-session:Token-A Token-A 独享数据
最后活跃时间 satoken:login:last-active:Token-A 最近一次操作时间

一个账号 100000050 对应一个 session,这个 Authorization:admin:session:100000050 是 Account-session,然后这个账号下的所有 token 又都对应一个映射,他不是 Token-Session,它只是 Token 的映射,去找 LoginId 的,我们没有为 Token 创建 Session,默认是不会创建的,它的 Redis-Key 是 Authorization:admin:token-session:一个很长的Token

5.1 Token 与 LoginId 映射

Token 映射解决的问题是:

客户端只提交了一个 Token,服务端怎么知道它属于哪个用户?

例如 PC 端请求携带:

http 复制代码
Authorization: Bearer Token-A

Sa-Token 会查询:

Token-A → 100000050

得到 LoginId 后,系统便知道当前请求属于用户 100000050
Redis Sa-Token 客户端 Redis Sa-Token 客户端 #mermaid-svg-0DVy9gjkZftbbvPp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0DVy9gjkZftbbvPp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0DVy9gjkZftbbvPp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0DVy9gjkZftbbvPp .error-icon{fill:#552222;}#mermaid-svg-0DVy9gjkZftbbvPp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0DVy9gjkZftbbvPp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0DVy9gjkZftbbvPp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0DVy9gjkZftbbvPp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0DVy9gjkZftbbvPp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0DVy9gjkZftbbvPp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0DVy9gjkZftbbvPp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0DVy9gjkZftbbvPp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0DVy9gjkZftbbvPp .marker.cross{stroke:#333333;}#mermaid-svg-0DVy9gjkZftbbvPp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0DVy9gjkZftbbvPp p{margin:0;}#mermaid-svg-0DVy9gjkZftbbvPp .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0DVy9gjkZftbbvPp text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0DVy9gjkZftbbvPp .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0DVy9gjkZftbbvPp .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0DVy9gjkZftbbvPp .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0DVy9gjkZftbbvPp .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0DVy9gjkZftbbvPp #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0DVy9gjkZftbbvPp .sequenceNumber{fill:white;}#mermaid-svg-0DVy9gjkZftbbvPp #sequencenumber{fill:#333;}#mermaid-svg-0DVy9gjkZftbbvPp #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0DVy9gjkZftbbvPp .messageText{fill:#333;stroke:none;}#mermaid-svg-0DVy9gjkZftbbvPp .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0DVy9gjkZftbbvPp .labelText,#mermaid-svg-0DVy9gjkZftbbvPp .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0DVy9gjkZftbbvPp .loopText,#mermaid-svg-0DVy9gjkZftbbvPp .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0DVy9gjkZftbbvPp .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0DVy9gjkZftbbvPp .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0DVy9gjkZftbbvPp .noteText,#mermaid-svg-0DVy9gjkZftbbvPp .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0DVy9gjkZftbbvPp .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0DVy9gjkZftbbvPp .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0DVy9gjkZftbbvPp .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0DVy9gjkZftbbvPp .actorPopupMenu{position:absolute;}#mermaid-svg-0DVy9gjkZftbbvPp .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0DVy9gjkZftbbvPp .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0DVy9gjkZftbbvPp .actor-man circle,#mermaid-svg-0DVy9gjkZftbbvPp line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0DVy9gjkZftbbvPp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 携带 Token-A 请求接口查询 Token-A 对应的 LoginId返回 10001当前用户为 10001

这条映射是登录认证最核心的数据。没有它,服务端就无法根据 Token 恢复用户身份。

5.2 Account-Session

Account-Session 是以 LoginId 为单位保存的会话,一个账号通常只有一个 Account-Session。

用户 100000050 即使同时在 PC、APP 和平板登录,这些终端仍然共同对应:

Account-Session:100000050

Account-Session 主要有两个作用:

  1. 保存账号级共享数据;
  2. 管理该账号当前登录的所有终端。

例如,可以在 Account-Session 中保存用户基础信息:

java 复制代码
SaSession session = StpUtil.getSession();
session.set("user", loginUser);

由于它属于账号 10001,所以该账号的 PC 端和 APP 端获取到的是同一个 Account-Session。
#mermaid-svg-55XTtJQMGxlcXnh7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-55XTtJQMGxlcXnh7 .error-icon{fill:#552222;}#mermaid-svg-55XTtJQMGxlcXnh7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-55XTtJQMGxlcXnh7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-55XTtJQMGxlcXnh7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-55XTtJQMGxlcXnh7 .marker.cross{stroke:#333333;}#mermaid-svg-55XTtJQMGxlcXnh7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-55XTtJQMGxlcXnh7 p{margin:0;}#mermaid-svg-55XTtJQMGxlcXnh7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-55XTtJQMGxlcXnh7 .cluster-label text{fill:#333;}#mermaid-svg-55XTtJQMGxlcXnh7 .cluster-label span{color:#333;}#mermaid-svg-55XTtJQMGxlcXnh7 .cluster-label span p{background-color:transparent;}#mermaid-svg-55XTtJQMGxlcXnh7 .label text,#mermaid-svg-55XTtJQMGxlcXnh7 span{fill:#333;color:#333;}#mermaid-svg-55XTtJQMGxlcXnh7 .node rect,#mermaid-svg-55XTtJQMGxlcXnh7 .node circle,#mermaid-svg-55XTtJQMGxlcXnh7 .node ellipse,#mermaid-svg-55XTtJQMGxlcXnh7 .node polygon,#mermaid-svg-55XTtJQMGxlcXnh7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-55XTtJQMGxlcXnh7 .rough-node .label text,#mermaid-svg-55XTtJQMGxlcXnh7 .node .label text,#mermaid-svg-55XTtJQMGxlcXnh7 .image-shape .label,#mermaid-svg-55XTtJQMGxlcXnh7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-55XTtJQMGxlcXnh7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-55XTtJQMGxlcXnh7 .rough-node .label,#mermaid-svg-55XTtJQMGxlcXnh7 .node .label,#mermaid-svg-55XTtJQMGxlcXnh7 .image-shape .label,#mermaid-svg-55XTtJQMGxlcXnh7 .icon-shape .label{text-align:center;}#mermaid-svg-55XTtJQMGxlcXnh7 .node.clickable{cursor:pointer;}#mermaid-svg-55XTtJQMGxlcXnh7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-55XTtJQMGxlcXnh7 .arrowheadPath{fill:#333333;}#mermaid-svg-55XTtJQMGxlcXnh7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-55XTtJQMGxlcXnh7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-55XTtJQMGxlcXnh7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-55XTtJQMGxlcXnh7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-55XTtJQMGxlcXnh7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-55XTtJQMGxlcXnh7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-55XTtJQMGxlcXnh7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-55XTtJQMGxlcXnh7 .cluster text{fill:#333;}#mermaid-svg-55XTtJQMGxlcXnh7 .cluster span{color:#333;}#mermaid-svg-55XTtJQMGxlcXnh7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-55XTtJQMGxlcXnh7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-55XTtJQMGxlcXnh7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-55XTtJQMGxlcXnh7 .icon-shape,#mermaid-svg-55XTtJQMGxlcXnh7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-55XTtJQMGxlcXnh7 .icon-shape p,#mermaid-svg-55XTtJQMGxlcXnh7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-55XTtJQMGxlcXnh7 .icon-shape .label rect,#mermaid-svg-55XTtJQMGxlcXnh7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-55XTtJQMGxlcXnh7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-55XTtJQMGxlcXnh7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-55XTtJQMGxlcXnh7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 10001
Account-Session 10001
账号共享数据
PC / Token-A
APP / Token-B
平板 / Token-C

Account-Session 适合保存所有终端都可以共享的数据,例如:

  • 用户基础信息;
  • 租户信息;
  • 数据权限范围;
  • 当前账号的登录终端列表。

需要注意,Account-Session 并不是 Token 映射。Token 映射负责由 Token 找到 LoginId;Account-Session 负责保存该 LoginId 对应的账号级会话数据。

当登录的时候会根据 Token-session 找到用户 Id,但是当你想让某个用户下线时,就得利用 Account-Session 了

是不是觉着这个 Token-Session 也适合存储用户权限哈,但是 Account-Session 有个问题,如果这个 Account 不存在任何 Token 了,那么 Account-Session 会自动删除!!!你存储的用户权限也没了。可以怎么解决呢?比如每次登录都查询一下权限并重写一下放入 Account-Session,登录频率很低,所以每次登录都查询不算浪费,并且还能保证权限较新。

更正式的写法就是我们用独立的 RedisKey 去缓存,我们自己去控制,更方便

5.3 Token-Session

Token-Session 是以 Token 为单位保存的会话,每个 Token 都可以拥有自己独立的 Token-Session。

例如:

  • Token-A 对应 PC 端;
  • Token-B 对应 APP 端。

它们可以分别保存不同的数据:

java 复制代码
SaSession tokenSession = StpUtil.getTokenSession();
tokenSession.set("loginUser", loginUser);
tokenSession.set("deviceName", "MacBook");

此时数据只属于当前 Token。APP 端携带 Token-B 请求时,不会读取到 Token-A 的 Token-Session。
#mermaid-svg-KVkE24umo9UBdElb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KVkE24umo9UBdElb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KVkE24umo9UBdElb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KVkE24umo9UBdElb .error-icon{fill:#552222;}#mermaid-svg-KVkE24umo9UBdElb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KVkE24umo9UBdElb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KVkE24umo9UBdElb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KVkE24umo9UBdElb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KVkE24umo9UBdElb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KVkE24umo9UBdElb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KVkE24umo9UBdElb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KVkE24umo9UBdElb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KVkE24umo9UBdElb .marker.cross{stroke:#333333;}#mermaid-svg-KVkE24umo9UBdElb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KVkE24umo9UBdElb p{margin:0;}#mermaid-svg-KVkE24umo9UBdElb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KVkE24umo9UBdElb .cluster-label text{fill:#333;}#mermaid-svg-KVkE24umo9UBdElb .cluster-label span{color:#333;}#mermaid-svg-KVkE24umo9UBdElb .cluster-label span p{background-color:transparent;}#mermaid-svg-KVkE24umo9UBdElb .label text,#mermaid-svg-KVkE24umo9UBdElb span{fill:#333;color:#333;}#mermaid-svg-KVkE24umo9UBdElb .node rect,#mermaid-svg-KVkE24umo9UBdElb .node circle,#mermaid-svg-KVkE24umo9UBdElb .node ellipse,#mermaid-svg-KVkE24umo9UBdElb .node polygon,#mermaid-svg-KVkE24umo9UBdElb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KVkE24umo9UBdElb .rough-node .label text,#mermaid-svg-KVkE24umo9UBdElb .node .label text,#mermaid-svg-KVkE24umo9UBdElb .image-shape .label,#mermaid-svg-KVkE24umo9UBdElb .icon-shape .label{text-anchor:middle;}#mermaid-svg-KVkE24umo9UBdElb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KVkE24umo9UBdElb .rough-node .label,#mermaid-svg-KVkE24umo9UBdElb .node .label,#mermaid-svg-KVkE24umo9UBdElb .image-shape .label,#mermaid-svg-KVkE24umo9UBdElb .icon-shape .label{text-align:center;}#mermaid-svg-KVkE24umo9UBdElb .node.clickable{cursor:pointer;}#mermaid-svg-KVkE24umo9UBdElb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KVkE24umo9UBdElb .arrowheadPath{fill:#333333;}#mermaid-svg-KVkE24umo9UBdElb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KVkE24umo9UBdElb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KVkE24umo9UBdElb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KVkE24umo9UBdElb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KVkE24umo9UBdElb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KVkE24umo9UBdElb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KVkE24umo9UBdElb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KVkE24umo9UBdElb .cluster text{fill:#333;}#mermaid-svg-KVkE24umo9UBdElb .cluster span{color:#333;}#mermaid-svg-KVkE24umo9UBdElb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KVkE24umo9UBdElb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KVkE24umo9UBdElb rect.text{fill:none;stroke-width:0;}#mermaid-svg-KVkE24umo9UBdElb .icon-shape,#mermaid-svg-KVkE24umo9UBdElb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KVkE24umo9UBdElb .icon-shape p,#mermaid-svg-KVkE24umo9UBdElb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KVkE24umo9UBdElb .icon-shape .label rect,#mermaid-svg-KVkE24umo9UBdElb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KVkE24umo9UBdElb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KVkE24umo9UBdElb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KVkE24umo9UBdElb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 10001
Token-A / PC
Token-B / APP
Token-Session A

deviceName=MacBook
Token-Session B

deviceName=iPhone

Account-Session 和 Token-Session 的区别可以这样记:

对比项 Account-Session Token-Session
绑定对象 LoginId Token
数量 一个账号通常一个 一个 Token 一个
多端是否共享 共享 不共享
获取方式 StpUtil.getSession() StpUtil.getTokenSession()
适合保存 账号公共数据 当前设备独有数据

例如用户在 PC 和 APP 同时登录:

  • 用户名称、租户 ID 等公共数据,可以放在 Account-Session;
  • 设备名称、登录 IP、客户端版本等终端数据,可以放在 Token-Session。

Token-Session 不是登录认证必须的数据。默认情况下,它可以在第一次调用 getTokenSession() 时再创建;如果配置了立即创建,也可以在登录时直接创建。

这里有个细节问题,原因是因为我每次浏览器登录都会产生一个 Token,但是 Token 明明没有过期,原因是因为我用的 localCache 存储的 Token(sessionStorage 也是这样),浏览器关闭后 Token 就没了,再次登录又给的新 Token,所以前端应该用 localStorage 存储或者最好用 Cookie。前端启动时先检查本地是否存在 Token:不存在就跳转登录页;存在则向服务端校验。不能因为本地有 Token 就直接认定登录有效,也不能仅依靠前端记录判断 Sa-Token 是否过期。

5.4 最后活跃时间

最后活跃时间用于实现"长时间无操作自动退出"。

例如配置:

yaml 复制代码
sa-token:
  timeout: 7200
  active-timeout: 1800

表示:

  • Token 最长可以存在 2 小时;
  • 如果连续 30 分钟没有访问系统,也会提前失效。

对于 Token-AToken-B,Sa-Token 会分别记录活跃时间,因为 PC 和 APP 的操作时间可能不同。
#mermaid-svg-SC1hQU4SbZh43RSa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SC1hQU4SbZh43RSa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SC1hQU4SbZh43RSa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SC1hQU4SbZh43RSa .error-icon{fill:#552222;}#mermaid-svg-SC1hQU4SbZh43RSa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SC1hQU4SbZh43RSa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SC1hQU4SbZh43RSa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SC1hQU4SbZh43RSa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SC1hQU4SbZh43RSa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SC1hQU4SbZh43RSa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SC1hQU4SbZh43RSa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SC1hQU4SbZh43RSa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SC1hQU4SbZh43RSa .marker.cross{stroke:#333333;}#mermaid-svg-SC1hQU4SbZh43RSa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SC1hQU4SbZh43RSa p{margin:0;}#mermaid-svg-SC1hQU4SbZh43RSa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SC1hQU4SbZh43RSa .cluster-label text{fill:#333;}#mermaid-svg-SC1hQU4SbZh43RSa .cluster-label span{color:#333;}#mermaid-svg-SC1hQU4SbZh43RSa .cluster-label span p{background-color:transparent;}#mermaid-svg-SC1hQU4SbZh43RSa .label text,#mermaid-svg-SC1hQU4SbZh43RSa span{fill:#333;color:#333;}#mermaid-svg-SC1hQU4SbZh43RSa .node rect,#mermaid-svg-SC1hQU4SbZh43RSa .node circle,#mermaid-svg-SC1hQU4SbZh43RSa .node ellipse,#mermaid-svg-SC1hQU4SbZh43RSa .node polygon,#mermaid-svg-SC1hQU4SbZh43RSa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SC1hQU4SbZh43RSa .rough-node .label text,#mermaid-svg-SC1hQU4SbZh43RSa .node .label text,#mermaid-svg-SC1hQU4SbZh43RSa .image-shape .label,#mermaid-svg-SC1hQU4SbZh43RSa .icon-shape .label{text-anchor:middle;}#mermaid-svg-SC1hQU4SbZh43RSa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SC1hQU4SbZh43RSa .rough-node .label,#mermaid-svg-SC1hQU4SbZh43RSa .node .label,#mermaid-svg-SC1hQU4SbZh43RSa .image-shape .label,#mermaid-svg-SC1hQU4SbZh43RSa .icon-shape .label{text-align:center;}#mermaid-svg-SC1hQU4SbZh43RSa .node.clickable{cursor:pointer;}#mermaid-svg-SC1hQU4SbZh43RSa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SC1hQU4SbZh43RSa .arrowheadPath{fill:#333333;}#mermaid-svg-SC1hQU4SbZh43RSa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SC1hQU4SbZh43RSa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SC1hQU4SbZh43RSa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SC1hQU4SbZh43RSa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SC1hQU4SbZh43RSa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SC1hQU4SbZh43RSa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SC1hQU4SbZh43RSa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SC1hQU4SbZh43RSa .cluster text{fill:#333;}#mermaid-svg-SC1hQU4SbZh43RSa .cluster span{color:#333;}#mermaid-svg-SC1hQU4SbZh43RSa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SC1hQU4SbZh43RSa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SC1hQU4SbZh43RSa rect.text{fill:none;stroke-width:0;}#mermaid-svg-SC1hQU4SbZh43RSa .icon-shape,#mermaid-svg-SC1hQU4SbZh43RSa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SC1hQU4SbZh43RSa .icon-shape p,#mermaid-svg-SC1hQU4SbZh43RSa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SC1hQU4SbZh43RSa .icon-shape .label rect,#mermaid-svg-SC1hQU4SbZh43RSa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SC1hQU4SbZh43RSa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SC1hQU4SbZh43RSa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SC1hQU4SbZh43RSa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 10:00

PC 端登录
10:10

访问接口

更新活跃时间
10:25

再次访问

更新活跃时间
10:55

连续 30 分钟无操作

登录状态失效

最后活跃时间是 Token 级别的数据,不是账号级别的数据。

❓ 怎么更新的最后活跃时间呢?不是所有到达服务器的请求都会自动更新。请求必须执行到 Sa-Token 的相关鉴权逻辑,例如:

  • StpUtil.checkLogin() --> 请求每次经过网关时都会执行 checkLogin()
  • StpUtil.getLoginId()
  • @SaCheckLogin
  • @SaCheckPermission
  • @SaCheckRole
  • StpUtil.getSession()
  • StpUtil.getTokenSession()

5.5 一次请求到底会用到哪些数据

假设 PC 端携带 Token-A 请求订单接口,登录校验的主要过程是:
#mermaid-svg-2ltJwjoa2tMenbkS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2ltJwjoa2tMenbkS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2ltJwjoa2tMenbkS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2ltJwjoa2tMenbkS .error-icon{fill:#552222;}#mermaid-svg-2ltJwjoa2tMenbkS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2ltJwjoa2tMenbkS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2ltJwjoa2tMenbkS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2ltJwjoa2tMenbkS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2ltJwjoa2tMenbkS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2ltJwjoa2tMenbkS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2ltJwjoa2tMenbkS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2ltJwjoa2tMenbkS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2ltJwjoa2tMenbkS .marker.cross{stroke:#333333;}#mermaid-svg-2ltJwjoa2tMenbkS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2ltJwjoa2tMenbkS p{margin:0;}#mermaid-svg-2ltJwjoa2tMenbkS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2ltJwjoa2tMenbkS .cluster-label text{fill:#333;}#mermaid-svg-2ltJwjoa2tMenbkS .cluster-label span{color:#333;}#mermaid-svg-2ltJwjoa2tMenbkS .cluster-label span p{background-color:transparent;}#mermaid-svg-2ltJwjoa2tMenbkS .label text,#mermaid-svg-2ltJwjoa2tMenbkS span{fill:#333;color:#333;}#mermaid-svg-2ltJwjoa2tMenbkS .node rect,#mermaid-svg-2ltJwjoa2tMenbkS .node circle,#mermaid-svg-2ltJwjoa2tMenbkS .node ellipse,#mermaid-svg-2ltJwjoa2tMenbkS .node polygon,#mermaid-svg-2ltJwjoa2tMenbkS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2ltJwjoa2tMenbkS .rough-node .label text,#mermaid-svg-2ltJwjoa2tMenbkS .node .label text,#mermaid-svg-2ltJwjoa2tMenbkS .image-shape .label,#mermaid-svg-2ltJwjoa2tMenbkS .icon-shape .label{text-anchor:middle;}#mermaid-svg-2ltJwjoa2tMenbkS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2ltJwjoa2tMenbkS .rough-node .label,#mermaid-svg-2ltJwjoa2tMenbkS .node .label,#mermaid-svg-2ltJwjoa2tMenbkS .image-shape .label,#mermaid-svg-2ltJwjoa2tMenbkS .icon-shape .label{text-align:center;}#mermaid-svg-2ltJwjoa2tMenbkS .node.clickable{cursor:pointer;}#mermaid-svg-2ltJwjoa2tMenbkS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2ltJwjoa2tMenbkS .arrowheadPath{fill:#333333;}#mermaid-svg-2ltJwjoa2tMenbkS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2ltJwjoa2tMenbkS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2ltJwjoa2tMenbkS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ltJwjoa2tMenbkS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2ltJwjoa2tMenbkS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ltJwjoa2tMenbkS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2ltJwjoa2tMenbkS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2ltJwjoa2tMenbkS .cluster text{fill:#333;}#mermaid-svg-2ltJwjoa2tMenbkS .cluster span{color:#333;}#mermaid-svg-2ltJwjoa2tMenbkS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2ltJwjoa2tMenbkS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2ltJwjoa2tMenbkS rect.text{fill:none;stroke-width:0;}#mermaid-svg-2ltJwjoa2tMenbkS .icon-shape,#mermaid-svg-2ltJwjoa2tMenbkS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ltJwjoa2tMenbkS .icon-shape p,#mermaid-svg-2ltJwjoa2tMenbkS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2ltJwjoa2tMenbkS .icon-shape .label rect,#mermaid-svg-2ltJwjoa2tMenbkS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ltJwjoa2tMenbkS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2ltJwjoa2tMenbkS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2ltJwjoa2tMenbkS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是,得到 10001


已超时
未超时
客户端提交 Token-A
查询 Token-A 映射
是否找到 LoginId
未登录
是否开启 active-timeout
检查 Token-A 最后活跃时间
登录校验通过
更新最后活跃时间

普通登录校验主要依赖:

  1. Token 与 LoginId 的映射;
  2. Token 的有效期;
  3. 开启 active-timeout 时的最后活跃时间。

Account-Session 和 Token-Session 并不是每次登录校验都必须读取。只有业务代码需要账号共享数据、终端列表或 Token 独享数据时,才会使用对应的 Session。

5.6 四类数据分别解决什么问题

数据 解决的问题
Token 映射 这个 Token 属于哪个账号
Account-Session 这个账号有哪些共享数据和登录终端
Token-Session 当前这个 Token 有哪些独享数据
最后活跃时间 当前 Token 是否长时间没有操作

最终可以将 Sa-Token 的会话结构理解为:
#mermaid-svg-zwZbuAguGZj2jkir{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zwZbuAguGZj2jkir .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zwZbuAguGZj2jkir .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zwZbuAguGZj2jkir .error-icon{fill:#552222;}#mermaid-svg-zwZbuAguGZj2jkir .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zwZbuAguGZj2jkir .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zwZbuAguGZj2jkir .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zwZbuAguGZj2jkir .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zwZbuAguGZj2jkir .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zwZbuAguGZj2jkir .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zwZbuAguGZj2jkir .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zwZbuAguGZj2jkir .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zwZbuAguGZj2jkir .marker.cross{stroke:#333333;}#mermaid-svg-zwZbuAguGZj2jkir svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zwZbuAguGZj2jkir p{margin:0;}#mermaid-svg-zwZbuAguGZj2jkir .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zwZbuAguGZj2jkir .cluster-label text{fill:#333;}#mermaid-svg-zwZbuAguGZj2jkir .cluster-label span{color:#333;}#mermaid-svg-zwZbuAguGZj2jkir .cluster-label span p{background-color:transparent;}#mermaid-svg-zwZbuAguGZj2jkir .label text,#mermaid-svg-zwZbuAguGZj2jkir span{fill:#333;color:#333;}#mermaid-svg-zwZbuAguGZj2jkir .node rect,#mermaid-svg-zwZbuAguGZj2jkir .node circle,#mermaid-svg-zwZbuAguGZj2jkir .node ellipse,#mermaid-svg-zwZbuAguGZj2jkir .node polygon,#mermaid-svg-zwZbuAguGZj2jkir .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zwZbuAguGZj2jkir .rough-node .label text,#mermaid-svg-zwZbuAguGZj2jkir .node .label text,#mermaid-svg-zwZbuAguGZj2jkir .image-shape .label,#mermaid-svg-zwZbuAguGZj2jkir .icon-shape .label{text-anchor:middle;}#mermaid-svg-zwZbuAguGZj2jkir .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zwZbuAguGZj2jkir .rough-node .label,#mermaid-svg-zwZbuAguGZj2jkir .node .label,#mermaid-svg-zwZbuAguGZj2jkir .image-shape .label,#mermaid-svg-zwZbuAguGZj2jkir .icon-shape .label{text-align:center;}#mermaid-svg-zwZbuAguGZj2jkir .node.clickable{cursor:pointer;}#mermaid-svg-zwZbuAguGZj2jkir .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zwZbuAguGZj2jkir .arrowheadPath{fill:#333333;}#mermaid-svg-zwZbuAguGZj2jkir .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zwZbuAguGZj2jkir .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zwZbuAguGZj2jkir .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zwZbuAguGZj2jkir .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zwZbuAguGZj2jkir .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zwZbuAguGZj2jkir .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zwZbuAguGZj2jkir .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zwZbuAguGZj2jkir .cluster text{fill:#333;}#mermaid-svg-zwZbuAguGZj2jkir .cluster span{color:#333;}#mermaid-svg-zwZbuAguGZj2jkir div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zwZbuAguGZj2jkir .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zwZbuAguGZj2jkir rect.text{fill:none;stroke-width:0;}#mermaid-svg-zwZbuAguGZj2jkir .icon-shape,#mermaid-svg-zwZbuAguGZj2jkir .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zwZbuAguGZj2jkir .icon-shape p,#mermaid-svg-zwZbuAguGZj2jkir .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zwZbuAguGZj2jkir .icon-shape .label rect,#mermaid-svg-zwZbuAguGZj2jkir .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zwZbuAguGZj2jkir .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zwZbuAguGZj2jkir .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zwZbuAguGZj2jkir :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端 Token
Token 映射
恢复 LoginId
Account-Session

账号级、所有终端共享
Token-Session

Token 级、当前终端独享
最后活跃时间

判断无操作超时

最关键的区别是:

LoginId 代表账号,Token 代表一次具体的登录终端。Account-Session 跟着账号走,Token-Session 和最后活跃时间跟着 Token 走。


6 StpUtil.login() 底层流程

StpUtil.login(userId) 可以分成两部分:

  1. 在服务端创建或更新登录会话;
  2. 将 Token 注入当前客户端环境。

#mermaid-svg-XQyRP93qkPheoIYq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XQyRP93qkPheoIYq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XQyRP93qkPheoIYq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XQyRP93qkPheoIYq .error-icon{fill:#552222;}#mermaid-svg-XQyRP93qkPheoIYq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XQyRP93qkPheoIYq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XQyRP93qkPheoIYq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XQyRP93qkPheoIYq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XQyRP93qkPheoIYq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XQyRP93qkPheoIYq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XQyRP93qkPheoIYq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XQyRP93qkPheoIYq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XQyRP93qkPheoIYq .marker.cross{stroke:#333333;}#mermaid-svg-XQyRP93qkPheoIYq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XQyRP93qkPheoIYq p{margin:0;}#mermaid-svg-XQyRP93qkPheoIYq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XQyRP93qkPheoIYq .cluster-label text{fill:#333;}#mermaid-svg-XQyRP93qkPheoIYq .cluster-label span{color:#333;}#mermaid-svg-XQyRP93qkPheoIYq .cluster-label span p{background-color:transparent;}#mermaid-svg-XQyRP93qkPheoIYq .label text,#mermaid-svg-XQyRP93qkPheoIYq span{fill:#333;color:#333;}#mermaid-svg-XQyRP93qkPheoIYq .node rect,#mermaid-svg-XQyRP93qkPheoIYq .node circle,#mermaid-svg-XQyRP93qkPheoIYq .node ellipse,#mermaid-svg-XQyRP93qkPheoIYq .node polygon,#mermaid-svg-XQyRP93qkPheoIYq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XQyRP93qkPheoIYq .rough-node .label text,#mermaid-svg-XQyRP93qkPheoIYq .node .label text,#mermaid-svg-XQyRP93qkPheoIYq .image-shape .label,#mermaid-svg-XQyRP93qkPheoIYq .icon-shape .label{text-anchor:middle;}#mermaid-svg-XQyRP93qkPheoIYq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XQyRP93qkPheoIYq .rough-node .label,#mermaid-svg-XQyRP93qkPheoIYq .node .label,#mermaid-svg-XQyRP93qkPheoIYq .image-shape .label,#mermaid-svg-XQyRP93qkPheoIYq .icon-shape .label{text-align:center;}#mermaid-svg-XQyRP93qkPheoIYq .node.clickable{cursor:pointer;}#mermaid-svg-XQyRP93qkPheoIYq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XQyRP93qkPheoIYq .arrowheadPath{fill:#333333;}#mermaid-svg-XQyRP93qkPheoIYq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XQyRP93qkPheoIYq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XQyRP93qkPheoIYq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XQyRP93qkPheoIYq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XQyRP93qkPheoIYq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XQyRP93qkPheoIYq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XQyRP93qkPheoIYq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XQyRP93qkPheoIYq .cluster text{fill:#333;}#mermaid-svg-XQyRP93qkPheoIYq .cluster span{color:#333;}#mermaid-svg-XQyRP93qkPheoIYq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XQyRP93qkPheoIYq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XQyRP93qkPheoIYq rect.text{fill:none;stroke-width:0;}#mermaid-svg-XQyRP93qkPheoIYq .icon-shape,#mermaid-svg-XQyRP93qkPheoIYq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XQyRP93qkPheoIYq .icon-shape p,#mermaid-svg-XQyRP93qkPheoIYq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XQyRP93qkPheoIYq .icon-shape .label rect,#mermaid-svg-XQyRP93qkPheoIYq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XQyRP93qkPheoIYq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XQyRP93qkPheoIYq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XQyRP93qkPheoIYq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



调用 StpUtil.login(LoginId)
校验 LoginId 和登录参数
处理并发登录和 Token 复用策略
分配可用 Token
获取或创建 Account-Session
记录本次登录的终端信息
保存 Token → LoginId 映射
是否开启 active-timeout
记录最后活跃时间
跳过活跃时间记录
Token-Session 是否已存在

或配置为立即创建
创建或续期 Token-Session
暂不创建 Token-Session
发布登录成功事件
检查是否超过最大登录数量
将 Token 注入客户端环境

具体过程如下:

  1. 校验 LoginId 是否为空,以及设备类型、超时时间等登录参数是否合法;
  2. 根据 is-concurrent 处理并发登录,根据 is-share 判断是否复用已有 Token;
  3. 如果没有可复用的 Token,则按照 Token 生成策略创建新的唯一 Token;
  4. 获取或创建该 LoginId 对应的 Account-Session;
  5. 在 Account-Session 中记录设备类型、设备 ID、Token、扩展数据和登录时间;
  6. 保存 Token → LoginId 映射,用于后续根据 Token 恢复用户身份;
  7. 如果开启了 active-timeout,记录当前 Token 的最后活跃时间;
  8. 如果 Token-Session 已存在,则更新其有效期;如果配置为立即创建,则创建 Token-Session,否则默认暂不创建;
  9. 发布登录成功事件;
  10. 检查该账号的登录数量是否超过 max-login-count,超过时处理最早的登录终端;
  11. 将 Token 写入当前请求上下文,并根据配置写入 Cookie 或响应头。

需要注意,StpUtil.login() 的返回值是 void,它不会直接将 Token 作为方法返回值返回。前后端分离项目通常在登录后主动获取 Token 信息并放入 JSON 响应:

Token 分配不一定是生成新 Token

Token 的来源可能有三种:

  1. 调用登录方法时手动指定 Token;
  2. is-share=true 时复用该账号已有的 Token;
  3. 无法复用时,根据配置生成新的 Token。

常见 Token 风格包括:uuid simple-uuid random-32 random-64 random-128 tik

最后活跃时间不是一定创建

只有开启 active-timeout 时,Sa-Token 才需要记录 Token 的最后活跃时间:

yaml 复制代码
sa-token:
  active-timeout: 1800

如果没有开启无操作超时,则不会保存最后活跃时间。

Token-Session 默认不是登录时创建

登录一定会创建或获取 Account-Session,但 Token-Session 默认采用延迟创建:

  1. Token-Session 已经存在:登录时为其续期;
  2. 配置为立即创建:登录时直接创建;
  3. 默认情况:第一次调用 getTokenSession() 时才创建。

因此,登录后 Redis 中一定会出现的是:

  • Account-Session;
  • Token → LoginId 映射。

不一定出现的是:

  • 最后活跃时间;
  • Token-Session。

7 请求到达后的登录校验

客户端携带 Token 请求接口时,服务端通常调用:

java 复制代码
StpUtil.checkLogin();

checkLogin() 本身主要用于确认当前会话是否已经登录,其内部会尝试获取当前 Token 对应的 LoginId。能够正常获取 LoginId,说明登录有效;无法获取时,则根据具体原因抛出 NotLoginException
#mermaid-svg-reuu14j6MAE9Yzhw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-reuu14j6MAE9Yzhw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-reuu14j6MAE9Yzhw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-reuu14j6MAE9Yzhw .error-icon{fill:#552222;}#mermaid-svg-reuu14j6MAE9Yzhw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-reuu14j6MAE9Yzhw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-reuu14j6MAE9Yzhw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-reuu14j6MAE9Yzhw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-reuu14j6MAE9Yzhw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-reuu14j6MAE9Yzhw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-reuu14j6MAE9Yzhw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-reuu14j6MAE9Yzhw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-reuu14j6MAE9Yzhw .marker.cross{stroke:#333333;}#mermaid-svg-reuu14j6MAE9Yzhw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-reuu14j6MAE9Yzhw p{margin:0;}#mermaid-svg-reuu14j6MAE9Yzhw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-reuu14j6MAE9Yzhw .cluster-label text{fill:#333;}#mermaid-svg-reuu14j6MAE9Yzhw .cluster-label span{color:#333;}#mermaid-svg-reuu14j6MAE9Yzhw .cluster-label span p{background-color:transparent;}#mermaid-svg-reuu14j6MAE9Yzhw .label text,#mermaid-svg-reuu14j6MAE9Yzhw span{fill:#333;color:#333;}#mermaid-svg-reuu14j6MAE9Yzhw .node rect,#mermaid-svg-reuu14j6MAE9Yzhw .node circle,#mermaid-svg-reuu14j6MAE9Yzhw .node ellipse,#mermaid-svg-reuu14j6MAE9Yzhw .node polygon,#mermaid-svg-reuu14j6MAE9Yzhw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-reuu14j6MAE9Yzhw .rough-node .label text,#mermaid-svg-reuu14j6MAE9Yzhw .node .label text,#mermaid-svg-reuu14j6MAE9Yzhw .image-shape .label,#mermaid-svg-reuu14j6MAE9Yzhw .icon-shape .label{text-anchor:middle;}#mermaid-svg-reuu14j6MAE9Yzhw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-reuu14j6MAE9Yzhw .rough-node .label,#mermaid-svg-reuu14j6MAE9Yzhw .node .label,#mermaid-svg-reuu14j6MAE9Yzhw .image-shape .label,#mermaid-svg-reuu14j6MAE9Yzhw .icon-shape .label{text-align:center;}#mermaid-svg-reuu14j6MAE9Yzhw .node.clickable{cursor:pointer;}#mermaid-svg-reuu14j6MAE9Yzhw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-reuu14j6MAE9Yzhw .arrowheadPath{fill:#333333;}#mermaid-svg-reuu14j6MAE9Yzhw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-reuu14j6MAE9Yzhw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-reuu14j6MAE9Yzhw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-reuu14j6MAE9Yzhw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-reuu14j6MAE9Yzhw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-reuu14j6MAE9Yzhw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-reuu14j6MAE9Yzhw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-reuu14j6MAE9Yzhw .cluster text{fill:#333;}#mermaid-svg-reuu14j6MAE9Yzhw .cluster span{color:#333;}#mermaid-svg-reuu14j6MAE9Yzhw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-reuu14j6MAE9Yzhw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-reuu14j6MAE9Yzhw rect.text{fill:none;stroke-width:0;}#mermaid-svg-reuu14j6MAE9Yzhw .icon-shape,#mermaid-svg-reuu14j6MAE9Yzhw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-reuu14j6MAE9Yzhw .icon-shape p,#mermaid-svg-reuu14j6MAE9Yzhw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-reuu14j6MAE9Yzhw .icon-shape .label rect,#mermaid-svg-reuu14j6MAE9Yzhw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-reuu14j6MAE9Yzhw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-reuu14j6MAE9Yzhw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-reuu14j6MAE9Yzhw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

不存在
TOKEN_TIMEOUT
BE_REPLACED
KICK_OUT
正常 LoginId






收到请求
从请求中读取 Token
是否读取到 Token
抛出异常:未提交 Token
查询 Token → LoginId 映射
映射查询结果
抛出异常:Token 无效
抛出异常:Token 已过期
抛出异常:Token 已被顶下线
抛出异常:Token 已被踢下线
是否开启 active-timeout
登录校验通过
是否超过最大空闲时间
抛出异常:Token 已冻结
是否开启自动续签
更新最后活跃时间

完整校验过程可以概括为:

  1. 从当前请求中读取 Token;
  2. 配置了 token-prefix 时,校验并去掉 Token 前缀;
  3. 如果没有读取到 Token,抛出"未提交 Token"异常;
  4. 根据 Token 查询 Token → LoginId 映射;
  5. 如果映射不存在,说明 Token 无效;
  6. 如果映射值是特殊状态标记,则分别判断 Token 是否过期、被顶下线或被踢下线;
  7. 如果得到正常 LoginId,并且开启了 active-timeout,则检查 Token 是否超过最大空闲时间;
  8. Token 未冻结且开启自动续签时,更新最后活跃时间;
  9. 最终返回 LoginId,表示当前登录校验通过。

7.1 Token 从哪里读取

Sa-Token 可以从多个位置读取 Token,主要包括:

  • 当前请求上下文;
  • 请求参数;
  • Header;
  • Cookie。

前后端分离项目一般只允许从 Header 中读取 Token:

yaml 复制代码
sa-token:
  token-name: Authorization
  token-prefix: Bearer

  is-read-header: true
  is-read-cookie: false
  is-read-body: false

此时客户端请求格式为:

http 复制代码
Authorization: Bearer xxxxxxxxx

其中:

  • Authorizationtoken-name 决定;
  • Bearertoken-prefix 决定;
  • xxxxxxxxx 才是真正的 Token 值。

#mermaid-svg-tZbp5M7w06a9I1Ub{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tZbp5M7w06a9I1Ub .error-icon{fill:#552222;}#mermaid-svg-tZbp5M7w06a9I1Ub .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tZbp5M7w06a9I1Ub .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tZbp5M7w06a9I1Ub .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tZbp5M7w06a9I1Ub .marker.cross{stroke:#333333;}#mermaid-svg-tZbp5M7w06a9I1Ub svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tZbp5M7w06a9I1Ub p{margin:0;}#mermaid-svg-tZbp5M7w06a9I1Ub .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tZbp5M7w06a9I1Ub .cluster-label text{fill:#333;}#mermaid-svg-tZbp5M7w06a9I1Ub .cluster-label span{color:#333;}#mermaid-svg-tZbp5M7w06a9I1Ub .cluster-label span p{background-color:transparent;}#mermaid-svg-tZbp5M7w06a9I1Ub .label text,#mermaid-svg-tZbp5M7w06a9I1Ub span{fill:#333;color:#333;}#mermaid-svg-tZbp5M7w06a9I1Ub .node rect,#mermaid-svg-tZbp5M7w06a9I1Ub .node circle,#mermaid-svg-tZbp5M7w06a9I1Ub .node ellipse,#mermaid-svg-tZbp5M7w06a9I1Ub .node polygon,#mermaid-svg-tZbp5M7w06a9I1Ub .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tZbp5M7w06a9I1Ub .rough-node .label text,#mermaid-svg-tZbp5M7w06a9I1Ub .node .label text,#mermaid-svg-tZbp5M7w06a9I1Ub .image-shape .label,#mermaid-svg-tZbp5M7w06a9I1Ub .icon-shape .label{text-anchor:middle;}#mermaid-svg-tZbp5M7w06a9I1Ub .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tZbp5M7w06a9I1Ub .rough-node .label,#mermaid-svg-tZbp5M7w06a9I1Ub .node .label,#mermaid-svg-tZbp5M7w06a9I1Ub .image-shape .label,#mermaid-svg-tZbp5M7w06a9I1Ub .icon-shape .label{text-align:center;}#mermaid-svg-tZbp5M7w06a9I1Ub .node.clickable{cursor:pointer;}#mermaid-svg-tZbp5M7w06a9I1Ub .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tZbp5M7w06a9I1Ub .arrowheadPath{fill:#333333;}#mermaid-svg-tZbp5M7w06a9I1Ub .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tZbp5M7w06a9I1Ub .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tZbp5M7w06a9I1Ub .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tZbp5M7w06a9I1Ub .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tZbp5M7w06a9I1Ub .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tZbp5M7w06a9I1Ub .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tZbp5M7w06a9I1Ub .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tZbp5M7w06a9I1Ub .cluster text{fill:#333;}#mermaid-svg-tZbp5M7w06a9I1Ub .cluster span{color:#333;}#mermaid-svg-tZbp5M7w06a9I1Ub div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tZbp5M7w06a9I1Ub .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tZbp5M7w06a9I1Ub rect.text{fill:none;stroke-width:0;}#mermaid-svg-tZbp5M7w06a9I1Ub .icon-shape,#mermaid-svg-tZbp5M7w06a9I1Ub .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tZbp5M7w06a9I1Ub .icon-shape p,#mermaid-svg-tZbp5M7w06a9I1Ub .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tZbp5M7w06a9I1Ub .icon-shape .label rect,#mermaid-svg-tZbp5M7w06a9I1Ub .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tZbp5M7w06a9I1Ub .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tZbp5M7w06a9I1Ub .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tZbp5M7w06a9I1Ub :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Authorization: Bearer Token-A
校验 Bearer 前缀
去掉 Bearer 和空格
得到真实 Token-A
查询 Token-A → LoginId

如果配置了:

yaml 复制代码
token-prefix: Bearer

但客户端只提交:

http 复制代码
Authorization: xxxxxxxxx

由于缺少正确的 Bearer 前缀,Sa-Token 无法按照预期解析 Token。

如果用 Cookie 可以用下面的方式

yaml 复制代码
sa-token:
  # 同时也是 Cookie 名称
  token-name: satoken

  # 只从 Cookie 读取 Token
  is-read-cookie: true
  is-read-header: false
  is-read-body: false

  # 关闭浏览器后 Cookie 是否保留
  is-lasting-cookie: true

  cookie:
    path: /
    http-only: true
    secure: true
    same-site: Lax

7.2 Token 映射的查询结果

Sa-Token 查询的核心数据是:

Token → LoginId

例如:

Token-A → 100000050

查询结果并不一定是正常的用户 ID,也可能是特殊状态标记:

查询结果 含义
不存在 Token 无效,或者已经被删除
TOKEN_TIMEOUT Token 已过期
BE_REPLACED Token 已被其他登录顶下线
KICK_OUT Token 已被管理员踢下线
正常 LoginId Token 对应的登录账号

因此,"Token 是否过期、被踢或被顶"并不一定需要分别查询多条数据,而是可以根据 Token 映射查询结果判断。
#mermaid-svg-StT49vu10PVZWD45{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-StT49vu10PVZWD45 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-StT49vu10PVZWD45 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-StT49vu10PVZWD45 .error-icon{fill:#552222;}#mermaid-svg-StT49vu10PVZWD45 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-StT49vu10PVZWD45 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-StT49vu10PVZWD45 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-StT49vu10PVZWD45 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-StT49vu10PVZWD45 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-StT49vu10PVZWD45 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-StT49vu10PVZWD45 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-StT49vu10PVZWD45 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-StT49vu10PVZWD45 .marker.cross{stroke:#333333;}#mermaid-svg-StT49vu10PVZWD45 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-StT49vu10PVZWD45 p{margin:0;}#mermaid-svg-StT49vu10PVZWD45 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-StT49vu10PVZWD45 .cluster-label text{fill:#333;}#mermaid-svg-StT49vu10PVZWD45 .cluster-label span{color:#333;}#mermaid-svg-StT49vu10PVZWD45 .cluster-label span p{background-color:transparent;}#mermaid-svg-StT49vu10PVZWD45 .label text,#mermaid-svg-StT49vu10PVZWD45 span{fill:#333;color:#333;}#mermaid-svg-StT49vu10PVZWD45 .node rect,#mermaid-svg-StT49vu10PVZWD45 .node circle,#mermaid-svg-StT49vu10PVZWD45 .node ellipse,#mermaid-svg-StT49vu10PVZWD45 .node polygon,#mermaid-svg-StT49vu10PVZWD45 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-StT49vu10PVZWD45 .rough-node .label text,#mermaid-svg-StT49vu10PVZWD45 .node .label text,#mermaid-svg-StT49vu10PVZWD45 .image-shape .label,#mermaid-svg-StT49vu10PVZWD45 .icon-shape .label{text-anchor:middle;}#mermaid-svg-StT49vu10PVZWD45 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-StT49vu10PVZWD45 .rough-node .label,#mermaid-svg-StT49vu10PVZWD45 .node .label,#mermaid-svg-StT49vu10PVZWD45 .image-shape .label,#mermaid-svg-StT49vu10PVZWD45 .icon-shape .label{text-align:center;}#mermaid-svg-StT49vu10PVZWD45 .node.clickable{cursor:pointer;}#mermaid-svg-StT49vu10PVZWD45 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-StT49vu10PVZWD45 .arrowheadPath{fill:#333333;}#mermaid-svg-StT49vu10PVZWD45 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-StT49vu10PVZWD45 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-StT49vu10PVZWD45 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-StT49vu10PVZWD45 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-StT49vu10PVZWD45 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-StT49vu10PVZWD45 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-StT49vu10PVZWD45 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-StT49vu10PVZWD45 .cluster text{fill:#333;}#mermaid-svg-StT49vu10PVZWD45 .cluster span{color:#333;}#mermaid-svg-StT49vu10PVZWD45 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-StT49vu10PVZWD45 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-StT49vu10PVZWD45 rect.text{fill:none;stroke-width:0;}#mermaid-svg-StT49vu10PVZWD45 .icon-shape,#mermaid-svg-StT49vu10PVZWD45 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-StT49vu10PVZWD45 .icon-shape p,#mermaid-svg-StT49vu10PVZWD45 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-StT49vu10PVZWD45 .icon-shape .label rect,#mermaid-svg-StT49vu10PVZWD45 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-StT49vu10PVZWD45 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-StT49vu10PVZWD45 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-StT49vu10PVZWD45 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Token-A
查询 Token 映射
返回值
100000050

正常登录
TOKEN_TIMEOUT

已经过期
BE_REPLACED

被顶下线
KICK_OUT

被踢下线

7.3 active-timeout 检查

如果配置了最大空闲时间:

yaml 复制代码
sa-token:
  timeout: 7200
  active-timeout: 1800

Sa-Token 在成功得到 LoginId 后,还会读取当前 Token 的最后活跃时间:

空闲时间 = 当前时间 - 最后活跃时间

如果空闲时间超过 1800 秒,则认为 Token 已冻结。
#mermaid-svg-RH7h0SMOnnGPGZMh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RH7h0SMOnnGPGZMh .error-icon{fill:#552222;}#mermaid-svg-RH7h0SMOnnGPGZMh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RH7h0SMOnnGPGZMh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RH7h0SMOnnGPGZMh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RH7h0SMOnnGPGZMh .marker.cross{stroke:#333333;}#mermaid-svg-RH7h0SMOnnGPGZMh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RH7h0SMOnnGPGZMh p{margin:0;}#mermaid-svg-RH7h0SMOnnGPGZMh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RH7h0SMOnnGPGZMh .cluster-label text{fill:#333;}#mermaid-svg-RH7h0SMOnnGPGZMh .cluster-label span{color:#333;}#mermaid-svg-RH7h0SMOnnGPGZMh .cluster-label span p{background-color:transparent;}#mermaid-svg-RH7h0SMOnnGPGZMh .label text,#mermaid-svg-RH7h0SMOnnGPGZMh span{fill:#333;color:#333;}#mermaid-svg-RH7h0SMOnnGPGZMh .node rect,#mermaid-svg-RH7h0SMOnnGPGZMh .node circle,#mermaid-svg-RH7h0SMOnnGPGZMh .node ellipse,#mermaid-svg-RH7h0SMOnnGPGZMh .node polygon,#mermaid-svg-RH7h0SMOnnGPGZMh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RH7h0SMOnnGPGZMh .rough-node .label text,#mermaid-svg-RH7h0SMOnnGPGZMh .node .label text,#mermaid-svg-RH7h0SMOnnGPGZMh .image-shape .label,#mermaid-svg-RH7h0SMOnnGPGZMh .icon-shape .label{text-anchor:middle;}#mermaid-svg-RH7h0SMOnnGPGZMh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RH7h0SMOnnGPGZMh .rough-node .label,#mermaid-svg-RH7h0SMOnnGPGZMh .node .label,#mermaid-svg-RH7h0SMOnnGPGZMh .image-shape .label,#mermaid-svg-RH7h0SMOnnGPGZMh .icon-shape .label{text-align:center;}#mermaid-svg-RH7h0SMOnnGPGZMh .node.clickable{cursor:pointer;}#mermaid-svg-RH7h0SMOnnGPGZMh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RH7h0SMOnnGPGZMh .arrowheadPath{fill:#333333;}#mermaid-svg-RH7h0SMOnnGPGZMh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RH7h0SMOnnGPGZMh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RH7h0SMOnnGPGZMh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RH7h0SMOnnGPGZMh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RH7h0SMOnnGPGZMh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RH7h0SMOnnGPGZMh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RH7h0SMOnnGPGZMh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RH7h0SMOnnGPGZMh .cluster text{fill:#333;}#mermaid-svg-RH7h0SMOnnGPGZMh .cluster span{color:#333;}#mermaid-svg-RH7h0SMOnnGPGZMh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RH7h0SMOnnGPGZMh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RH7h0SMOnnGPGZMh rect.text{fill:none;stroke-width:0;}#mermaid-svg-RH7h0SMOnnGPGZMh .icon-shape,#mermaid-svg-RH7h0SMOnnGPGZMh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RH7h0SMOnnGPGZMh .icon-shape p,#mermaid-svg-RH7h0SMOnnGPGZMh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RH7h0SMOnnGPGZMh .icon-shape .label rect,#mermaid-svg-RH7h0SMOnnGPGZMh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RH7h0SMOnnGPGZMh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RH7h0SMOnnGPGZMh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RH7h0SMOnnGPGZMh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

读取最后活跃时间
计算当前空闲时间
是否超过 active-timeout
Token 冻结
登录校验通过
更新最后活跃时间

例如:

  • 10:00 登录,最后活跃时间为 10:00;
  • 10:10 请求接口,空闲 10 分钟,校验通过并更新为 10:10;
  • 10:25 再次请求,空闲 15 分钟,校验通过并更新为 10:25;
  • 11:00 再次请求,空闲 35 分钟,超过 30 分钟,Token 被判定为冻结。

冻结了之后就需要用户重新登录了

7.4 自动续签

开启 active-timeout 后,如果同时开启自动续签,Token 每次通过登录校验时都会更新最后活跃时间。

自动续签更新的是:

最后活跃时间

不会重新延长 Token 的绝对有效期 timeout

例如:

yaml 复制代码
sa-token:
  timeout: 7200
  active-timeout: 1800
  auto-renew: true

用户持续访问可以避免因为空闲 30 分钟而冻结,但 Token 仍然不能突破 2 小时的绝对有效期。
#mermaid-svg-qvhEZaxDKrHOwcqN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qvhEZaxDKrHOwcqN .error-icon{fill:#552222;}#mermaid-svg-qvhEZaxDKrHOwcqN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qvhEZaxDKrHOwcqN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qvhEZaxDKrHOwcqN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qvhEZaxDKrHOwcqN .marker.cross{stroke:#333333;}#mermaid-svg-qvhEZaxDKrHOwcqN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qvhEZaxDKrHOwcqN p{margin:0;}#mermaid-svg-qvhEZaxDKrHOwcqN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qvhEZaxDKrHOwcqN .cluster-label text{fill:#333;}#mermaid-svg-qvhEZaxDKrHOwcqN .cluster-label span{color:#333;}#mermaid-svg-qvhEZaxDKrHOwcqN .cluster-label span p{background-color:transparent;}#mermaid-svg-qvhEZaxDKrHOwcqN .label text,#mermaid-svg-qvhEZaxDKrHOwcqN span{fill:#333;color:#333;}#mermaid-svg-qvhEZaxDKrHOwcqN .node rect,#mermaid-svg-qvhEZaxDKrHOwcqN .node circle,#mermaid-svg-qvhEZaxDKrHOwcqN .node ellipse,#mermaid-svg-qvhEZaxDKrHOwcqN .node polygon,#mermaid-svg-qvhEZaxDKrHOwcqN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qvhEZaxDKrHOwcqN .rough-node .label text,#mermaid-svg-qvhEZaxDKrHOwcqN .node .label text,#mermaid-svg-qvhEZaxDKrHOwcqN .image-shape .label,#mermaid-svg-qvhEZaxDKrHOwcqN .icon-shape .label{text-anchor:middle;}#mermaid-svg-qvhEZaxDKrHOwcqN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qvhEZaxDKrHOwcqN .rough-node .label,#mermaid-svg-qvhEZaxDKrHOwcqN .node .label,#mermaid-svg-qvhEZaxDKrHOwcqN .image-shape .label,#mermaid-svg-qvhEZaxDKrHOwcqN .icon-shape .label{text-align:center;}#mermaid-svg-qvhEZaxDKrHOwcqN .node.clickable{cursor:pointer;}#mermaid-svg-qvhEZaxDKrHOwcqN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qvhEZaxDKrHOwcqN .arrowheadPath{fill:#333333;}#mermaid-svg-qvhEZaxDKrHOwcqN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qvhEZaxDKrHOwcqN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qvhEZaxDKrHOwcqN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qvhEZaxDKrHOwcqN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qvhEZaxDKrHOwcqN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qvhEZaxDKrHOwcqN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qvhEZaxDKrHOwcqN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qvhEZaxDKrHOwcqN .cluster text{fill:#333;}#mermaid-svg-qvhEZaxDKrHOwcqN .cluster span{color:#333;}#mermaid-svg-qvhEZaxDKrHOwcqN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qvhEZaxDKrHOwcqN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qvhEZaxDKrHOwcqN rect.text{fill:none;stroke-width:0;}#mermaid-svg-qvhEZaxDKrHOwcqN .icon-shape,#mermaid-svg-qvhEZaxDKrHOwcqN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qvhEZaxDKrHOwcqN .icon-shape p,#mermaid-svg-qvhEZaxDKrHOwcqN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qvhEZaxDKrHOwcqN .icon-shape .label rect,#mermaid-svg-qvhEZaxDKrHOwcqN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qvhEZaxDKrHOwcqN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qvhEZaxDKrHOwcqN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qvhEZaxDKrHOwcqN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Token 创建
timeout 开始倒计时
用户访问接口
更新最后活跃时间
active-timeout 重新计算
timeout 不会重新计算

可以理解为:

  • timeout:Token 从创建开始最多能活多久;
  • active-timeout:Token 最多允许多久没有操作;
  • auto-renew:每次有效操作后,重新记录最后活跃时间。

7.5 登录校验会不会查询 Session

普通的:

java 复制代码
StpUtil.checkLogin();

主要依赖以下数据:

  • 当前请求中的 Token;
  • Token → LoginId 映射;
  • 开启 active-timeout 时的最后活跃时间。

它通常不需要读取 Account-Session,也不需要读取 Token-Session。
#mermaid-svg-J18iwGlk0WIPsHQM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J18iwGlk0WIPsHQM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J18iwGlk0WIPsHQM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J18iwGlk0WIPsHQM .error-icon{fill:#552222;}#mermaid-svg-J18iwGlk0WIPsHQM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J18iwGlk0WIPsHQM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J18iwGlk0WIPsHQM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J18iwGlk0WIPsHQM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J18iwGlk0WIPsHQM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J18iwGlk0WIPsHQM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J18iwGlk0WIPsHQM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J18iwGlk0WIPsHQM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J18iwGlk0WIPsHQM .marker.cross{stroke:#333333;}#mermaid-svg-J18iwGlk0WIPsHQM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J18iwGlk0WIPsHQM p{margin:0;}#mermaid-svg-J18iwGlk0WIPsHQM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J18iwGlk0WIPsHQM .cluster-label text{fill:#333;}#mermaid-svg-J18iwGlk0WIPsHQM .cluster-label span{color:#333;}#mermaid-svg-J18iwGlk0WIPsHQM .cluster-label span p{background-color:transparent;}#mermaid-svg-J18iwGlk0WIPsHQM .label text,#mermaid-svg-J18iwGlk0WIPsHQM span{fill:#333;color:#333;}#mermaid-svg-J18iwGlk0WIPsHQM .node rect,#mermaid-svg-J18iwGlk0WIPsHQM .node circle,#mermaid-svg-J18iwGlk0WIPsHQM .node ellipse,#mermaid-svg-J18iwGlk0WIPsHQM .node polygon,#mermaid-svg-J18iwGlk0WIPsHQM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J18iwGlk0WIPsHQM .rough-node .label text,#mermaid-svg-J18iwGlk0WIPsHQM .node .label text,#mermaid-svg-J18iwGlk0WIPsHQM .image-shape .label,#mermaid-svg-J18iwGlk0WIPsHQM .icon-shape .label{text-anchor:middle;}#mermaid-svg-J18iwGlk0WIPsHQM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J18iwGlk0WIPsHQM .rough-node .label,#mermaid-svg-J18iwGlk0WIPsHQM .node .label,#mermaid-svg-J18iwGlk0WIPsHQM .image-shape .label,#mermaid-svg-J18iwGlk0WIPsHQM .icon-shape .label{text-align:center;}#mermaid-svg-J18iwGlk0WIPsHQM .node.clickable{cursor:pointer;}#mermaid-svg-J18iwGlk0WIPsHQM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J18iwGlk0WIPsHQM .arrowheadPath{fill:#333333;}#mermaid-svg-J18iwGlk0WIPsHQM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J18iwGlk0WIPsHQM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J18iwGlk0WIPsHQM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J18iwGlk0WIPsHQM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J18iwGlk0WIPsHQM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J18iwGlk0WIPsHQM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J18iwGlk0WIPsHQM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J18iwGlk0WIPsHQM .cluster text{fill:#333;}#mermaid-svg-J18iwGlk0WIPsHQM .cluster span{color:#333;}#mermaid-svg-J18iwGlk0WIPsHQM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J18iwGlk0WIPsHQM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J18iwGlk0WIPsHQM rect.text{fill:none;stroke-width:0;}#mermaid-svg-J18iwGlk0WIPsHQM .icon-shape,#mermaid-svg-J18iwGlk0WIPsHQM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J18iwGlk0WIPsHQM .icon-shape p,#mermaid-svg-J18iwGlk0WIPsHQM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J18iwGlk0WIPsHQM .icon-shape .label rect,#mermaid-svg-J18iwGlk0WIPsHQM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J18iwGlk0WIPsHQM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J18iwGlk0WIPsHQM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J18iwGlk0WIPsHQM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 普通登录校验通常不读取
普通登录校验通常不读取
StpUtil.checkLogin()
读取 Token
查询 Token → LoginId
检查特殊登录状态
检查最后活跃时间
返回 LoginId
Account-Session
Token-Session

三类数据的作用不同:

数据 主要作用
Token → LoginId 根据 Token 恢复当前用户身份
Account-Session 管理账号级数据和所有登录终端
Token-Session 保存某个 Token 独享的数据
最后活跃时间 判断 Token 是否长时间无操作

StpUtil.checkLogin() 的核心就是:读取 Token,通过 Token → LoginId 映射恢复用户身份,检查 Token 是否无效、过期、被顶或被踢,并在开启 active-timeout 时检查和更新最后活跃时间。


8 Spring Boot 3 集成 Sa-Token

#mermaid-svg-gxHQHAu2oiLamSrn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gxHQHAu2oiLamSrn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gxHQHAu2oiLamSrn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gxHQHAu2oiLamSrn .error-icon{fill:#552222;}#mermaid-svg-gxHQHAu2oiLamSrn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gxHQHAu2oiLamSrn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gxHQHAu2oiLamSrn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gxHQHAu2oiLamSrn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gxHQHAu2oiLamSrn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gxHQHAu2oiLamSrn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gxHQHAu2oiLamSrn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gxHQHAu2oiLamSrn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gxHQHAu2oiLamSrn .marker.cross{stroke:#333333;}#mermaid-svg-gxHQHAu2oiLamSrn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gxHQHAu2oiLamSrn p{margin:0;}#mermaid-svg-gxHQHAu2oiLamSrn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gxHQHAu2oiLamSrn .cluster-label text{fill:#333;}#mermaid-svg-gxHQHAu2oiLamSrn .cluster-label span{color:#333;}#mermaid-svg-gxHQHAu2oiLamSrn .cluster-label span p{background-color:transparent;}#mermaid-svg-gxHQHAu2oiLamSrn .label text,#mermaid-svg-gxHQHAu2oiLamSrn span{fill:#333;color:#333;}#mermaid-svg-gxHQHAu2oiLamSrn .node rect,#mermaid-svg-gxHQHAu2oiLamSrn .node circle,#mermaid-svg-gxHQHAu2oiLamSrn .node ellipse,#mermaid-svg-gxHQHAu2oiLamSrn .node polygon,#mermaid-svg-gxHQHAu2oiLamSrn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gxHQHAu2oiLamSrn .rough-node .label text,#mermaid-svg-gxHQHAu2oiLamSrn .node .label text,#mermaid-svg-gxHQHAu2oiLamSrn .image-shape .label,#mermaid-svg-gxHQHAu2oiLamSrn .icon-shape .label{text-anchor:middle;}#mermaid-svg-gxHQHAu2oiLamSrn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gxHQHAu2oiLamSrn .rough-node .label,#mermaid-svg-gxHQHAu2oiLamSrn .node .label,#mermaid-svg-gxHQHAu2oiLamSrn .image-shape .label,#mermaid-svg-gxHQHAu2oiLamSrn .icon-shape .label{text-align:center;}#mermaid-svg-gxHQHAu2oiLamSrn .node.clickable{cursor:pointer;}#mermaid-svg-gxHQHAu2oiLamSrn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gxHQHAu2oiLamSrn .arrowheadPath{fill:#333333;}#mermaid-svg-gxHQHAu2oiLamSrn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gxHQHAu2oiLamSrn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gxHQHAu2oiLamSrn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gxHQHAu2oiLamSrn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gxHQHAu2oiLamSrn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gxHQHAu2oiLamSrn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gxHQHAu2oiLamSrn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gxHQHAu2oiLamSrn .cluster text{fill:#333;}#mermaid-svg-gxHQHAu2oiLamSrn .cluster span{color:#333;}#mermaid-svg-gxHQHAu2oiLamSrn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gxHQHAu2oiLamSrn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gxHQHAu2oiLamSrn rect.text{fill:none;stroke-width:0;}#mermaid-svg-gxHQHAu2oiLamSrn .icon-shape,#mermaid-svg-gxHQHAu2oiLamSrn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gxHQHAu2oiLamSrn .icon-shape p,#mermaid-svg-gxHQHAu2oiLamSrn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gxHQHAu2oiLamSrn .icon-shape .label rect,#mermaid-svg-gxHQHAu2oiLamSrn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gxHQHAu2oiLamSrn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gxHQHAu2oiLamSrn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gxHQHAu2oiLamSrn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户提交登录信息
Auth 服务校验身份
StpUtil.login
Redis 保存登录状态
返回 Token
客户端携带 Token
Gateway
StpUtil.checkLogin
角色和权限校验
转发业务服务

8.1 引入依赖

普通 Spring Boot 服务使用:

xml 复制代码
<properties>
    <sa-token.version>1.45.0</sa-token.version>
</properties>

<dependencies>
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-spring-boot3-starter</artifactId>
        <version>${sa-token.version}</version>
    </dependency>

    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-redis-template</artifactId>
        <version>${sa-token.version}</version>
    </dependency>
</dependencies>

Spring Cloud Gateway 基于 WebFlux,需要将第一个依赖换成:

xml 复制代码
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
    <version>${sa-token.version}</version>
</dependency>

Gateway 也需要连接 Redis,因为登录状态保存在 Redis 中,Gateway 需要读取同一份 Token 数据。

8.2 Sa-Token 配置

结合当前项目,配置如下:

yaml 复制代码
sa-token:
  # 请求头名称
  token-name: Authorization

  # 请求格式:Authorization: Bearer Token值
  token-prefix: Bearer

  # Token 绝对有效期,30 天
  timeout: 2592000

  # 不开启长时间无操作冻结
  active-timeout: -1

  # 允许同一账号多端登录
  is-concurrent: true

  # 每次登录生成不同 Token
  is-share: false

  # Token 生成风格
  token-style: random-128

  # 打印 Sa-Token 日志
  is-log: true

配置含义如下:

配置 含义
timeout: 2592000 Token 最长有效 30 天
active-timeout: -1 不会因为长时间无操作而冻结
is-concurrent: true 允许同一账号多端同时登录
is-share: false 每次登录都生成新的 Token
random-128 使用随机 128 位风格生成 Token

例如用户 10001 分别在电脑和手机登录:
#mermaid-svg-7dKx0qxkwGOppsu5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7dKx0qxkwGOppsu5 .error-icon{fill:#552222;}#mermaid-svg-7dKx0qxkwGOppsu5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7dKx0qxkwGOppsu5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7dKx0qxkwGOppsu5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7dKx0qxkwGOppsu5 .marker.cross{stroke:#333333;}#mermaid-svg-7dKx0qxkwGOppsu5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7dKx0qxkwGOppsu5 p{margin:0;}#mermaid-svg-7dKx0qxkwGOppsu5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7dKx0qxkwGOppsu5 .cluster-label text{fill:#333;}#mermaid-svg-7dKx0qxkwGOppsu5 .cluster-label span{color:#333;}#mermaid-svg-7dKx0qxkwGOppsu5 .cluster-label span p{background-color:transparent;}#mermaid-svg-7dKx0qxkwGOppsu5 .label text,#mermaid-svg-7dKx0qxkwGOppsu5 span{fill:#333;color:#333;}#mermaid-svg-7dKx0qxkwGOppsu5 .node rect,#mermaid-svg-7dKx0qxkwGOppsu5 .node circle,#mermaid-svg-7dKx0qxkwGOppsu5 .node ellipse,#mermaid-svg-7dKx0qxkwGOppsu5 .node polygon,#mermaid-svg-7dKx0qxkwGOppsu5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7dKx0qxkwGOppsu5 .rough-node .label text,#mermaid-svg-7dKx0qxkwGOppsu5 .node .label text,#mermaid-svg-7dKx0qxkwGOppsu5 .image-shape .label,#mermaid-svg-7dKx0qxkwGOppsu5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-7dKx0qxkwGOppsu5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7dKx0qxkwGOppsu5 .rough-node .label,#mermaid-svg-7dKx0qxkwGOppsu5 .node .label,#mermaid-svg-7dKx0qxkwGOppsu5 .image-shape .label,#mermaid-svg-7dKx0qxkwGOppsu5 .icon-shape .label{text-align:center;}#mermaid-svg-7dKx0qxkwGOppsu5 .node.clickable{cursor:pointer;}#mermaid-svg-7dKx0qxkwGOppsu5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7dKx0qxkwGOppsu5 .arrowheadPath{fill:#333333;}#mermaid-svg-7dKx0qxkwGOppsu5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7dKx0qxkwGOppsu5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7dKx0qxkwGOppsu5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7dKx0qxkwGOppsu5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7dKx0qxkwGOppsu5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7dKx0qxkwGOppsu5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7dKx0qxkwGOppsu5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7dKx0qxkwGOppsu5 .cluster text{fill:#333;}#mermaid-svg-7dKx0qxkwGOppsu5 .cluster span{color:#333;}#mermaid-svg-7dKx0qxkwGOppsu5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7dKx0qxkwGOppsu5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7dKx0qxkwGOppsu5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-7dKx0qxkwGOppsu5 .icon-shape,#mermaid-svg-7dKx0qxkwGOppsu5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7dKx0qxkwGOppsu5 .icon-shape p,#mermaid-svg-7dKx0qxkwGOppsu5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7dKx0qxkwGOppsu5 .icon-shape .label rect,#mermaid-svg-7dKx0qxkwGOppsu5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7dKx0qxkwGOppsu5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7dKx0qxkwGOppsu5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7dKx0qxkwGOppsu5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 10001
PC Token-A
APP Token-B
Token-A → 10001
Token-B → 10001

两个 Token 可以同时使用,但都属于同一个用户。

8.3 登录接口

Sa-Token 不负责校验密码。业务系统需要先校验用户身份,再调用 StpUtil.login() 创建登录状态。

java 复制代码
@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserService userService;
    private final PasswordEncoder passwordEncoder;

    public LoginResponse login(LoginRequest request) {
        User user = userService.getByPhone(request.getPhone());

        if (user == null
                || !passwordEncoder.matches(
                        request.getPassword(),
                        user.getPasswordHash()
                )) {
            throw new BizException("手机号或密码错误");
        }

        if (!user.isEnabled()) {
            throw new BizException("账号已被禁用");
        }

        // 身份校验成功后,建立登录状态
        StpUtil.login(user.getId());

        return new LoginResponse(
                user.getId(),
                StpUtil.getTokenValue()
        );
    }
}

登录过程如下:
#mermaid-svg-hFHEnhClbFLpwYaZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hFHEnhClbFLpwYaZ .error-icon{fill:#552222;}#mermaid-svg-hFHEnhClbFLpwYaZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hFHEnhClbFLpwYaZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hFHEnhClbFLpwYaZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hFHEnhClbFLpwYaZ .marker.cross{stroke:#333333;}#mermaid-svg-hFHEnhClbFLpwYaZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hFHEnhClbFLpwYaZ p{margin:0;}#mermaid-svg-hFHEnhClbFLpwYaZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hFHEnhClbFLpwYaZ .cluster-label text{fill:#333;}#mermaid-svg-hFHEnhClbFLpwYaZ .cluster-label span{color:#333;}#mermaid-svg-hFHEnhClbFLpwYaZ .cluster-label span p{background-color:transparent;}#mermaid-svg-hFHEnhClbFLpwYaZ .label text,#mermaid-svg-hFHEnhClbFLpwYaZ span{fill:#333;color:#333;}#mermaid-svg-hFHEnhClbFLpwYaZ .node rect,#mermaid-svg-hFHEnhClbFLpwYaZ .node circle,#mermaid-svg-hFHEnhClbFLpwYaZ .node ellipse,#mermaid-svg-hFHEnhClbFLpwYaZ .node polygon,#mermaid-svg-hFHEnhClbFLpwYaZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hFHEnhClbFLpwYaZ .rough-node .label text,#mermaid-svg-hFHEnhClbFLpwYaZ .node .label text,#mermaid-svg-hFHEnhClbFLpwYaZ .image-shape .label,#mermaid-svg-hFHEnhClbFLpwYaZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-hFHEnhClbFLpwYaZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hFHEnhClbFLpwYaZ .rough-node .label,#mermaid-svg-hFHEnhClbFLpwYaZ .node .label,#mermaid-svg-hFHEnhClbFLpwYaZ .image-shape .label,#mermaid-svg-hFHEnhClbFLpwYaZ .icon-shape .label{text-align:center;}#mermaid-svg-hFHEnhClbFLpwYaZ .node.clickable{cursor:pointer;}#mermaid-svg-hFHEnhClbFLpwYaZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hFHEnhClbFLpwYaZ .arrowheadPath{fill:#333333;}#mermaid-svg-hFHEnhClbFLpwYaZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hFHEnhClbFLpwYaZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hFHEnhClbFLpwYaZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hFHEnhClbFLpwYaZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hFHEnhClbFLpwYaZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hFHEnhClbFLpwYaZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hFHEnhClbFLpwYaZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hFHEnhClbFLpwYaZ .cluster text{fill:#333;}#mermaid-svg-hFHEnhClbFLpwYaZ .cluster span{color:#333;}#mermaid-svg-hFHEnhClbFLpwYaZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hFHEnhClbFLpwYaZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hFHEnhClbFLpwYaZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-hFHEnhClbFLpwYaZ .icon-shape,#mermaid-svg-hFHEnhClbFLpwYaZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hFHEnhClbFLpwYaZ .icon-shape p,#mermaid-svg-hFHEnhClbFLpwYaZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hFHEnhClbFLpwYaZ .icon-shape .label rect,#mermaid-svg-hFHEnhClbFLpwYaZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hFHEnhClbFLpwYaZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hFHEnhClbFLpwYaZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hFHEnhClbFLpwYaZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交手机号和密码
查询用户
校验密码和账号状态
StpUtil.login
生成 Token
Redis 保存登录状态
返回 Token

客户端后续请求携带:

http 复制代码
Authorization: Bearer xxxxxxxxx

8.4 Gateway 登录校验

Gateway 可以通过 SaReactorFilter 统一校验登录状态。

java 复制代码
@Configuration
public class SaTokenConfiguration {

    @Bean
    public SaReactorFilter saReactorFilter() {
        return new SaReactorFilter()
                .addInclude("/**")
                .addExclude("/favicon.ico")
                .setAuth(obj -> {
                    SaRouter.match("/**")
                            // CORS 预检请求不校验
                            .notMatch(SaHttpMethod.OPTIONS)
                            // 登录和验证码接口放行
                            .notMatch(
                                    "/auth/login",
                                    "/auth/verification/code/send"
                            )
                            .check(StpUtil::checkLogin);

                    // 商品分类查询权限
                    SaRouter.match(
                                    "/admin/commodity/categories/tree"
                            )
                            .match(SaHttpMethod.GET)
                            .check(() ->
                                    StpUtil.checkPermission(
                                            "commodity:category:view"
                                    )
                            );

                    // 商品分类新增权限
                    SaRouter.match(
                                    "/admin/commodity/categories"
                            )
                            .match(SaHttpMethod.POST)
                            .check(() ->
                                    StpUtil.checkPermission(
                                            "commodity:category:add"
                                    )
                            );

                    // 商品分类修改权限
                    SaRouter.match(
                                    "/admin/commodity/categories/**"
                            )
                            .match(SaHttpMethod.PATCH)
                            .check(() ->
                                    StpUtil.checkPermission(
                                            "commodity:category:update"
                                    )
                            );

                    // 商品分类删除权限
                    SaRouter.match(
                                    "/admin/commodity/categories/**"
                            )
                            .match(SaHttpMethod.DELETE)
                            .check(() ->
                                    StpUtil.checkPermission(
                                            "commodity:category:delete"
                                    )
                            );
                })
                .setError(e -> {
                    if (e instanceof NotLoginException) {
                        return SaResult.code(401)
                                .setMsg("登录状态已失效");
                    }

                    if (e instanceof NotPermissionException
                            || e instanceof NotRoleException) {
                        return SaResult.code(403)
                                .setMsg("无权访问该资源");
                    }

                    return SaResult.error("鉴权服务异常");
                });
    }
}

Gateway 的处理流程如下:
#mermaid-svg-05QzCYl4olxHrt6X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-05QzCYl4olxHrt6X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-05QzCYl4olxHrt6X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-05QzCYl4olxHrt6X .error-icon{fill:#552222;}#mermaid-svg-05QzCYl4olxHrt6X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-05QzCYl4olxHrt6X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-05QzCYl4olxHrt6X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-05QzCYl4olxHrt6X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-05QzCYl4olxHrt6X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-05QzCYl4olxHrt6X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-05QzCYl4olxHrt6X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-05QzCYl4olxHrt6X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-05QzCYl4olxHrt6X .marker.cross{stroke:#333333;}#mermaid-svg-05QzCYl4olxHrt6X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-05QzCYl4olxHrt6X p{margin:0;}#mermaid-svg-05QzCYl4olxHrt6X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-05QzCYl4olxHrt6X .cluster-label text{fill:#333;}#mermaid-svg-05QzCYl4olxHrt6X .cluster-label span{color:#333;}#mermaid-svg-05QzCYl4olxHrt6X .cluster-label span p{background-color:transparent;}#mermaid-svg-05QzCYl4olxHrt6X .label text,#mermaid-svg-05QzCYl4olxHrt6X span{fill:#333;color:#333;}#mermaid-svg-05QzCYl4olxHrt6X .node rect,#mermaid-svg-05QzCYl4olxHrt6X .node circle,#mermaid-svg-05QzCYl4olxHrt6X .node ellipse,#mermaid-svg-05QzCYl4olxHrt6X .node polygon,#mermaid-svg-05QzCYl4olxHrt6X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-05QzCYl4olxHrt6X .rough-node .label text,#mermaid-svg-05QzCYl4olxHrt6X .node .label text,#mermaid-svg-05QzCYl4olxHrt6X .image-shape .label,#mermaid-svg-05QzCYl4olxHrt6X .icon-shape .label{text-anchor:middle;}#mermaid-svg-05QzCYl4olxHrt6X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-05QzCYl4olxHrt6X .rough-node .label,#mermaid-svg-05QzCYl4olxHrt6X .node .label,#mermaid-svg-05QzCYl4olxHrt6X .image-shape .label,#mermaid-svg-05QzCYl4olxHrt6X .icon-shape .label{text-align:center;}#mermaid-svg-05QzCYl4olxHrt6X .node.clickable{cursor:pointer;}#mermaid-svg-05QzCYl4olxHrt6X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-05QzCYl4olxHrt6X .arrowheadPath{fill:#333333;}#mermaid-svg-05QzCYl4olxHrt6X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-05QzCYl4olxHrt6X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-05QzCYl4olxHrt6X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-05QzCYl4olxHrt6X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-05QzCYl4olxHrt6X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-05QzCYl4olxHrt6X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-05QzCYl4olxHrt6X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-05QzCYl4olxHrt6X .cluster text{fill:#333;}#mermaid-svg-05QzCYl4olxHrt6X .cluster span{color:#333;}#mermaid-svg-05QzCYl4olxHrt6X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-05QzCYl4olxHrt6X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-05QzCYl4olxHrt6X rect.text{fill:none;stroke-width:0;}#mermaid-svg-05QzCYl4olxHrt6X .icon-shape,#mermaid-svg-05QzCYl4olxHrt6X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-05QzCYl4olxHrt6X .icon-shape p,#mermaid-svg-05QzCYl4olxHrt6X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-05QzCYl4olxHrt6X .icon-shape .label rect,#mermaid-svg-05QzCYl4olxHrt6X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-05QzCYl4olxHrt6X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-05QzCYl4olxHrt6X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-05QzCYl4olxHrt6X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是







请求到达 Gateway
是否为公共接口
直接放行
StpUtil.checkLogin
登录是否有效
返回 401
接口是否要求权限
转发业务服务
StpUtil.checkPermission
是否拥有权限
返回 403

8.5 实现角色和权限查询

Sa-Token 不知道用户拥有哪些角色和权限,需要业务系统实现 StpInterface

为了方便入门,可以直接按照用户 ID 缓存两份数据:

  • auth:user:roles:{userId}:用户角色;
  • auth:user:permissions:{userId}:用户权限。
java 复制代码
@Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {

    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public List<String> getPermissionList(
            Object loginId,
            String loginType) {

        String key = "auth:user:permissions:" + loginId;

        Object value =
                redisTemplate.opsForValue().get(key);

        return toStringList(value);
    }

    @Override
    public List<String> getRoleList(
            Object loginId,
            String loginType) {

        String key = "auth:user:roles:" + loginId;

        Object value =
                redisTemplate.opsForValue().get(key);

        return toStringList(value);
    }

    private List<String> toStringList(Object value) {
        if (!(value instanceof Collection<?> collection)) {
            return List.of();
        }

        return collection.stream()
                .filter(Objects::nonNull)
                .map(String::valueOf)
                .distinct()
                .toList();
    }
}

当 Gateway 执行:

java 复制代码
StpUtil.checkPermission("commodity:category:view");

Sa-Token 会调用:

java 复制代码
getPermissionList(loginId, loginType);

然后判断返回的权限集合中是否包含:

text 复制代码
commodity:category:view

完整过程如下:
#mermaid-svg-sHNUepfKoReMi9cd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sHNUepfKoReMi9cd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sHNUepfKoReMi9cd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sHNUepfKoReMi9cd .error-icon{fill:#552222;}#mermaid-svg-sHNUepfKoReMi9cd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sHNUepfKoReMi9cd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sHNUepfKoReMi9cd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sHNUepfKoReMi9cd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sHNUepfKoReMi9cd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sHNUepfKoReMi9cd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sHNUepfKoReMi9cd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sHNUepfKoReMi9cd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sHNUepfKoReMi9cd .marker.cross{stroke:#333333;}#mermaid-svg-sHNUepfKoReMi9cd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sHNUepfKoReMi9cd p{margin:0;}#mermaid-svg-sHNUepfKoReMi9cd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sHNUepfKoReMi9cd .cluster-label text{fill:#333;}#mermaid-svg-sHNUepfKoReMi9cd .cluster-label span{color:#333;}#mermaid-svg-sHNUepfKoReMi9cd .cluster-label span p{background-color:transparent;}#mermaid-svg-sHNUepfKoReMi9cd .label text,#mermaid-svg-sHNUepfKoReMi9cd span{fill:#333;color:#333;}#mermaid-svg-sHNUepfKoReMi9cd .node rect,#mermaid-svg-sHNUepfKoReMi9cd .node circle,#mermaid-svg-sHNUepfKoReMi9cd .node ellipse,#mermaid-svg-sHNUepfKoReMi9cd .node polygon,#mermaid-svg-sHNUepfKoReMi9cd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sHNUepfKoReMi9cd .rough-node .label text,#mermaid-svg-sHNUepfKoReMi9cd .node .label text,#mermaid-svg-sHNUepfKoReMi9cd .image-shape .label,#mermaid-svg-sHNUepfKoReMi9cd .icon-shape .label{text-anchor:middle;}#mermaid-svg-sHNUepfKoReMi9cd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sHNUepfKoReMi9cd .rough-node .label,#mermaid-svg-sHNUepfKoReMi9cd .node .label,#mermaid-svg-sHNUepfKoReMi9cd .image-shape .label,#mermaid-svg-sHNUepfKoReMi9cd .icon-shape .label{text-align:center;}#mermaid-svg-sHNUepfKoReMi9cd .node.clickable{cursor:pointer;}#mermaid-svg-sHNUepfKoReMi9cd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sHNUepfKoReMi9cd .arrowheadPath{fill:#333333;}#mermaid-svg-sHNUepfKoReMi9cd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sHNUepfKoReMi9cd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sHNUepfKoReMi9cd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sHNUepfKoReMi9cd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sHNUepfKoReMi9cd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sHNUepfKoReMi9cd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sHNUepfKoReMi9cd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sHNUepfKoReMi9cd .cluster text{fill:#333;}#mermaid-svg-sHNUepfKoReMi9cd .cluster span{color:#333;}#mermaid-svg-sHNUepfKoReMi9cd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sHNUepfKoReMi9cd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sHNUepfKoReMi9cd rect.text{fill:none;stroke-width:0;}#mermaid-svg-sHNUepfKoReMi9cd .icon-shape,#mermaid-svg-sHNUepfKoReMi9cd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sHNUepfKoReMi9cd .icon-shape p,#mermaid-svg-sHNUepfKoReMi9cd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sHNUepfKoReMi9cd .icon-shape .label rect,#mermaid-svg-sHNUepfKoReMi9cd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sHNUepfKoReMi9cd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sHNUepfKoReMi9cd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sHNUepfKoReMi9cd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

StpUtil.checkPermission
根据 Token 得到 LoginId
调用 StpInterface
Redis 查询用户权限
是否包含目标权限
权限校验通过
抛出无权限异常

8.6 登录时缓存角色和权限

用户登录时,可以将查询到的角色和权限写入 Redis。

java 复制代码
public LoginResponse login(LoginRequest request) {
    UserAuthInfo user =
            userService.getAuthInfo(request.getPhone());

    verifyPassword(
            request.getPassword(),
            user.getPasswordHash()
    );

    StpUtil.login(user.getUserId());

    redisTemplate.opsForValue().set(
            "auth:user:roles:" + user.getUserId(),
            user.getRoles(),
            Duration.ofDays(30)
    );

    redisTemplate.opsForValue().set(
            "auth:user:permissions:" + user.getUserId(),
            user.getPermissions(),
            Duration.ofDays(30)
    );

    return new LoginResponse(
            user.getUserId(),
            StpUtil.getTokenValue()
    );
}

这样:

  • 登录时查询一次角色和权限;
  • 后续鉴权直接查询 Redis;
  • 不需要每次请求都查询数据库。

#mermaid-svg-JJyfFfZfhv5s9upt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JJyfFfZfhv5s9upt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JJyfFfZfhv5s9upt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JJyfFfZfhv5s9upt .error-icon{fill:#552222;}#mermaid-svg-JJyfFfZfhv5s9upt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JJyfFfZfhv5s9upt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JJyfFfZfhv5s9upt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JJyfFfZfhv5s9upt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JJyfFfZfhv5s9upt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JJyfFfZfhv5s9upt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JJyfFfZfhv5s9upt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JJyfFfZfhv5s9upt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JJyfFfZfhv5s9upt .marker.cross{stroke:#333333;}#mermaid-svg-JJyfFfZfhv5s9upt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JJyfFfZfhv5s9upt p{margin:0;}#mermaid-svg-JJyfFfZfhv5s9upt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JJyfFfZfhv5s9upt .cluster-label text{fill:#333;}#mermaid-svg-JJyfFfZfhv5s9upt .cluster-label span{color:#333;}#mermaid-svg-JJyfFfZfhv5s9upt .cluster-label span p{background-color:transparent;}#mermaid-svg-JJyfFfZfhv5s9upt .label text,#mermaid-svg-JJyfFfZfhv5s9upt span{fill:#333;color:#333;}#mermaid-svg-JJyfFfZfhv5s9upt .node rect,#mermaid-svg-JJyfFfZfhv5s9upt .node circle,#mermaid-svg-JJyfFfZfhv5s9upt .node ellipse,#mermaid-svg-JJyfFfZfhv5s9upt .node polygon,#mermaid-svg-JJyfFfZfhv5s9upt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JJyfFfZfhv5s9upt .rough-node .label text,#mermaid-svg-JJyfFfZfhv5s9upt .node .label text,#mermaid-svg-JJyfFfZfhv5s9upt .image-shape .label,#mermaid-svg-JJyfFfZfhv5s9upt .icon-shape .label{text-anchor:middle;}#mermaid-svg-JJyfFfZfhv5s9upt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JJyfFfZfhv5s9upt .rough-node .label,#mermaid-svg-JJyfFfZfhv5s9upt .node .label,#mermaid-svg-JJyfFfZfhv5s9upt .image-shape .label,#mermaid-svg-JJyfFfZfhv5s9upt .icon-shape .label{text-align:center;}#mermaid-svg-JJyfFfZfhv5s9upt .node.clickable{cursor:pointer;}#mermaid-svg-JJyfFfZfhv5s9upt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JJyfFfZfhv5s9upt .arrowheadPath{fill:#333333;}#mermaid-svg-JJyfFfZfhv5s9upt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JJyfFfZfhv5s9upt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JJyfFfZfhv5s9upt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JJyfFfZfhv5s9upt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JJyfFfZfhv5s9upt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JJyfFfZfhv5s9upt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JJyfFfZfhv5s9upt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JJyfFfZfhv5s9upt .cluster text{fill:#333;}#mermaid-svg-JJyfFfZfhv5s9upt .cluster span{color:#333;}#mermaid-svg-JJyfFfZfhv5s9upt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JJyfFfZfhv5s9upt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JJyfFfZfhv5s9upt rect.text{fill:none;stroke-width:0;}#mermaid-svg-JJyfFfZfhv5s9upt .icon-shape,#mermaid-svg-JJyfFfZfhv5s9upt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JJyfFfZfhv5s9upt .icon-shape p,#mermaid-svg-JJyfFfZfhv5s9upt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JJyfFfZfhv5s9upt .icon-shape .label rect,#mermaid-svg-JJyfFfZfhv5s9upt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JJyfFfZfhv5s9upt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JJyfFfZfhv5s9upt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JJyfFfZfhv5s9upt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户登录
查询数据库角色权限
写入 Redis
后续请求
Gateway 从 Redis 读取权限

角色或权限发生变化后,需要删除或更新 Redis 中的旧数据,否则用户可能继续使用旧权限。

8.7 退出登录

当前配置为:

yaml 复制代码
is-concurrent: true
is-share: false

一个用户可以同时拥有多个 Token。无参 logout() 只退出当前请求携带的 Token:

java 复制代码
@PostMapping("/logout")
public SaResult logout() {
    StpUtil.logout();
    return SaResult.ok("退出成功");
}

例如用户同时在 PC 和 APP 登录:
#mermaid-svg-IkVzg4DGVJ3gBKDS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IkVzg4DGVJ3gBKDS .error-icon{fill:#552222;}#mermaid-svg-IkVzg4DGVJ3gBKDS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IkVzg4DGVJ3gBKDS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .marker.cross{stroke:#333333;}#mermaid-svg-IkVzg4DGVJ3gBKDS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IkVzg4DGVJ3gBKDS p{margin:0;}#mermaid-svg-IkVzg4DGVJ3gBKDS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .cluster-label text{fill:#333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .cluster-label span{color:#333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .cluster-label span p{background-color:transparent;}#mermaid-svg-IkVzg4DGVJ3gBKDS .label text,#mermaid-svg-IkVzg4DGVJ3gBKDS span{fill:#333;color:#333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .node rect,#mermaid-svg-IkVzg4DGVJ3gBKDS .node circle,#mermaid-svg-IkVzg4DGVJ3gBKDS .node ellipse,#mermaid-svg-IkVzg4DGVJ3gBKDS .node polygon,#mermaid-svg-IkVzg4DGVJ3gBKDS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IkVzg4DGVJ3gBKDS .rough-node .label text,#mermaid-svg-IkVzg4DGVJ3gBKDS .node .label text,#mermaid-svg-IkVzg4DGVJ3gBKDS .image-shape .label,#mermaid-svg-IkVzg4DGVJ3gBKDS .icon-shape .label{text-anchor:middle;}#mermaid-svg-IkVzg4DGVJ3gBKDS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IkVzg4DGVJ3gBKDS .rough-node .label,#mermaid-svg-IkVzg4DGVJ3gBKDS .node .label,#mermaid-svg-IkVzg4DGVJ3gBKDS .image-shape .label,#mermaid-svg-IkVzg4DGVJ3gBKDS .icon-shape .label{text-align:center;}#mermaid-svg-IkVzg4DGVJ3gBKDS .node.clickable{cursor:pointer;}#mermaid-svg-IkVzg4DGVJ3gBKDS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .arrowheadPath{fill:#333333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IkVzg4DGVJ3gBKDS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IkVzg4DGVJ3gBKDS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IkVzg4DGVJ3gBKDS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IkVzg4DGVJ3gBKDS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IkVzg4DGVJ3gBKDS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IkVzg4DGVJ3gBKDS .cluster text{fill:#333;}#mermaid-svg-IkVzg4DGVJ3gBKDS .cluster span{color:#333;}#mermaid-svg-IkVzg4DGVJ3gBKDS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IkVzg4DGVJ3gBKDS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IkVzg4DGVJ3gBKDS rect.text{fill:none;stroke-width:0;}#mermaid-svg-IkVzg4DGVJ3gBKDS .icon-shape,#mermaid-svg-IkVzg4DGVJ3gBKDS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IkVzg4DGVJ3gBKDS .icon-shape p,#mermaid-svg-IkVzg4DGVJ3gBKDS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IkVzg4DGVJ3gBKDS .icon-shape .label rect,#mermaid-svg-IkVzg4DGVJ3gBKDS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IkVzg4DGVJ3gBKDS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IkVzg4DGVJ3gBKDS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IkVzg4DGVJ3gBKDS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 10001
PC Token-A
APP Token-B
PC 调用 logout
Token-A 失效
Token-B 仍然有效

可以用自己的 Response 哈,没问题的

Auth 服务负责校验用户身份并调用 StpUtil.login() 创建 Token;Gateway 通过 StpUtil.checkLogin() 校验登录状态,再通过 StpInterface 从 Redis 获取角色和权限,决定请求是否允许通过。


9 并发登录、多设备和失效状态

9.1 is-concurrent

is-concurrent 控制是否允许同一账号同时存在多个登录终端。设置为 true 时,PC、APP 和平板可以同时登录;设置为 false 时,新登录可能把旧登录顶下线,或者拒绝新设备登录。

9.2 is-share

is-share 控制同一账号多次登录是否复用同一个 Token。设置为 false 时,每个设备拥有独立 Token,更方便单独退出、单独踢下线、记录设备信息和定位异常登录。
#mermaid-svg-y4GYYUjgzQxxYfZO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y4GYYUjgzQxxYfZO .error-icon{fill:#552222;}#mermaid-svg-y4GYYUjgzQxxYfZO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y4GYYUjgzQxxYfZO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y4GYYUjgzQxxYfZO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y4GYYUjgzQxxYfZO .marker.cross{stroke:#333333;}#mermaid-svg-y4GYYUjgzQxxYfZO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y4GYYUjgzQxxYfZO p{margin:0;}#mermaid-svg-y4GYYUjgzQxxYfZO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-y4GYYUjgzQxxYfZO .cluster-label text{fill:#333;}#mermaid-svg-y4GYYUjgzQxxYfZO .cluster-label span{color:#333;}#mermaid-svg-y4GYYUjgzQxxYfZO .cluster-label span p{background-color:transparent;}#mermaid-svg-y4GYYUjgzQxxYfZO .label text,#mermaid-svg-y4GYYUjgzQxxYfZO span{fill:#333;color:#333;}#mermaid-svg-y4GYYUjgzQxxYfZO .node rect,#mermaid-svg-y4GYYUjgzQxxYfZO .node circle,#mermaid-svg-y4GYYUjgzQxxYfZO .node ellipse,#mermaid-svg-y4GYYUjgzQxxYfZO .node polygon,#mermaid-svg-y4GYYUjgzQxxYfZO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-y4GYYUjgzQxxYfZO .rough-node .label text,#mermaid-svg-y4GYYUjgzQxxYfZO .node .label text,#mermaid-svg-y4GYYUjgzQxxYfZO .image-shape .label,#mermaid-svg-y4GYYUjgzQxxYfZO .icon-shape .label{text-anchor:middle;}#mermaid-svg-y4GYYUjgzQxxYfZO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-y4GYYUjgzQxxYfZO .rough-node .label,#mermaid-svg-y4GYYUjgzQxxYfZO .node .label,#mermaid-svg-y4GYYUjgzQxxYfZO .image-shape .label,#mermaid-svg-y4GYYUjgzQxxYfZO .icon-shape .label{text-align:center;}#mermaid-svg-y4GYYUjgzQxxYfZO .node.clickable{cursor:pointer;}#mermaid-svg-y4GYYUjgzQxxYfZO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-y4GYYUjgzQxxYfZO .arrowheadPath{fill:#333333;}#mermaid-svg-y4GYYUjgzQxxYfZO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-y4GYYUjgzQxxYfZO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-y4GYYUjgzQxxYfZO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y4GYYUjgzQxxYfZO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-y4GYYUjgzQxxYfZO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y4GYYUjgzQxxYfZO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-y4GYYUjgzQxxYfZO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-y4GYYUjgzQxxYfZO .cluster text{fill:#333;}#mermaid-svg-y4GYYUjgzQxxYfZO .cluster span{color:#333;}#mermaid-svg-y4GYYUjgzQxxYfZO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-y4GYYUjgzQxxYfZO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-y4GYYUjgzQxxYfZO rect.text{fill:none;stroke-width:0;}#mermaid-svg-y4GYYUjgzQxxYfZO .icon-shape,#mermaid-svg-y4GYYUjgzQxxYfZO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y4GYYUjgzQxxYfZO .icon-shape p,#mermaid-svg-y4GYYUjgzQxxYfZO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-y4GYYUjgzQxxYfZO .icon-shape .label rect,#mermaid-svg-y4GYYUjgzQxxYfZO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y4GYYUjgzQxxYfZO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-y4GYYUjgzQxxYfZO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-y4GYYUjgzQxxYfZO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 10001
PC: Token-A
APP: Token-B
平板: Token-C

9.3 logout、kickout、replaced

状态 含义 典型场景
logout 用户主动退出 点击退出登录
kickout 被管理员强制踢下线 风险账号、封禁用户
replaced 被其他设备登录顶下线 禁止并发登录
java 复制代码
StpUtil.logout();
StpUtil.kickout(10001L);

Sa-Token 会保留不同的失效原因,因此客户端可以区分"正常退出""被管理员踢下线"和"账号在其他设备登录"。


10 微服务与 Gateway 鉴权

微服务架构通常在 Gateway 统一完成登录认证,再将请求转发到业务服务。
#mermaid-svg-ggHx5CTg9ZpzaDnV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ggHx5CTg9ZpzaDnV .error-icon{fill:#552222;}#mermaid-svg-ggHx5CTg9ZpzaDnV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ggHx5CTg9ZpzaDnV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .marker.cross{stroke:#333333;}#mermaid-svg-ggHx5CTg9ZpzaDnV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ggHx5CTg9ZpzaDnV p{margin:0;}#mermaid-svg-ggHx5CTg9ZpzaDnV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .cluster-label text{fill:#333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .cluster-label span{color:#333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .cluster-label span p{background-color:transparent;}#mermaid-svg-ggHx5CTg9ZpzaDnV .label text,#mermaid-svg-ggHx5CTg9ZpzaDnV span{fill:#333;color:#333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .node rect,#mermaid-svg-ggHx5CTg9ZpzaDnV .node circle,#mermaid-svg-ggHx5CTg9ZpzaDnV .node ellipse,#mermaid-svg-ggHx5CTg9ZpzaDnV .node polygon,#mermaid-svg-ggHx5CTg9ZpzaDnV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ggHx5CTg9ZpzaDnV .rough-node .label text,#mermaid-svg-ggHx5CTg9ZpzaDnV .node .label text,#mermaid-svg-ggHx5CTg9ZpzaDnV .image-shape .label,#mermaid-svg-ggHx5CTg9ZpzaDnV .icon-shape .label{text-anchor:middle;}#mermaid-svg-ggHx5CTg9ZpzaDnV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ggHx5CTg9ZpzaDnV .rough-node .label,#mermaid-svg-ggHx5CTg9ZpzaDnV .node .label,#mermaid-svg-ggHx5CTg9ZpzaDnV .image-shape .label,#mermaid-svg-ggHx5CTg9ZpzaDnV .icon-shape .label{text-align:center;}#mermaid-svg-ggHx5CTg9ZpzaDnV .node.clickable{cursor:pointer;}#mermaid-svg-ggHx5CTg9ZpzaDnV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .arrowheadPath{fill:#333333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ggHx5CTg9ZpzaDnV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ggHx5CTg9ZpzaDnV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ggHx5CTg9ZpzaDnV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ggHx5CTg9ZpzaDnV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ggHx5CTg9ZpzaDnV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ggHx5CTg9ZpzaDnV .cluster text{fill:#333;}#mermaid-svg-ggHx5CTg9ZpzaDnV .cluster span{color:#333;}#mermaid-svg-ggHx5CTg9ZpzaDnV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ggHx5CTg9ZpzaDnV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ggHx5CTg9ZpzaDnV rect.text{fill:none;stroke-width:0;}#mermaid-svg-ggHx5CTg9ZpzaDnV .icon-shape,#mermaid-svg-ggHx5CTg9ZpzaDnV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ggHx5CTg9ZpzaDnV .icon-shape p,#mermaid-svg-ggHx5CTg9ZpzaDnV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ggHx5CTg9ZpzaDnV .icon-shape .label rect,#mermaid-svg-ggHx5CTg9ZpzaDnV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ggHx5CTg9ZpzaDnV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ggHx5CTg9ZpzaDnV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ggHx5CTg9ZpzaDnV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 失败
通过
客户端携带 Token
Gateway
Sa-Token 校验
返回 401 或 403
写入可信用户信息
业务微服务

Gateway 示例:

java 复制代码
@Configuration
public class SaTokenGatewayConfig {

    @Bean
    public SaReactorFilter saReactorFilter() {
        return new SaReactorFilter()
                .addInclude("/**")
                .addExclude("/favicon.ico")
                .setAuth(obj -> {
                    SaRouter.match("/**")
                            .notMatch("/auth/login")
                            .notMatch("/auth/register")
                            .check(StpUtil::checkLogin);

                    SaRouter.match("/admin/**")
                            .check(() -> StpUtil.checkRole("admin"));
                })
                .setError(e -> SaResult.error(e.getMessage()));
    }
}

只在 Gateway 鉴权并不天然安全。如果业务微服务直接暴露公网,攻击者可能绕过 Gateway;如果下游服务直接相信 X-User-Id,攻击者还可能伪造身份请求头。因此应同时做到:

  1. Gateway 先删除客户端提交的用户身份请求头,再写入经过认证的真实身份;
  2. 下游服务只部署在内网,不能直接暴露公网;
  3. 服务之间通过 Same-Token、mTLS、内部签名或 OAuth2 Client Credentials 验证调用方身份;
  4. 订单归属、数据范围等资源级权限仍应由业务服务校验。

#mermaid-svg-wLbYvMHKH5uvYRQz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wLbYvMHKH5uvYRQz .error-icon{fill:#552222;}#mermaid-svg-wLbYvMHKH5uvYRQz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wLbYvMHKH5uvYRQz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wLbYvMHKH5uvYRQz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wLbYvMHKH5uvYRQz .marker.cross{stroke:#333333;}#mermaid-svg-wLbYvMHKH5uvYRQz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wLbYvMHKH5uvYRQz p{margin:0;}#mermaid-svg-wLbYvMHKH5uvYRQz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wLbYvMHKH5uvYRQz .cluster-label text{fill:#333;}#mermaid-svg-wLbYvMHKH5uvYRQz .cluster-label span{color:#333;}#mermaid-svg-wLbYvMHKH5uvYRQz .cluster-label span p{background-color:transparent;}#mermaid-svg-wLbYvMHKH5uvYRQz .label text,#mermaid-svg-wLbYvMHKH5uvYRQz span{fill:#333;color:#333;}#mermaid-svg-wLbYvMHKH5uvYRQz .node rect,#mermaid-svg-wLbYvMHKH5uvYRQz .node circle,#mermaid-svg-wLbYvMHKH5uvYRQz .node ellipse,#mermaid-svg-wLbYvMHKH5uvYRQz .node polygon,#mermaid-svg-wLbYvMHKH5uvYRQz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wLbYvMHKH5uvYRQz .rough-node .label text,#mermaid-svg-wLbYvMHKH5uvYRQz .node .label text,#mermaid-svg-wLbYvMHKH5uvYRQz .image-shape .label,#mermaid-svg-wLbYvMHKH5uvYRQz .icon-shape .label{text-anchor:middle;}#mermaid-svg-wLbYvMHKH5uvYRQz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wLbYvMHKH5uvYRQz .rough-node .label,#mermaid-svg-wLbYvMHKH5uvYRQz .node .label,#mermaid-svg-wLbYvMHKH5uvYRQz .image-shape .label,#mermaid-svg-wLbYvMHKH5uvYRQz .icon-shape .label{text-align:center;}#mermaid-svg-wLbYvMHKH5uvYRQz .node.clickable{cursor:pointer;}#mermaid-svg-wLbYvMHKH5uvYRQz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wLbYvMHKH5uvYRQz .arrowheadPath{fill:#333333;}#mermaid-svg-wLbYvMHKH5uvYRQz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wLbYvMHKH5uvYRQz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wLbYvMHKH5uvYRQz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wLbYvMHKH5uvYRQz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wLbYvMHKH5uvYRQz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wLbYvMHKH5uvYRQz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wLbYvMHKH5uvYRQz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wLbYvMHKH5uvYRQz .cluster text{fill:#333;}#mermaid-svg-wLbYvMHKH5uvYRQz .cluster span{color:#333;}#mermaid-svg-wLbYvMHKH5uvYRQz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wLbYvMHKH5uvYRQz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wLbYvMHKH5uvYRQz rect.text{fill:none;stroke-width:0;}#mermaid-svg-wLbYvMHKH5uvYRQz .icon-shape,#mermaid-svg-wLbYvMHKH5uvYRQz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wLbYvMHKH5uvYRQz .icon-shape p,#mermaid-svg-wLbYvMHKH5uvYRQz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wLbYvMHKH5uvYRQz .icon-shape .label rect,#mermaid-svg-wLbYvMHKH5uvYRQz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wLbYvMHKH5uvYRQz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wLbYvMHKH5uvYRQz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wLbYvMHKH5uvYRQz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部客户端
Gateway
清除伪造身份头
校验用户 Token
写入可信用户信息
内网业务服务
资源级权限校验

用户 Token 证明"当前用户是谁",Same-Token 或服务签名证明"请求来自哪个可信服务",两者不能混为一谈。


11 Sa-Token 与 Spring Security

对比项 Sa-Token Spring Security
学习成本 较低 较高
API 风格 工具类、注解,直接 FilterChain、Provider、Context
多设备登录 内置支持方便 通常需要扩展
踢人下线 内置 通常结合 SessionRegistry
OAuth2/OIDC 提供相关模块 标准体系更成熟
扩展能力 偏业务友好 更完整、更标准化
适用场景 国内中后台、快速业务开发 复杂安全体系、标准 OAuth2/OIDC

面试时不要说"Sa-Token 就是 Token,Spring Security 就是 Session"。两者都可以使用 Session 或 Token,真正区别在于抽象层次、扩展体系和使用复杂度。


12 Token 存储与安全

前端常见两种存储方式:

方案 优点 主要风险
LocalStorage 使用简单,便于放入 Header XSS 可直接读取 Token
HttpOnly Cookie JavaScript 无法直接读取 需要防范 CSRF

Cookie 方案通常需要配置 HttpOnlySecure 和合适的 SameSite,并根据业务增加 CSRF Token 或 Origin 校验。不能简单说 LocalStorage 一定不安全、Cookie 一定安全,两者面对的主要风险不同。

生产环境还应做到:

  1. 全链路 HTTPS;
  2. 不把 Token 放在 URL 中;
  3. 不在日志中打印完整 Token;
  4. 登录接口按 IP、账号、设备进行组合限流;
  5. 修改密码、支付、提现等高风险操作增加二次认证;
  6. Redis 故障时采用 Fail Closed,无法确认身份就拒绝请求;
  7. 不使用永久有效 Token;
  8. 权限必须由服务端校验,前端隐藏按钮不等于权限控制。

13 一些问题

13.1 Sa-Token 与分布式 Session 有什么关系?

二者底层思想相似,都需要服务端共享登录状态。Sa-Token 在此基础上进一步提供 Token 读取、权限校验、多设备登录、踢人下线、顶人下线、多账号体系、网关鉴权和 SSO 等能力。

13.2 权限修改后如何立即生效?

如果权限使用 Redis 或本地缓存,修改角色或权限后必须主动删除对应缓存;如果权限直接写入 JWT,还要处理旧 JWT 在过期前继续携带旧权限的问题。

13.3 只在 Gateway 鉴权有什么风险?

攻击者可能绕过 Gateway 直接访问下游服务,也可能伪造 Gateway 透传的用户请求头。应配合内网隔离、请求头清理、服务间认证和资源级权限校验。

13.4 Redis 宕机后还能鉴权吗?

默认有状态模式无法可靠鉴权,因为系统无法确认 Token 是否有效。生产环境需要 Redis 高可用、合理超时、故障监控,并在无法确认身份时拒绝访问。

13.5 Sa-Token 和 JWT 怎么选?

需要主动注销、踢人下线、多设备管理、权限快速生效时,更适合有状态 Token;服务多、Token 生命周期短、可以接受撤销延迟并具备完善密钥与 Refresh Token 机制时,可以考虑 JWT。


Sa-Token 的核心不是几个 API,而是一套围绕 Token 构建的有状态会话与权限认证模型。

相关推荐
Wang's Blog10 小时前
Go-Zero 项目开发47:自研微服务框架的必要性与核心结构设计
开发语言·微服务·golang
小罗水14 小时前
附录A 各微服务完整 application.yml 配置汇总
数据库·elasticsearch·微服务
前端开发江鸟15 小时前
Agent 卡在人工确认时,重启后怎么继续?我用检查点和 LangGraph 看懂状态恢复
架构
heimeiyingwang15 小时前
【架构实战】Kubernetes调度器深度剖析:从Pod调度到自定义调度器
java·架构·kubernetes
Vince的修炼之路16 小时前
大模型推理框架SGLang的源码分析
人工智能·架构
小渔村的拉线工16 小时前
1.HPM6E80 解析芯片整体工作原理和存储架构
单片机·嵌入式硬件·mcu·架构·hpm6e80
天天鸭17 小时前
5 万处中文的老项目实现国际化,如何用架构思维完成改造?
前端·javascript·架构
上海安当技术17 小时前
统一身份认证平台怎么落地?11 个异构业务系统接入 ASP 的完整实施路径
数据库·servlet·架构·kubernetes·jenkins
明月_清风17 小时前
🚀 OpenAI 数据代理架构全解析:从 600 PB 到自然语言的六层上下文工程
前端·后端·架构