在微服务架构中,服务拆分带来灵活性的同时,也引入了复杂的身份认证和授权问题。每个微服务都进行单独的认证,不仅增加开发和维护成本,也影响用户体验。本文将分享如何利用 Gateway 集成 JWT 身份认证,实现微服务统一认证,并分享一些实战中的避坑经验。
问题场景:微服务认证的困境
假设我们有一个电商平台,包含用户服务、商品服务、订单服务等多个微服务。如果每个服务都各自进行用户认证,会导致:
- 重复开发: 每个服务都需要编写认证逻辑,增加开发工作量。
- 维护困难: 用户信息变更时,需要在多个服务中同步,容易出错。
- 安全风险: 每个服务都需要独立管理密钥,容易出现安全漏洞。
- 用户体验差: 用户每次访问不同服务都需要重新登录。
因此,我们需要一个统一的认证中心,负责处理所有用户的身份验证,并生成令牌(Token)供各个服务使用。Gateway 作为流量入口,是集成统一认证的理想位置。
底层原理:JWT 认证流程
JWT(JSON Web Token)是一种基于 JSON 的开放标准(RFC 7519),用于在各方之间安全地传递声明。JWT 通常包含以下三个部分:
- Header(头部): 包含令牌类型和使用的签名算法。
- Payload(载荷): 包含声明(Claims),例如用户ID、用户名、权限等。
- Signature(签名): 通过头部指定的算法对头部和载荷进行签名,防止篡改。
使用 JWT 进行身份认证的流程如下:
- 用户向认证服务器(例如,用户服务)提供用户名和密码进行登录。
- 认证服务器验证用户身份,如果验证成功,则生成 JWT 并返回给客户端。
- 客户端将 JWT 存储在本地(例如,Cookie 或 LocalStorage)。
- 客户端每次请求微服务时,都将 JWT 放在请求头中(例如,
Authorization: Bearer <token>)。 - Gateway 接收到请求后,验证 JWT 的有效性。
- 如果 JWT 有效,则提取 JWT 中的信息(例如,用户ID、权限),并将这些信息添加到请求头中,然后将请求转发给相应的微服务。
- 微服务根据请求头中的信息进行授权。
代码/配置解决方案:Spring Cloud Gateway + Spring Security + JWT
以下是一个基于 Spring Cloud Gateway、Spring Security 和 JWT 实现统一认证的示例:
1. 添加依赖
在 pom.xml 文件中添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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>
</dependencies>
2. JWT 工具类
创建一个 JWT 工具类,用于生成和验证 JWT:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.function.Function;
@Component
public class JwtUtil {
private final SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512); // 密钥,生产环境应使用更安全的方式存储
public String generateToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 小时过期
.signWith(secretKey)
.compact();
}
public boolean validateToken(String token) {
return !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
}
3. Spring Security 配置
配置 Spring Security,添加 JWT 过滤器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
@EnableWebFluxSecurity
public class SecurityConfig {
@Autowired
private JwtUtil jwtUtil;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, JwtAuthenticationConverter jwtAuthenticationConverter) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/auth/**").permitAll() // 允许访问 /auth/** 接口,例如登录、注册
.anyExchange().authenticated() // 其他所有请求需要认证
.and()
.addFilterAt(new JwtAuthenticationFilter(jwtUtil), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
return new JwtAuthenticationConverter();
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Component
public class JwtAuthenticationFilter implements WebFilter {
@Autowired
private JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7); // 去掉 "Bearer " 前缀
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.extractUsername(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null); // 没有权限信息
return chain.filter(exchange).contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
}
}
return chain.filter(exchange);
}
}
4. Gateway 路由配置
在 application.yml 或 application.properties 文件中配置 Gateway 路由规则:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service # 使用服务发现
predicates:
- Path=/user/**
filters:
- AddRequestHeader=X-Request-User, #[{authentication_principal}]
5. 用户服务提供认证接口
创建一个 /auth/login 接口,用于用户登录并返回 JWT。
实战避坑经验总结
- 密钥管理: JWT 的密钥至关重要,必须安全存储,防止泄露。可以使用 Vault 等密钥管理工具。
- Token 过期时间: 合理设置 Token 的过期时间,避免 Token 被滥用。可以根据业务需求设置不同的过期时间。
- 刷新 Token: 为了避免用户频繁登录,可以实现刷新 Token 的机制。当 Token 即将过期时,客户端可以向认证服务器请求新的 Token。
- JWT 的大小: JWT 不宜过大,避免影响网络传输性能。尽量减少在 Payload 中包含的信息。
- 安全性: JWT 只是验证用户身份,不能防止 CSRF 攻击。对于敏感操作,仍需要使用 CSRF 防护。
- 监控和日志: 监控 Gateway 的认证和授权情况,记录必要的日志,方便排查问题。
- 性能优化: 使用缓存机制,减少 JWT 的验证次数,提高 Gateway 的性能。例如使用 Redis 缓存 JWT 验证结果。
通过以上方案,我们可以在 Gateway 中集成 JWT 身份认证,实现微服务统一认证,简化开发和维护工作,并提升用户体验。同时,在实际应用中,还需要根据具体业务场景进行调整和优化,例如使用 OAuth 2.0 协议,或集成第三方认证服务。
Nginx 作为常用的反向代理服务器,也可以与 Gateway 配合使用,提高系统的可用性和可扩展性。 例如通过 Nginx 负载均衡,将流量分发到多个 Gateway 实例,保证系统的高可用性。
冠军资讯
加班到秃头