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 表示账号体系。系统中可以同时存在 user、admin、merchant 等多个账号体系,每个体系拥有独立的登录状态、权限数据和会话配置。
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 共享等,下面讲述的是这个配置下的情况
yamlsa-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=satoken 和 login-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 主要有两个作用:
- 保存账号级共享数据;
- 管理该账号当前登录的所有终端。
例如,可以在 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-A 和 Token-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@SaCheckRoleStpUtil.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 最后活跃时间
登录校验通过
更新最后活跃时间
普通登录校验主要依赖:
- Token 与 LoginId 的映射;
- Token 的有效期;
- 开启
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) 可以分成两部分:
- 在服务端创建或更新登录会话;
- 将 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 注入客户端环境
具体过程如下:
- 校验
LoginId是否为空,以及设备类型、超时时间等登录参数是否合法; - 根据
is-concurrent处理并发登录,根据is-share判断是否复用已有 Token; - 如果没有可复用的 Token,则按照 Token 生成策略创建新的唯一 Token;
- 获取或创建该
LoginId对应的 Account-Session; - 在 Account-Session 中记录设备类型、设备 ID、Token、扩展数据和登录时间;
- 保存
Token → LoginId映射,用于后续根据 Token 恢复用户身份; - 如果开启了 active-timeout,记录当前 Token 的最后活跃时间;
- 如果 Token-Session 已存在,则更新其有效期;如果配置为立即创建,则创建 Token-Session,否则默认暂不创建;
- 发布登录成功事件;
- 检查该账号的登录数量是否超过 max-login-count,超过时处理最早的登录终端;
- 将 Token 写入当前请求上下文,并根据配置写入 Cookie 或响应头。
需要注意,
StpUtil.login()的返回值是 void,它不会直接将 Token 作为方法返回值返回。前后端分离项目通常在登录后主动获取 Token 信息并放入 JSON 响应:
Token 分配不一定是生成新 Token
Token 的来源可能有三种:
- 调用登录方法时手动指定 Token;
is-share=true时复用该账号已有的 Token;- 无法复用时,根据配置生成新的 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 默认采用延迟创建:
- Token-Session 已经存在:登录时为其续期;
- 配置为立即创建:登录时直接创建;
- 默认情况:第一次调用
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 已冻结
是否开启自动续签
更新最后活跃时间
完整校验过程可以概括为:
- 从当前请求中读取 Token;
- 配置了
token-prefix时,校验并去掉 Token 前缀; - 如果没有读取到 Token,抛出"未提交 Token"异常;
- 根据 Token 查询
Token → LoginId映射; - 如果映射不存在,说明 Token 无效;
- 如果映射值是特殊状态标记,则分别判断 Token 是否过期、被顶下线或被踢下线;
- 如果得到正常 LoginId,并且开启了
active-timeout,则检查 Token 是否超过最大空闲时间; - Token 未冻结且开启自动续签时,更新最后活跃时间;
- 最终返回 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
其中:
Authorization由token-name决定;Bearer由token-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,攻击者还可能伪造身份请求头。因此应同时做到:
- Gateway 先删除客户端提交的用户身份请求头,再写入经过认证的真实身份;
- 下游服务只部署在内网,不能直接暴露公网;
- 服务之间通过 Same-Token、mTLS、内部签名或 OAuth2 Client Credentials 验证调用方身份;
- 订单归属、数据范围等资源级权限仍应由业务服务校验。
#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 方案通常需要配置 HttpOnly、Secure 和合适的 SameSite,并根据业务增加 CSRF Token 或 Origin 校验。不能简单说 LocalStorage 一定不安全、Cookie 一定安全,两者面对的主要风险不同。
生产环境还应做到:
- 全链路 HTTPS;
- 不把 Token 放在 URL 中;
- 不在日志中打印完整 Token;
- 登录接口按 IP、账号、设备进行组合限流;
- 修改密码、支付、提现等高风险操作增加二次认证;
- Redis 故障时采用 Fail Closed,无法确认身份就拒绝请求;
- 不使用永久有效 Token;
- 权限必须由服务端校验,前端隐藏按钮不等于权限控制。
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 构建的有状态会话与权限认证模型。