网关是介于用户和微服务之前的中间层。说白了,网关就像是小区的保安,无论你想到小区的哪一户人家去,你都得先通过小区的大门。所以,小区的保安可以做人员统计,还可以控制某个时间段进去小区的人数,限制进入小区的资格等。保证了小区业主们的安全。微服务网关同样起着这些作用。
不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
那么有了微服务网关之后,这些问题就可以得到解决。它有着以下优点。
总结: 微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能 。
一个项目中可能会用到不止一个网关,所以我们将网关微服务放在changgou-gateway父工程下。现在我们创建一个名为changou-gateway- web的微服务。有些依赖是所有网关微服务都要用到的,所以将这些依赖放在父工程下:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> <version>2.1.3.RELEASE</version> </dependency>
启动类和配置文件不能少,启动类就不贴了,配置文件如下👇
spring: application: name: gateway-web cloud: gateway: globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowedOrigins: "*" #跨域处理 允许所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE server: port: 8001 eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true
Host 路由
# 用户请求的域名规格配置,所有以robod.changgou.com开头的请求都将被路由到http://localhost:18081微服务 # 例如 http://robod.changgou.com:8001/brand ——> http://localhost:18081/brand # 但是首先得在hosts文件中配置一下: 127.0.0.1 robod.changgou.com spring: cloud: gateway: routes: - id: changgou_goods_route # 唯一标识符 uri: http://localhost:18081 predicates: - Host=robod.changgou.com**
- Path 路径匹配过滤配置
# 所有以/brand开头的请求都将路由到http://localhost:18081 # 例如 localhost:8001/brand ——> localhost:18081/brand spring: cloud: gateway: routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: - Path=/brand/**
PrefixPath 过滤配置
# 自动加上某个前缀,用户请求/** ——>/brand/** # 例如 localhost:8001/111 ——> localhost:8001/brand/111 ——> localhost:18081/brand/111 spring: cloud: gateway: routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: - Path=/** filters: - PrefixPath=/brand
StripPrefix 过滤配置
# 将请求路径中的前n个路径去掉,请求路径以/区分,一个/代表一个路径 # 例如 localhost:8001/api/brand/111 ——> localhost:8001/brand/111 ——> localhost:18081/brand/111 spring: cloud: gateway: routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: - Path=/** filters: - StripPrefix=1
LoadBalancerClient 路由过滤器(客户端负载均衡)
# 使用LoadBalancerClient实现负载均衡,后面的goods是微服务的名称,主要应用于集群环境 # 比如现在有5台服务器都是goods微服务,网关就会自动将请求发送给不同的服务器达到负载均衡的目的 spring: cloud: gateway: routes: - id: changgou_goods_route uri: lb://goods
当访问量多大的时候,我们的服务就可能会挂掉,所以我们需要对每个微服务进行限流,但是这样比较麻烦。有了网关之后,我们可以对网关进行限流,因为所有的请求必须通过网关才能到达微服务,这样比较方便。
常见的限流算法有计数器,漏斗,令牌桶算法。令牌桶算法有以下几个特点:
spring cloud gateway 默认使用redis的RateLimter限流算法来实现。首先在changgou-gateway- web中添加Redis的依赖:
<!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> <version>2.1.3.RELEASE</version> </dependency>
然后我们需要有限流的Key,这里用IP来当作限流的Key,限制某一个IP在一定时间段的访问次数,在 启动类中定义一个Bean用于获取key :
@Bean(name = "ipKeyResolver") public KeyResolver userKeyResolver() { return exchange -> { String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName(); return Mono.just(ip); }; }
我这里使用了Lamda去简化书写。 接下来还得在配置文件中配置一下 :
spring: application: name: gateway-web cloud: gateway: routes: filters: - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的factory args: # 用户身份唯一标识符 key-resolver: "#{@ipKeyResolver}" # 允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率 redis-rate-limiter.replenishRate: 1 # 令牌桶的容量,允许在一秒钟内完成的最大请求数 redis-rate-limiter.burstCapacity: 1
既然是使用redis的RateLimter限流算法,那么Redis的配置自然不能少。
#Redis配置 spring: application: redis: host: 192.168.31.200 port: 6379
限流的配置就配置好了,现在如果在1秒内请求超过1次的话就会被拒绝。
在实现用户登录功能之前,我们先来介绍一下JWT(JSON Web Token)。是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。为了能够直观的看到JWT的结构,我画了一张思维导图:
最终生成的JWT令牌就是下面这样,有三部分,用 . 分隔。
.
base64UrlEncode(JWT 头)+"."+base64UrlEncode(载荷)+"."+HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密钥) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
base64UrlEncode(JWT 头)+"."+base64UrlEncode(载荷)+"."+HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密钥)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
public String createToken() { JwtBuilder builder = Jwts.builder() .setId("test1") .setSubject("Robod") .setAudience("马化腾") .setIssuedAt(new Date()); .signWith(SignatureAlgorithm.HS256,"robod666"); Map<String,Object> map = new HashMap<>(); map.put("ha","哈哈哈"); builder.addClaims(map); return builder.compact(); }
public String parseToken() { String compactJwt="eyJhbGciOiJIUzI1NiJ9" + ".eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9" + ".RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4"; Claims claims = Jwts.parser(). setSigningKey("robod666"). parseClaimsJws(compactJwt). getBody(); return claims.toString(); }
介绍了JWT之后,我们就来用JWT实现用户登录与鉴权。流程如下:
首先我们需要 准备一个JWT的工具类,JWTUtil ,放在changgou-common下:
public class JwtUtil { //默认有效期,一个小时 public static final Long JWT_TTL = 3600000L; //Jwt令牌信息 public static final String JWT_KEY = "RobodLee"; //密钥 public static SecretKey secretKey = generalKey(); //生成令牌 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); //封装Jwt令牌信息 JwtBuilder builder = Jwts.builder() .setId(id) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("robod") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm,secretKey) // 签名算法以及密匙 .setExpiration(expDate); // 设置过期时间 return builder.compact(); } //生成加密 secretKey public static SecretKey generalKey() { byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes()); return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); } //解析令牌 public static Claims parseJWT(String jwt) throws Exception { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
我发现资料提供的代码中每次调用generalKey()、parseJWT()方法的时候都去调用generalKey()方法去生成SecretKey,但是generalKey()方法内容是不变的,所以可以将SecretKey单独提取出来,这样就不用每次都调用generalKey()去生成了。
然后 创建一个用户微服务changou-service-user , 在UserController中编写登录逻辑 👇
@RequestMapping("/login") public Result<String> login(String username, String password, HttpServletResponse response) { User user = userService.findById(username); if (BCrypt.checkpw(password,user.getPassword())){ Map<String,Object> tokenInfo = new HashMap<>(4); tokenInfo.put("role","USER"); tokenInfo.put("success","SUCCESS"); tokenInfo.put("username",username); String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(tokenInfo), null); Cookie cookie = new Cookie("Authorization",token); cookie.setDomain("localhost"); cookie.setPath("/"); response.addCookie(cookie); return new Result<>(true,StatusCode.OK,"登录成功",token); } return new Result<>(false,StatusCode.LOGIN_ERROR,"登录失败"); }
在这段代码中,调用Service层从数据库中查出对应的User,然后比对password,看密码是否正确。如果正确,就调用JwtUtil创建一个JWT令牌,并放入一些简单的信息。然后将JWT令牌存入Cookie中,并返回给前端。如果登录失败就返回登录失败的信息。
然后就是在网关微服务中添加相应的逻辑了,在changgou-gateway-web中配置一下,配置一下User微服务的路由。
spring: application: name: gateway-web cloud: gateway: routes: - id: changgou_user_route # 唯一标识符 uri: http://localhost:18088 predicates: - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/** filters: - StripPrefix=1
再添加一个过滤器:
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { private static final String AUTHORIZE_TOKEN = "Authorization"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); String token; //从头中获取Token token = request.getHeaders().getFirst(AUTHORIZE_TOKEN); //请求头中没有Token就从参数中获取 if (StringUtils.isEmpty(token)){ token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN); } //参数中再没有Token就从Cookie中获取 if (StringUtils.isEmpty(token)){ HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN); if (cookie!=null){ token = cookie.getValue(); } } //还是没有Token就拦截 if (StringUtils.isEmpty(token)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //Token不为空就校验Token try { JwtUtil.parseJWT(token); } catch (Exception e) { //报异常说明Token是错误的,拦截 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
这段代码就是分别从Header,参数,Cookie中看有没有Token信息,没有的话就说明用户没有权限,拦截下来。有Token的话就解析一下Token有没有错,错误就拦截下来。如果都没有问题的话就放行,将请求路由到用户微服务中。
这是没有Token的情况下👆
当我们登陆后就会获取到Token👇
当我们携带着token去访问就没有问题了👇
原文链接:https://www.cnblogs.com/robod/p/13440603.html