在网关层实现JWT验证,下层接口服务可以完全不考虑验证逻辑,减少重复工作量,下面通过自定义过滤器实现JWT验证功能。

JWT工具类

首先定义一个JWT工具类,提供JWT生成和校验功能,加入依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.7</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>

工具类实现如下:

package top.wteng.sggateway.util;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;

import javax.crypto.SecretKey;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import io.jsonwebtoken.security.SignatureException;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpServerErrorException;

public class JwtUtil {
    private static final int EXPIRATION_MINUTES = 5 * 24 * 60; // token过期时间
    private static final String ROLE_CLAIMS = "role"; // 角色字段
    private static final String TOKEN_PREFIX = ""; // Token前缀,如可以是“bearer ”
      // 签名密钥
    private static final String SECRET_KEY_STR = "YT2Sdf32^l2u116lp32m3fklo785fda0q0pMHq454tr8Ujh32ok5";
    private static final byte[] API_KEY_SECRET_BYTES = SECRET_KEY_STR.getBytes(StandardCharsets.UTF_8);
    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(API_KEY_SECRET_BYTES);

    // 生成token
    public static String createToken(String username, String id, List<String> roles) {
        long expiration = EXPIRATION_MINUTES * 60;
        final Date createdDate = new Date();
        final Date expirationDate = new Date(createdDate.getTime() + expiration * 1000);
        String tokenPrefix = Jwts.builder()
                .setHeaderParam("type", "JWT")
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256)
                .claim(ROLE_CLAIMS, String.join(",", roles))
                .setId(id)
                .setIssuer("SnailClimb")
                .setIssuedAt(createdDate)
                .setSubject(username)
                .setExpiration(expirationDate)
                .compact();
        return TOKEN_PREFIX + tokenPrefix; // 添加 token 前缀 "Bearer ";
    }

    // 解析用户信息,解析失败证明token非法
    public static GrantedAuthority getGrantedAuthority(String token) {
        try {
            Claims claims = getClaims(token);
            String roles = (String) claims.get(ROLE_CLAIMS);
            String idStr = claims.getId();
            return new GrantedAuthority(idStr, roles);
        } catch (SignatureException e) {
            // 解析失败
            return null;
        }
    }

    public static String getId(String token) {
        Claims claims = getClaims(token);
        return claims.getId();
    }

    private static Claims getClaims(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    // 用户的信息
    @Data
    @AllArgsConstructor
    public static class GrantedAuthority {
        private String id;
        private String role;
    }
}

定义验证过滤器

下面实现名为JwtAuthorizeGatewayFilterFactoryJWT的校验过滤器,通过继承AbstractGatewayFilterFactory实现,继承该类实现过滤器需要一个配置类,用来配置过滤器需要的参数,这里给JWT过滤器设置两个配置参数,一个是是否启用(enabled),一个是token所在header的名字,在配置文件中配置时如下所示:

# 配置成全局过滤器
default-filters:
    - JwtAuthorize=true,access-token # 逗号分割参数,第一个参数对应enabled,第二个参数对应name

# 路由单独配置
routes:
  - id: user-rest
    uri: lb://user-rest
    predicates:
  - Path=/user-rest/**
  filters:
    - StripPrefix=1
    - JwtAuthorize=true,access-token
过滤器的命名规则是取GatewayFilterFactory前面的名字,因此定义的过滤器的类名是JwtAuthorizeGatewayFilterFactory,则在配置文件中配置的过滤器名字是JwtAuthorize

过滤器实现如下:

package top.wteng.sggateway.filter;

import java.util.Arrays;
import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Mono;

import top.wteng.sggateway.util.JwtUtil;
import top.wteng.sggateway.util.ResultUtil;

@Order(1)
@Component
public class JwtAuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<JwtAuthorizeGatewayFilterFactory.Config> {
    private static final Logger logger = LoggerFactory.getLogger(JwtAuthorizeGatewayFilterFactory.class);
    private final ObjectMapper objectMapper = new ObjectMapper();

    public JwtAuthorizeGatewayFilterFactory() {
        super(Config.class);
        logger.info("Loaded GatewayFilterFactory [JwtAuthorize]");
    }

    // 在请求中获取到token字符串
    private String getToken(ServerHttpRequest request, Config config) {
        HttpHeaders headers = request.getHeaders();
        // 查找header
        List<String> headersOfToken = headers.get(config.getName());
        if (headersOfToken != null && headersOfToken.size() > 0) {
            return headersOfToken.get(0);
        }
        // header中未找到,在url中查找
        MultiValueMap<String, String> queryParams = request.getQueryParams();
        List<String> tokenParams = queryParams.get(config.getName());
        if (tokenParams != null && tokenParams.size() > 0) {
            return tokenParams.get(0);
        }
        return null;
    }

    // 返回401未授权响应
    private Mono<Void> completeResponseWith401(ServerHttpResponse response) {
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        ResultUtil errorResult = ResultUtil.fail(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
        response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            try {
                return bufferFactory.wrap(objectMapper.writeValueAsBytes(errorResult));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
                return bufferFactory.wrap(new byte[0]);
            }
        }));
        return response.setComplete(); // 直接结束请求
    }

    // 核心实现
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            // 若enable是false或者是登录接口,跳过验证
            if (!config.isEnabled() || request.getPath().toString().startsWith("/auth/login")) {
                return chain.filter(exchange);
            }
            String token = getToken(request, config); // 取出token
            if (token == null) {
                // 未找到token,直接返回401
                return completeResponseWith401(exchange.getResponse());
            }
            logger.info(String.format("token detected: %s", token));
            // 解析token,实际应用中这里也可以调用专门负责授权验证的接口进行验证
            JwtUtil.GrantedAuthority grantedAuthority = JwtUtil.getGrantedAuthority(token);
            if (grantedAuthority == null) {
                // token解析失败
                return completeResponseWith401(exchange.getResponse());
            }
            // token解析成功,证明验证通过,将解析后的用户id与role信息放到header里,下层服务可直接在header里获取到
            ServerHttpRequest requestWithHeader = request.mutate().headers(httpHeaders -> {
                httpHeaders.add("x-id", grantedAuthority.getId());
                httpHeaders.add("x-role", grantedAuthority.getRole());
            }).build();
            exchange.mutate().request(requestWithHeader).build();
            return chain.filter(exchange);
        };
    }

    // 定义配置文件中逗号分割的参数与配置字段的顺序对应关系
    @Override
    public List<String> shortcutFieldOrder() {
        // 第一个参数对应enabled,第二个参数对应name
        return Arrays.asList("enabled", "name");
    }

    // 配置类
    public static class Config {
        private boolean enabled; // 是否启用
        private String name; // 存放token的header名称

        public Config() {}

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}