【Java・SpringBoot】Springセキュリティ① - 直リンク禁止

Springで、ログイン画面に入力されたID・パスワードをDBに確認し、ユーザーの権限で特定のURLへのアクセスを禁止する機能を簡単に作成しましょう〜♪

#SpringSecurityの認証・認可
* **ログイン機能での認証**
    * ログインに成功したユーザーだけが、ログイン後の画面に遷移することができる
    * Springでは、パスワードの暗号化・復号も簡単
    * CSRF対策も可能
* **ログイン機能での認可**
    * 権限による機能制限
    * 権限によって画面表示項目を変更することも可能

#ログイン機能・認証機能の内容
* 順番に以下の実装をします
    * ログイン画面から**入力したユーザーID、パスワードをDBに検索**し、登録されていれば、ログインできる
    * ユーザー登録の際に、**パスワードを暗号化して保存**する
    * 一般ユーザーは**アドミン権限専用画面`http://localhost:8080/admin`にアクセスできない**
    * 一般ユーザーのホーム画面には、アドミン権限専用画面へのリンクを表示しない


#アドミン専用画面へのコントローラーを作成

```HomeController.java
//省略(全文は下記参考)

@Controller
public class HomeController {

    @Autowired
    UserService userService;

//省略(全文は下記参考)
    /**
     * アドミン権限専用画面のGET用メソッド.
     * @param model Modelクラス
     * @return 画面のテンプレート名
     */
    @GetMapping("/admin")
    public String getAdmin(Model model) {

        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/admin :: admin_contents");

        //レイアウト用テンプレート
        return "login/homeLayout";
    }
}

```

#ログイン設定 (直リンク禁止)
* まずは**直リンクの禁止**から実装します
    * ログインしていない状態では、**ホーム画面やユーザー管理画面にはアクセスできない**ようにする（ログイン画面と、ユーザー登録画面以外）
* もし直接アクセスしようとしたら、403エラーで、共通エラーページに遷移
* webjarsやcssなどの静的リソースをセキュリティの対象外とする

###セキュリティ設定用クラス
* **@EnableWebSecurity**を付ける
* **WebSecurityConfigurerAdapterクラス**を継承
    * 各種メソッドをオーバーライドすることで、セキュリティの設定を行う
    * セキュリティ用にBean定義を行うので@Configurationアノテーションを付ける

###静的リソースを除外
* webjarsやcssなどの**静的リソースには、誰でもアクセスできる**ようにする
* `web.ignoring().antMatchers("/webjars/∗∗", "/css/∗∗");`
    * `/∗∗`は、正規表現でいずれかのファイルという意味
    * つまり、/webjars配下と/css配下のファイルはセキュリティの対象外

###直リンクの禁止
* **http.authorizeRequests()**にメソッドチェーンでリンク禁止先の条件を追加
    * メソッドチェーンとは、`.`でメソッドを連続して呼び出すこと
* **antMatchers("<リンク先>").permitAll()**
    * antMatchersメソッドの引数に、リンク先をセットすると、そのリンク先に対する設定ができる
    * リンク先にpermitAllメソッドを使うことで、ログインしてないユーザーでもリンク先にアクセスすることができる=**直リンクができる**
* **anyRequest().authenticated()**
    * anyRequestメソッドで、全てのリンク先が対象になり、authenticatedメソッドで、認証しないとアクセスできないように設定する
    * permitAllしたリンク先以外の直リンクを禁止できる
    * 🌟メソッドチェーンでは上から順番に設定がされていくので`anyRequest.authenticated()`を一番最初に設定すると、そのあとに`antMatchers("<リンク先>").permitAll()`を設定しても、全てのリクエストで認証が必要になってしまうので注意

```SecurityConfig.java
package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {

        //静的リソースへのアクセスには、セキュリティを適用しない
        web.ignoring().antMatchers("/webjars/∗∗", "/css/∗∗");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // ログイン不要ページの設定
        http
            .authorizeRequests()
                .antMatchers("/webjars/**").permitAll() //webjarsへアクセス許可
                .antMatchers("/css/**").permitAll() //cssへアクセス許可
                .antMatchers("/login").permitAll() //ログインページは直リンクOK
                .antMatchers("/signup").permitAll() //ユーザー登録画面は直リンクOK
                .anyRequest().authenticated(); //それ以外は直リンク禁止

        //CSRF対策を無効に設定（一時的）
        http.csrf().disable();
     
}

```

##起動してログイン画面へのアクセスを確認！
* http://localhost:8080/login
* ログイン画面とユーザー登録画面はログインしなくても表示される
* http://localhost:8080/home　へアクセス
* 404エラーが帰ってきたので、ホーム画面への直リンクを禁止できました〜^^
* 次回ログイン機能を実装していきます！

![loginOK.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/d32c8220-d98d-4cfc-c81c-5ce98b24d9db.png)

![home403.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/2c694e89-1ab3-f3c9-8aef-9896bdcd434c.png)



#(参考)コード全文

```HomeController.java
package com.example.demo.login.controller;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

@Controller
public class HomeController {

    @Autowired
    UserService userService;

    // 結婚ステータスのラジオボタン用変数
    private Map<String, String> radioMarriage;

    /**
     * ラジオボタンの初期化メソッド（ユーザー登録画面と同じ）.
     */
    private Map<String, String> initRadioMarrige() {

        Map<String, String> radio = new LinkedHashMap<>();

        // 既婚、未婚をMapに格納
        radio.put("既婚", "true");
        radio.put("未婚", "false");

        return radio;
    }

    /**
     * ホーム画面のGET用メソッド
     */
    @GetMapping("/home")
    public String getHome(Model model) {

        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/home :: home_contents");

        return "login/homeLayout";
    }

    /**
     * ユーザー一覧画面のGETメソッド用処理.
     */
    @GetMapping("/userList")
    public String getUserList(Model model) {

        //コンテンツ部分にユーザー一覧を表示するための文字列を登録
        model.addAttribute("contents", "login/userList :: userList_contents");

        //ユーザー一覧の生成
        List<User> userList = userService.selectMany();

        //Modelにユーザーリストを登録
        model.addAttribute("userList", userList);

        //データ件数を取得
        int count = userService.count();
        model.addAttribute("userListCount", count);

        return "login/homeLayout";
    }

    /**
     * ユーザー詳細画面のGETメソッド用処理.
     */
    @GetMapping("/userDetail/{id:.+}")
    public String getUserDetail(@ModelAttribute SignupForm form,
            Model model,
            @PathVariable("id") String userId) {

        // ユーザーID確認（デバッグ）
        System.out.println("userId = " + userId);

        // コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/userDetail :: userDetail_contents");

        // 結婚ステータス用ラジオボタンの初期化
        radioMarriage = initRadioMarrige();

        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);

        // ユーザーIDのチェック
        if (userId != null && userId.length() > 0) {

            // ユーザー情報を取得
            User user = userService.selectOne(userId);

            // Userクラスをフォームクラスに変換
            form.setUserId(user.getUserId()); //ユーザーID
            form.setUserName(user.getUserName()); //ユーザー名
            form.setBirthday(user.getBirthday()); //誕生日
            form.setAge(user.getAge()); //年齢
            form.setMarriage(user.isMarriage()); //結婚ステータス

            // Modelに登録
            model.addAttribute("signupForm", form);
        }

        return "login/homeLayout";
    }

    /**
     * ユーザー更新用処理.
     */
    @PostMapping(value = "/userDetail", params = "update")
    public String postUserDetailUpdate(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("更新ボタンの処理");

        //Userインスタンスの生成
        User user = new User();

        //フォームクラスをUserクラスに変換
        user.setUserId(form.getUserId());
        user.setPassword(form.getPassword());
        user.setUserName(form.getUserName());
        user.setBirthday(form.getBirthday());
        user.setAge(form.getAge());
        user.setMarriage(form.isMarriage());

        try {

            //更新実行
            boolean result = userService.updateOne(user);

            if (result == true) {
                model.addAttribute("result", "更新成功");
            } else {
                model.addAttribute("result", "更新失敗");
            }

        } catch(DataAccessException e) {

            model.addAttribute("result", "更新失敗(トランザクションテスト)");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ユーザー削除用処理.
     */
    @PostMapping(value = "/userDetail", params = "delete")
    public String postUserDetailDelete(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("削除ボタンの処理");

        //削除実行
        boolean result = userService.deleteOne(form.getUserId());

        if (result == true) {

            model.addAttribute("result", "削除成功");

        } else {

            model.addAttribute("result", "削除失敗");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ログアウト用処理.
     */
    @PostMapping("/logout")
    public String postLogout() {

        //ログイン画面にリダイレクト
        return "redirect:/login";
    }

    /**
     * ユーザー一覧のCSV出力用処理.
     */
    @GetMapping("/userList/csv")
    public ResponseEntity<byte[]> getUserListCsv(Model model) {

        //ユーザーを全件取得して、CSVをサーバーに保存する
        userService.userCsvOut();

        byte[] bytes = null;

        try {

            //サーバーに保存されているsample.csvファイルをbyteで取得する
            bytes = userService.getFile("sample.csv");

        } catch (IOException e) {
            e.printStackTrace();
        }

        //HTTPヘッダーの設定
        HttpHeaders header = new HttpHeaders();
        header.add("Content-Type", "text/csv; charset=UTF-8");
        header.setContentDispositionFormData("filename", "sample.csv");

        //sample.csvを戻す
        return new ResponseEntity<>(bytes, header, HttpStatus.OK);
    }

    /**
     * アドミン権限専用画面のGET用メソッド.
     * @param model Modelクラス
     * @return 画面のテンプレート名
     */
    @GetMapping("/admin")
    public String getAdmin(Model model) {

        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/admin :: admin_contents");

        //レイアウト用テンプレート
        return "login/homeLayout";
    }
}

```