Macula Boot Starter Cloud Gateway
概述
网关服务模块,给每个平台应用依赖用的。主要提供token认证、鉴权、接口加解密等功能。
组件坐标
<dependency>
<groupId>dev.macula.boot</groupId>
<artifactId>macula-boot-starter-cloud-alibaba</artifactId>
<version>${macula.version}</version>
</dependency>
<dependency>
<groupId>dev.macula.boot</groupId>
<artifactId>macula-boot-starter-cloud-alibaba-scg</artifactId>
<version>${macula.version}</version>
</dependency>
使用配置
spring:
gateway:
routes:
- id: macula-cloud-system
uri: lb://macula-cloud-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
security:
oauth2:
resourceserver:
opaquetoken:
client-id: e4da4a32-592b-46f0-ae1d-784310e88423
client-secret: secret
introspection-uri: http://127.0.0.1:9010/oauth2/introspect
redis: # 网关自己的redis配置
database: 0
host: 127.0.0.1
port: 6379
system: # macula-cloud的system模块的redis配置
database: 0
host: 127.0.0.1
port: 6379
macula:
gateway:
sign-switch: true # 接口签名全局开关,默认true
force-sign: false # 是否强制校验指定URL的接口签名,默认false
crypto-switch: true # 接口加解密全局开关,默认true
force-crypto: false # 是否强制校验指定URL的接口要不要加解密,默认false
protect-urls: # 需要保护的URL,前端可通过/gateway/protect/urls获取
crypto: # 加密
- /system/xxx/**
- /mall/api/v1/xxx/**
sign: # 签名
- /system/xxx/*
- /mall/api/v1/**
security:
ignore-urls: /usr/xxx,/bbb/xxx # 忽略认证的路径,Ant Path格式
only-auth-urls: /usr/xx, /bbb/xxx # 仅需认证无需鉴权的路径
核心功能
Token认证
将oauth2的token转换为JWT传递给微服务,微服务通过JWT获取用户信息和角色信息。
- 前端访问gateway接口,在HTTP请求头添加
Authorization Bear xxxxx
- gateway收到token后,调用iam服务器的introspect url返回用户信息和角色信息(同时会以token有效期来缓存用户信息)
- 通过AddJwtFilter将用户信息和角色信息转为JWT Token放入请求头给后续的微服务的安全模块校验认证(JWT也会缓存)
AK/SK认证
将hmac的签名等信息转为JWT传递给微服务,微服务通过JWT获取用户信息和角色信息。
- 如果请求携带hmac信息,则校验签名,根据appId检查应用,如果应用有租户ID,则设置到租户上下文
- 通过AddJwtFilter生成JWT,具体同上
URL安全
根据用户的角色和URL所需角色对比,控制URL权限。
macula-cloud-system模块在维护菜单、角色等信息后会定时将URL和角色关系缓存到redis
gateway根据请求URL匹配redis中的URL角色关系找出访问该URL所需角色
根据当前用户的角色列表是否满足上一步的角色要求决定是否放行
提示
可以配置URL是否认证或者鉴权定制JWT的Claims
框架开放了JwtClaimsCustomizer接口,可以用来添加你自己的Claim
@Bean
JwtClaimsCustomizer jwtClaimsCustomizer() {
return builder -> {
builder.claim("sal", "demo");
builder.claim("abc", "aaa");
};
}
接口的加解密和签名
网关要支持接口加解密和签名的话,首先要实现CryptoService,加解密所需方法。比如接入密钥服务系统。
/**
* {@code CryptoService} 接口加解密服务
*
* @author rain
* @since 2023/3/22 19:36
*/
public interface CryptoService {
/**
* 获取用于加密前端生成的SM4Key的公钥
*
* @return 公钥
*/
String getSm2PublicKey();
/**
* 解密前端传过来经过非对称加密的SM4 KEY
*
* @param key 加密过的sm4 key
* @return SM4KEY明文
*/
String decryptSm4Key(String key);
/**
* 加密数据
*
* @param plainText 明文
* @param sm4Key 加密的密钥
* @return base64密文
*/
String encrypt(String plainText, String sm4Key);
/**
* 解密数据
*
* @param secretText base64密文
* @param sm4Key 解密的密钥
* @return 明文
*/
String decrypt(String secretText, String sm4Key);
}
本地加解密实现示例如下:
/**
* {@code CryptoLocaleServiceImpl} 本地加解密服务
*
* @author rain
* @since 2023/3/23 22:01
*/
@Component
@Slf4j
public class CryptoLocaleServiceImpl implements CryptoService, InitializingBean {
private SM2 sm2;
@Override
public String getSm2PublicKey() {
return HexUtil.encodeHexStr(sm2.getPublicKey().getEncoded());
}
@Override
public String decryptSm4Key(String key) {
return sm2.decryptStr(key, KeyType.PrivateKey);
}
@Override
public String encrypt(String plainText, String sm4Key) {
return SmUtil.sm4(sm4Key.getBytes(StandardCharsets.UTF_8)).encryptBase64(plainText);
}
@Override
public String decrypt(String secretText, String sm4Key) {
return SmUtil.sm4(sm4Key.getBytes(StandardCharsets.UTF_8)).decryptStr(secretText);
}
@Override
public void afterPropertiesSet() throws Exception {
KeyPair pair = SecureUtil.generateKeyPair("SM2");
sm2 = SmUtil.sm2(pair.getPrivate(), pair.getPublic());
if (log.isDebugEnabled()) {
log.debug("sm2 public key: {}", HexUtil.encodeHexStr(sm2.getPublicKey().getEncoded()));
log.debug("sm2 private key: {}", HexUtil.encodeHexStr(sm2.getPrivateKey().getEncoded()));
log.debug("sm4 encrypted key: {}", sm2.encryptBase64("1234567890abcdef", KeyType.PublicKey));
}
}
}
前端流程
- 密钥协商
- 前端获取需要加密或者签名的接口:/gateway/protect/urls,返回{ crypto: [], sign: [] },如果与当前请求匹配,则执行下述流程
- 前端获取公钥:/gateway/protect/key
- 前端根据URL规则判断是否要加密和签名
- 前端随机产生一串密钥key并使用SM4公钥加密,将该密钥放入HTTP请求头
sm4-key
- 请求加密
- 前端对GET的Query参数param1=value1¶m2=value2用上述随机串key进行SM4加密(注意param要排序)
- 前端对POST的JSON Body用上述key进行SM4加密
- 前端GET请求的加密参数附加在URL?data=xxx中,POST请求的加密参数也是以JSON格式放在data这个key中
- 加密后以Base64编码(GET请求要encodeURI,Base64含有+=/等符号)
- 请求头添加sym-alg,标识加密算法SM4
- 请求签名(加密后的,非空参数值才参与签名)
- 生成当前时间戳,为UTC 1970年1月1日0时开始的毫秒数(Unix 时间戳)
- 随机生成nonce随机串,注意要保证随机唯一性
- GET请求签名sha256(path+param1=value1¶m2=value2…+key+timestamp+nonce)
- POST请求签名sha256(POST签名体 = path+param1=value1¶m2=value2…+SHA-256=sha256(body)) +key+timestamp+nonce)
- SHA256是16进制字符串格式
- timestamp、signature、nonce、algorithm(默认SHA-256)放入Header
- 签名算法支持MD2、MD5、SHA-1、SHA-256
- 响应解密
- 响应体的加密内容在JSON串的data这个key中,使用SM4解密
后端流程
- 验证签名
- 后端根据URL规则和macula.gateway.force-sign判断是否强制验证签名
- 后端根据验签规则验证签名,不通过则返回错误
- 请求解密
- 后端根据macula.gateway.force-crypto判断是否强制加解密,如果请求URL在列表中但是没有携带sm4-key则返回错误
- 后端解密请求数据,然后将加密的返回数据替换Result的data
- 响应加密
- 根据需要对响应加密,放入Result的data字段中
配置System的Redis
网关的URL与角色对应关系数据、应用数据是缓存在macula-cloud的system模块的redis中,需要配置system的redis。利用多redis配置方式来进行配置:
/**
* {@code RedisConfiguration} Redis配置
*
* @author rain
* @since 2023/4/21 11:50
*/
@Configuration
public class RedisConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.redis")
public RedisProperties redisProperties() {
return new RedisProperties();
}
@Bean
@ConfigurationProperties(prefix = "spring.redis.system")
public RedisProperties sysRedisProperties() {
return new RedisProperties();
}
@Primary
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(ApplicationContext ctx, RedisProperties redisProperties) throws Exception {
Config config = RedissonConfigBuilder.create().build(ctx, redisProperties, new RedissonProperties());
return Redisson.create(config);
}
@Bean(destroyMethod = "shutdown")
public RedissonClient sysRedissonClient(ApplicationContext ctx, RedisProperties sysRedisProperties)
throws Exception {
Config config = RedissonConfigBuilder.create().build(ctx, sysRedisProperties, new RedissonProperties());
return Redisson.create(config);
}
@Bean(name = "sysRedisTemplate")
public RedisTemplate<String, Object> sysRedisTemplate(
@Qualifier("sysRedissonClient") RedissonClient sysRedissonClient) {
//数据泛型类型
RedisTemplate<String, Object> template = new RedisTemplate<>();
//设置连接工厂(Jedis或Lettuce)
template.setConnectionFactory(new RedissonConnectionFactory(sysRedissonClient));
//设置key的序列化方式---String
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
//初始化RedisTemplate的参数设置
template.afterPropertiesSet();
return template;
}
}
开启CORS
spring:
cloud:
gateway:
globalcors: # 全局CORS配置,适用于所有路由
cors-configurations:
'[/**]': # 匹配所有路径的请求,这里的'/**'表示所有路径
allowed-origin-patterns: "*" # 允许所有来源(使用通配符*),可根据需要限制特定域名
allowed-methods: "*" # 允许所有HTTP方法(GET, POST, PUT, DELETE等),可以指定具体的方法
allowed-headers: "*" # 允许所有请求头,或指定特定请求头
allow-credentials: true # 是否允许客户端发送cookie或其他凭证
max-age: 1800 # 预检请求的缓存时间,单位为秒(1800秒=30分钟),在这个时间内,浏览器可以缓存CORS的预检请求结果,不必每次都发送预检请求
add-to-simple-url-handler-mapping: true # 是否将CORS配置应用于简单的URL处理器映射(通常用于非路由路径)
依赖引入
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>dev.macula.boot</groupId>
<artifactId>macula-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>dev.macula.boot</groupId>
<artifactId>macula-boot-commons</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
版权说明
- oauth2-oidc-sdk:https://github.com/hidglobal/oauth-2.0-sdk-with-openid-connect-extensions/blob/master/LICENSE.txt
- caffeine:https://github.com/ben-manes/caffeine/blob/master/LICENSE
- spring-cloud-gateway:https://github.com/spring-cloud/spring-cloud-gateway/blob/main/LICENSE.txt