This commit is contained in:
jiangh277
2025-12-24 14:17:19 +08:00
parent 3eb445291f
commit 4c7d59f87b
89 changed files with 3525 additions and 311 deletions

View File

@@ -3,9 +3,12 @@ package com.timeline.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = {"com.timeline"})
@EnableDiscoveryClient
public class TimelineGatewayApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,22 @@
package com.timeline.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import reactor.netty.http.client.HttpClient;
@Configuration
public class WebSocketConfig {
@Bean
public WebSocketHandlerAdapter webSocketHandlerAdapter() {
return new WebSocketHandlerAdapter();
}
@Bean
public WebSocketClient webSocketClient() {
return new ReactorNettyWebSocketClient(HttpClient.create());
}
}

View File

@@ -1,22 +1,28 @@
package com.timeline.gateway.filter;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.response.ResponseEnum;
import com.timeline.gateway.util.JwtUtils;
import com.timeline.gateway.utils.JwtUtils;
import com.timeline.common.utils.RedisUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
@Component
@Slf4j
@@ -25,6 +31,20 @@ public class AuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private RedisUtils redisUtils;
private static final String TOKEN_BLACKLIST_PREFIX = "auth:token:blacklist:";
// 需要跳过认证的路径列表
private static final List<String> WHITELIST_PATHS = Arrays.asList(
"/user/auth/login",
"/user/auth/register",
"/user/auth/refresh",
"/ping",
"/actuator"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
@@ -35,12 +55,44 @@ public class AuthenticationFilter implements GlobalFilter, Ordered {
return chain.filter(exchange);
}
// 从请求头中获取token
// WebSocket 连接需要 token但不强制要求允许通过由下游服务处理
if (path.startsWith("/user/ws")) {
String token = extractToken(request);
if (token != null && !token.isEmpty()) {
// 如果有 token验证并传递用户信息
if (jwtUtils.validateToken(token) && !isBlacklisted(token)) {
try {
Claims claims = jwtUtils.getClaimsFromToken(token);
if (claims != null && !jwtUtils.isTokenExpired(claims)) {
String userId = jwtUtils.getUserIdFromClaims(claims);
String username = jwtUtils.getUsernameFromClaims(claims);
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", userId)
.header("X-Username", username)
.build();
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return chain.filter(mutatedExchange);
}
} catch (Exception e) {
// token 验证失败,继续传递(由下游服务处理)
}
}
}
// 即使没有 token 也允许通过,由下游服务决定是否接受连接
return chain.filter(exchange);
}
// 从请求头或查询参数中获取token
String token = extractToken(request);
if (token == null || token.isEmpty()) {
return handleUnauthorized(exchange, ResponseEnum.UNAUTHORIZED.getMessage());
}
// 黑名单拦截
if (isBlacklisted(token)) {
return handleUnauthorized(exchange, "Token已注销");
}
// 验证token
if (!jwtUtils.validateToken(token)) {
@@ -73,31 +125,44 @@ public class AuthenticationFilter implements GlobalFilter, Ordered {
private boolean isWhitelisted(String path) {
// 白名单路径不需要认证
return path.startsWith("/auth/login") ||
path.startsWith("/auth/register") ||
path.startsWith("/ping") ||
path.startsWith("/actuator");
return WHITELIST_PATHS.stream().anyMatch(path::startsWith);
}
private String extractToken(ServerHttpRequest request) {
// 首先尝试从请求头中获取token
String bearerToken = request.getHeaders().getFirst("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
// 如果请求头中没有token则尝试从查询参数中获取
URI uri = request.getURI();
String query = uri.getRawQuery();
if (query != null) {
String[] params = query.split("&");
for (String param : params) {
if (param.startsWith("Authorization=")) {
return param.substring(21);
}
}
}
return null;
}
private boolean isBlacklisted(String token) {
Boolean exists = redisUtils.hasKey(TOKEN_BLACKLIST_PREFIX + token);
return Boolean.TRUE.equals(exists);
}
@SuppressWarnings("null")
private Mono<Void> handleUnauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<String> responseEntity = ResponseEntity.error(ResponseEnum.UNAUTHORIZED.getCode(), message);
String responseBody = com.alibaba.fastjson.JSON.toJSONString(responseEntity);
byte[] bytes = responseBody.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
String body = ResponseEntity.error(ResponseEnum.UNAUTHORIZED, message).toString();
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
return response.writeWith(Mono.just(response.bufferFactory().wrap(bytes)));
}
}
}

View File

@@ -1,21 +1,29 @@
// JwtUtils.java
package com.timeline.gateway.util;
package com.timeline.gateway.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JwtUtils {
@Value("${jwt.secret:timelineSecretKey}")
private String secret;
private byte[] secretBytes() {
return secret.getBytes(StandardCharsets.UTF_8);
}
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secret)
.setSigningKey(Keys.hmacShaKeyFor(secretBytes()))
.build()
.parseClaimsJws(token)
.getBody();
@@ -26,7 +34,10 @@ public class JwtUtils {
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secret).build().parseClaimsJws(token);
Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretBytes()))
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
@@ -37,7 +48,7 @@ public class JwtUtils {
if (claims == null) {
return true;
}
return claims.getExpiration().before(new java.util.Date());
return claims.getExpiration().before(new Date());
}
public String getUserIdFromClaims(Claims claims) {

View File

@@ -1,27 +1,48 @@
# application.properties
spring.application.name=timeline-gateway
server.port=30000
# ????
spring.datasource.url=jdbc:mysql://59.80.22.43:33306/timeline?serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=WoCloud@9ol7uj
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 路由配置
spring.cloud.gateway.routes[0].id=story-service
spring.cloud.gateway.routes[0].uri=lb://timeline.story
spring.cloud.gateway.routes[0].uri=http://localhost:30001
spring.cloud.gateway.routes[0].predicates[0]=Path=/story/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=0
spring.cloud.gateway.routes[1].id=file-service
spring.cloud.gateway.routes[1].uri=lb://timeline.file
spring.cloud.gateway.routes[1].uri=http://localhost:30002
spring.cloud.gateway.routes[1].predicates[0]=Path=/file/**
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=0
# JWT??
jwt.secret=timelineSecretKey
spring.cloud.gateway.routes[2].id=user-service
spring.cloud.gateway.routes[2].uri=http://localhost:30003
spring.cloud.gateway.routes[2].predicates[0]=Path=/user/**
spring.cloud.gateway.routes[2].filters[0]=StripPrefix=0
# 添加WebSocket路由配置
spring.cloud.gateway.routes[3].id=user-service-ws
spring.cloud.gateway.routes[3].uri=http://localhost:30003
spring.cloud.gateway.routes[3].predicates[0]=Path=/user/ws/**
spring.cloud.gateway.routes[3].filters[0]=StripPrefix=0
# JWT配置
jwt.secret=6f3f9c2b9d9a4e3f8c0d6a7b5c4e3f1a6f3f9c2b9d9a4e3f8c0d6a7b5c4e3f1a
jwt.expiration=86400
# Actuator??
# Redis
spring.data.redis.host=localhost
spring.data.redis.port=36379
spring.data.redis.password=123456
spring.data.redis.timeout=5000
# Actuator配置
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
# ????
# 日志配置
logging.level.org.springframework.cloud.gateway=DEBUG
logging.level.com.timeline.gateway=DEBUG
logging.level.com.timeline.gateway=DEBUG