blog

マイクロサービス・ゲートウェイとJWTトークン

ゲートウェイはユーザーとマイクロサービスの間の中間層です。大げさに言えば、ゲートウェイは近所の警備員のようなもので、近所のどの家に行くにしても、まず近所の門を通らなければなりません。そのため、コミュニ...

May 7, 2020 · 13 min. read
シェア

マイクロサービスゲートウェイ

ゲートウェイはユーザーとマイクロサービスの間の中間層です。大げさに言えば、ゲートウェイは近所の警備員のようなもので、近所のどの家に行くにしても、まず近所の門を通らなければなりません。そのため、近隣の警備員は人事統計を行うだけでなく、ある時間に近隣に入る人の数をコントロールしたり、近隣に入る資格を制限したりすることができます。これにより、近隣の所有者の安全が確保されます。マイクロサービスゲートウェイは、このような役割も果たします。

なぜマイクロサービスゲートウェイなのか

一般的に異なるマイクロサービスは異なるネットワークアドレスを持っており、外部クライアントはビジネス要件を完了するために複数のサービスのインターフェイスを呼び出す必要があるかもしれません。 クライアントが個々のマイクロサービスと直接通信することが許可されている場合、次のような問題が発生します:

  • クライアントは異なるマイクロサービスを何度もリクエストし、クライアントの複雑さを増大させます。
  • クロスドメインリクエストがありますが、これは特定のシナリオで処理するのが比較的複雑です。
  • 認証は複雑で、各サービスは独立した認証を必要とします。
  • 再設定が難しい プロジェクトが反復するにつれて、マイクロサービスを再分類する必要があるかもしれません。例えば、複数のサービスを1つに統合したり、1つのサービスを複数に分割したりすることが考えられます。クライアントがマイクロサービスと直接通信する場合、リファクタリングの実装は困難です。
  • マイクロサービスによっては、ファイアウォールやブラウザに不親切なプロトコルを使用している場合があり、直接アクセスするのが難しい場合があります。

そこで、Microservices Gatewayを使えば、これらの問題を解決することができます。次のような利点があります。

  • セキュリティ , ゲートウェイシステムだけが外部に公開され、マイクロサービスはイントラネットの中に隠すことができ、ファイアウォールで保護されます。
  • モニタリングが容易。監視データはゲートウェイで収集し、分析用に外部システムにプッシュすることができます。
  • 簡単な認証。リクエストはゲートウェイで認証され、各マイクロサービスで認証することなく、バックエンドのマイクロサービスに転送されます。
  • クライアントと個々のマイクロサービス間のインタラクションの数を削減します。
  • 権限の統一が容易

要約:マイクロサービスゲートウェイは、システム、マイクロサービスゲートウェイシステムの公開を介して、関連する認証、セキュリティ制御、統一されたログ処理を容易にするために、関連する機能を監視することです。

ゲートウェイマイクロサービス

マイクロサービス構築

プロジェクト内で複数のゲートウェイを使うことができるので、ゲートウェイのマイクロサービスを changgou-gateway の親プロジェクトの下に置いてください。次に changou-gateway-web というマイクロサービスを作成します。すべてのゲートウェイマイクロサービスで使用される依存関係がいくつかあるので、それらを親プロジェクトの下に置きます:

xml
<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>

スタートアップクラスとコンフィギュレーションファイルが欠落することはできません、スタートアップクラスが投稿されません、コンフィギュレーションファイルは次のとおりです。

yml
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:7100"/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true

ゲートウェイフィルタリングの設定

  • ホストルーティング
# ユーザーが要求するドメイン指定コンフィギュレーション、すべてrobodを使用する.changgou.comで始まるリクエスト"http://localhost:18180" 
#   "http://robod.changgou.com:8100"/brand  > "http://localhost:18180"/brand
# しかし、その前にhostsファイルに設定する必要がある: 127.0.0.1 robod.changgou.com
spring:
 cloud:
 gateway:
 routes:
 - id: changgou_goods_route # 一意な識別子
 uri: "http://localhost:18180"
 predicates:
 - Host=robod.changgou.com**
  • - パスマッチングフィルタの設定
#  /brandで始まるリクエスト"http://localhost:18180"
#   localhost:8001/brand  > localhost:18081/brand
spring:
 cloud:
 gateway:
 routes:
 - id: changgou_goods_route
 uri: "http://localhost:18180"
 predicates:
 - Path=/brand/**
  • PrefixPathフィルタリングの設定
yml
# ユーザーリクエストに特定の接頭辞を自動的に追加する/** >/brand/** # localhost:8001/111 > localhost:8001/brand/111 > localhost:18081/brand/111 spring: cloud: gateway: routes: - id: changgou_goods_route uri: "http://localhost:18180" predicates: - Path=/** filters: - PrefixPath=/brand
  • StripPrefix フィルタの設定
yml
# リクエストパスから最初の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:18180" predicates: - Path=/** filters: - StripPrefix=1
  • LoadBalancerClient ルーティング・フィルター
yml
# ロードバランシングは、LoadBalancerClientと、マイクロサービスの名前であり、主にクラスタ環境で使用されるgoodsを使用して達成される。 # 例えば、マイクロサービスを提供するサーバーが5つある場合、ゲートウェイは負荷分散のために自動的にリクエストを異なるサーバーに送る。 spring: cloud: gateway: routes: - id: changgou_goods_route uri: lb://goods

ゲートウェイ流量制限

アクセス数が多くなるとサービスがハングアップする可能性があるので、マイクロサービスごとにフローを制限する必要がありますが、これはより面倒です。ゲートウェイを使えば、マイクロサービスに到達するためにすべてのリクエストがゲートウェイを通過しなければならないので、ゲートウェイへのフローを制限するのは簡単です。

トークンバケットアルゴリズム

一般的な流量制限アルゴリズムには、カウンター、ファンネル、トークンバケットアルゴリズムがあります。トークンバケットアルゴリズムには以下の特徴があります:

  • すべてのリクエストは、処理される前に利用可能なトークンを取得する必要があります;
  • フローリミットのサイズに応じて、一定の割合でバケツにトークンが追加されます;
  • バケツには配置されるトークンの上限が設定され、バケツがいっぱいになると、新しく追加されたトークンは破棄されるか拒否されます;
  • リクエストに到達したら、まずトークン・バケットにトークンを取得し、トークンを取って他のビジネスロジックを実行し、ビジネスロジックの処理後はトークンを直接削除します;
  • トークンのバケットには最小限の制限があり、バケット内のトークンが最小限の制限に達した場合、トークンはリクエストが処理された後も削除されないため、十分なフロー制限が保証されます。

トークン・バケットを使用してリクエスト数を制限するフロー

xml
<!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> <version>2.1.3.RELEASE</version> </dependency>

そして、Keyのフローを制限する必要があります。ここでは、Keyのフローを制限するためのKeyとしてIPを使用し、一定期間内にIPにアクセスできる回数を制限するために、startupクラスでKeyを取得するためのBeanを定義します:

java
@Bean(name = "ipKeyResolver") public KeyResolver userKeyResolver() { return exchange -> { String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName(); return Mono.just(ip); }; }

ここでは、記述を簡単にするためにLamdaを使いました。次に、コンフィグファイルで行うちょっとした設定があります

yml
spring: application: name: gateway-web cloud: gateway: routes: filters: - name: RequestRateLimiter #リクエストの数を制限する。 名前は任意ではなく、デフォルトの工場出荷時のものを使用する。 args: # ユーザーID ユニーク識別子 key-resolver: "#{@ipKeyResolver}" # ユーザーが1秒間にどれだけのリクエストをドロップせずに実行できるかを示す。これはトークン・バケツが満たされる速度である。 redis-rate-limiter.replenishRate: 1 # トークンバケットの容量、1秒間に完了できるリクエストの最大数 redis-rate-limiter.burstCapacity: 1

redisのRateLimterフロー制限アルゴリズムを使っているのですから、redisの設定が欠けてはいけないのは当然です。

yml
#Redis spring: application: redis: host: 192.168.31.200 port: 6379

フローを制限するためのコンフィギュレーションが設定され、1秒間に1回以上リクエストされると拒否されるようになりました。

JWT

ユーザーログイン機能を実装するにあたり、まずJWT(JSON Web Token)を紹介しましょう。JWT(JSON Web Token)とは、2つの通信当事者間で安全な情報をやり取りするために使用される、簡潔でURLセーフな表現宣言の仕様です。

JWTの構成

JWTは実際には文字列で、ヘッダー、ロード、シグネチャの3つの部分から構成されています。JWTの構造を視覚化するために、マインドマップを描きました:

最終的に生成されるJWTトークンは以下のようになり、3つの部分が. で区切られています。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWTの使用

  • 依存関係をインポートします:
xml
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
  • トークンの作成
java
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(); }
  • トークンの解析
java
public String parseToken() { String compactJwt="eyJhbGciOiJIUzI1NiJ9" + ".eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9" + ".RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4"; Claims claims = Jwts.parser(). setSigningKey("robod666"). parseClaimsJws(compactJwt). getBody(); return claims.toString(); }

ユーザーログインと認証

JWTを紹介した後、JWTを使ってユーザーログインと認証を実装してみましょう。手順は以下の通りです:

まず、changgou-common配下にJWTツールクラスJWTUtilを用意します:

java
public class JwtUtil { //デフォルトの有効期限、1時間 public static final Long JWT_TTL = 3600000L; //Jwtトークン情報 public static final String JWT_KEY = "RobodLee"; // public static SecretKey secretKey = generalKey(); //トークンを生成する public static String createJWT { //アルゴリズムを指定する 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 throws Exception { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }

ソースで提供されているコードでは、generalKey()メソッドやparseJWT()メソッドが呼ばれるたびにSecretKeyを生成するためにgeneralKey()メソッドを呼びに行っていましたが、generalKey()メソッドの中身はそのままなので、SecretKeyを別途抽出することで、毎回generalKey()メソッドを呼んでSecretKeyを生成する必要がなくなります。generalKey() で生成します。

次に、ユーザーマイクロサービス changou-service-user を作成し、UserController にログインロジックを記述します。

java
@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を検索し、パスワードが正しいかどうかを比較します。もし正しければ、JwtUtilを呼び出してJWTトークンを作成し、簡単な情報を入れます。その後、JWTトークンがクッキーに保存され、フロントエンドに返されます。ログインに失敗した場合は、ログイン失敗のメッセージを返します。

次に、ゲートウェイマイクロサービスに適切なロジックを追加し、changgou-gateway-webで設定し、Userマイクロサービスのルーティングを設定します。

yml
spring: application: name: gateway-web cloud: gateway: routes: - id: changgou_user_route # 一意な識別子 uri: "http://localhost:18880" predicates: - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/** filters: - StripPrefix=1

別のフィルターを追加します:

java
@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 = request.getHeaders().getFirst(AUTHORIZE_TOKEN); //リクエストヘッダにTokenがない場合は、パラメータから取得する。 if (StringUtils.isEmpty(token)){ token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN); } //パラメータにトークンがない場合は、クッキーから取得する。 if (StringUtils.isEmpty(token)){ HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN); if (cookie!=null){ token = cookie.getValue(); } } //まだトークンなしでインターセプトしている if (StringUtils.isEmpty(token)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //TokenNULLでなければチェックサム・トークン try { JwtUtil.parseJWT(token); } catch (Exception e) { //トークンが間違っているという例外を報告する。 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }

このコードはそれぞれHeader、parameters、CookieからTokenの情報があるかどうかを調べます。もし Token があれば、Token を解析してエラーがあるかどうかを調べ、エラーがあればインターセプトします。問題がなければ、リクエストは解放され、ユーザのマイクロサービスにルーティングされます。

Read next

銀行免許自体がデジタル口座を直接サポートできるかどうか

これは、銀行制度システムの特殊な状況に関連しています。つまり、ある商業銀行がデジタル決済にアクセスすれば、他の商業銀行も追随せざるを得なくなり、商業銀行システムを全体的に支えることになります。

May 6, 2020 · 1 min read

関数の照合

May 6, 2020 · 9 min read

SwiftyJSONを使ったHandyJSON

May 6, 2020 · 2 min read

ESLintルールの説明

May 6, 2020 · 16 min read

Js-new

May 5, 2020 · 4 min read