不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口,存在一些问题:
微服务网关的主要作用:
实现微服务网关的技术有很多:
微服务网关项目需要导入的依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
<version>2.2.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
<version>2.2.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
<version>2.2.4.RELEASEversion>
dependency>
dependencies>
application.yml的配置:
spring:
# spring-cloud网关跨域问题
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" # 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
spring:
cloud:
gateway:
routes:
# 唯一标识
- id: changgou_goods_route
# 指定要路由的服务地址
uri: http://localhost:9960
# 路由断言,路由规则配置
predicates:
# 所有以指定域名开始的请求都将被路由到上面指定的服务地址
- Host=cloud.changgou.com**
设置host文件:
127.0.0.1 cloud.changgou.com
访问http://cloud.changgou.com:5656/category
,网关将会把当前请求路由到http://localhost:9960/category
。
spring:
cloud:
gateway:
routes:
# 唯一标识
- id: changgou_goods_route
# 指定要路由的服务地址
uri: http://localhost:9960
# 路由断言,路由规则配置
predicates:
# 所有以/brand/路径开头的请求都将路由到上面指定的服务地址
- Path=/brand/**
访问http://localhost:5656/brand
,网关将会把当前请求路由到http://localhost:9960/brand
。
有些情况下,我们可以给真实请求加一个统一的前缀,例如,所有微服务路径统一以api开头。
spring:
cloud:
gateway:
routes:
# 唯一标识
- id: changgou_goods_route
# 指定要路由的服务地址
uri: http://localhost:9960
# 路由断言,路由规则配置
predicates:
# 所有以/api/brand开始的请求,都将被路由到上面指定的服务地址,希望该路径由服务网关自动添加/api前缀,每次请求真实微服务网关时,需要使用微服务网关将/api去掉。
- Path=/api/brand/**
filters:
#将请求路径中的第一个路径去掉,请求路径以/区分,一个代表一个路径
- StripPrefix=1
访问http://localhost:5656/api/brand
时,网关将会把当前请求路由到http://localhost:9960/brand
。
spring:
cloud:
gateway:
routes:
# 唯一标识
- id: changgou_goods_route
# 指定要路由的服务地址
uri: http://localhost:9960
# 路由断言,路由规则配置
predicates:
- Path=/**
filters:
# 用户请求/**->/brand/**,并且该请求路由到指定的服务地址
- PrefixPath=/brand
访问http://localhost:5656/
时,网关将会把当前请求路由到http://localhost:9960/brand
。
LoadBalanceClientFilter实现负载均衡调用。它会作用在uri以lb开头的路由,然后利用loadBalancer来获取服务实例,构造目标requestUrl,设置到GATEWAY_REQUEST_URL_ATTR属性中,供NettyRoutingFilter使用。
spring:
cloud:
gateway:
routes:
# 唯一标识
- id: changgou_goods_route
# 使用LoadBalancerClient实现负载均衡
uri: lb://goods
# 路由断言,路由规则配置
predicates:
- Path=/**
filters:
# 用户请求/**->/brand/**,并且该请求路由到指定的服务地址
- PrefixPath=/brand
虽然Nginx会拦截第一波并发流量,但是仍然无法应对用户恶意访问某一个微服务的问题。网关限流可以通过限制每秒钟某一用户并发访问某一微服务的次数,达到保护微服务,防止雪崩的产生。
通常限流采用的算法有:
spring-cloud-gateway默认使用redis的RateLimter限流算法实现。我们可以根据IP进行限流,例如限制每个IP每秒钟只能请求一次,在主程序中定义KeyResolver来获取客户端IP,并根据IP生成key:
/**
* 创建用户唯一标识,使用IP作为用户唯一标识来进行限流
* @return
*/
@Bean("ipKeyResolver")
public KeyResolver userKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
// 获取用户IP
String hostString = exchange.getRequest().getRemoteAddress().getHostString();
System.out.println(hostString);
return Mono.just(hostString);
}
};
}
配置
routes:
# 唯一标识
- id: changgou_goods_route
# 指定要路由的服务地址
# uri: http://localhost:9960
uri: lb://goods
# 路由断言,路由规则配置
predicates:
- Path=/**
filters:
# 用户请求/**->/brand/**,并且该请求路由到指定的服务地址
- PrefixPath=/brand
- name: RequestRateLimiter # 局部限流过滤器
args:
# 指定唯一标识
key-resolver: "#{@ipKeyResolver}"
# 每秒钟只允许一个请求
redis-rate-limiter.replenishRate: 1
# 允许并发有4个请求
redis-rate-limiter.burstCapacity: 1
Ctrl+Shift+N可以找到RequestRateLimiter的过滤器
当并发请求超过配置中的限制时,将会直接返回429。
Api网关实现用户鉴权的流程:
商品微服务和用户微服的路由配置如下:
spring:
cloud:
gateway:
routes:
# 商品微服务唯一标识
- id: changgou_goods_route
# 指定要路由的服务
uri: lb://goods
predicates:
- Path=/api/goods/**
filters:
- StripPrefix=2 # 访问http://localhost:5656/api/goods/category时,将路由到goods微服务,并去掉/api/goods
- name: RequestRateLimiter # 局部限流过滤器
args:
# 指定唯一标识
key-resolver: "#{@ipKeyResolver}"
# 每秒钟只允许1个请求
redis-rate-limiter.replenishRate: 1
# 最大允许并发请求的数量
redis-rate-limiter.burstCapacity: 1
# 用户微服务唯一标识
- id: changgou_user_route
# 指定要路由的服务
uri: lb://user
predicates:
- Path=/api/user/**,/api/address/**,/api/area/**,/api/cities/**,/api/provinces/**
filters:
- StripPrefix=1
JSON Web Tocken (JWT) 是一个非常轻巧的规范,它允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT字符串由三部分组成,通过.
分割:
{"typ":"JWT","alg":"HS256"}
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
{"sub":"1234567890","name":"John Doe","admin":true}
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
.
拼接在一起;定义依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.6.0version>
dependency>
令牌创建帮助类
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时
//Jwt令牌信息
public static final String JWT_KEY = "itcast";
/**
* 生成令牌
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
//指定算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//当前系统时间
long nowMillis = System.currentTimeMillis();
//令牌签发时间
Date now = new Date(nowMillis);
//如果令牌有效期为null,则默认设置有效期1小时
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
//令牌过期时间设置
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
//生成秘钥
SecretKey secretKey = generalKey();
//封装Jwt令牌信息
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("admin") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙(盐)
.setExpiration(expDate); // 设置过期时间
return builder.compact();// 获取令牌信息,每次都不一样,因为签发时间每次都不同
}
/**
* 生成加密 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析令牌数据
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)// 传入秘钥(盐)
.parseClaimsJws(jwt)// 解析令牌
.getBody();// 获取解析的内容
}
public static void main(String[] args) {
String jwt = JwtUtil.createJWT("weiyibiaoshi", "aaaaaa", null);
System.out.println(jwt);
try {
Claims claims = JwtUtil.parseJWT(jwt);
System.out.println(claims);
} catch (Exception e) {
e.printStackTrace();
}
}
}
创建一个全局过滤器,实现GlobalFilter接口和Ordered接口。下面的全局过滤器的处理流程:
import com.changgou.gateway.util.JwtUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpCookie;
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.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 全局过滤器:需要实现GlobalFilter接口和Ordered接口
* 用户权限鉴别过滤器
*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
// 令牌头的名字
private static final String AUTHORIZE_TOCKEN = "Authorization";
/**
* 全局过滤
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取Request、Response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 获取用户令牌信息:可能存在于请求头、请求参数和Cookie中
String jwt = request.getHeaders().getFirst(AUTHORIZE_TOCKEN);
boolean jwtInHeader = true;// 标记jwt是否在请求头
if(StringUtils.isEmpty(jwt)){
jwtInHeader = false;
jwt = request.getQueryParams().getFirst(AUTHORIZE_TOCKEN);
if(StringUtils.isEmpty(jwt)){
HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOCKEN);
if(cookie != null) {
jwt = cookie.getValue();
}
}
}
// 没有令牌,则拦截
if(StringUtils.isEmpty(jwt)){
// 设置401状态码,提示用户没有权限,用户收到该提示后需要重定向到登陆页面
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 响应空数据
return response.setComplete();
}
// 有令牌
try {
JwtUtil.parseJWT(jwt);
} catch (Exception e) {
// 无效令牌
// 设置401状态码,提示用户没有权限,用户收到该提示后需要重定向到登陆页面
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 响应空数据
return response.setComplete();
}
// 令牌正常解析,为了方便在其他微服务进行认证,这里将jwt放入到请求头中
request.mutate().header(AUTHORIZE_TOCKEN, jwt);
// 放行
return chain.filter(exchange);
}
/**
* 排序,越小越先执行
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
直接访问用户微服务正常访问
登陆前,通过微服务网关访问用户微服务返回401状态码。登陆后,通过微服务网关访问用户微服务正常。
另外,对于令牌可能被他人盗用的情况,解决方案如下: