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

📅 发布时间:2026/10/16 18:01:20
Token放请求头的正确姿势:Authorization与Bearer详解
对接第三方接口或者自己公司后端接口的时候登录接口成功会返回一个token后面再请求业务接口就得把这个token带上。我第一次做这个的时候以为随便放到请求参数里就行结果后端一直返回401后来才知道token要放在请求头Header里而且还有一套严格的格式要求。这个问题看似简单但几乎每过一段时间就会有人踩一遍本文就把我在实际对接中积累的经验整理出来覆盖原理、格式、多语言代码示例、踩坑记录和安全实践希望看完你能直接照着写。1. 先搞清楚为什么token要放在请求头里1.1 HTTP是“无状态”的服务器怎么认出你是谁先说一个最基本的概念HTTP协议本身是“无状态”的。意思是同一个客户端连续发多次HTTP请求服务器单从协议层面看不出这些请求是来自同一个人或者同一个客户端。服务端默认把每个请求当作独立的请求来对待既不会记得你上一次登录过也不会记得你的身份。这就带来一个现实问题登录的目的是让服务端知道“你是谁”但HTTP本身又没有记忆。于是各类认证机制就出现了。常见的做法是登录成功之后由服务端生成一串凭证这串凭证就代表“你已经登录过的这个身份”。客户端在之后每次请求时把这串凭证带上服务端通过校验这串凭证就知道这个请求对应的用户是谁、权限范围是什么。这个凭证可以是sessionId也可以是token我们现在讨论的token就是其中一种主流方案。所以核心逻辑是token不是“放在请求里”这么简单而是要通过一种服务端能识别、解析的方式传递。请求头就是专门干这个的。相比放在URL参数里或Body里请求头在语义上更合适也更符合被广泛接受的标准。1.2 三种常见认证方式对比出请求头方案的优势实际开发中我见到过的认证方式大致有三类。第一类是CookieSession。登录后服务端下发一个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项目里用得最多大家也相对熟悉。先看原生fetchconst 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本身绝对不能动。再看axiosimport 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 Pythonrequests库与urllibPython里最爽的就是requests库代码很简单import requests token your-token headers { Authorization: Bearer token } resp requests.get(https://api.example.com/data, headersheaders) 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 JavaOKHttp、RestTemplate、原生HttpURLConnectionJava生态里我实际用过的有三种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); HttpEntityString entity new HttpEntity(headers); ResponseEntityString 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本身不建议出现多个值。命令行里调试最快的是curlcurl -H Authorization: Bearer your-token \ https://api.example.com/data如果按了POST并要带表单数据可以这样curl -X POST -H Authorization: Bearer your-token \ -d keyvalue \ 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 调试阶段的排查步骤我总结一个比较通用的排查流程在接口文档或后端确认token应该放在哪个头、用什么格式。先用curl手动请求一次把token原样放进去确认接口本身没问题。再用Postman或Apifox模拟一次顺便看看响应和请求。最后回到代码里实现写完立刻在日志或Network面板里打印最终请求头。如果线上环境有问题一定要确认token的来源、过期时间以及SSL证书是否正常。这五步走完绝大多数请求头token相关的问题都能定位。5. 安全视角如何让token在请求头里更稳妥5.1 HTTPS是底线请求头里的token是明文可见的。如果用的是HTTP而不是HTTPStoken在网络传输过程中相当于裸奔任何中间节点只要能看到流量就能拿到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的代码而是你对“服务端期望的格式”“请求链路上哪里会丢”“安全上哪里会漏”这三个维度的理解。把这三点想清楚写代码就是几分钟的事。