在网关层实现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;
}
}
定义验证过滤器
下面实现名为JwtAuthorizeGatewayFilterFactory
的JWT
的校验过滤器,通过继承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;
}
}
}