SpringBoot Java で 簡単なLINE認証を実装してECSにデプロイする

### 目的

:::note info
LINE SNS k認証機能を実装し、LINE認証の仕組みと機能実装方法を理解すること  
:::

</aside>

### 構築環境と必要なもの

- Mac(windowsでも可能)
  - Terraformコマンドを叩けること
- LINEアカウント
- AWSアカウント

### どう作るか

#### 構成の説明

AWS環境で構築・公開を行う
ECSへデプロイする
LINE認証Callbackドメインの登録は ALB の DNS(Aレコード)を登録する

#### 内部ドメイン
- /
  - トップページを表示
- /login
  - login認証ドメインへリダイレクトする 
- /callback
    - LINE認証後にリクエストされるドメイン
    - `/` へリダイレクトする

### 大まかな手順
1. Webアプリケーションを開発
1. アプリケーションのImageを作成
1. ImageをECRへPush
1. インフラ環境を構築 & アプリケーションをデプロイ
1. LINE CHANNELを作成 & CallBackURLを登録
1. 動作確認

### 開発


1. Webアプリケーションを開発
    以下のRepositoryをcloneすると早い
    https://github.com/GitEngHar/SnsAuthenticationApp/pull/1
    
    - 認証用 フロントエンド
    ```html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Hello</title>
    </head>
    <body>
        <p>HelloWorld</p>
        <!--    リダイレクト機能確認用URL-->
        <a th:href="@{/redirect}">リダイレクト機能テスト</a><br>
        <a th:href="@{/login}">ログイン</a>
    </body>
    </html>
    ```
    
    - トップページを表示するController
    
    ```java
    package com.example.snsauthenticationapp.web;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    /** Topページ*/
    
    @Controller
    @RequestMapping("/")
    public class IndexController {
    
        /** トップページ */
        @GetMapping("")
        public String index(Model model) {
            return "index";
        }
    
        /** リダイレクト確認用実装 */
        @GetMapping("redirect")
        public String redirect() {
            return "redirect:/";
        }
    
    }
    
    ```
    
    - 認証リダイレクト処理 と CallBack処理
    
    ```java
    package com.example.snsauthenticationapp.web;
    
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.servlet.view.RedirectView;
    
    /**
     * LINE認証リダイレクトURLへリダイレクト
     * CallBackハンドリング
     * */
    
    @Controller
    public class LoginController {
    
        final LineAuthenticationService lineAuthenticationService;
    
        public LoginController(LineAuthenticationService lineAuthenticationService){
            this.lineAuthenticationService = lineAuthenticationService;
        }
    
        /** LINE認証用リダイレクト */
        @GetMapping("/login")
        public RedirectView login() {
            String redirectUrl = lineAuthenticationService.createRedirectURL();
            return new RedirectView(redirectUrl);
        }
    		
    		/** CallBack処理 */
        @RequestMapping(value = "/callback", method = RequestMethod.GET)
        public String getToken(@RequestParam("code") String code,
                               HttpServletRequest req, HttpServletResponse res) throws Exception{
            return "redirect:/";
        }
    }
    
    ```
    
    - LINE認証用 プロパティ をオブジェクトへ代入する
    
    ```java
    package com.example.snsauthenticationapp.domain;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    /**
     * 認可コードの取得
     * アクセストークンの発行 / ユーザー情報を取得
     * */
    @Component
    @ConfigurationProperties(prefix = "lineauth")
    public class LineAuthElement {
        private String responseType;
        private String redirectUri;
        private String clientId;
        private String scope;
        private String state;
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    
        public String getResponseType() {
            return responseType;
        }
    
        public void setResponseType(String responseType) {
            this.responseType = responseType;
        }
    
        public String getRedirectUri() {
            return redirectUri;
        }
    
        public void setRedirectUri(String redirectUri) {
            this.redirectUri = redirectUri;
        }
    
        public String getClientId() {
            return clientId;
        }
    
        public void setClientId(String clientId) {
            this.clientId = clientId;
        }
    
        public String getScope() {
            return scope;
        }
    
        public void setScope(String scope) {
            this.scope = scope;
        }
    }
    
    ```
    
    - 認証ドメインの生成
    
    ```java
    package com.example.snsauthenticationapp.web;
    
    import java.util.ArrayList;
    import java.util.List;
    import org.springframework.stereotype.Service;
    import org.apache.http.NameValuePair;
    import org.apache.http.message.BasicNameValuePair;
    import org.apache.http.client.utils.URIBuilder;
    import org.apache.commons.lang3.RandomStringUtils;
    import com.example.snsauthenticationapp.domain.LineAuthElement;
    
    /**
     * state, nonce用 検証ランダム文字列の生成
     * アクセストークンの検証や破棄
     * */
    @Service
    public class LineAuthenticationService {
        private final LineAuthElement lineAuthElement;
        public LineAuthenticationService(LineAuthElement lineAuthElement) {
            this.lineAuthElement = lineAuthElement;
        }
    
        /** リダイレクト先のドメインを生成する
         * https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id=1234567890&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%3Fkey%3Dvalue&state=12345abcde&scope=profile%20openid&nonce=09876xyz
         * */
        public String createRedirectURL() {
            try {
                // csrf検証用 stringを生成
                lineAuthElement.setState(createUniqueState());
    
                // 認証用リダイレクトパラメータを生成
                List<NameValuePair> params = new ArrayList<>();
                params.add(new BasicNameValuePair("response_type", lineAuthElement.getResponseType()));
                params.add(new BasicNameValuePair("client_id", lineAuthElement.getClientId()));
                params.add(new BasicNameValuePair("redirect_uri", lineAuthElement.getRedirectUri()));
                params.add(new BasicNameValuePair("scope", lineAuthElement.getScope()));
                params.add(new BasicNameValuePair("state", lineAuthElement.getState()));
    
                // 認証用ドメインを生成
                URIBuilder builder = new URIBuilder();
                builder.setScheme("https");
                builder.setHost("access.line.me");
                builder.setPath("oauth2/v2.1/authorize");
    
                // 認証用ドメインにパラメータを付与
                builder.setParameters(params);
    
                // リダイレクトURIを生成
                return builder.build().toURL().toString();
    
            } catch (Exception e) {
                System.out.println(e.getMessage());
                return null;
            }
        }
    
        private static String createUniqueState(){
            return RandomStringUtils.randomAlphanumeric(20);
        }
    }
    
    ```
    
    - 認証用property
    
    ```yaml
    spring:
      application:
        name: "SnsAuthenticationApp"
    lineauth:
      response_type: "code"
      client_id: ${CLIENT_ID}
      redirect_uri: ${REDIRECT_URI}
      # state
      scope: "profile"
      # nonce
      #  prompt
      #  max_age
      #  ui_locales
      #  bot_prompt
      #  initial_amr_display
      #  switch_amr
      #  disable_auto_login
      #  disable_ios_auto_login
      #  code_challenge
      #  code_challenge_method
    
    ```

1. アプリケーションのImageを作成
    **JarのBuild IntelliJ**
    ```bash
    ~/Documents/SnsAuthenticationApp
    $ ./gradlew build
    ```
    **Jarの出力先**
    
    作業場所/`リポジトリ名`/build/libs
    
    ```bash
    
    ~/Documents/SnsAuthenticationApp/build/libs
    ```
    **JarでWebAppが動作することを確認**
    ```bash
    # jarを起動
    java -jar SnsAuthenticationApp-0.0.1-SNAPSHOT.jar
    
    # 起動ログ
      .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
    
     :: Spring Boot ::                (v3.4.1)
    
    2025-01-19T10:22:06.301+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] c.e.s.SnsAuthenticationAppApplication    : Starting SnsAuthenticationAppApplication v0.0.1-SNAPSHOT using Java 23.0.1 with PID 41855 (/Users/harukisugiyama/Documents/SnsAuthenticationApp/build/libs/SnsAuthenticationApp-0.0.1-SNAPSHOT.jar started by harukisugiyama in /Users/harukisugiyama/Documents/SnsAuthenticationApp/build/libs)
    2025-01-19T10:22:06.302+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] c.e.s.SnsAuthenticationAppApplication    : No active profile set, falling back to 1 default profile: "default"
    2025-01-19T10:22:06.572+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
    2025-01-19T10:22:06.577+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
    2025-01-19T10:22:06.577+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.34]
    2025-01-19T10:22:06.588+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
    2025-01-19T10:22:06.589+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 270 ms
    2025-01-19T10:22:06.624+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
    2025-01-19T10:22:06.708+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
    2025-01-19T10:22:06.715+09:00  INFO 41855 --- [SnsAuthenticationApp] [           main] c.e.s.SnsAuthenticationAppApplication    : Started SnsAuthenticationAppApplication in 0.538 seconds (process running for 0.702)
    2025-01-19T10:22:15.885+09:00  INFO 41855 --- [SnsAuthenticationApp] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
    2025-01-19T10:22:15.885+09:00  INFO 41855 --- [SnsAuthenticationApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
    2025-01-19T10:22:15.886+09:00  INFO 41855 --- [SnsAuthenticationApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
    
    ```
    **動作確認**
    ```bash
    # localにcurlする
    ~/Documents/SnsAuthenticationApp/build/libs
    ❯ curl localhost:8080
    
    # 出力
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Hello</title>
    </head>
    <body>
        <p>HelloWorld</p>
    </body>
    </html>
    ```
    **DockerFileの作成**
    ```bash
    # Java build Base Image
    FROM eclipse-temurin
    
    # 作業ディレクトリを設定
    WORKDIR /app
    
    # Buildするjarをコピー
    COPY ./app/SnsAuthenticationApp-0.0.1-SNAPSHOT.jar app.jar
    
    # 環境変数を設定(検証用)
    # ENV REDIRECT_URI=hogepoge
    
    # portを設定
    EXPOSE 8080
    
    # jarの起動
    ENTRYPOINT ["java", "-jar", "app.jar"]
    ```    
    **Docker Build**
    ```bash
    docker build -t sbs-authentication-app . --platform linux/amd64
    ```

1. ImageをECRへPush
    「プッシュコマンドを表示」をクリックして内容を参考に認証してpush
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2737245/fda6ce35-587e-bfdb-d036-208b1bfd5637.png)

1. インフラ環境を構築 & アプリケーションをデプロイ
    1. Repositoryをcloneする 
         https://github.com/GitEngHar/TfSnsAuthenticationApp
    1. Repository内に terraform.tfvars を作成
        - `aws_account_id = "1234"` のように環境変数を設定する
    1. `terraform apply` でリソースを構築

1. LINE CHANNELを作成 & CallBackURLを登録
    - [ここ](https://developers.line.biz/ja/docs/line-login/getting-started/#step-1-create-channel)を参考にCHANNELを作成する
    - AWS ALB の DNSを取得し CallBackURLを設定する [参考](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#setting-callback-url)
1. 動作確認 (ALB DNS(Aレコード) を取得して設定)
  - 自分のLINEアカウントが表示されて認証後 リダイレクト先にアクセスできたら成功
    

## 苦労したこと

### 検証する際にBuildImageのアーキテクチャと実行アーキテクチャの差でエラー

- ビルドしたImageでコンテナを立てるためにMacに適した platformを指定してimageをbuildする必要があった
    - `amd64` でアーキテクチャを指定して Imageを構築する

**実際のエラー**

```bash

 JRE version:  (21.0.5+11) (build )
2025-01-19T01:27:39.572762094Z # Java VM: OpenJDK 64-Bit Server VM (21.0.5+11-LTS, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-aarch64)
2025-01-19T01:27:39.572762844Z # Problematic frame:
2025-01-19T01:27:39.572763469Z # j  java.lang.System.registerNatives()V+0 java.base@21.0.5
2025-01-19T01:27:39.572764094Z #
2025-01-19T01:27:39.572764594Z # No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
2025-01-19T01:27:39.572765427Z #
2025-01-19T01:27:39.573900719Z # An error report file with more information is saved as:
2025-01-19T01:27:39.573961052Z # /app/hs_err_pid1.log
2025-01-19T01:27:39.577807927Z [0.024s][warning][os] Loading hsdis library failed
2025-01-19T01:27:39.580877844Z #
2025-01-19T01:27:39.580902261Z # The crash happened outside the Java Virtual Machine in native code.
2025-01-19T01:27:39.580904302Z # See problematic frame for where to report the bug.
2025-01-19T01:27:39.580905594Z #
2025-01-19T01:27:39.580906636Z 
2025-01-19T01:27:39.580907886Z [error occurred during error reporting (), id 0x5, SIGTRAP (0x5) at pc=0x0000ffffadf57ebc]
```

**解決**

ビルド時に アーキテクチャ (`--platform linux/amd64`) を指定 

```bash
docker build -t spring-docker . --platform linux/amd64
```

## 参考

- https://www.docker.com/ja-jp/blog/9-tips-for-containerizing-your-spring-boot-code/
- https://y-ohgi.com/introduction-terraform/handson/ecs/