微服务系列(一)——Gateway

645 阅读4分钟

什么是微服务网关

Gateway用于寻址路由、认证授权、保护微服务安全而产生的一个中间系统。我们通常一个功能需要访问多个微服务来实现,这会导致调用混乱(ip+端口+url因素),所以加入网关来进行统一寻址和路由。

网关能做什么

  1.统一处理跨域请求

  2.认证和授权JWT

  3.寻址和路由

  4.限流保护微服务

如何搭建网关

(1)pom文件引入依赖

    <dependencies>
        <!--网关依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--hystrix限流-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--eureka注册客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

(2)常用的yml配置

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 匹配所有请求
              allowedOrigins: "*" #跨域处理 允许所有的域
              allowedMethods: # 支持的方法
                - GET
                - POST
                - PUT
                - DELETE
      routes:
            - id: gateway_route
              uri: lb://users #lb表示使用负载均衡,users指的是启动的users微服务名称
              predicates:
              - Path=/api/users/**  #访问路径
              filters:
              - StripPrefix=1  #访问路径截取前一位
              - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的facatory
                args:
                  key-resolver: "#{@ipKeyResolver}"
                  redis-rate-limiter.replenishRate: 10
                  redis-rate-limiter.burstCapacity: 10

  application:
    name: gateway-web
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

(2)相关的配置代码

/***
 * IP限流
 * @return
 */
@Bean(name="ipKeyResolver")
public KeyResolver userKeyResolver() {
    return new KeyResolver() {
        @Override
        public Mono<String> resolve(ServerWebExchange exchange) {
            //获取远程客户端IP
            String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
            return Mono.just(hostName);
        }
    };
}

网关测试

当我们访问user微服务时(例如:127.0.0.1:10086/users/login),可以通过访问127.0.0.1:8001/api/users/login网关url间接访问用户服务

什么是JWT

JWT(javawebtoken)我们可以理解为令牌。以token形式存在,用户认证和授权,jwt包含三部分(头部,载荷,签名)头部存放的是加密类型和加密算法,载荷存放默认信息和要传递的信息,签名(头部+载荷+秘钥加密)用于保证传输的安全性

JWT工具包和依赖

<!--鉴权-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时

    //Jwt令牌信息
    public static final String JWT_KEY = "itcast";

    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();
    }
}

网关整合JWT

自定义拦截器进行拦截 MyAuthorizeFilter,检测和校验jwt

package com.changgou.filter;

import com.changgou.utils.JwtUtil;
import io.jsonwebtoken.Claims;
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.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class MyAuthorizeFilter implements GlobalFilter, Ordered {

    //令牌头名字
    private static final String AUTHORIZE_TOKEN = "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();

        //获取请求的URI
        String path = request.getURI().getPath();

        //如果是登录、goods等开放的微服务[这里的goods部分开放],则直接放行,这里不做完整演示,完整演示需要设计一套权限系统
        if (path.startsWith("/api/user/login")) {
            //放行
            Mono<Void> filter = chain.filter(exchange);
            return filter;
        }
        //获取头文件中的令牌信息
        String tokent = request.getHeaders().getFirst(AUTHORIZE_TOKEN);

        //如果头文件中没有,则从请求参数中获取
        if (StringUtils.isEmpty(tokent)) {
            tokent = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
        }

        //cookie中获取令牌
        if (StringUtils.isEmpty(tokent)) {
            HttpCookie first = request.getCookies().getFirst(AUTHORIZE_TOKEN);
            if (first!=null){
                tokent = first.getValue();

            }
        }

        //如果为空,则输出错误代码
        if (StringUtils.isEmpty(tokent)) {
            //设置方法不允许被访问,405错误代码
            response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
            return response.setComplete();
        }

        //解析令牌数据
        try {
            Claims claims = JwtUtil.parseJWT(tokent);
            //存放在头中
            request.mutate().header(AUTHORIZE_TOKEN,claims.toString());
        } catch (Exception e) {
            e.printStackTrace();
            //解析失败,响应401错误
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //放行
        return chain.filter(exchange);
    }

    /**
     * 过滤器执行顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

编写Contorller测试JWT

@GetMapping("/login")
   public Result login(@RequestParam("username")String username, @RequestParam("password")String password, HttpServletResponse response){
       //判断用户名或者密码,不能为空
       if (StringUtils.isEmpty(username)|| StringUtils.isEmpty(password)){
           return new Result(false,StatusCode.LOGINERROR,"用户名或者密码不能为空",null);
       }
       //获取user对象
       User user = userService.findById(username);
       //校验用户名和密码
       if (!StringUtils.isEmpty(user) && BCrypt.checkpw(password,user.getPassword())){

           //jwt保持会话
           Map<String,Object> content  = new HashMap<>();
           content.put("role","user");
           content.put("username",user.getUsername());
           content.put("level",user.getUserLevel());
           //生成令牌
           String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(content), null);
           //设置cookie
           Cookie cookie = new Cookie("Authorization",jwt);
           cookie.setPath("/");
           //cookie.setDomain("changgou"); //域谨慎设置,容易导致cookie获取不到问题
           //添加cookie
           response.addCookie(cookie);

           return new Result(true,StatusCode.OK,"登录校验成功",user);
       }
       return new Result(false,StatusCode.LOGINERROR,"账号或者密码错误!");
   }

JWT注意事项

(1)在存放cookie的时候要谨慎设置域和路径,容易导致拦截器获取不到cookie的问题,鉴权失败 (2)网关依赖需要排除springboot-start-web依赖,否则容易报spirngmvc的错误