网关实现登陆校验(Spring Cloud Gateway)

完整实现步骤:JWT 网关校验 + 微服务信息传递

JWT网关校验:在网关层使用全局过滤器进行拦截
微服务信息传递:在微服务里面用拦截器拦截到信息,然后统一使用

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/10c5eb2f83ad4272bd0a0f70a11a4148.png![

一、技术选型说明
  • JWT 解析库推荐
    推荐使用 jjwt(Java JWT),它是 Java 生态中最流行的 JWT 库,具有以下优势:
    1. API 简洁:生成、解析 JWT 仅需几行代码。
    2. 安全性高:支持多种签名算法(如 HS512、RS256)。
    3. 社区活跃:长期维护,文档完善,与 Spring 兼容性好。

二、网关层实现 JWT 校验

1. 添加依赖

在网关模块的 pom.xml 中添加 jjwt 和必要依赖:

<!-- JJWT -->
<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>
2. 实现 JWT 工具类
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 javax.annotation.PostConstruct;
import java.security.Key;
import java.util.Date;

@Component
public class JwtUtils {

    @Value("${jwt.secret}")  // 从配置文件中读取密钥
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    private Key key;

    // 初始化密钥
    @PostConstruct
    public void init() {
        this.key = Keys.hmacShaKeyFor(secret.getBytes());
    }

    // 生成 JWT
    public String generateToken(String username, String roles) {
        return Jwts.builder()
                .setSubject(username)
                .claim("roles", roles)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(key)
                .compact();
    }

    // 解析 JWT
    public Claims parseToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    // 验证 Token 是否过期
    public boolean isTokenExpired(String token) {
        return parseToken(token).getExpiration().before(new Date());
    }
}
3. 全局过滤器实现 JWT 校验
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtUtils jwtUtils;

    public JwtAuthGlobalFilter(JwtUtils jwtUtils) {
        this.jwtUtils = jwtUtils;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 排除登录接口
        String path = exchange.getRequest().getPath().value();
        if (path.startsWith("/api/auth/login")) {
            return chain.filter(exchange);
        }

        // 2. 获取 Token
        String token = extractToken(exchange.getRequest().getHeaders());

        // 3. 验证 Token
        if (token == null) {
            return buildUnauthorizedResponse(exchange, "Missing token");
        }
        try {
            Claims claims = jwtUtils.parseToken(token);
            if (jwtUtils.isTokenExpired(token)) {
                return buildUnauthorizedResponse(exchange, "Token expired");
            }

            // 4. 传递用户信息到下游服务
            ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                    .header("X-User-Name", claims.getSubject())
                    .header("X-User-Roles", claims.get("roles", String.class))
                    .build();
            return chain.filter(exchange.mutate().request(mutatedRequest).build());
        } catch (Exception e) {
            return buildUnauthorizedResponse(exchange, "Invalid token");
        }
    }

    private String extractToken(HttpHeaders headers) {
        String authHeader = headers.getFirst(HttpHeaders.AUTHORIZATION);
        return (authHeader != null && authHeader.startsWith("Bearer ")) ? authHeader.substring(7) : null;
    }

    private Mono<Void> buildUnauthorizedResponse(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().add("Content-Type", "application/json");
        String body = String.format("{\"code\": 401, \"msg\": \"%s\"}", message);
        return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body.getBytes())));
    }

    @Override
    public int getOrder() {
        return -1;  // 设置过滤器优先级(数字越小优先级越高)
    }
}
4. 配置文件 application.yml
jwt:
  secret: "my-ultra-secure-jwt-secret-key-1234567890"  # 密钥(至少 32 位)
  expiration: 3600000  # Token 有效期(1小时)

spring:
  cloud:
    gateway:
      routes:
        - id: auth-service
          uri: lb://auth-service
          predicates:
            - Path=/api/auth/**
          filters:
            - StripPrefix=1
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1

三、微服务信息传递

为了在微服务中统一处理JWT校验并使用UserContext传递用户信息,可以按照以下步骤实现:

public class UserContext {
    private static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();

    public static void setUserInfo(Map<String, String> userInfo) {
        context.set(userInfo);
    }

    public static Map<String, String> getUserInfo() {
        return context.get();
    }

    public static void clear() {
        context.remove();
    }

    // 获取具体字段(示例)
    public static String getUserId() {
        return getUserInfo() != null ? getUserInfo().get("userId") : null;
    }

    public static String getUsername() {
        return getUserInfo() != null ? getUserInfo().get("username") : null;
    }
}

2. 实现JWT校验拦截器

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtils jwtUtils; // 你的JWT工具类

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求头获取token
        String token = request.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            throw new UnauthorizedException("缺少Token");
        }

        // 校验并解析Token
        Claims claims = jwtUtils.parseToken(token);
        if (claims == null) {
            throw new UnauthorizedException("Token无效或已过期");
        }

        // 提取用户信息存入UserContext
        Map<String, String> userInfo = new HashMap<>();
        userInfo.put("userId", claims.get("userId", String.class));
        userInfo.put("username", claims.getSubject());
        UserContext.setUserInfo(userInfo);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 请求完成后清理ThreadLocal,防止内存泄漏
        UserContext.clear();
    }
}

3. 注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/auth/login", "/swagger**"); // 排除登录和Swagger
    }
}

4. JWT工具类示例(JwtUtils)

@Component
public class JwtUtils {

    @Value("${jwt.secret}")
    private String secret;

    public Claims parseToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret.getBytes())
                    .parseClaimsJws(token.replace("Bearer ", ""))
                    .getBody();
        } catch (Exception e) {
            return null; // Token解析失败
        }
    }
}

5. 在微服务中使用UserContext

@RestController
@RequestMapping("/api/user")
public class UserController {

    @GetMapping("/profile")
    public ResponseEntity<?> getUserProfile() {
        String userId = UserContext.getUserId();
        String username = UserContext.getUsername();
        // 查询数据库或其他业务逻辑...
        return ResponseEntity.ok("用户信息: " + username);
    }
}

关键点说明:

  1. 拦截流程

    • 拦截器从Authorization头提取JWT。
    • 使用JwtUtils验证并解析Token。
    • 将用户关键信息存入UserContext的ThreadLocal中。
  2. 线程安全

    • UserContext使用ThreadLocal确保每个请求线程独立存储。
    • 拦截器afterCompletion方法中调用UserContext.clear(),避免内存泄漏。
  3. 异常处理

    • 如果Token校验失败,直接抛出异常(需配合全局异常处理器返回401状态码)。
  4. 网关与微服务协作

    • 网关负责路由转发和初步鉴权(如权限校验)。
    • 微服务拦截器做二次校验(可选,根据安全级别决定),确保请求合法性。

扩展优化建议:

  • Feign传递用户信息:如果微服务间调用需要传递用户身份,可以添加Feign拦截器:
    @Bean
    public RequestInterceptor feignRequestInterceptor() {
        return requestTemplate -> {
            String userId = UserContext.getUserId();
            if (userId != null) {
                requestTemplate.header("X-User-Id", userId);
            }
        };
    }
    
  • 更严格的校验:检查Token的签名、过期时间、黑名单等。
  • 结合Spring Security:如果需要更复杂的权限控制,可整合Spring Security + JWT。

四、OpenFeign 传递用户信息(如JWT令牌或用户ID)

根据你的实际业务调整UserContext存储的字段和校验逻辑即可。
为了在微服务间通过 OpenFeign 传递用户信息(如JWT令牌或用户ID),需通过 Feign拦截器 自动将上下文信息添加到请求头中。以下是完整步骤:

1. 添加Feign拦截器(传递用户信息)

@Component
public class FeignUserInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 从UserContext获取用户信息
        String userId = UserContext.getUserId();
        String token = UserContext.getToken(); 

        // 将信息添加到Feign请求头
        if (userId != null) {
            template.header("X-User-Id", userId);
        }
        if (token != null) {
            template.header("Authorization", "Bearer " + token);
        }
    }
}

2. 确保UserContext包含所需字段

public class UserContext {
    private static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();

    // 其他方法...

    public static String getToken() {
        return getUserInfo() != null ? getUserInfo().get("token") : null;
    }

    // 网关过滤器或拦截器中存入Token
    public static void setToken(String token) {
        Map<String, String> userInfo = getUserInfo();
        if (userInfo == null) {
            userInfo = new HashMap<>();
        }
        userInfo.put("token", token);
        context.set(userInfo);
    }
}

3. 下游服务接收并校验信息

下游服务需通过拦截器或过滤器读取请求头中的用户信息,并存入本地上下文:

下游服务拦截器示例
@Component
public class DownstreamUserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 从请求头获取信息
        String userId = request.getHeader("X-User-Id");
        String token = request.getHeader("Authorization");

        // 校验Token(可选)
        if (token != null) {
            Claims claims = jwtUtils.parseToken(token.replace("Bearer ", ""));
            if (claims == null) {
                throw new UnauthorizedException("Token无效");
            }
        }

        // 存入当前服务的UserContext
        Map<String, String> userInfo = new HashMap<>();
        userInfo.put("userId", userId);
        userInfo.put("token", token);
        UserContext.setUserInfo(userInfo);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        UserContext.clear();
    }
}

4. Feign客户端调用示例

在服务A中,通过Feign调用服务B的API,用户信息会自动传递:

@FeignClient(name = "service-b")
public interface ServiceBClient {

    @GetMapping("/api/data")
    ResponseEntity<String> getData();
}

// 业务代码中直接调用
public class ServiceA {
    @Autowired
    private ServiceBClient serviceBClient;

    public void doSomething() {
        // UserContext已在拦截器中填充
        ResponseEntity<String> response = serviceBClient.getData();
    }
}

关键配置说明

  1. 拦截器自动生效
    Spring会自动发现FeignUserInterceptor(因标注了@Component),无需手动注册到Feign客户端。

  2. 确保UserContext正确填充

    • 网关层需将JWT Token存入UserContext
    • 微服务自身的拦截器(如JwtInterceptor)需在校验Token后,将信息存入UserContext
  3. 下游服务校验(可选)

    • 如果下游服务需要严格校验Token,需配置相同的JWT密钥。
    • 如果仅需传递用户ID,可省略Token校验步骤。

扩展场景:仅传递必要字段

如果不想传递完整Token,可在网关将用户ID、角色等关键信息注入请求头,微服务直接读取:

网关过滤器添加自定义头
public class GatewayFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从JWT解析用户信息
        String userId = ...;
        String role = ...;

        // 添加自定义请求头
        ServerHttpRequest request = exchange.getRequest().mutate()
                .header("X-User-Id", userId)
                .header("X-User-Role", role)
                .build();

        return chain.filter(exchange.mutate().request(request).build());
    }
}
Feign拦截器透传这些头
public class FeignUserInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 从当前请求的上下文中获取头信息(需配合RequestContextHolder)
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String userId = request.getHeader("X-User-Id");
            String role = request.getHeader("X-User-Role");
            template.header("X-User-Id", userId);
            template.header("X-User-Role", role);
        }
    }
}

注意事项

  1. 线程池隔离问题
    如果使用Hystrix或异步线程,需通过HystrixRequestContext或自定义线程上下文传递数据。

  2. 敏感信息安全

    • 始终使用HTTPS加密传输。
    • 避免在请求头中传递敏感数据(如密码),必要时进行加密。
  3. 性能影响
    频繁解析JWT可能影响性能,可在网关层完成主要校验,微服务按需二次校验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值