401鉴权问题完全指南:从“门卫拦人“到“畅通无阻“

401鉴权问题完全指南:从“门卫拦人“到“畅通无阻“

401鉴权问题完全指南:从"门卫拦人"到"畅通无阻"

一句话总结 :不同域名服务之间的401问题,本质是**"身份证在跨区通行时失效了"。最佳解法是在API网关层**统一处理,让业务服务"无感通行"。

一、先搞懂:401到底是什么?

1.1 401 vs 403:别再把"没进门"和"没权限"搞混了

状态码

通俗解释

谁的问题

该谁处理

401 Unauthorized

"请出示证件!"------你没带身份证,或身份证是假的/过期的

认证问题(你是谁?)

网关层统一处理

403 Forbidden

"证件是真的,但你不准进这个房间"

授权问题(你能做什么?)

应用层业务处理

关键区别:401是"不认识你",403是"认识你不让你进"。

1.2 401的底层逻辑

复制代码

┌─────────┐ 请求带凭证 ┌─────────┐ 校验凭证 ┌─────────┐

│ 客户端 │ ───────────────→ │ 服务端 │ ─────────────→ │ 认证中心 │

│ │ │ │ │ │

│ │ ←──── 401 ─────── │ │ ←── 凭证无效 ─── │ │

└─────────┘ "我不认识你" └─────────┘ └─────────┘

服务端返回401,只有三种可能:

没带凭证(请求头里没有Token/Cookie)

凭证格式错误 (有Token但拼写错了、漏了Bearer 前缀)

凭证无效(过期了、被吊销了、签名对不上)

二、为什么多域名场景下401特别频发?

2.1 浏览器的"安全洁癖"

浏览器有个核心安全机制叫同源策略(Same-Origin Policy):

同源 = 协议 + 域名 + 端口 完全相同

前端页面

请求API

是否同源

结果

https://a.com

https://a.com/api

✅ 同源

正常通行

https://a.com

https://b.com/api

❌ 跨域

被浏览器拦截或限制

https://a.com

https://api.a.com

❌ 跨域(子域不同)

被浏览器拦截或限制

跨域时浏览器会做什么?

复制代码

前端(a.com) ──→ 请求API(b.com)

浏览器:"等等!这是跨域请求!"

├── 先发送 OPTIONS 预检请求(不带凭证)

│ └── 服务端:"我允许a.com访问,允许带凭证"

└── 再发送真实请求(此时才带Token)

└── 但如果CORS配置错了,Token根本发不出去!

2.2 凭证的"地域限制"

Cookie的域限制

javascript

复制代码

// 在 a.com 登录后,服务端设置Cookie

document.cookie = "token=abc123; domain=.a.com; path=/";

// ↑ 关键!必须显式设置 domain

// 如果不设置domain,默认只在当前域名生效,子域读不到!

Cookie配置

a.com能否读取

api.a.com能否读取

b.com能否读取

无domain(默认)

domain=.a.com

domain=.parent.com

✅(如果b.com是子域)

localStorage/sessionStorage 的隔离

javascript

复制代码

// 在 a.com 执行

localStorage.setItem('token', 'abc123');

// 在 b.com 执行

localStorage.getItem('token'); // null!完全读不到!

结论 :localStorage是严格同源的,不同域名之间完全隔离,无法共享。

2.3 Token本身的"生命周期"

复制代码

Token生命周期:

签发 ──→ 有效 ──→ 即将过期 ──→ 过期 ──→ 被吊销

│ │ │ │ │

│ │ │ │ └── 加入Redis黑名单

│ │ │ └────────────── 返回401,需刷新

│ │ └────────────────────────── 提前刷新(推荐)

│ └────────────────────────────────────── 正常使用

└──────────────────────────────────────────────── 登录时签发

三、四层架构:每层能做什么?

3.1 各层能力总览

层级

典型组件

能否处理401

主要职责

处理401的优缺点

网络层

Nginx、F5、ELB、Envoy

❌ 不能

负载均衡、SSL卸载、TCP转发

不感知HTTP状态码,只能做简单header存在性检查

网关层

Spring Cloud Gateway、Kong、APISIX、Traefik

✅ 最佳位置

统一认证、JWT校验、路由、限流

无侵入、集中管理;增加一跳网络延迟

应用层

各业务服务

✅ 可以

业务逻辑、二次鉴权

灵活精细,但重复实现、侵入性强

数据层

MySQL、Redis、ES

❌ 不能

存储认证数据(Token黑名单、权限表)

只提供数据,不处理鉴权逻辑

3.2 网络层:只能"看门",不能"验人"

nginx

复制代码

# Nginx 只能做最简单的存在性检查

location /api/ {

# ❌ 无法解析JWT内容

# ❌ 无法验证签名

# ❌ 无法处理Token刷新

if ($http_authorization = "") {

return 401; # 只能判断"有没有",不能判断"对不对"

}

proxy_pass http://backend;

}

网络层的局限:

只能看到TCP/UDP包,不解析HTTP内容

无法验证JWT签名、过期时间

无法处理复杂的认证逻辑(如OAuth2、OIDC)

3.3 网关层:最推荐的"统一检查站"

为什么网关是最佳位置?

优势

说明

无侵入

业务服务零改动,专注业务逻辑

统一性

所有跨域请求必经网关,一处配置全局生效

解耦

认证逻辑与业务分离,升级不影响业务

可观测

集中记录鉴权日志、监控401频率、统一告警

安全

无效请求在网关层就被拦截,不穿透到业务服务

网关处理跨域认证的完整流程:

复制代码

┌─────────┐ 1.请求带Token ┌─────────┐ 2.校验Token ┌─────────┐

│ 前端A │ ───────────────────→ │ 网关 │ ───────────────────→ │ 认证中心 │

│ (a.com) │ Authorization: │ (网关) │ 验证签名/过期时间 │ (Redis) │

│ │ Bearer xxx │ │ 查黑名单 │ │

└─────────┘ └────┬────┘ └─────────┘

│ 3.校验通过,注入用户信息

┌─────────┐

│ 业务服务 │

│(b.com) │ ← 收到的请求已带X-User-Id

└─────────┘ 无需再处理认证

网关配置示例(Kong/APISIX风格):

yaml

复制代码

# 网关路由配置

routes:

- name: api_route

paths: ["/api/*"]

upstream: "http://backend-service"

plugins:

# 1. CORS插件:解决跨域预检

- name: cors

config:

allow_origins: ["http://a.com", "http://b.com"] # 明确域名,不能用*

allow_credentials: true # 允许携带凭证

allow_headers: ["Authorization", "Content-Type", "X-Request-Id"]

allow_methods: ["GET", "POST", "PUT", "DELETE"]

max_age: 86400 # 预检缓存24小时

# 2. JWT插件:统一校验Token

- name: jwt

config:

secret: "${JWT_SECRET}"

header_names: ["Authorization"]

# 校验失败直接返回401,请求不会转发到业务服务

# 3. 限流插件(防暴力破解)

- name: rate-limiting

config:

minute: 100

policy: redis

# 4. 日志插件(可观测性)

- name: file-log

config:

path: "/var/log/kong/auth.log"

3.4 应用层:做"细粒度权限"的守门员

应用层处理的典型场景:

场景

示例

返回码

资源归属校验

用户A只能看自己的订单

403

动态权限

上班时间可访问,下班不可

403

多租户隔离

租户A的数据租户B看不到

403

特殊业务规则

VIP用户才能下载报告

403

java

复制代码

// Spring Boot 应用层示例

@RestController

public class OrderController {

@GetMapping("/order/{id}")

public Order getOrder(@PathVariable Long id,

@RequestHeader("X-User-Id") Long userId) {

// 网关已校验Token有效性,这里只需校验"能不能看"

Order order = orderService.findById(id);

if (!order.getUserId().equals(userId)) {

// ❌ 不要返回401!凭证是有效的,只是没权限

throw new AccessDeniedException("无权查看该订单"); // 返回403

}

return order;

}

}

3.5 数据层:只做"档案室",不做"门卫"

redis

复制代码

# Redis 只存储认证相关数据,不执行鉴权逻辑

# 1. Token黑名单(用户登出、Token被吊销)

SET "blacklist:token_abc123" "revoked" EX 7200

# 2. 用户权限缓存(网关查询用)

HSET "user:1001:permissions" "order:read" "true"

HSET "user:1001:permissions" "order:delete" "false"

# 3. Token刷新记录(防止并发刷新导致Token混乱)

SET "refresh:token_abc123" "new_token_xyz789" EX 300

# 网关或应用层查询Redis做二次校验,但逻辑在网关层

四、实战:多域名场景的典型架构

4.1 推荐架构:统一网关 + SSO

复制代码

┌─────────────────────────────┐

│ 统一认证中心 (SSO) │

│ auth.company.com │

│ • 签发Token │

│ • 刷新Token │

│ • 管理用户/权限 │

└──────────────┬──────────────┘

┌────────────────────────┼────────────────────────┐

│ │ │

▼ ▼ ▼

┌──────────┐ ┌──────────────┐ ┌──────────┐

│ 前端项目A │ │ API网关 │ │ 前端项目B │

│ a.com │──────────→│ gateway.com │←─────────│ b.com │

│ │ │ │ │ │

│ Vue项目 │ │ • 统一CORS │ │ React项目 │

└──────────┘ │ • JWT校验 │ └──────────┘

│ • 限流/降级 │

│ • 日志/监控 │

└──────┬───────┘

┌──────────────────────┼──────────────────────┐

│ │ │

▼ ▼ ▼

┌──────────┐ ┌──────────┐ ┌──────────┐

│ 订单服务 │ │ 用户服务 │ │ 商品服务 │

│ order │ │ user │ │ product │

└──────────┘ └──────────┘ └──────────┘

│ │ │

└──────────────────────┼──────────────────────┘

┌──────┴──────┐

│ Redis │

│ • Token黑名单│

│ • 权限缓存 │

│ • 限流计数 │

└─────────────┘

4.2 前端请求完整流程

javascript

复制代码

// 前端A (a.com) 请求后端API

const api = axios.create({

baseURL: 'https://gateway.company.com', // 统一走网关

withCredentials: true, // 允许携带Cookie

timeout: 10000

});

// 请求拦截器:自动加Token

api.interceptors.request.use(config => {

const token = localStorage.getItem('token');

if (token) {

config.headers.Authorization = `Bearer ${token}`;

}

return config;

});

// 响应拦截器:统一处理401

api.interceptors.response.use(

response => response,

async error => {

const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {

originalRequest._retry = true;

try {

// 调用刷新Token接口

const { data } = await axios.post('https://auth.company.com/refresh', {

refreshToken: localStorage.getItem('refreshToken')

});

// 更新Token

localStorage.setItem('token', data.token);

originalRequest.headers.Authorization = `Bearer ${data.token}`;

// 重试原请求

return api(originalRequest);

} catch (refreshError) {

// 刷新失败,跳转登录页

window.location.href = '/login';

return Promise.reject(refreshError);

}

}

return Promise.reject(error);

}

);

五、排查401的标准SOP(由外到内)

5.1 排查流程图

复制代码

遇到401错误

├── Step 1: 检查浏览器Network面板

│ ├── Request Headers里有Authorization吗?

│ │ ├── ❌ 没有 → 检查前端拦截器/存储逻辑

│ │ └── ✅ 有 → 继续

│ └── 查看Cookie是否携带(如果是Cookie方案)

│ ├── ❌ 没有 → 检查withCredentials + CORS配置

│ └── ✅ 有 → 继续

├── Step 2: 用Postman/Apifox模拟请求

│ ├── 能通 → 问题在浏览器/CORS/前端

│ └── 不能通 → 问题在后端

├── Step 3: 检查服务端日志

│ ├── 收到Token了吗?

│ │ ├── ❌ 没有 → 网络层/网关层丢了

│ │ └── ✅ 有 → 继续

│ └── 认证服务日志:为什么校验失败?

│ ├── "Token expired" → 过期,检查刷新机制

│ ├── "Signature verification failed" → 密钥/算法不匹配

│ ├── "Token not found in cache" → Redis/缓存问题

│ └── "Token revoked" → 被吊销,检查黑名单逻辑

└── Step 4: 检查网关配置

├── CORS配置是否正确?

├── JWT密钥/算法是否一致?

└── 路由是否匹配?

5.2 排查检查清单

检查项

工具/方法

期望结果

前端是否携带Token

浏览器F12 → Network → Request Headers

Authorization: Bearer xxx

Cookie是否跨域

浏览器F12 → Application → Cookies

Domain设置正确,Secure/HttpOnly符合场景

CORS预检是否通过

浏览器F12 → Network → OPTIONS请求

Status 200,响应头包含正确CORS配置

后端是否收到Token

后端网关/应用日志

日志中显示Token内容或长度

Token是否有效

认证服务日志/调试

签名验证通过,未过期,不在黑名单

网络层是否丢头

Nginx/网关访问日志

原始请求头完整传递

六、解决方案决策树

复制代码

是否所有服务都经过统一入口(网关)?

├─ ✅ 是 → 使用网关层统一处理(强烈推荐)

│ │

│ ├─ 前端请求 → 网关统一校验JWT → 注入用户信息 → 转发到后端服务

│ │

│ ├─ 网关统一配置CORS,业务服务无需关心

│ │

│ └─ 网关统一拦截401,前端只需对接网关

└─ ❌ 否(直连模式)→ 只能在应用层各自处理

├─ 每个服务都实现JWT校验(代码冗余,维护困难)

└─ 建议:引入通用认证SDK,减少重复代码

└─ 例如:封装一个`@RequireAuth`注解或中间件

七、核心原则总结

维度

推荐做法

原因

跨域Token校验

统一在网关层完成

避免每个服务重复实现,集中管理

CORS跨域配置

也在网关层统一配置

业务服务零侵入,一处修改全局生效

Token刷新逻辑

网关层处理401时统一拦截

前端只需对接网关,无需关心后端细节

业务权限校验

应用层处理

返回403而非401,语义准确

认证数据存储

数据层只存凭证状态

Redis存黑名单/权限缓存,不处理逻辑

前端存储方案

同父域用Cookie共享,跨父域用统一登录页

localStorage无法跨域共享

开发环境跨域

配置反向代理(devServer proxy)

彻底绕开浏览器CORS限制

八、一句话结论

不同域名服务之间的401问题,应在「API网关层」统一处理。 这是架构上的最佳位置------既能统一管理跨域认证,又能让业务服务无感,同时保持扩展性和可观测性。

如果架构中没有网关,则只能在「应用层」各自处理,但建议通过引入通用认证SDK 来减少重复代码。记住:401是"不认识你"(网关管),403是"不让你进"(业务管),别把两者搞混!

Copyright © 2088 神之射手基地-网游活动专题 All Rights Reserved.
友情链接