KeyCloak PT 2による角度とクォークの固定

71474 ワード

以前の記事では、KeyCloakのログインとロールの認識を可能にする初期ユーザーインターフェイスを作成しました.次のステップは、安全で、ベースのサービスを提供するバックエンドサービスにこれを配線することです.
この記事のコードはGitHubにあります.
  • Petstore UI
  • Petstore API
  • 典型的なシナリオでは、2種類の異なるサービスを実装します.
  • 店舗従業員が従業員の特定機能を利用するサービス
  • 顧客特有のサービス
  • また、複数のレベルの統合を実装します.今のところ、我々は簡単なものから始め、単一のクォークマイクロサービスを作成し、我々が作成したロールを管理します.
    だから、それをキックしましょう!

    APIクライアントの作成


    KeyCloak Admin UIに戻り、使用するAPIの新しいクライアントを作成します.
    クライアントに移動しCreate :

    クライアントが作成されると、ロールタブに移動してクライアントが想定している役割を作成します.

    そして、我々は前の記事のようにこれらの背中を我々のユーザーに写像したいです.
    グループへ移動します.api-employee 役割

    同じことをするapi-customer 役割

    次に、私たちのサービスAPIで仕事を始めたいです.

    サービスの作成


    私たちはゼロから始め、標準的なクォークサービスを作ります.
    $ mvn io.quarkus:quarkus-maven-plugin:1.7.2.Final:create \
        -DprojectGroupId=com.brightfield.streams \
        -DprojectArtifactId=petstore-api
    
    生命をより簡単にするいくつかの依存関係は以下を含みます:
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-resteasy-jackson</artifactId>
        </dependency>
    
    我々のアプリケーションを確保する前に、何かを実行してみましょう.

    エンドポイントの実装


    3つのエンドポイントが実装されます.
  • ゲット/ペッツ
  • 両方のグループ(店従業員と顧客)によってアクセス可能
  • ゲット/セールス
  • 店員だけがアクセスできる
  • ゲット/リワード
  • ログインしている顧客によってアクセス可能
  • プロジェクトでは、いくつかのリソースと表現を作成します.
    資源ジャバ
    package com.cloudyengineering.pets;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.core.Response;
    
    
    @Path("/v1/pets")
    @Produces("application/json")
    public class PetResource {
    
        @GET
        public Response getPets() {
            List<Pet> pets = new ArrayList<>();
    
            return Response.ok(pets).build();
        }
    }
    
    販売資源ジャバ
    package com.cloudyengineering.pets;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.core.Response;
    
    @Path("/v1/admin")
    @Produces("application/json")
    public class SalesResource {
    
        @GET
        public Response getSales() {
            List<Transaction> transactions = new ArrayList<>();
    
            return Response.ok(transactions).build();
        }
    }
    
    ペットジャバ
    package com.cloudyengineering.pets;
    
    import com.fasterxml.jackson.annotation.JsonProperty;
    
    public class Pet {
    
        @JsonProperty("pet_id")
        private Integer petId;
    
        @JsonProperty("pet_type")
        private String petType;
    
        @JsonProperty("pet_name")
        private String petName;
    
        @JsonProperty("pet_age")
        private Integer petAge;
    
        public Integer getPetId() {
            return petId;
        }
    
        public void setPetId(Integer petId) {
            this.petId = petId;
        }
    
        public String getPetType() {
            return petType;
        }
    
        public void setPetType(String petType) {
            this.petType = petType;
        }
    
        public String getPetName() {
            return petName;
        }
    
        public void setPetName(String petName) {
            this.petName = petName;
        }
    
        public Integer getPetAge() {
            return petAge;
        }
    
        public void setPetAge(Integer petAge) {
            this.petAge = petAge;
        }
    
    }
    
    トランザクション.ジャバ
    package com.cloudyengineering.pets;
    
    import java.util.Date;
    
    import com.fasterxml.jackson.annotation.JsonProperty;
    
    public class Transaction {
    
        @JsonProperty("txn_id")
        private String transactionId;
    
        @JsonProperty("txn_amount")
        private Double transactionAmount;
    
        @JsonProperty("txn_date")
        private Date transactionDate;
    
        @JsonProperty("txn_method")
        private String transactionMethod;
    
        public String getTransactionId() {
            return transactionId;
        }
    
        public void setTransactionId(String transactionId) {
            this.transactionId = transactionId;
        }
    
        public Double getTransactionAmount() {
            return transactionAmount;
        }
    
        public void setTransactionAmount(Double transactionAmount) {
            this.transactionAmount = transactionAmount;
        }
    
        public Date getTransactionDate() {
            return transactionDate;
        }
    
        public void setTransactionDate(Date transactionDate) {
            this.transactionDate = transactionDate;
        }
    
        public String getTransactionMethod() {
            return transactionMethod;
        }
    
        public void setTransactionMethod(String transactionMethod) {
            this.transactionMethod = transactionMethod;
        }
    }
    
    ご覧の通り、これらは単なるプレースホルダですので、いくつかのロジックを埋めましょう.
        @GET
        public Response getPets() {
            List<Pet> pets = new ArrayList<>();
            Pet pet1 = new Pet();
            pet1.setPetId(1);
            pet1.setPetAge(6);
            pet1.setPetName("Oliver");
            pet1.setPetType("Dog");
    
            Pet pet2 = new Pet();
            pet2.setPetId(2);
            pet2.setPetAge(1);
            pet2.setPetName("Buster");
            pet2.setPetType("Cat");
    
            Pet pet3 = new Pet();
            pet3.setPetId(3);
            pet3.setPetAge(2);
            pet3.setPetName("Violet");
            pet3.setPetType("Bird");
    
            pets = Lists.asList(pet1, new Pet[]{pet2, pet3});
    
            return Response.ok(pets).build();
        }
    
    トランザクションリソース.ジャバ
        @GET
        public Response getSales() {
            List<Transaction> transactions = new ArrayList<>();
    
            Transaction txn1 = new Transaction();
            txn1.setTransactionId(UUID.randomUUID().toString());
            txn1.setTransactionAmount(12.56);
            txn1.setTransactionDate(Date.from(Instant.now()));
            txn1.setTransactionMethod("Cash");
    
            Transaction txn2 = new Transaction();
            txn2.setTransactionId(UUID.randomUUID().toString());
            txn2.setTransactionAmount(56.16);
            txn2.setTransactionDate(Date.from(Instant.now()));
            txn2.setTransactionMethod("Credit Card");
    
            Transaction txn3 = new Transaction();
            txn3.setTransactionId(UUID.randomUUID().toString());
            txn3.setTransactionAmount(88.99);
            txn3.setTransactionDate(Date.from(Instant.now()));
            txn3.setTransactionMethod("Credit Card");
    
            transactions = Lists.asList(txn1, new Transaction[]{txn2, txn3});
    
            return Response.ok(transactions).build();
        }
    
    すぐにそれをテストしましょう!
    $ ./mvnw quarkus:dev
    Listening for transport dt_socket at address: 5005
    __  ____  __  _____   ___  __ ____  ______
     --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
     -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
    --\___\_\____/_/ |_/_/|_/_/|_|\____/___/
    2020-10-04 18:30:09,620 INFO  [io.quarkus] (Quarkus Main Thread) pet-store-api 1.0-SNAPSHOT on JVM (powered by Quarkus 1.8.1.Final) started in 3.296s. Listening on: http://0.0.0.0:
    8080
    2020-10-04 18:30:09,622 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
    2020-10-04 18:30:09,622 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy, resteasy-jackson]
    
    $ http :8080/v1/pets
    HTTP/1.1 200 OK
    Content-Length: 188
    Content-Type: application/json
    
    [
        {
            "pet_age": 6,
            "pet_id": 1,
            "pet_name": "Oliver",
            "pet_type": "Dog"
        },
        {
            "pet_age": 1,
            "pet_id": 2,
            "pet_name": "Buster",
            "pet_type": "Cat"
        },
        {
            "pet_age": 2,
            "pet_id": 3,
            "pet_name": "Violet",
            "pet_type": "Bird"
        }
    ]
    
    $ http :8080/v1/admin
    HTTP/1.1 200 OK
    Content-Length: 357
    Content-Type: application/json
    
    [
        {
            "txn_amount": 12.56,
            "txn_date": 1601862235453,
            "txn_id": "cb09b51d-541a-45b5-9c27-13a09b480dfd",
            "txn_method": "Cash"
        },
        {
            "txn_amount": 56.16,
            "txn_date": 1601862235454,
            "txn_id": "2895fe10-31ae-417a-8d7d-28ccdf0fa08b",
            "txn_method": "Credit Card"
        },
        {
            "txn_amount": 88.99,
            "txn_date": 1601862235454,
            "txn_id": "afbec85c-b919-40e4-9b02-9e5779534b0b",
            "txn_method": "Credit Card"
        }
    ]
    
    うーんそれらの日付はあまりにも友好的に見えない、それらを変更しましょう
    トランザクション.ジャバ
        @JsonProperty("txn_date")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
        private Date transactionDate;
    
    そして再び.
    $ http :8080/v1/admin
    HTTP/1.1 200 OK
    Content-Length: 381
    Content-Type: application/json
    
    [
        {
            "txn_amount": 12.56,
            "txn_date": "05-10-2020 01:46:17",
            "txn_id": "8d5e875b-06ec-4cf6-b357-4e14525b831c",
            "txn_method": "Cash"
        },
        {
            "txn_amount": 56.16,
            "txn_date": "05-10-2020 01:46:17",
            "txn_id": "5764bef6-1c78-4efd-8016-119759b263c5",
            "txn_method": "Credit Card"
        },
        {
            "txn_amount": 88.99,
            "txn_date": "05-10-2020 01:46:17",
            "txn_id": "67a5f766-4a9f-4fa4-8e22-59ecf9171e55",
            "txn_method": "Credit Card"
        }
    ]
    
    いいねそれは動作しますが、まだ非常に安全ではないです.

    APIのセキュリティー保護


    2つの依存関係があります.
    POMXML
      <dependencies>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-oidc</artifactId>
        </dependency>
    
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-keycloak-authorization</artifactId>
        </dependency>
      </dependencies>
    
    アプリケーション.プロパティ
    quarkus.oidc.auth-server-url=http://localhost:8081/auth/realms/petshop-realm
    quarkus.oidc.client-id=pet-store-api
    quarkus.oidc.credentials.secret=itsasecret
    quarkus.oidc.authentication.scopes=profile
    
    quarkus.http.cors.origins=http://localhost:4200
    quarkus.http.cors.methods=GET,OPTIONS
    quarkus.http.cors=true
    
    ご覧の通り、クライアントIDを宣言しましたpetshop-api しかし、我々も秘密を供給しましたitsasecret , が設定されていることを確認しましょう!
    移動先:クライアント->ペットストアAPI
    アクセスタイプをbearer only とヒットを保存します.あなたは、上記のタブを見るべきです!

    次のタブの資格情報に移動し、クライアントの秘密を参照してください.

    この生成された秘密をペットストアAPIにコピーしましょう.
    quarkus.oidc.auth-server-url=http://localhost:8081/auth/realms/petshop-realm
    quarkus.oidc.client-id=pet-store-api
    quarkus.oidc.credentials.secret=05da844f-c975-4571-8767-cbc8078e7b64
    quarkus.oidc.authentication.scopes=profile
    
    APIを今すぐ実行してみて、それにアクセスしてみると、私たちは不正な401を得るべきです.
    $ http :8080/v1/admin
    HTTP/1.1 200 OK
    Content-Length: 381
    Content-Type: application/json
    
    [
        {
            "txn_amount": 12.56,
            "txn_date": "06-10-2020 09:34:58",
            "txn_id": "a19f4089-5f20-4355-ab4a-a18367347d6d",
            "txn_method": "Cash"
        },
        {
            "txn_amount": 56.16,
            "txn_date": "06-10-2020 09:34:58",
            "txn_id": "6457746c-9c07-4cea-b8c5-fb30dc4e2f3d",
            "txn_method": "Credit Card"
        },
        {
            "txn_amount": 88.99,
            "txn_date": "06-10-2020 09:34:58",
            "txn_id": "09125408-0787-41d0-9976-8e56c5937bb3",
            "txn_method": "Credit Card"
        }
    ]
    
    ちょっと待って!それは違いますああ覚えて、我々のエンドポイントの周りのセキュリティを指定していない!コードを更新しましょう
    販売資源ジャバ
    @Path("/v1/admin")
    @Produces("application/json")
    public class SalesResource {
    
        @GET
        @RolesAllowed({"api-employee"})
        public Response getSales() {
            List<Transaction> transactions;
    
            Transaction txn1 = new Transaction();
            txn1.setTransactionId(UUID.randomUUID().toString());
            txn1.setTransactionAmount(12.56);
            txn1.setTransactionDate(Date.from(Instant.now()));
            txn1.setTransactionMethod("Cash");
    
            Transaction txn2 = new Transaction();
            txn2.setTransactionId(UUID.randomUUID().toString());
            txn2.setTransactionAmount(56.16);
            txn2.setTransactionDate(Date.from(Instant.now()));
            txn2.setTransactionMethod("Credit Card");
    
            Transaction txn3 = new Transaction();
            txn3.setTransactionId(UUID.randomUUID().toString());
            txn3.setTransactionAmount(88.99);
            txn3.setTransactionDate(Date.from(Instant.now()));
            txn3.setTransactionMethod("Credit Card");
    
            transactions = Lists.asList(txn1, new Transaction[]{txn2, txn3});
    
            return Response.ok(transactions).build();
        }
    }
    
    資源ジャバ
    @Path("/v1/pets")
    @Produces("application/json")
    public class PetResource {
    
        @GET
        @RolesAllowed({"api-customer"})
        public Response getPets() {
            List<Pet> pets;
            Pet pet1 = new Pet();
            pet1.setPetId(1);
            pet1.setPetAge(6);
            pet1.setPetName("Oliver");
            pet1.setPetType("Dog");
    
            Pet pet2 = new Pet();
            pet2.setPetId(2);
            pet2.setPetAge(1);
            pet2.setPetName("Buster");
            pet2.setPetType("Cat");
    
            Pet pet3 = new Pet();
            pet3.setPetId(3);
            pet3.setPetAge(2);
            pet3.setPetName("Violet");
            pet3.setPetType("Bird");
    
            pets = Lists.asList(pet1, new Pet[]{pet2, pet3});
    
            return Response.ok(pets).build();
        }
    }
    
    また試してみてください.
    $ http :8080/v1/admin
    HTTP/1.1 401 Unauthorized
    Content-Length: 0
    $ http :8080/v1/pets
    HTTP/1.1 401 Unauthorized
    Content-Length: 0
    
    すごい!それは安全です!
    準備は、ユーザーインターフェイスを統合するには?

    セキュアサービスとUIの接続


    ユーザーインターフェイスを訪問したとき、ユーザーのログイン作業を行うことができました.UIへの変更は以下のようになります.
  • 顧客と従業員のためにペットをリストするために、新しい見解を加えてください
  • 従業員のためにリストを売るために新しい見解を加えてください
  • 顧客のための報酬をリストに新しいビューを追加
  • 別のロールを試してみないと不正行為を回避し、バイパスを確認するAuthGuardに追加
  • ビューの作成


    新しいビューを作成し始めましょう!
    新しいPetSponent , SalesComoponent , RewardsComoponentを作成して起動します.
    $ ng g c pet
    CREATE src/app/pet/pet.component.css (0 bytes)
    CREATE src/app/pet/pet.component.html (18 bytes)
    CREATE src/app/pet/pet.component.spec.ts (605 bytes)
    CREATE src/app/pet/pet.component.ts (263 bytes)
    UPDATE src/app/app.module.ts (947 bytes)
    $ ng g c sales
    CREATE src/app/sales/sales.component.css (0 bytes)
    CREATE src/app/sales/sales.component.html (20 bytes)
    CREATE src/app/sales/sales.component.spec.ts (619 bytes)
    CREATE src/app/sales/sales.component.ts (271 bytes)
    UPDATE src/app/app.module.ts (1025 bytes)
    $ ng g c rewards
    CREATE src/app/rewards/rewards.component.css (0 bytes)
    CREATE src/app/rewards/rewards.component.html (22 bytes)
    CREATE src/app/rewards/rewards.component.spec.ts (633 bytes)
    CREATE src/app/rewards/rewards.component.ts (279 bytes)
    UPDATE src/app/app.module.ts (1111 bytes)
    $ ng g guard auth
    ? Which interfaces would you like to implement? CanActivate
    CREATE src/app/auth.guard.spec.ts (331 bytes)
    CREATE src/app/auth.guard.ts (457 bytes)
    
    私はMVCアプローチで完全には行かないので、異なるAPIを呼び出すためのサービスを1つ作成します.
    $ ng g s store
    CREATE src/app/store.service.spec.ts (352 bytes)
    CREATE src/app/store.service.ts (134 bytes)
    
    つの主要分野に焦点を合わせましょう
  • 倉庫
  • オーサガード
  • 倉庫


    オリジナルのエンドポイントに基づいて、3つの関数を作成します.
  • getPets(): Observable<Pet[]>
  • getSales(): Observable<Transaction[]>
  • getRewards(): Observable<Reward[]>
  • ストア.サービスTS
    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs';
    import { Pet, Reward, Transaction } from './_model';
    import { environment as env } from '../environments/environment';
    import { HttpClient } from '@angular/common/http';
    
    @Injectable({
      providedIn: 'root'
    })
    export class StoreService {
    
      constructor(private http : HttpClient) { }
    
      getPets(): Observable<Pet[]> {
        const uri = `${env.api_host}/v1/pet`;
        return this.http.get<Pet[]>(uri);
      }
    
      getSales(): Observable<Transaction[]> {
        const uri = `${env.api_host}/v1/sales`;
        return this.http.get<Transaction[]>(uri);
      }
    
      getRewards(): Observable<Reward[]> {
        const uri = `${env.api_host}/v1/rewards`;
        return this.http.get<Reward[]>(uri);
      }
    }
    
    コンポーネントにサービスを提供し、結果を出力しましょう.
    販売.コンポーネント.TS
    import { Component, OnInit } from '@angular/core';
    import { StoreService } from '../store.service';
    import { map, catchError } from 'rxjs/operators';
    import { Transaction } from '../_model';
    import { of } from 'rxjs';
    
    @Component({
      selector: 'app-sales',
      templateUrl: './sales.component.html',
      styleUrls: ['./sales.component.css']
    })
    export class SalesComponent implements OnInit {
    
      sales: Transaction[];
    
      constructor(private store: StoreService) { }
    
      ngOnInit(): void {
        const sale$ = this.store.getSales().pipe(
          map(results => {
            this.sales = results;
          }),
          catchError(error => {
            console.log(error);
            return of([]);
          })
        );
    
        sale$.subscribe(data => data);
      }
    
    }
    
    販売.コンポーネント.HTML
    <p>sales works!</p>
    <table>
        <tr>
            <th>Transaction ID</th>
            <th>Transaction Date</th>
            <th>Transaction Amount</th>
            <th>Payment Method</th>
        </tr>
        <tr *ngFor="let sale of sales">
            <td>{{sale.txn_id}}</td>
            <td>{{sale.txn_date}}</td>
            <td>{{sale.txn_amount}}</td>
            <td>{{sale.txn_method}}</td>
        </tr>
        <tr *ngIf="sales === undefined">
            <td colspan="4">No data</td>
        </tr>
    </table>
    
    ペットコンポーネント.TS
    import { Component, OnInit } from '@angular/core';
    import { StoreService } from '../store.service';
    import { map } from 'rxjs/operators';
    import { Reward } from '../_model';
    
    @Component({
      selector: 'app-rewards',
      templateUrl: './rewards.component.html',
      styleUrls: ['./rewards.component.css']
    })
    export class RewardsComponent implements OnInit {
    
      rewards: Reward[];
    
      constructor(private store: StoreService) { }
    
      ngOnInit(): void {
        this.store.getRewards().pipe( 
          map(results => {
            this.rewards = results;
          })
        );
      }
    
    }
    
    ペットコンポーネント.HTML
    <p>pet works!</p>
    <table>
        <tr>
            <th>Pet ID</th>
            <th>Pet Type</th>
            <th>Pet Age</th>
            <th>Pet Name</th>
        </tr>
        <tr *ngFor="let pet of pets">
            <td>{{pet.pet_id}}</td>
            <td>{{pet.pet_type}}</td>
            <td>{{pet.pet_age}}</td>
            <td>{{pet.pet_name}}</td>
        </tr>
        <tr *ngIf="pets === undefined">
            <td colspan="4">No data</td>
        </tr>
    </table>
    
    見ることができるように、私たちはenvironment それから我々のURIをオブジェクト化して、構築してください.
    モデルクラスの構造のソースコードを見ることができます.
    charleneマスターズとしてログインして、解決策を見ましょう


    ご覧の通り、割り当てられたロールに基づいた結果が得られます.しかし、どのようにDOS APIはどのような役割をユーザーが知っている?開発コンソールを開き、ネットワークタブを選択した場合は、サーバーにXHR要求を見てください.

    見てみると、APIリクエストの一部として渡されるベアラートークンを見ることができます!
    なぜなら、私たちはpet-store-api グループに対するクライアントの役割、これらのロールは、APIに対するベアラートークンの一部として渡されます.

    ガードの設定


    したがって、任意の標準的なアプリケーションでは、Charleneすべての販売ビューにアクセスすることはできません.次のセクションでは、彼女の役割を確認し、SalesComponentへのアクセスを拒否するようにAuthGuardを設定します.
    最初にKeyCloakを使用してルートをガードするには、AuthGuardにいくつか変更を加える必要があります.
    Authガード.TS
    import { Injectable } from '@angular/core';
    import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
    import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
    
    @Injectable({
      providedIn: 'root'
    })
    export class AuthGuard extends KeycloakAuthGuard {
    
      constructor(protected router: Router, protected service: KeycloakService) {
        super(router, service)
      }
    
      isAccessAllowed(route: ActivatedRouteSnapshot,state: RouterStateSnapshot) : Promise<boolean> {
        return new Promise((resolve, reject) => {
            resolve(true)
        });
      }  
    }
    
    ここでは、KeyCloakラッパーを使用して、KeyCloakサーバへの正しい認証コールを確実にします.
    このクラスで重要なのはisAccessAllowed() メソッド.これは、ユーザーがコンポーネントをアクティブにすることができる場合に指示するエントリポイントです.
    パスのチェックを行い、販売データにアクセスするかどうかを確認します.
    Authガード.TS
      isAccessAllowed(route: ActivatedRouteSnapshot,state: RouterStateSnapshot) : Promise<boolean> {
        return new Promise((resolve, reject) => {
          const userRoles: string[] = this.service.getUserRoles();
          console.log(`Roles: ${userRoles}`);
    
          if (state.url === '/sales' && userRoles.indexOf('employee') >= 0) {
            console.log('Permission allowed');
            resolve(true)
          } else {
            console.log('Permission not allowed');
            resolve(false);
          }
    
        });
    
    このガードの設定app-routing.module.ts 以下のように簡単です.
    const routes: Routes = [
      { path: 'pets', component: PetComponent },
      { path: 'rewards', component: RewardsComponent },
      { path: 'sales', component: SalesComponent, canActivate: [AuthGuard] }
    ];
    
    今すぐアクセスする場合sales.component あなたがメッセージを見ることができるはずですPermission not allowed コンソールでは、コンポーネントはアクティブになりません.
    ノート
    あなたは、我々がユーザーが持っている役割を印刷しているのを見ることができます
    次のロールがあります.api-customer,customer,manage-account,manage-account-links,view-profile,offline_access,uma_authorizationそれで、我々は現在我々のユーザーインターフェースと我々のバックエンドサービスを確保することができました.もちろん、より多くのあなたがこれを行うことができますが、それはあなたの安全なApplcationsにヘッドスタートを与える必要があります.
    パート3では、RewardsComponentを配線し、ユーザ固有の情報をロードします!