ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Token放请求头的正确姿势:Authorization与Bearer详解

Token放请求头的正确姿势:Authorization与Bearer详解

对接第三方接口或者自己公司后端接口的时候,登录接口成功会返回一个token,后面再请求业务接口就得把这个token带上。我第一次做这个的时候,以为随便放到请求参数里就行,结果后端一直返回401,后来才知道token要放在请求头(Header)里,而且还有一套严格的格式要求。这个问题看似简单,但几乎每过一段时间就会有人踩一遍,本文就把我在实际对接中积累的经验整理出来,覆盖原理、格式、多语言代码示例、踩坑记录和安全实践,希望看完你能直接照着写。

1. 先搞清楚为什么token要放在请求头里

1.1 HTTP是“无状态”的,服务器怎么认出你是谁

先说一个最基本的概念:HTTP协议本身是“无状态”的。意思是同一个客户端连续发多次HTTP请求,服务器单从协议层面看不出这些请求是来自同一个人或者同一个客户端。服务端默认把每个请求当作独立的请求来对待,既不会记得你上一次登录过,也不会记得你的身份。

这就带来一个现实问题:登录的目的是让服务端知道“你是谁”,但HTTP本身又没有记忆。于是各类认证机制就出现了。

常见的做法是,登录成功之后,由服务端生成一串凭证,这串凭证就代表“你已经登录过的这个身份”。客户端在之后每次请求时把这串凭证带上,服务端通过校验这串凭证就知道这个请求对应的用户是谁、权限范围是什么。这个凭证可以是sessionId,也可以是token,我们现在讨论的token就是其中一种主流方案。

所以核心逻辑是:token不是“放在请求里”这么简单,而是要通过一种服务端能识别、解析的方式传递。请求头就是专门干这个的。相比放在URL参数里或Body里,请求头在语义上更合适,也更符合被广泛接受的标准。

1.2 三种常见认证方式,对比出请求头方案的优势

实际开发中我见到过的认证方式大致有三类。

第一类是Cookie+Session。登录后服务端下发一个sessionId写在Set-Cookie里,浏览器自动保存,后续请求自动带上Cookie。这个方案在传统网页应用里很常见,优点是对前端透明,缺点是存在CSRF风险;如果是前后端分离、又涉及到App或者小程序,Cookie这套就没那么顺手了。

第二类是Basic Auth,也就是把用户名密码做Base64编码后放到Authorization头里,形如Authorization: Basic dXNlcjpwYXNzd29yZA==。这种方式虽然也放在请求头,但基本等于把账号密码一直挂在请求上,适合内部工具或简单的临时场景,生产环境一般不用来当主认证。

第三类是Token认证,也就是我们说的Bearer Token,登录后拿到一个token,后续请求在Authorization头里写成Authorization: Bearer <token>。它跟Basic Auth的核心区别在于,头里放的不是账号密码,而是服务端签发的凭证;凭证丢失或者过期可以单独吊销,不牵动用户密码,权限也可以做得更细。

所以请求头+Token这个组合之所以流行,不是偶然的。它既绕开了Cookie的浏览器绑定和CSRF问题,也比Basic Auth更安全、更灵活。

1.3 请求头带token的本质

token本质上是一段被服务端认可的凭证,但它自身不会告诉服务器“我是谁”,而是要由服务端去解析和校验。也就是说,把token放到请求头里,只是完成了“传递”这一步;真正让token生效的,是服务端读取请求头中的对应字段、校验签名、过期时间、权限范围,然后决定放行还是拒绝。

这里有个容易混淆的点:token放在请求头里的位置、字段名、值的格式,不是随便定一个就行的。比如你可以写Authorization: Bearer xxxx,也可以写X-Token: xxxx,但你的服务端必须按同一套约定去解析。如果你只是跟着感觉写个自定义的header,而后端根本不认,那结果就是401。

因此,在写代码之前,最重要的事情是搞清楚服务端到底期望的是什么格式。这恰恰是很多新手最容易忽略的点。

2. 请求头token的标准格式与服务端匹配逻辑

2.1 Authorization头与Bearer Token,标准是怎么来的

服务端期望的token位置,最标准的就是HTTP的Authorization头。配合OAuth 2.0等认证框架时,常见的格式是:

Authorization: Bearer <token>

其中Bearer是一个固定的类型标识,意思是“持有者凭证”,后面跟一个空格,再跟token字符串。服务端解析的时候,会把Authorization的值先按空格拆开,看前半部分是不是Bearer,如果不是就会拒绝;后半部分再作为token去校验。

这个写法来自RFC 6750,属于Bearer Token的规范用法。虽然平时我们不需要背RFC,但知道来龙去脉有助于理解服务端的解析逻辑。在使用Axios、requests、OKHttp这些库的时候,你只需要把Authorization的值设置成字符串"Bearer " + token。

还要提醒一下,很多服务端在实现时要求Authorization必须是标准的Bearer写法,但也有的服务端会偷懒,直接取整个Authorization头的值当作token,或者要求你写Token <token>这种老式前缀。所以在没有统一标准的情况下,接口文档或后端定义就是你的“标准”。文档怎么写,代码就怎么写,实务大于理论。

2.2 自定义头方案什么时候用,以及坑在哪

除了Authorization,不少系统也会用自定义请求头,比如X-Auth-Token、X-Token、Access-Token。这些自定义头不是不能用,我见过很多老旧系统、内网接口就是靠X-Auth-Token这类字段认身份的。

但自定义头有个明显的坑:一旦你的前端页面是浏览器环境,并且和后端不在同一个源(跨域),只要请求头里带了非标准的自定义头,浏览器就会先发一个OPTIONS预检请求去试探服务端允不允许。预检请求一般不带业务参数,服务端如果没处理好,就会直接拦掉,导致后续真正的请求根本发不出去。这个问题在纯后端程序、App、命令行场景里不存在,但浏览器里很常见。

另外,自定义头没有一个统一规范,不同团队可能叫法完全不同,交接或者对接第三方的时候特别容易踩到字段名不一致的坑。相比之下,Authorization和Bearer是广泛承认的行业约定,出问题的概率小很多。所以我的建议是:只要服务端没有强制规定必须用自定义头,就优先用Authorization: Bearer。

2.3 动手前先看接口文档,这决定了你写哪一行

这里讲一个我自己的习惯:拿到任何接口的对接任务,第一步不是打开代码编辑器,而是先把接口文档翻到底,找到“认证方式”这一节。文档里一般会写清楚三种信息:token放在哪个头里、值的完整格式是什么、过期时间怎么处理。

如果文档写得不清楚,可以看服务端示例代码;示例代码也没有,就直接问后端的同事。因为这个问题只有后端最清楚,他看一眼自己的过滤器或者拦截器就知道。绝大多数人写“token怎么加请求头”出问题,问题不是不会写代码,而是没有对齐服务端的期望。

我见过一个A同学接手某个项目,文档上写的是“token放在header中”,他就真的写了个自定义头X-Token传过去,后端却在读取Authorization头,两个人都觉得自己没问题,结果排查了好几个小时。这种低级却高消耗的问题,几乎都源于没有先确认格式。

3. 实操:主流语言/工具里把token加到请求头

下面我按不同使用场景,把常见的写法整理出来。每种写法的关键点不在代码本身,而在当你发现请求失败时,知道自己应该去检查哪些细节。

3.1 前端JavaScript:原生Fetch、axios、XHR

前端场景里,最常见的三个手段就是原生fetch、axios和XMLHttpRequest。其中axios因为在Vue、React项目里用得最多,大家也相对熟悉。

先看原生fetch:

const token = "your-token"; fetch("https://api.example.com/data", { method: "GET", headers: { "Authorization": "Bearer " + token } }) .then(res => res.json()) .then(data => console.log(data));

注意点:headers里面header名的写法,HTTP规范本身是不区分大小写的,所以写成authorization也可以,但要保持值部分的大小写精确,尤其是token本身,绝对不能动。

再看axios:

import axios from "axios"; const token = "your-token"; axios.get("https://api.example.com/data", { headers: { "Authorization": `Bearer ${token}` } });

axios使用中有一个容易踩的细节:如果你在请求拦截器里统一设置头,那么要注意拦截器执行顺序和token获取时机。很多人把token存在localStorage里,在拦截器里取出,再挂到请求头上,这是通用做法。如果遇到某个请求里token没挂上或者挂成undefined,先检查拦截器里取token时异步操作是不是还没完成。

XMLHttpRequest虽然现在用得少,但老项目里还有:

const xhr = new XMLHttpRequest(); xhr.open("GET", "https://api.example.com/data"); xhr.setRequestHeader("Authorization", "Bearer " + token); xhr.send();

3.2 Python:requests库与urllib

Python里最爽的就是requests库,代码很简单:

import requests token = "your-token" headers = { "Authorization": "Bearer " + token } resp = requests.get("https://api.example.com/data", headers=headers) print(resp.json())

因为这行代码太简单了,很多人反而会漏掉headers参数和headers参数的数据类型。注意headers必须是一个字典,键是header名称,值是header值。requests库在发送时会自动把字典转成HTTP请求头。

有些项目不允许引入第三方库,只能用标准库urllib,写法会稍微繁琐一点:

from urllib.request import Request, urlopen req = Request( "https://api.example.com/data", headers={ "Authorization": "Bearer your-token" } ) resp = urlopen(req) print(resp.read().decode("utf-8"))

注意urllib的Request对象在构造时传入的headers,在发送请求时会作为请求头的初始值。如果你后面还想再追加其他头,可以用req.add_header("X-Requested-With", "XMLHttpRequest")。

3.3 Java:OKHttp、RestTemplate、原生HttpURLConnection

Java生态里我实际用过的有三种:OKHttp、Spring的RestTemplate、以及原生HttpURLConnection。

OKHttp是很多后端中间件和Android项目里常用的HTTP客户端,加token的典型写法是:

OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://api.example.com/data") .header("Authorization", "Bearer " + token) .build(); Response response = client.newCall(request).execute();

这里推荐用.header()而不是.addHeader()。区别在于:header()会覆盖同名字段,addHeader()会追加一个额外字段。如果你的代码在别的地方已经设置过Authorization,用addHeader会导致出现两个Authorization头,服务端校验时通常只会取其中一个,行为比较难预测。

RestTemplate在Spring生态里很常用:

RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(token); // 等价于 headers.set("Authorization", "Bearer " + token); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<String> response = restTemplate.exchange( "https://api.example.com/data", HttpMethod.GET, entity, String.class );

这里有一个进阶细节:RestTemplate的exchange方法接收的是HttpEntity实例,所以请求头要封装在HttpEntity里,而不是直接塞进exchange参数。很多人第一次写会忘了这层封装,导致请求头根本没发出去。

原生HttpURLConnection比较老,但兼容性最好:

URL url = new URL("https://api.example.com/data"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Authorization", "Bearer " + token); conn.connect(); int code = conn.getResponseCode();

3.4 Go、curl与其他调试工具

Go的标准库net/http,代码如下:

req, _ := http.NewRequest("GET", "https://api.example.com/data", nil) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close()

注意req.Header.Set和req.Header.Add的区别,跟Java里header/addHeader的差异一个道理:Set会覆盖,Add会追加。同一个key出现多个值时,Go会把它们合并成用逗号分隔的字符串,Authorization本身不建议出现多个值。

命令行里调试最快的是curl:

curl -H "Authorization: Bearer your-token" \ https://api.example.com/data

如果按了POST并要带表单数据,可以这样:

curl -X POST -H "Authorization: Bearer your-token" \ -d "key=value" \ https://api.example.com/submit

用curl调试的好处是,你可以先确认请求头写对了、接口逻辑没问题,再回过去排查业务代码。我调试接口时习惯先用curl或者其他工具验证一遍,再动代码。

另外,Postman这类可视化工具非常方便。操作步骤是在Headers标签页里新增一行,左边key填Authorization,右边value填Bearer your-token。Apifox也类似。这类工具的价值在于,它们的界面直接展示了最终发出去的HTTP报文,你能很直观地看到你的请求头到底长什么样,这对排查问题特别有帮助。

3.5 统一封装:项目里建议把token设置收敛到一个地方

在真实项目里,我不建议在每个请求方法里都手动写headers: { Authorization: Bearer ... },那样一旦token获取方式变了,或者要加统一的超时处理、日志处理,你就得改无数个文件。

更好的做法是统一封装。前端用axios就在拦截器里统一设置;后端用RestTemplate就写一个拦截器或者用ClientHttpRequestInterceptor统一往请求头里塞token;中间用网关的地方,甚至可以让网关统一把token从用户请求里解析出来再转发给下游。这样做的最大好处是:认证逻辑只出现一次,后续出问题时定位范围也小。

4. 实战踩坑记录:请求头token最常见的问题

4.1 401错误排查顺序

遇到401 Unauthorized,我建议按这个顺序排查。

第一步,确认token有没有真的过期。token一般有时效,比如2小时、24小时,过期后服务端直接返回401。这时候重新登录拿新token再试一次即可。

第二步,确认Authorization的值格式是否正确。常见错误就是漏了"Bearer "前缀,直接把token字符串塞进Authorization;或者是前缀大小写写错,写成了bearer;或者是多了空格、少了空格。

第三步,确认代码里设置的请求头真的发出去了。用浏览器F12的Network面板,或者抓包工具看一下实际请求的Request Headers部分,有没有你设置的那一行。这一步最直接,很多问题看到真实报文就真相大白了。

第四步,确认服务端读取的是哪个头。有的后端读Authorization,有的读X-Auth-Token,两边没对齐,就会一直401。这个只能通过查文档或问后端来确认。

4.2 跨域CORS预检带来的二次请求

这是个非常典型的浏览器侧问题。当你的页面和接口不在同一个源时,浏览器会先发一个OPTIONS预检请求,目的是询问服务端是否允许这个跨域请求。如果请求头里带了自定义头,比如X-Auth-Token,预检请求就一定会触发;Authorization这个标准头在某些情况下也会触发预检,但整体上比自定义头更稳。

预检请求通常不会带真正的Authorization头,服务端如果对OPTIONS请求也强制校验认证,就会返回401或者403,导致真正的业务请求被浏览器拦截。遇到这种情况,需要后端在网关或过滤器中放行OPTIONS请求,并且正确返回Access-Control-Allow-Origin、Access-Control-Allow-Headers这些跨域响应头。

在本地开发阶段,这个问题很容易被误认为“token没带上”,尤其是当你在Network面板里看到发出去的第一个请求是OPTIONS并且返回失败时,不要慌,先判断这是不是预检请求。

4.3 大小写、空格、引号的隐藏陷阱

HTTP的header名称在规范上不区分大小写,也就是Authorization、authorization、AUTHORIZATION可以被服务端同等对待。但header的值是区分大小写的,token本身必须原样传递,多一个空格、少一个空格都不行。

有一种情况要特别小心:代码里写成"Bearer" + token,中间没有空格,结果变成"Bearertokenxxx",这肯定解析失败。还有的配置管理软件会在环境变量里把token前后多加了一个换行符,程序取出来再拼进Authorization,肉眼看不出来,但服务端一校验就挂了。所以如果你怎么改都不对,试着打印一下最终拼好的Authorization值,用repr或者JSON.stringify看有没有不可见字符。

另外还要注意,某些HTTP客户端库会默认对header值做trim操作,而有些不会。如果你依赖的是自己手动拼字符串,最好先在文档里确认这个库的行为。

4.4 调试阶段的排查步骤

我总结一个比较通用的排查流程:

  1. 在接口文档或后端确认,token应该放在哪个头、用什么格式。
  2. 先用curl手动请求一次,把token原样放进去,确认接口本身没问题。
  3. 再用Postman或Apifox模拟一次,顺便看看响应和请求。
  4. 最后回到代码里实现,写完立刻在日志或Network面板里打印最终请求头。
  5. 如果线上环境有问题,一定要确认token的来源、过期时间,以及SSL证书是否正常。

这五步走完,绝大多数请求头token相关的问题都能定位。

5. 安全视角:如何让token在请求头里更稳妥

5.1 HTTPS是底线

请求头里的token是明文可见的。如果用的是HTTP而不是HTTPS,token在网络传输过程中相当于裸奔,任何中间节点只要能看到流量,就能拿到token,然后冒充你调接口。所以生产环境中,只要涉及token,传输层必须走HTTPS。这不是建议,是底线。

有些地方觉得内网系统无所谓,就走HTTP,但内网一样有被劫持或误操作的风险。token一旦泄露,就相当于把钥匙挂在了门边。

5.2 日志、埋点、第三方监控里务必脱敏

这个坑很多人踩过:为了排查问题,在代码里把完整请求头打印到日志,结果把Authorization值里的token也打出来了,然后引到日志管理系统或第三方监控里,等于把钥匙交给了运维平台和监控厂商。这本身不一定马上出事,但日志文件被拷贝、备份、误发布时,token就跟着散出去了。

我的建议是:在日志输出前,对Authorization头做脱敏处理,只保留前几位和最后几位,中间用星号代替,或者直接用***代替整个token。同理,错误上报、接口监控、全链路追踪系统里也要过滤掉这类敏感字段。

5.3 过期续期与主动失效

最后再讲一下token的生命周期。token一般有有效期,到了时间服务端就会拒绝。客户端需要做的通常有两件事。

第一,在收到401时,不要简单报错,而是判断是不是token过期了,主动走刷新流程。刷新通常有一个refresh_token,用它去换新的access_token,换完之后再重放原请求。

第二,用户退出登录时,客户端要主动清除本地的token,避免token停留在浏览器缓存、本地存储或者App的持久化空间里。虽然服务端也可以把token拉黑,但本地清理是客户端自己的责任,双管齐下才稳妥。

这里我个人的体会是,token放在请求头里这件事,真正考验的不是你会不会写那一行设置header的代码,而是你对“服务端期望的格式”“请求链路上哪里会丢”“安全上哪里会漏”这三个维度的理解。把这三点想清楚,写代码就是几分钟的事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进