Spring Boot と Spring Security を使用した認証・認可システムの実装手順
Spring BootとSpring Securityを活用して、ユーザーごとに権限を持ち、複数の画面へのアクセスを制御する認証・認可システムを構築するための最適な手順を以下に示します。このガイドは、プロジェクトのセットアップからセキュリティ強化まで、包括的にカバーしています。
目次
- プロジェクトのセットアップ
- データベース設計
- エンティティとリポジトリの作成
- ユーザー詳細サービスの実装
- Spring Securityの設定
- コントローラーの作成と権限の設定
- パスワードのエンコーディングとセキュリティの強化
- セッション管理とCSRF対策
- オプション: JWTを使用したステートレス認証
- テストとセキュリティ監査
- まとめ
1. プロジェクトのセットアップ
1.1 Spring Initializrを使用してプロジェクトを作成
-
Spring Initializrにアクセスします。
-
以下の設定を行います:
- Project: Maven Project
- Language: Java
- Spring Boot: 最新の安定版(例: 3.x)
- Project Metadata: グループ名、アーティファクト名などを設定
-
Dependencies:
- Spring Web: Webアプリケーションの構築に必要
- Spring Security: セキュリティ機能の提供
- Spring Data JPA: データアクセスの簡素化
- H2 Database: 開発用のインメモリデータベース
- Thymeleaf: ビューテンプレートエンジン
- Lombok(オプション): ボイラープレートコードの削減
-
Generateボタンをクリックしてプロジェクトをダウンロードし、IDE(例: IntelliJ IDEA, Eclipse)で開きます。
1.2 pom.xml
の依存関係確認
生成された pom.xml
を確認し、必要な依存関係が含まれていることを確認します。必要に応じて追加・修正します。
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok (オプション) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT (オプション) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- その他必要な依存関係 -->
</dependencies>
1.3 プロパティの設定
src/main/resources/application.properties
に以下の設定を追加します。
# H2 Database設定
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
# JPA設定
spring.jpa.hibernate.ddl-auto=update
# サーバー設定
server.port=8080
# Thymeleaf設定(必要に応じて)
spring.thymeleaf.cache=false
2. データベース設計
認証・認可システムでは、ユーザーとその権限(ロール)を管理するために、以下のエンティティを設計します。
- User: ユーザー情報を保持
- Role: ロール(権限)情報を保持
- UserRole: ユーザーとロールの多対多関係を管理(必要に応じて)
2.1 テーブル構造の例
-
users
-
id
(主キー) -
username
(ユニーク) password
enabled
-
-
roles
-
id
(主キー) -
name
(ユニーク)
-
-
users_roles
-
user_id
(外部キー to users) -
role_id
(外部キー to roles)
-
3. エンティティとリポジトリの作成
3.1 エンティティの作成
Userエンティティ
package com.example.demo.model;
import javax.persistence.*;
import java.util.Set;
import lombok.*;
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;
}
Roleエンティティ
package com.example.demo.model;
import javax.persistence.*;
import lombok.*;
@Entity
@Table(name = "roles")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
}
3.2 リポジトリの作成
UserRepository
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
RoleRepository
package com.example.demo.repository;
import com.example.demo.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}
4. ユーザー詳細サービスの実装
Spring Securityが認証と認可のためにユーザー情報を取得するためのサービスを実装します。
package com.example.demo.service;
import com.example.demo.model.Role;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
Set<GrantedAuthority> authorities = user.getRoles()
.stream()
.map(Role::getName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true,
true,
true,
authorities);
}
}
5. Spring Securityの設定
Spring Securityを設定して、認証と認可のルールを定義します。
package com.example.demo.config;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.crypto.bcrypt.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // CSRF対策
.and()
.authorizeRequests()
.antMatchers("/", "/home", "/login", "/h2-console/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") // カスタムログインページ
.defaultSuccessUrl("/home", true)
.permitAll()
.and()
.logout()
.permitAll()
.and()
.sessionManagement()
.sessionFixation().migrateSession()
.maximumSessions(1).expiredUrl("/login?expired");
// H2コンソールを使用する場合の設定
http.headers().frameOptions().sameOrigin();
return http.build();
}
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}
重要な設定点:
-
パスワードエンコーディング:
BCryptPasswordEncoder
を使用してパスワードを安全にハッシュ化。 -
CSRF保護:
CookieCsrfTokenRepository
を使用してCSRFトークンを管理。 - セッション管理: セッション固定攻撃を防ぐため、認証時にセッションIDを再生成し、同時セッション数を制限。
- H2コンソールの設定: 開発中のみ有効化し、本番環境では無効化。
6. コントローラーの作成と権限の設定
各画面やエンドポイントに対して、権限を設定します。以下に例を示します。
6.1 HomeControllerの作成
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home"; // Thymeleafテンプレート名
}
@GetMapping("/home")
public String homePage() {
return "home";
}
@GetMapping("/login")
public String loginPage() {
return "login"; // カスタムログインページ
}
}
6.2 UserControllerの作成
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.security.access.prepost.PreAuthorize;
@Controller
public class UserController {
@GetMapping("/user/dashboard")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public String userDashboard() {
return "userDashboard";
}
}
6.3 AdminControllerの作成
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.security.access.prepost.PreAuthorize;
@Controller
public class AdminController {
@GetMapping("/admin/dashboard")
@PreAuthorize("hasRole('ADMIN')")
public String adminDashboard() {
return "adminDashboard";
}
}
注記:
-
@PreAuthorize
を使用して、メソッドレベルでの権限チェックを行っています。 -
@EnableGlobalMethodSecurity(prePostEnabled = true)
をSecurityConfig
クラスに追加して、メソッドレベルのセキュリティを有効化。
7. パスワードのエンコーディングとセキュリティの強化
7.1 パスワードのハッシュ化
ユーザーを登録する際に、パスワードをハッシュ化して保存します。BCryptPasswordEncoder
を使用します。
package com.example.demo.service;
import com.example.demo.model.Role;
import com.example.demo.model.User;
import com.example.demo.repository.RoleRepository;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.HashSet;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
public void initializeDefaultUsers() {
// ロールの初期化
Role adminRole = roleRepository.findByName("ROLE_ADMIN")
.orElseGet(() -> roleRepository.save(new Role(null, "ROLE_ADMIN")));
Role userRole = roleRepository.findByName("ROLE_USER")
.orElseGet(() -> roleRepository.save(new Role(null, "ROLE_USER")));
// デフォルトユーザーの作成
if (!userRepository.findByUsername("admin").isPresent()) {
User admin = new User();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("adminpass"));
admin.setEnabled(true);
admin.setRoles(new HashSet<>());
admin.getRoles().add(adminRole);
userRepository.save(admin);
}
if (!userRepository.findByUsername("user").isPresent()) {
User user = new User();
user.setUsername("user");
user.setPassword(passwordEncoder.encode("userpass"));
user.setEnabled(true);
user.setRoles(new HashSet<>());
user.getRoles().add(userRole);
userRepository.save(user);
}
}
// ユーザー登録メソッドなど
}
7.2 パスワードポリシーの強化
パスワードの複雑さや長さを検証するために、バリデーションを追加します。例えば、ユーザー登録時に以下のような検証を行います。
package com.example.demo.dto;
import javax.validation.constraints.*;
public class UserRegistrationDto {
@NotBlank
@Size(min = 4, max = 20)
private String username;
@NotBlank
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(regexp = "^(?=.*[A-Z])(?=.*\\d).+$", message = "Password must contain at least one uppercase letter and one number")
private String password;
// ゲッターとセッター
}
8. セッション管理とCSRF対策
8.1 セッション管理
セッション固定攻撃を防ぐために、認証時にセッションIDを再生成します。Spring Securityはデフォルトでこれを行いますが、明示的に設定することも可能です。
http.sessionManagement()
.sessionFixation().migrateSession()
.maximumSessions(1).expiredUrl("/login?expired");
8.2 CSRF対策
Spring SecurityはデフォルトでCSRF保護を有効にしています。フォームベースのアプリケーションでは、テンプレートにCSRFトークンを埋め込む必要があります。
Thymeleafを使用する場合
<form th:action="@{/login}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- 他の入力フィールド -->
<button type="submit">Login</button>
</form>
注意:
- ステートレスなAPI(例:REST API)を構築する場合は、CSRF保護を無効化するか、他の手段で保護します。
- JWTを使用する場合、CSRFの脅威は低減しますが、必要に応じて対策を講じます。
9. オプション: JWTを使用したステートレス認証
アプリケーションがRESTful APIの場合、JWT(JSON Web Token)を使用したステートレスな認証を検討できます。以下はその概要です。
9.1 依存関係の追加
pom.xml
にJWT関連の依存関係を追加します。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
9.2 JWTユーティリティクラスの作成
package com.example.demo.util;
import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;
@Component
public class JwtUtil {
private final String SECRET_KEY = "your_secret_key"; // 環境変数で管理すること推奨
private final long EXPIRATION_TIME = 86400000; // 1日
public String generateToken(UserDetails userDetails) {
Set<String> roles = userDetails.getAuthorities()
.stream()
.map(auth -> auth.getAuthority())
.collect(Collectors.toSet());
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", roles)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
Date expiration = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
}
9.3 JWTフィルタの作成
package com.example.demo.filter;
import com.example.demo.service.CustomUserDetailsService;
import com.example.demo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getJwtFromRequest(request);
if (StringUtils.hasText(token)) {
try {
String username = jwtUtil.extractUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
// トークンの検証失敗時の処理(例: ログ出力)
}
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
9.4 SecurityConfigの更新
import com.example.demo.filter.JwtAuthenticationFilter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
// 既存のBean定義
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // REST APIの場合は無効化
.authorizeRequests()
.antMatchers("/auth/**", "/public/**", "/h2-console/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // ステートレスに設定
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// H2コンソールを使用する場合の設定
http.headers().frameOptions().sameOrigin();
return http.build();
}
// 既存のAuthenticationManager設定
}
9.5 認証エンドポイントの作成
package com.example.demo.controller;
import com.example.demo.util.JwtUtil;
import com.example.demo.dto.LoginRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public String authenticateUser(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
return jwtUtil.generateToken(userDetails);
}
// LoginRequestクラスを定義
}
LoginRequestクラス
package com.example.demo.dto;
public class LoginRequest {
private String username;
private String password;
// ゲッターとセッター
}
10. テストとセキュリティ監査
10.1 テストの作成
認証・認可システムが正しく機能することを確認するために、以下のテストを作成します。
- 単体テスト: 各コンポーネント(サービス、リポジトリなど)の動作を確認
- 統合テスト: エンドツーエンドのフローをテスト(例: ログイン、アクセス制御)
例として、Spring Bootのテストフレームワークを使用した簡単な統合テストを示します。
package com.example.demo;
import com.example.demo.dto.LoginRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AuthIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testLogin() {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("admin");
loginRequest.setPassword("adminpass");
ResponseEntity<String> response = restTemplate.postForEntity("/auth/login", loginRequest, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty(); // JWTトークンが返される
}
}
10.2 セキュリティ監査
- 定期的なコードレビュー: セキュリティ上の脆弱性がないか確認
- 外部のセキュリティ専門家による監査: 第三者の視点でセキュリティを評価
- 依存関係の脆弱性チェック: 使用しているライブラリやフレームワークに既知の脆弱性がないか確認(例: OWASP Dependency-Check)
11. まとめ
以上で、Spring BootとSpring Securityを使用した認証・認可システムの実装手順が完了しました。主なポイントは以下の通りです:
- エンティティ設計: ユーザーとロールの関係を適切に設計
- ユーザー詳細サービス: Spring Securityと連携してユーザー情報を提供
- セキュリティ設定: 認証・認可のルールを明確に定義
- パスワード管理: パスワードを安全にハッシュ化して保存
- セッションとCSRF対策: 適切なセッション管理とCSRF保護を実装
- オプションでJWT認証: RESTful APIの場合、ステートレスなJWT認証を導入
- テストとセキュリティ監査: システムの堅牢性を確保するためのテストと監査
ベストプラクティスと追加のセキュリティ対策
-
環境変数の使用:
SECRET_KEY
などの機密情報は環境変数やセキュアな設定管理ツールを使用して管理 - HTTPSの使用: デプロイ環境では必ずHTTPSを使用して通信を暗号化
- ログイン試行の制限: ブルートフォース攻撃を防ぐために、ログイン試行回数を制限
- 入力検証: SQLインジェクションやXSS攻撃を防ぐために、すべての入力を適切に検証
- セキュリティアップデート: 依存関係やフレームワークのセキュリティアップデートを定期的に確認・適用
このガイドが、Spring Bootでの認証・認可システム構築の参考になれば幸いです。具体的な要件やユースケースに応じて、適宜カスタマイズしてください。