0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VSCode で DOMA3 と Spring Security を用いた API ログイン認証を実装する

Last updated at Posted at 2025-05-01

必要な拡張機能をインストールする

code --install-extension humao.rest-client
code --install-extension vmware.vscode-boot-dev-pack
code --install-extension cweijan.vscode-postgresql-client2
code --install-extension yzhang.markdown-all-in-one

Spring Initializer で新規プロジェクトを作成する

以下を選択する

  • spring-boot-devtools
  • spring-boot-starter-web
  • spring-boot-starter-security
  • spring-boot-starter-validation
  • lombok

初期化した後のGradle.buildは下記の通り。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.5'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// Spring Boot dependencies
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	// lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// test
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

Doma3 を導入する

build.gradleにDoma系のライブラリを追記する。
※詳細はEclipse + Java + Gradle の環境で Doma を動かすを参照してください。

build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.5'
	id 'io.spring.dependency-management' version '1.1.7'

    id 'com.diffplug.eclipse.apt' version '3.44.0' 		// Eclipse のアノテーションプロセッシング関連の設定ファイルを生成するプラグイン
    id 'org.domaframework.doma.compile' version '2.0.0' // Doma を使ったビルドをサポートするプラグイン
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// Spring Boot dependencies
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	// lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// testing
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	// Doma	   
    implementation 'org.seasar.doma:doma-core'  			    // Doma の核となるライブラリ。ビルド時も実行時も必要。
    implementation'org.seasar.doma:doma-slf4j' 				    // Doma のログをSLF4J経由で出力するライブラリ。
	annotationProcessor 'org.seasar.doma:doma-processor:3.6.0'	// Doma のアノテーションプロセッサー。ビルド時のみ必要。

	// logback(SLF4J)
    runtimeOnly 'ch.qos.logback:logback-classic'    		    // SLF4J の実装ライブラリ。

	// postgresql JDBCドライバ
	runtimeOnly 'org.postgresql:postgresql'					    // データベース接続用のJDBCドライバ。
}

tasks.named('test') {
	useJUnitPlatform()
}

// Eclipse 関連の設定
eclipse {
    // .classpath ファイルに関する設定
    classpath {
        file {
            whenMerged { classpath ->
                // アノテーションプロセッサーで生成したソースコードを保存するフォルダ
                def folder = new org.gradle.plugins.ide.eclipse.model.SourceFolder(".apt_generated", "bin/main")
                // クラスパスに追加
                classpath.entries.add(folder)
                // 存在しなければ作成
                def dir = file(folder.path)
                if (!dir.exists()) {
                    dir.mkdir()
                }
            }
        }
    }
    // .project ファイルに関する設定
    project {
        buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
        natures 'org.eclipse.buildship.core.gradleprojectnature'
    }
    // Eclipse から "Refresh Gradle Project" を呼び出した時に実行されるタスク
    synchronizationTasks 'cleanEclipse', 'eclipse'
}

ただし今回は IDE ではなく、VSCodeを利用しているため、このままでは動作しない。

gradle eclipseを実行する

gradle eclipseを実行し、下記のファイルが作成されることを確認する。

  • .settings/org.eclipse.jdt.apt.core.prefs
  • .settings/org.eclipse.jdt.core.prefs
  • .classpath
  • .factorypath
  • .project

その後下記のエラーが出ている場合は、.settings/org.eclipse.buildship.core.prefsを手動で作成してください。

Missing Gradle project configuration file: .settings/org.eclipse.buildship.core.prefs

.settings/org.eclipse.buildship.core.prefsには下記の内容を記入しておいてください。

connection.project.dir=
eclipse.preferences.version=1

ここで一旦gradle buildを実行し、成功するか確認してください。
問題なければ、次へ進みます。

※エラーが出る場合は gradle cleanEclipse eclipse を実行し、設定ファイルを再生成してみてください。

doma code-gen を導入する

Doma CodeGen プラグインを用いてDomaソース一式を作成します。

まずは対象のテーブルを作成します。

CREATE TABLE user_admin (
    user_id VARCHAR(100) PRIMARY KEY,
    user_name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

その後、build.gradleに Doma CodeGen の設定を追記します。

build.gradle
// buildscriptを追加
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.postgresql:postgresql:42.7.5' // 利用するJDBCドライバを指定する
    }
}


plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.5'
	id 'io.spring.dependency-management' version '1.1.7'

    id 'com.diffplug.eclipse.apt' version '4.3.0' 		
    id 'org.domaframework.doma.compile' version '3.0.1'

    id 'org.domaframework.doma.codegen' version '3.0.0' // doma-codegen プラグインを追加する
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// Spring Boot dependencies
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	// lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// testing
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	// Doma	   
    implementation 'org.seasar.doma:doma-core:3.6.0'
    implementation'org.seasar.doma:doma-slf4j:3.6.0'
	implementation 'org.seasar.doma:doma-processor:3.6.0'

	// logback(SLF4J)
    runtimeOnly 'ch.qos.logback:logback-classic'

	// postgresql JDBCドライバ
	runtimeOnly 'org.postgresql:postgresql'
}

tasks.named('test') {
	useJUnitPlatform()
}

eclipse {
    classpath {
        file {
            whenMerged { classpath ->
                def folder = new org.gradle.plugins.ide.eclipse.model.SourceFolder(".apt_generated", "bin/main")
                classpath.entries.add(folder)
                def dir = file(folder.path)
                if (!dir.exists()) {
                    dir.mkdir()
                }
            }
        }
    }
    project {
        buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
        natures 'org.eclipse.buildship.core.gradleprojectnature'
    }
    synchronizationTasks 'cleanEclipse', 'eclipse'
}

// SQLファイルを'bin/default/META-INF'にコピーするタスクを定義しておく。
task _copySqls(type: Copy) {
  from 'src/main/resources/META-INF'
  into 'bin/default/META-INF'
}
compileJava.dependsOn _copySqls //compileJavaタスクに依存させておく。

// Doma-codegen タスクを定義する
domaCodeGen {
    // make an arbitrary named block
    dev {
        // JDBC url
        url = 'jdbc:postgresql:postgres'
        // JDBC user
        user = 'postgres'
        // JDBC password
        password = 'postgres'
        // configuration for generated entity source files
        entity {
          packageName = 'com.example.demo.doma'
        }
        // configuration for generated DAO source files
        dao {
          packageName = 'com.example.demo.doma'
        }
    }
}

追記後、gradle domagenDevAllを実行し、各種ソースコードを生成してください。

Doma が動作するまで

VSCodeで操作していると、Domaが安定しない。。。
その場合は下記を確認してみてください。

  • gradle cleanEclipse eclipse を実行して、設定ファイルを再生成する。
  • 設定ファイルの内容を確認し、編集する。

簡単なAPIを作成する

管理ユーザ作成用APIを作成します。

AdminContoroller.java
package com.example.demo.domain.user.admin;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.domain.user.admin.entity.Admin;


@RestController
@RequestMapping("/api/user/admin")
public class AdminContoroller {

    private final AdminRepository adminRepository;
    private final AdminService adminService;

    AdminContoroller(AdminRepository adminRepository, AdminService adminService) {
        this.adminRepository = adminRepository;
        this.adminService = adminService;
    }
    
    @PostMapping("/create")
    public Admin create(@Validated @RequestBody Admin entity) {
        String encodedPassword = adminService.encodePassword(entity.getPassword());
        adminRepository.create(entity.getUserId(), entity.getUserName(), entity.getEmail(), encodedPassword);
        return entity;
    }
}

AdminRepository.java
package com.example.demo.domain.user.admin;

import org.springframework.stereotype.Repository;

import com.example.demo.doma.dao.UserAdminDao;
import com.example.demo.doma.entity.UserAdmin;

@Repository
public class AdminRepository {

    private final UserAdminDao userAdminDao;

    public AdminRepository(UserAdminDao userAdminDao) {
        this.userAdminDao = userAdminDao;
    }

    public void create(String userId, String userName, String email, String password) {
        
        UserAdmin userAdnmin = new UserAdmin();
        userAdnmin.setUserId(userId);
        userAdnmin.setUserName(userName);
        userAdnmin.setEmail(email);
        userAdnmin.setPasswordHash(password);

        userAdminDao.insert(userAdnmin);
    }

    public UserAdmin findByUserId(String userId) {
        return userAdminDao.selectById(userId);
    }
}
AdminService.java

package com.example.demo.domain.user.admin;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AdminService {
    
    private final PasswordEncoder passwordEncoder;
    AdminService(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
    
    public String encodePassword(String password) {
        return passwordEncoder.encode(password);
    }

}
UserAdminListener.java
package com.example.demo.doma.entity;

import java.time.LocalDateTime;

import org.seasar.doma.jdbc.entity.EntityListener;
import org.seasar.doma.jdbc.entity.PostDeleteContext;
import org.seasar.doma.jdbc.entity.PostInsertContext;
import org.seasar.doma.jdbc.entity.PostUpdateContext;
import org.seasar.doma.jdbc.entity.PreDeleteContext;
import org.seasar.doma.jdbc.entity.PreInsertContext;
import org.seasar.doma.jdbc.entity.PreUpdateContext;

/**
 * 
 */
public class UserAdminListener implements EntityListener<UserAdmin> {

    @Override
    public void preInsert(UserAdmin entity, PreInsertContext<UserAdmin> context) {
        LocalDateTime now = LocalDateTime.now();    //リスナーでタイムスタンプを発行できるようにしておく。
        entity.setCreatedAt(now);
        entity.setUpdatedAt(now);
    }

    @Override
    public void preUpdate(UserAdmin entity, PreUpdateContext<UserAdmin> context) {
    }

    @Override
    public void preDelete(UserAdmin entity, PreDeleteContext<UserAdmin> context) {
    }

    @Override
    public void postInsert(UserAdmin entity, PostInsertContext<UserAdmin> context) {
    }

    @Override
    public void postUpdate(UserAdmin entity, PostUpdateContext<UserAdmin> context) {
    }

    @Override
    public void postDelete(UserAdmin entity, PostDeleteContext<UserAdmin> context) {
    }
}

REST Client で下記リクエストを送信してみると、無事管理者ユーザを登録できました。

# ユーザー作成APIを呼び出す
POST http://localhost:8080/api/user/admin/create HTTP/1.1
content-type: application/json

{
    "userId": "admin",
    "userName": "管理者",
    "password": "admin123",
    "email": "test@example.com"
}

Spring Security の導入

SecurityConfigを書きます。

SecurityConfig.java
package com.example.demo.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/api/auth/**").permitAll()
                        .requestMatchers("/api/user/admin/**").hasRole("ADMIN")
                        .anyRequest().permitAll())
                .csrf((csrf) -> csrf
                        .disable());
        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService,
            PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);

        return new ProviderManager(authenticationProvider);
    }
}

あとは、userDetailsServiceをDomaを使ってユーザーを取得するように、実装します。

UserDetailServiceImpl.java
package com.example.demo.domain.auth;

import java.util.Collections;
import java.util.Objects;

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.stereotype.Service;

import com.example.demo.doma.entity.UserAdmin;
import com.example.demo.domain.user.admin.AdminRepository;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    private final AdminRepository adminRepository;

    public UserDetailServiceImpl(AdminRepository adminRepository) {
        this.adminRepository = adminRepository;
    }

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

        UserAdmin user = adminRepository.findByUserId(username);

        if (Objects.isNull(user)) {
            throw new UsernameNotFoundException("User not found");
        } else {
            return new User(user.getUserId(), user.getPasswordHash(), 
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")));
        }
    }
    
}

最後にログイン用APIを作成します。

AuthController.java
package com.example.demo.domain.auth;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
    private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
            .getContextHolderStrategy();

    AuthController(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/login")
    public void postMethodName(@RequestBody AuthRequest entity, HttpServletRequest request,
            HttpServletResponse response) {
        UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
                entity.userName, entity.password);
        Authentication authentication = authenticationManager.authenticate(token);
        SecurityContext context = securityContextHolderStrategy.createEmptyContext();
        context.setAuthentication(authentication);
        securityContextHolderStrategy.setContext(context);
        securityContextRepository.saveContext(context, request, response);
    }

    public record AuthRequest(String userName, String password) {
    }

}

ログインせずに、下記リクエストを送信してみます。

# ユーザー作成APIを呼び出す
POST http://localhost:8080/api/user/admin/create HTTP/1.1
content-type: application/json

{
    "userId": "admin2",
    "userName": "管理者2",
    "password": "admin123",
    "email": "test@example.com"
}

今度は、403エラーが返ってきました。

{
    "timestamp":"2025-05-01T12:36:10.513+00:00",
    "status":403,
    "error":"Forbidden",
    "message":"Access Denied",
    "path":"/api/user/admin/create"
}

次に、下記のログイン用APIを呼び出します。
(事前にアカウントは登録しておいてください。)

# ログインAPIを呼び出す
POST http://localhost:8080/api/auth/login HTTP/1.1
content-type: application/json

{
    "userName": "admin",
    "password": "admin123"
}

ログインに成功したら、再度管理者ユーザ作成用APIにリクエストを投げてみると、無事ステータス200が返ってきました。

以上です。
今回作成したデモアプリは公開しているので、気になる人はソースを確認してください。

DOMA3 と Spring Security でログイン認証を実装する

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?