2024年現在の Spring Boot についての覚え書き。
※併せてSSR(サーバサイドレンダリング用)のテンプレート(Thymeleaf)やO/Rマッパーに関連する事項についても記載。

目次

JDKのインストール

(Mac)
https://jdk.java.net/archive/ から 対象の JDK をダウンロード 及び 解凍し /Library/Java/JavaVirtualMachines/ 配下にコピー。

VsCode環境設定

https://code.visualstudio.com/docs/java/java-spring-boot

以下の拡張機能をインストール

  • Extension Pack for Java
  • Spring Boot Extension Pack
  • Gradle for Java

プロジェクトの作成

01_create_new_project.png
02_create_new_project.png
03_create_new_project.png
04_create_new_project.png
05_create_new_project.png
06_create_new_project.png
07_create_new_project.png
08_create_new_project.png
09_create_new_project.png

build.gradle

まだ認証機能を構築していないので spring security を無効化しておく

:
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	//implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-aop'
	implementation 'org.springframework.session:spring-session-jdbc'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	//testImplementation 'org.springframework.security:spring-security-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
:

application.properties

とりあえず最初の起動に必要な分だけ記載。(DBは事前準備)

spring.application.name=demo
spring.datasource.url=jdbc:postgresql://localhost:5432/sample
spring.datasource.username=sample
spring.datasource.password=sample
spring.datasource.driver-class-name=org.postgresql.Driver
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always
spring.session.jdbc.platform=postgresql

TOPページ作成

コントローラ

src/main/java/com/example/demo/controller/IndexController.java

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
    @GetMapping("/")
    public String index() {
        return "index";
    }
}

Thymeleafテンプレート

src/main/resources/templates/index.html

<!doctype html>
<html lang="ja">
    <head>
    <meta charset="utf-8" />
    </head>
    <body>
        Index Page!
    </body>
</html>

テスト起動

test_boot_run.png

起動したら http://localhost:8080/ にアクセスしてTOPページを表示してみる。

以降は、諸々の解説を記載する

コントローラについて

コントローラの基本

  • class に @Controller または @RestController アノテーションを付与する
  • @GetMapping、@PostMapping 等を使用してルーティング定義を行う。
    https://spring.io/guides/gs/rest-service
  • RestAPI用のコントローラの場合
    • 戻り値(Map やDTO)を返却するロジックを書く。
      ※ httpヘッダの指定等が必要な場合は ResponseEntity を使用する。
  • SSR(サーバサイドレンダリング)用のコントローラの場合
    • 戻り値としてテンプレートファイル名を返却する。
    • Model テンプレートに展開したいデータは Model を使用して設定する。

例) RestAPI用のコントローラ

package com.example.demo.controller;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleRestController {

    @GetMapping("api1")
    public Map<String, String> samplejson() {
        Map<String, String> res = new HashMap<String, String>();
        res.put("var1", "test1");
        return res;
    }

    @GetMapping("api2")
    public ResponseEntity<Map<String, String>> samplejson3() {
        Map<String, String> res = new HashMap<String, String>();
        res.put("var1", "test1");
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
        return new ResponseEntity<Map<String, String>>(res, headers, HttpStatus.BAD_REQUEST);
    }
}

例) SSR(サーバサイドレンダリング)を行う場合

package com.example.demo.controller;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {
    
    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("var1", "Test1");
        return "index";
    }

    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("name", "山田");
        return "hello";
    }
}

リクエストデータの受け取り

アノテーションを使用してリクエストデータやセッションデータ、Cookieデータ、HTTPヘッダ等をコントローラーのメソッド引数にバインド出来る。
※複数のアノテーションを同時に使用する事も可能。

例) @RequestParam によるパラメータ取得

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestParam;
:
public class SampleController {

    public String index(@RequestParam(name="param1", required=false, defaultValue="0") String param1) {
        System.out.println("param1: " + param1);
        :
    }

Pathパラメータは PathVariable アノテーションで取得

package com.example.demo.controller;
:
import org.springframework.web.bind.annotation.PathVariable;
:
public class BookController {

    @GetMapping("/books/{isbn}")
    public String edit(@PathVariable(name="isbn") String isbn,
                       Model model) {
        System.out.println("isbn: " + isbn);
        :
    }
}

レスポンスデータの設定

Rest コントローラの場合は返却するモデルをそのまま返す事が可能(自動的にJSONに変換される)

package com.example.demo.controller;
:
import org.springframework.web.bind.annotation.PathVariable;
:
public class BookController {

    @GetMapping("/books/{isbn}")
    public Book edit(@PathVariable(name="isbn") String isbn) {
        Optional<Book> book = repository.findById(isbn);
        return book.isPresent() ? book.get() : null;
    }
}

テンプレート(Thymeleaf)に展開する値は引数にバインドした Model に設定し、戻り値でテンプレート名を返す。

package com.example.demo.controller;
:
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
:
@Controller
public class BookController {

    // 書籍リポジトリ
    private final BookRepository repository;

    // コンストラクタ
    public BookController(BookRepository repo) {
        this.repository = repo;
    }

    @GetMapping("/books/{isbn}")
    public String edit(@PathVariable(name="isbn") String isbn,
                       Model model) {
        if ("new".equals(isbn)) {
            model.addAttribute("book", new Book());
        } else {
            Optional<Book> book = this.repository.findById(isbn);
            model.addAttribute("book", book.isPresent() ? book.get() : null);
        }
        return "book_edit";
    }
}

セッションの利用

初期設定 (以下、セッションストアをPostgreSQLにする場合の例)

build.gradle

dependencies {
	:
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	implementation 'org.springframework.session:spring-session-jdbc'
	runtimeOnly 'org.postgresql:postgresql'
	:
}

application.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/sample
spring.datasource.username=sample
spring.datasource.password=sample
spring.datasource.driver-class-name=org.postgresql.Driver
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always
spring.session.jdbc.platform=postgresql

spring.session.jdbc.initialize-schema = always の場合、自動的にセッション管理用のテーブルが作成される。
https://github.com/spring-projects/spring-session/tree/main/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc

セッション情報の取得/設定方法

コンストラクタインジェクションで HttpSession セッションを取得する場合

package com.example.demo.controller;
:
import jakarta.servlet.http.HttpSession;

@Controller
public class BookController {

    private final HttpSession session;

    public BookController(HttpSession session) {
        this.session = session;
    }

    @GetMapping("/sample")
    public String sample(Model model) {

        int count = 0;
        // セッション情報の取得
        Object countObj = session.getAttribute("sample_count");
        if (countObj != null) {
            count = (int)countObj;
        }
        // セッション情報の設定
        session.setAttribute("sample_count",  ++count);

        System.out.println("sample_count: " + count);
        :
    }
}

RequestContextHolder から HttpSession セッションを取得する場合

package com.example.demo.controller;
:
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

@Controller
public class BookController {

    @GetMapping("/sample2")
    public String sample2(Model model) {

        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
            HttpServletRequest request = servletRequestAttributes.getRequest();
            HttpSession session = request.getSession();

            int count = 0;
            // セッション情報の取得
            Object countObj = session.getAttribute("sample_count");
            if (countObj != null) {
                count = (int)countObj;
            }
            // セッション情報の設定
            session.setAttribute("sample_count",  ++count);
            System.out.println("sample_count: " + count);
    
            model.addAttribute("sample_count", count);
        }

        return "sample";
    }
}

DIの使用

コンストラクタ、メソッド、フィールドに @Autowired を使用する事で依存性注入が可能となっている。
※DI可能なクラスは、XML bean 定義やアノテーション付きコンポーネント(@Component、@Controller などでアノテーションが付けられたクラス)。

コンストラクタインジェクション

@Controller
public class SampleController {

    private final BookRepository repository;
    private final HttpSession session;

    // Spring Framework 4.3 以降ではコンストラクタが1つの場合は Autowired は不要
    // @Autowired
    public SampleController(HttpSession session, BookRepository repository) {
        this.session = session;
        this.repository = repository;
    }
    :
}

フィールド 及び メソッドインジェクション

:
import org.springframework.beans.factory.annotation.Autowired;
:
@Controller
public class SampleController {

@Controller
public class SampleController {

    // フィールドインジェクトション
    @Autowired
    private BookRepository repository;

    private HttpSession session;

    //  メソッドインジェクトション
    @Autowired
    public void setSession(HttpSession session) {
        this.session = session;
    }
    :
}

AOPの使用

Spring によるアスペクト指向プログラミング
https://spring.pleiades.io/spring-framework/reference/core/aop.html

AOP の概念
https://spring.pleiades.io/spring-framework/reference/core/aop/introduction-defn.html
※ジョイントポイント、ポイントカット、アドバイス についての理解

依存ライブラリの追加

build.gradle

dependencies {
	:
	implementation 'org.springframework.boot:spring-boot-starter-aop'
	:
}

@AspectJ のサポートを有効にする

https://spring.pleiades.io/spring-framework/reference/core/aop/ataspectj/aspectj-support.html

@EnableAspectJAutoProxy アノテーションを追加

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class MyConfig {
	:
}

アスペクト、ポイントカット、アドバイスの宣言

Before、After 、Around などのアノテーションを使用して処理の前後に任意の処理を差し込む事ができる。

例) コントローラの前後に任意の処理を差し込む

package com.example.demo.advise;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyControllerAdvise {

    @Around("@within(org.springframework.stereotype.Controller)")
    public Object aroundController(ProceedingJoinPoint jp) throws Throwable {

        System.out.println("START " + jp.getSignature());
        Object result = jp.proceed();
        System.out.println("END   " + jp.getSignature());

        return result;
    }
}

例) アドバイスからセッション情報にアクセスする

package com.example.demo.advise;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

@Aspect
@Component
public class MyControllerAdvise {

    @Around("@within(org.springframework.stereotype.Controller)")
    public Object aroundController(ProceedingJoinPoint jp) throws Throwable {

        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
            HttpServletRequest request = servletRequestAttributes.getRequest();
            HttpSession session = request.getSession();
            :
        }

        System.out.println("START " + jp.getSignature());
        Object result = jp.proceed();
        System.out.println("END   " + jp.getSignature());

        return result;
    }
}

Spring Data JDBCの使用

Spring Data JDBC
https://spring.pleiades.io/spring-data/relational/reference/jdbc.html

設定

build.gradle

dependencies {
        :
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
        :
}

application.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/sample
spring.datasource.username=sample
spring.datasource.password=sample
spring.datasource.driver-class-name=org.postgresql.Driver
logging.level.org.springframework.jdbc.core=DEBUG

モデル定義

package com.example.demo.entity;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

import lombok.Data;

@Data
@Table("books")
public class Book {
    @Id
    private String isbn;
    private String title;
    private int price;
    // Lombok を使用している為、個別のsetter 、getter は定義なし
}

DAO定義

インターフェースを定義する事により様々なSQLを発行する事ができる。
https://spring.pleiades.io/spring-data/relational/reference/jdbc/query-methods.html

上記より一部抜粋

メソッド名生成されるSQL補足
findBy項目名... where 項目名 = ?
findBy項目名1And項目名2... where 項目名1 = ? and 項目名2 = ?
findBy項目名Like... where 項目名1 like ?% は補完されない為、自分で引数を編集して呼び出す必要がある
findBy項目名StartingWith... where 項目名1 like ?パラメータの値が "{値}%" として展開される

DAOの定義例

package com.example.demo.entity;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

public interface BookRepository extends CrudRepository<Book, String> {

    // ... where isbn like 'xxxx%' としてSQL生成される
    public Iterable<Book> findByIsbnStartingWith(String isbn);

    // ページネーションを利用する場合
    public Page<Book> findByIsbnStartingWithAndTitleStartingWith(String isbn, String title, Pageable pageable);

    // 任意のSQLを発行する場合
    @Query("select * from books where 任意の条件")
    public Iterable<Book> findByCustomConditions(@Param("field1") String field1, @Param("field2") String field2);
}

利用例

コントローラからの利用例

@Controller
public class BookController {

    private final BookRepository repository;

    public BookController(HttpSession session, BookRepository repo) {
        this.repository = repo;
    }

    @GetMapping("/books")
    public String index(@RequestParam(name="isbn", required=false, defaultValue="") String isbn, Model model) {
        Iterable<Book> books = this.repository.findByIsbnStartingWith(isbn);
        model.addAttribute("books", books);
        return "books";
    }
}

ページネーションを利用する場合

:
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
:

@Controller
public class BookController {

    private final BookRepository repository;

    public BookController(HttpSession session, BookRepository repo) {
        this.repository = repo;
    }

    @GetMapping("/books")
    public String index(@RequestParam(name="isbn", required=false, defaultValue="") String isbn,
                        @RequestParam(name="title", required=false, defaultValue="") String title,
                        @RequestParam(name="page", required=false, defaultValue="0") int page,
                        @RequestParam(name="limit", required=false, defaultValue="5") int limit,
                        Model model) {

        Pageable pageable = PageRequest.of(page, limit, Sort.by("isbn"));
        Page<Book> bookPage = this.repository.findByIsbnStartingWithAndTitleStartingWith(isbn, title, pageable);
        List<Book> books = bookPage.getContent();

        model.addAttribute("pager", bookPage);
        model.addAttribute("books", books);
        return "books";
    }
}

テンプレート側

:

    <div th:if="${pager}">
        Page: <span th:text="${pager.number}" /> 
        Count: <span th:text="${pager.numberOfElements}" /> 
        Total Count: <span th:text="${pager.totalElements}" /> 
        Total Pages: <span th:text="${pager.totalPages}" /> 
    </div>

    <table>
    <tr>
        <th>ISBN</th>
        <th>タイトル</th>
        <th>価格</th>
    </tr>
    <tr th:each="book : ${books}">
        <td><a th:text="${book.isbn}" th:href="|/books/${book.isbn}|">isbn</a></td>
        <td th:text="${book.title}">title</td>
        <td th:text="${book.price}">price</td>
    </tr>
    </table>
:

ログレベルの設定

application.properties

logging.level.org.springframework.jdbc.core=TRACE

Spring Security(JDBC)の使用

設定

build.gradle

dependencies {
        :
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
        :
}

application.properties

spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always
spring.session.jdbc.platform=postgresql

スキーマ

デフォルトでは以下の構成を前提として認証、認可が行われる。
https://spring.pleiades.io/spring-security/reference/servlet/authentication/passwords/jdbc.html#servlet-authentication-jdbc-schema より抜粋

create table users(
	username varchar_ignorecase(50) not null primary key,
	password varchar_ignorecase(500) not null,
	enabled boolean not null
);
create table authorities (
	username varchar_ignorecase(50) not null,
	authority varchar_ignorecase(50) not null,
	constraint fk_authorities_users foreign key(username) references users(username)
);
但し、以降では以下の通りカスタマイズして認証機能を利用する 手順を記載する。
  • ユーザテーブルに表示用のユーザ名を追加する
  • ロールの情報はデフォルトスキーマから変えない(但しテーブル名は user_roles に変更)
create table users(
	username varchar(50) not null primary key,
	username_disp varchar(50) not null,
	password varchar(500) not null,
	enabled boolean not null
);
create table user_roles (
	username varchar(50) not null,
	authority varchar(50) not null
);
create unique index user_roles_ix1 on user_roles (username,authority);

認証用のエンティティ等

MyUser.java

package com.example.demo.entity;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

import lombok.Data;

@Data
@Table("users")
public class MyUser {
    @Id
    private String username;
    private String usernameDisp;
    private String password;
    private boolean enabled;
}

MyUserRole.java

package com.example.demo.entity;

import org.springframework.data.relational.core.mapping.Table;

import lombok.Data;

@Data
@Table("user_roles")
public class MyUserRole {
    private String username;
    private String authority;
}

MyUserDetails.java

package com.example.demo.entity;

import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Data;

@Data
public class MyUserDetails implements UserDetails {

    private String username;
    private String password;
    private String usernameDisp;
    private List<GrantedAuthority> authorities;

    public MyUserDetails(String username, String password, String usernameDisp, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.usernameDisp = usernameDisp;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
}

MyUserRepository.java (DAO)

package com.example.demo.entity;

import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;


public interface MyUserRepository extends CrudRepository<MyUser, String> {

    /**
     * ユーザの保持しているロールを取得する.
     * @param username ユーザ名
     * @return ロール一覧
     */
    @Query("select * from user_roles where username = :username")
    public Iterable<MyUserRole> findUserRoles(@Param("username") String username);
}

認証用のサービス作成

package com.example.demo.service;

import org.springframework.stereotype.Service;

import com.example.demo.entity.MyUser;
import com.example.demo.entity.MyUserDetails;
import com.example.demo.entity.MyUserRepository;
import com.example.demo.entity.MyUserRole;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Service
public class MyAuthService implements UserDetailsService {

    private final MyUserRepository repository;

    public MyAuthService(MyUserRepository repo) {
        this.repository = repo;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // ユーザ情報取得
        Optional<MyUser> foundUser = this.repository.findById(username);
        if (!foundUser.isPresent()) {
            throw new UsernameNotFoundException("User not found");
        } else if (!foundUser.get().isEnabled()) {
            throw new UsernameNotFoundException("User not found");
        }
        MyUser user = foundUser.get();

        // ユーザの保持ロールを取得
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        Iterator<MyUserRole> roles = this.repository.findUserRoles(username).iterator();
        while (roles.hasNext()) {
            MyUserRole role = roles.next();
            authorities.add(new SimpleGrantedAuthority(role.getAuthority()));
        }

        // DBのパスワードが暗号化済みの場合
        String encodedPassword = user.getPassword();
        if (!encodedPassword.matches("\\{[a-zA-Z+0-9]+\\}.+")) {
            // DBのパスワードが暗号化されていない場合
            encodedPassword = "{bcrypt}" + new BCryptPasswordEncoder().encode(user.getPassword());
        }

        // UserDetails生成
        return (UserDetails) new MyUserDetails(user.getUsername(), encodedPassword, user.getUsernameDisp(), authorities);
    }
}

ログイン画面作成

login.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>

許可がないページにアクセス時のエラー画面

login.html

<!doctype html>
<html lang="ja"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
>
    <head>
    <meta charset="utf-8" />
    </head>
    <body>

        Access Denined!

    </body>
</html>

未ログイン時はログイン画面に飛ばす設定

WebSecurityConfig.java

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity
@EnableAspectJAutoProxy
public class WebSecurityConfig {

	// エラーハンドラ(許可されていないページへのアクセス時用)
	private final Customizer<ExceptionHandlingConfigurer<HttpSecurity>> authErrorHandler
	  = new Customizer<ExceptionHandlingConfigurer<HttpSecurity>>() {
		@Override
		public void customize(ExceptionHandlingConfigurer<HttpSecurity> t) {
			t.accessDeniedPage("/access_denied");
		}
	};

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((requests) -> requests
				.requestMatchers("/dummy").permitAll()  // for life check
				.anyRequest().authenticated()
			)
			.formLogin((form) -> form
				.loginPage("/login")
				.successForwardUrl("/")
				.permitAll()
			)
			.logout((logout) -> logout
				.logoutUrl("/logout")
				.logoutSuccessUrl("/login")
				.deleteCookies("SESSION")
				.invalidateHttpSession(true)
				.permitAll()
			)
			.exceptionHandling(authErrorHandler);

		return http.build();
	}
}

コントローラの作成

IndexController.java

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import lombok.RequiredArgsConstructor;


@Controller
@RequiredArgsConstructor
public class IndexController {

    // ログイン後の表示
    @PostMapping("/")
    public String logined() {
        return "redirect:/";
    }

    // TOPページ
    @GetMapping("/")
    public String index() {
        return "index";
    }

    // アクセス不可
    @GetMapping("/access_denied")
    public String accessDenied() {
        return "access_denied";
    }
}

認可の設定

コントローラにアノテーションを設定する事により、権限の有無によってアクセス制限をかける事ができる。

    :
    @GetMapping("/books")
    @PreAuthorize("hasAuthority('ADMIN') || hasAuthority('BOOK_SEARCH')")
    public String index(@RequestParam(name="isbn", required=false, defaultValue="") String isbn,
                        @RequestParam(name="title", required=false, defaultValue="") String title,
                        @RequestParam(name="page", required=false, defaultValue="0") int page,
                        @RequestParam(name="limit", required=false, defaultValue="5") int limit,
                        Model model) {
        :
    }

テンプレート(Thymeleaf)側で権限の有無によってボタン表示などを切り替えたい場合は、以下の通り制御できる。

<html lang="ja"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
>
<body>
        :
        <div sec:authorize="hasAuthority('BOOK_UPDATE')">
            <!-- 登録ボタン -->
            <div th:if="!${book.createdAt}">
                <button name="_method" value="post">登録</button>
            </div>
            <!-- 更新/削除ボタン -->
            <div th:if="${book.createdAt}">
                <button name="_method" value="put">更新</button>
                <button name="_method" value="delete">削除</button>
                </div>
        </div>
        :
</body>
</html>

ログインユーザ情報を画面に表示する

ここではヘッダにログインユーザ表示名を表示してみる。
※デフォルトの username ではなく、カスタマイズしたテーブルの username_disp の内容を表示してみる。

Model にログインユーザを設定する為のアドバイスを作成する。

package com.example.demo.advise;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

import com.example.demo.entity.MyUserDetails;

@ControllerAdvice
public class MyLoginUserAdvise {

    @ModelAttribute("currentUserName")
    String currentUserName(Authentication authentication) {
        if (authentication != null) {
            MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
            return myUserDetails.getUsernameDisp();
        }
        return null;
    }
}

ヘッダ用のhtml

    :
        <div th:fragment="header" id="header">
            <div id="header_bar">
                <div id="header_left">
                    Spring Boot サンプル
                </div>
                <div id="header_right">
                    <!-- ユーザ表示名 -->
                    <span th:text="${currentUserName}" />
                    <!-- ログアウトボタン -->
                    <form th:action="@{/logout}" method="post">
                        <input type="submit" value="ログアウト"/>
                    </form>
                </div>
            </div>
        </div>
    :

Thymeleaf の使用

TODO:

添付ファイル: file03_create_new_project.png 75件 [詳細] file01_create_new_project.png 74件 [詳細] file02_create_new_project.png 72件 [詳細] file04_create_new_project.png 71件 [詳細] file06_create_new_project.png 66件 [詳細] file05_create_new_project.png 72件 [詳細] file07_create_new_project.png 69件 [詳細] file09_create_new_project.png 71件 [詳細] file08_create_new_project.png 72件 [詳細] filetest_boot_run.png 47件 [詳細]

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2024-07-29 (月) 02:37:06 (231d)