Sa-Token 前后端分离鉴权实战:无 Cookie 模式下 Token 的下发、存储与提交
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
在 App、小程序、以及前后端分离的 Web 场景中,终端往往不支持 Cookie 或不便依赖 Cookie 自动携带凭证,这使得传统"后端写 Cookie、浏览器自动带"的鉴权方式失效。本文以 Sa-Token 官方文档 前后端分离(无Cookie模式) 为骨架,结合核心源码与官方示例,完整讲解后端如何把 Token 下发到前端、前端如何存储与提交 Token、后端如何再次读取 Token的整套闭环,并给出可直接落地的 uni-app 请求封装方案。读完本文,你将能在不依赖 Cookie 的任意终端(App、小程序、分离式 Web、Hybrid)上完整跑通 Sa-Token 的登录与鉴权流程。
一、何为"无 Cookie 模式"?
无 Cookie 模式,特指不支持 Cookie 功能的终端场景,通俗来讲就是我们常说的前后端分离模式。
常规 Web 端鉴权,一般由Cookie 模式完成。Cookie 有两个关键特性:
- 可由后端控制写入:后端在响应中下发
Set-Cookie,浏览器自动保存; - 每次请求自动提交:浏览器在同域请求中自动携带 Cookie,前端无需任何代码参与。
正是这两个特性,使得传统 PC 端在前端零代码介入的情况下,就能完成鉴权的全部流程(整个流程均由后端控制)。
而在 App、小程序等前后端分离场景中,一般没有 Cookie 这一功能。此时如何鉴权?见招拆招,答案其实很简单,只需要把 Cookie 的两个特性"手动化":
| Cookie 特性 | 分离模式下对应的解法 | 核心难点 |
|---|---|---|
| 后端控制写入 | 前端自己写入(存储到本地) | 后端如何将 Token 传递到前端 |
| 请求自动提交 | 前端手动提交(塞进 Header) | 前端如何提交 Token,以及后端如何将其读取出来 |
整个无 Cookie 模式的实现,就是围绕上面两个难点展开的。
二、后端将 Token 返回到前端
2.1 两步核心操作
- 首先调用
StpUtil.login(id)完成登录; - 调用
StpUtil.getTokenInfo()获取当前会话的 Token 详细参数。
getTokenInfo()返回一个SaTokenInfo对象,其中有两个关键属性:tokenName和tokenValue(即Token 的名称和Token 的值)。将此对象返回给前端,让前端把这两个值保存到本地即可。
2.2 登录接口代码示例
// 登录接口 @RequestMapping("doLogin") public SaResult doLogin() { // 第1步,先登录上 StpUtil.login(10001); // 第2步,获取 Token 相关参数 SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); // 第3步,返回给前端 return SaResult.data(tokenInfo); }仓库中的官方示例 NotCookieController.java 给出了更完整的对比写法:doLogin是前后端一体模式(登录后仅返回SaResult.ok(),Token 由后端写入 Cookie),而doLogin2是前后端分离模式——登录后通过SaResult.data(tokenInfo)把SaTokenInfo整体塞进响应体返回给前端。两者的差别,正是是否主动下发 Token 信息。
2.3 SaTokenInfo 对象各字段详解
SaTokenInfo定义于 SaTokenInfo.java,它描述了一个 Token 的全部常见参数。以源码中的 Javadoc 示例为参照,一次典型的返回结构如下:
{ "tokenName": "satoken", "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", "isLogin": true, "loginId": "10001", "loginType": "login", "tokenTimeout": 2591977, "sessionTimeout": 2591977, "tokenSessionTimeout": -2, "tokenActiveTimeout": -1, "loginDeviceType": "DEF" }各字段含义(依据源码字段定义与注释):
| 字段 | 含义 | 说明 |
|---|---|---|
tokenName | Token 名称 | 即全局配置项token-name,默认值为satoken |
tokenValue | Token 值 | 当前会话的 Token 值 |
isLogin | 此 Token 是否已登录 | true/false |
loginId | 此 Token 对应的账号 ID | 未登录时为null |
loginType | 账号类型标识 | 多账号体系下区分不同账号体系,默认login |
tokenTimeout | Token 剩余有效期(秒) | -1代表永久有效,-2代表值不存在 |
sessionTimeout | Account-Session 剩余有效时间(秒) | 同上约定 |
tokenSessionTimeout | Token-Session 剩余有效时间(秒) | -2表示系统中不存在这个缓存 |
tokenActiveTimeout | Token 距离被冻结还剩多少时间(秒) | 即无操作冻结机制的剩余活跃时间 |
loginDeviceType | 登录设备类型 | 默认DEF,可配合"同端互斥登录"使用 |
提示:前端真正必需保存的只有
tokenName与tokenValue两个字段,其余字段可用于展示登录状态、剩余有效期等信息。
三、前端将 Token 提交到后端
无论是 App 还是小程序,Token 的传递方式都大同小异:将 Token 塞到请求的Header里,格式为{tokenName: tokenValue}。
以经典跨端框架uni-app为例,官方文档提供了两种实现方式。
3.1 方式一:简单粗暴(只存 tokenValue)
把tokenValue存到本地,发起请求时硬编码 header 参数名(注意此处参数名是satoken,与默认tokenName保持一致):
// 1、首先在登录时,将 tokenValue 存储在本地,例如: uni.setStorageSync('tokenValue', tokenValue); // 2、在发起ajax请求的地方,获取这个值,并塞到header里 uni.request({ url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 header: { "content-type": "application/x-www-form-urlencoded", "satoken": uni.getStorageSync('tokenValue') // ⚠️ 关键代码, 注意参数名字是 satoken }, success: (res) => { console.log(res.data); } });3.2 方式二:更加灵活(tokenName 与 tokenValue 一起存)
把tokenName和tokenValue都存入本地,发起请求时动态组装 header,header 参数名完全跟随后端配置,更通用、更不易出错:
// 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如: uni.setStorageSync('tokenName', tokenName); uni.setStorageSync('tokenValue', tokenValue); // 2、在发起ajax的地方,获取这两个值, 并组织到head里 var tokenName = uni.getStorageSync('tokenName'); // 从本地缓存读取tokenName值 var tokenValue = uni.getStorageSync('tokenValue'); // 从本地缓存读取tokenValue值 var header = { "content-type": "application/x-www-form-urlencoded" }; if (tokenName != undefined && tokenName != '') { header[tokenName] = tokenValue; } // 3、后续在发起请求时将 header 对象塞到请求头部 uni.request({ url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 header: header, success: (res) => { console.log(res.data); } });只要按照上述方式将 Token 值传递到后端,Sa-Token 就能像传统 PC 端一样自动读取到 Token 值,正常完成鉴权。
你可能会问:难道每个 ajax 都要写这么一坨?岂不是麻烦死了?——当然不能每个 ajax 都写一遍,这种重复性代码应当封装在一个统一的请求函数里,例如封装
request(options)工具函数,在函数内部统一注入 header,业务页面只关心业务参数即可。
四、后端如何读取前端提交的 Token:源码级解读
前端提交 Token 后,Sa-Token 是怎么把它"找"出来的?这决定了前端的提交方式必须与后端的读取逻辑严格对齐。
4.1 Token 的读取顺序
从源码 StpLogic.java 的getTokenValueNotCut()方法可以看到,后端读取 Token 时按以下顺序依次尝试:
- 先尝试从 Storage 存储器里读取:即本次请求中登录动作刚刚创建、暂存在请求级缓存里的 Token(解决"登录接口立即取 Token"的场景);
- 再尝试从请求体 / URL 参数里读取:当配置项
isReadBody开启时,调用request.getParam(tokenName)从 Query 参数与表单体读取(例如GET /user/getInfo?satoken=xxx); - 再尝试从 Header 头里读取:当配置项
isReadHeader开启时,调用request.getHeader(tokenName)读取,即本文第三章前端所采用的方式; - 最后尝试从 Cookie 里读取:当配置项
isReadCookie开启时,调用request.getCookieValue(tokenName)读取(兼容传统 PC 端场景)。
只要在前面的步骤中读到了值,就不再继续向后尝试。官方示例 LoginAuthController.java 中的注释也印证了这一读取顺序:Query 参数 → Header 头 → Cookie,且明确说明"以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token"。
4.2 关键配置项
上述读取行为由全局配置控制,配置项定义于 SaTokenConfig.java:
| 配置项 | 默认值 | 含义 |
|---|---|---|
token-name | satoken | Token 名称,同时也是 Cookie 名称、提交 Token 时参数的名称、存储 Token 时的 key 前缀 |
is-read-body | true | 是否尝试从请求体里读取 Token |
is-read-header | true | 是否尝试从 header 里读取 Token |
is-read-cookie | true | 是否尝试从 cookie 里读取 Token |
因此前端 Header 中的参数名必须与token-name保持一致(默认satoken),后端才能读得到。这也是第三章"方式二"比"方式一"更推荐的原因:它从后端返回的tokenName动态取值,后端一旦修改token-name配置,前端无需改动代码。
4.3 响应头方式:另一种"后端下发"的变体
除了"登录接口返回SaTokenInfo"这种最常见的下发方式,源码还提供了setTokenValueToResponseHeader()方法(见 StpLogic.java):登录后将 Token 写入当前请求的响应头中,同时会自动添加Access-Control-Expose-Headers: tokenName响应头,否则跨域场景下前端 JS 无法读取到该自定义响应头。这种方式适合登录接口不方便改返回结构、希望通过响应头透传 Token 的团队,可结合项目实际情况选用。
五、其它解决方案:手动模拟 Cookie
如果你对 Cookie 非常了解,就会明白一个本质:所谓 Cookie,本质上就是一个特殊的 Header 参数而已(即Cookie: name=value)。
既然它只是一个 Header 参数,我们就能手动模拟实现它:前端自行维护一个"类 Cookie"机制(如把 Token 存入本地存储,并在每次请求时以Cookie头或自定义头的方式带上),从而在不依赖浏览器 Cookie 的前提下完成鉴权闭环。这其实是"无 Cookie 模式"的另一种通用解法,其思路与第三章的 Header 提交方案殊途同归,感兴趣的同学可以进一步研究 Cookie 的规范细节,在此不再赘述。
六、小结与更多延伸
至此,无 Cookie 模式下的完整闭环已经清晰:
- 下发:登录后调用
StpUtil.getTokenInfo(),把tokenName+tokenValue返回给前端保存; - 存储:前端将这两个值存入本地存储(如 uni-app 的
setStorageSync); - 提交:前端在统一请求封装中,将
{tokenName: tokenValue}塞入请求 Header; - 读取:Sa-Token 按 Storage → 请求体/Query → Header → Cookie 的顺序自动识别前端提交的 Token,完成鉴权。
在此基础上,还可以继续探索更进阶的配套能力(均为本仓库内官方文档):
- 登录认证详解:
StpUtil.login()的完整登录流程与多账号体系; - 框架配置:
token-name、is-read-header等全部配置项说明; - Token 前缀:如
Bearer等前缀模式下,前后端如何正确提交与裁剪 Token; - NotCookieController.java 完整示例:可直接运行的前后端分离登录样例(运行后可访问
http://localhost:8081/NotCookie/doLogin2?name=zhang&pwd=123456观察返回的SaTokenInfo结构)。
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考