在微服务架构中,身份认证是一个核心的挑战。如果没有统一的认证机制,每个微服务都需要单独处理用户认证,这会导致代码重复、安全风险增加,以及维护成本的上升。因此,在 Gateway 层集成 JWT 身份认证成为了一个常见的解决方案。它可以将认证逻辑集中在 Gateway,下游微服务只需要验证 JWT 的有效性即可,从而简化了微服务的开发。
JWT 认证原理与流程
JWT(JSON Web Token)是一个开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息,作为一个 JSON 对象。JWT 通常用于身份验证和授权。其基本流程如下:
- 用户登录: 用户提供用户名和密码等凭据进行登录。
- 认证服务器颁发 JWT: 认证服务器验证用户凭据,如果验证成功,则生成 JWT 并返回给客户端。
- 客户端存储 JWT: 客户端通常将 JWT 存储在 Cookie 或 Local Storage 中。
- 客户端请求携带 JWT: 客户端在后续的请求中,通过 HTTP 头部(Authorization: Bearer )或其他方式携带 JWT。
- Gateway 验证 JWT: Gateway 接收到请求后,验证 JWT 的签名和过期时间等信息。如果 JWT 有效,则将请求转发到下游微服务,否则返回认证失败的响应。
- 微服务信任 Gateway: 下游微服务信任 Gateway 已经完成了认证,只需要从请求头中获取用户信息即可。
Gateway 选型与配置:Nginx Plus 与 Spring Cloud Gateway
常见的 Gateway 方案包括 Nginx Plus、Spring Cloud Gateway 等。选择哪种方案取决于项目的技术栈和需求。如果项目主要使用 Java 技术栈,Spring Cloud Gateway 是一个不错的选择。如果对性能有较高要求,Nginx Plus 也是一个很好的选择,可以配合 Lua 脚本实现 JWT 认证。
1. Nginx Plus 集成 JWT 认证(以 OpenResty 为例):
# nginx.conf
http {
lua_package_path '/usr/local/openresty/lualib/?.lua;;'; # Lua 包路径
server {
listen 80;
server_name example.com;
location / {
access_by_lua_file /path/to/jwt_auth.lua; # Lua 脚本进行 JWT 认证
proxy_pass http://backend_service; # 后端服务地址
}
}
}
-- jwt_auth.lua
local jwt = require "resty.jwt"
local cjson = require "cjson"
local jwt_token = ngx.req.get_headers()['Authorization']
if not jwt_token then
ngx.status = 401
ngx.say("No token provided")
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local jwt_obj = jwt:new()
-- 替换为你的公钥
local verify, err = jwt_obj:verify(jwt_token, "your_public_key")
if not verify then
ngx.log(ngx.ERR, "failed to verify JWT: ", err)
ngx.status = 401
ngx.say("Invalid token")
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local payload = jwt_obj.payload
-- 将用户信息添加到请求头,传递给下游微服务
ngx.req.set_header("X-User-Id", payload.user_id)
ngx.req.set_header("X-User-Name", payload.user_name)
2. Spring Cloud Gateway 集成 JWT 认证:
// 依赖引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
// JwtTokenFilter.java
@Component
public class JwtTokenFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (token == null || !token.startsWith("Bearer ")) {
return chain.filter(exchange);
}
token = token.substring(7); // 去除 "Bearer " 前缀
try {
// 验证 JWT
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor("your_secret_key".getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(token);
Claims body = claims.getBody();
String userId = body.get("userId", String.class);
String userName = body.getSubject();
// 将用户信息添加到请求头,传递给下游微服务
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.header("X-User-Name", userName)
.build();
return chain.filter(exchange.mutate().request(request).build());
} catch (Exception e) {
// JWT 验证失败
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
}
@Override
public int getOrder() {
return -1; // 在所有 GatewayFilter 之前执行
}
}
实战避坑经验:JWT 续签与安全问题
- JWT 续签: JWT 的过期时间不宜设置过长,但频繁登录会影响用户体验。可以通过 Refresh Token 机制实现 JWT 的无感续签。当 JWT 即将过期时,客户端使用 Refresh Token 向认证服务器请求新的 JWT。需要注意的是,Refresh Token 的存储和管理也需要特别注意安全。
- JWT 安全: 保护好 JWT 的签名密钥至关重要。不要将密钥硬编码在代码中,而是应该使用环境变量或配置中心进行管理。同时,建议使用 HTTPS 协议来保护 JWT 在传输过程中的安全。对于敏感信息,不建议直接存储在 JWT 的 payload 中,而是存储在后端服务中,通过用户 ID 进行关联。
在微服务架构中,Gateway 集成 JWT 身份认证是一种有效的解决方案,可以简化微服务的开发和维护,提高系统的安全性。选择合适的 Gateway 方案,并注意 JWT 的续签和安全问题,可以更好地构建稳定可靠的微服务系统。特别是对于高并发场景,需要关注 Gateway 的性能瓶颈,例如 Nginx 的 worker 进程数、连接数限制等,进行合理的配置和优化。
冠军资讯
CoderPunk