結論

Spring Securityとは、誰が入れて何をしてよいかを、アプリの入口でまとめて確かめる仕組みです。認証・認可・攻撃対策の3つを受け持ちます。

この記事でわかること

  • 入れる前・後・設定後の curl 応答(200・401・403)
  • できること一覧(ログイン方式・認可・CSRF など9項目)
  • DB認証・BCrypt・CSRF の完成コードとエラー表8件

2026年10月17日更新|Spring Boot 4.1.1・Security 7.1.1で実行確認

リクエストはコントローラーに届く前に、Spring Security の入口(フィルター)を通ります。ここで攻撃対策・認証・認可を順に確かめ、通らなければ 401 や 403 で止めます。

flowchart TD
    A[リクエスト] --> B[攻撃対策<br>CSRF・ヘッダー]
    B --> C[認証<br>だれかを確かめる]
    C --> D[認可<br>入ってよいか決める]
    D --> E[コントローラー]
    C -.->|確かめられない| F[401・ログイン画面<br>403で止める]
    D -.->|権限がない| F

3つの役割と、Spring Security が代わりにしてくれることです。

役割 例 Spring Security がすること
認証(だれか) IDとパスワードで本人を確かめる フォーム・Basic・JWT などで確かめる
認可(何をしてよいか) 一般ユーザーは /admin に入れない URL・メソッドごとに許可を決め、権限がなければ403
攻撃対策 別サイトからの勝手な送信を防ぐ CSRFトークンと安全なヘッダーを既定で付ける

確認環境:Spring Boot 4.1.1(Spring Framework 7.0.9・Spring Security 7.1.1・Apache Tomcat 11.0.24・Logback 1.5.38・HikariCP 7.0.2 は Boot の依存管理の版)。これらを GitHub のタグのソースからビルドし、OpenJDK 21.0.10(Linux)で記事のアプリを「Spring Security なし」「starter を足しただけ」「設定あり」の3段階で起動して、起動ログと curl の応答を取りました。DB認証は PostgreSQL 16.15(JDBC ドライバー 42.7.2)、BCrypt の性質は Spring Security 7.1.1 の BCryptPasswordEncoder で確かめています。仕様は Spring Security 7.1.1・Spring Boot 4.1.1 の公式リファレンスとソースで確認しました(確認日 2026年10月17日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Securityとは|何のためにあるかと入門・DB認証まで」

Spring Securityは何のためにあるのか

Spring Security は、Spring で作ったアプリに「ログイン」「権限による出し分け」「よくある攻撃への備え」を加えるフレームワークです。 公式リファレンスは「認証、認可、よくある攻撃からの保護を提供するフレームワーク」と説明しています。Spring Boot で Web アプリや API を作るときの、事実上の標準の部品です。

3つの役割:認証・認可・攻撃対策

1認証:あなたはだれ?

ログインIDとパスワードなどで、本人かどうかを確かめます。英語で Authentication。確かめられないときの応答が 401(またはログイン画面への転送)です。

2認可:それをしてよい?

本人と分かったうえで、その操作をしてよいかを決めます。英語で Authorization。「一般ユーザーは管理画面に入れない」がこれで、許可されないときの応答が 403 です。

3攻撃対策:悪い使われ方を防ぐ

別サイトから勝手に送信させる攻撃(CSRF)、画面を別サイトに埋め込む攻撃(クリックジャッキング)などへの備えです。入れるだけで既定で有効になります。

たとえ:ビルの受付と入館ゲート

会社のビルにたとえると分かりやすくなります。

ビルでは アプリでは(Spring Security の部品)
受付で社員証を見て本人を確かめる ログインで本人を確かめる=認証(UserDetailsService・PasswordEncoder)
社員証に「何階に入れるか」が書いてある ロール(ROLE_ADMIN など)で許可を決める=認可(authorizeHttpRequests)
不審な人や荷物を入口で止める CSRF・危ないヘッダーを入口で止める=攻撃対策(CsrfFilter・HeaderWriterFilter)
確認はビルの入口1か所でまとめて行う すべてのリクエストを入口のフィルターで確かめる(SecurityFilterChain)

部屋ごとに鍵の確認を任せると、どこかで確認が漏れます。 入口1か所でまとめて確かめれば、新しい部屋(新しい URL)が増えても、確認が自動でかかります。Spring Security の「フィルター」は、この入口の役目です。

無いとどうなるか

Spring Security を使わずに同じことをしようとすると、ログインの確認やパスワードの扱いを自分で書くことになります。自作と比べると次のようになります。

ログインの確認

自作各コントローラーの先頭に「ログインしているか」の確認を書く
→
Spring Security入口のフィルターで全URLをまとめて確かめる。コントローラーには書かない

確認の書き忘れ

自作1か所でも書き忘れると、そのURLは誰でも見られる
→
Spring Security既定で全URLがログイン必須。開けたいURLだけを明示する

パスワードの保存

自作平文や弱いハッシュで保存してしまう危うさがある
→
Spring SecurityBCrypt などで、元に戻せない形にして保存・照合する部品がそろっている

CSRF・セッション

自作トークンの発行と検証、ログイン時のセッションIDの付け替えを自分で書く
→
Spring SecurityCSRF対策とセッション固定攻撃への対策が既定で有効

セキュリティヘッダー

自作レスポンスごとに付け忘れがないか気を配る
→
Spring SecurityX-Frame-Options や Cache-Control などを全レスポンスに付ける

Spring Security が必要かどうかの目安は、「ログインがあるか」「人によって見せる内容が違うか」です。 どちらかに当てはまるなら、自作するより Spring Security を使うほうが漏れを防げます。ログインの無い、誰でも見てよい静的なページだけのアプリなら、入れない選択もあります。

入れる前と後で何が変わるか(実際に動かす)

同じ小さなアプリを、「Spring Security なし」「spring-boot-starter-security を足しただけ」「設定を書いたあと」の3段階で起動し、curl で呼んだ結果です。3つの段階は別のポート(25100・25101・25102)で起動しています。

アプリのコントローラーは次の1つだけです。

@RestController
public class PageController {

    @GetMapping("/public")
    String publicPage() {
        return "public: だれでも見られるページ\n";
    }

    @GetMapping("/mypage")
    String myPage(Principal principal) {
        return "mypage: " + (principal == null ? "(ログインなし)" : principal.getName()) + "\n";
    }

    @GetMapping("/admin")
    String admin() {
        return "admin: 管理者だけのページ\n";
    }

    @PostMapping("/mypage/profile")
    String updateProfile() {
        return "updated\n";
    }
}

入れる前:管理画面も誰でも見える

$ curl -i http://localhost:25100/admin
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 35
Date: Sat, 17 Oct 2026 13:16:35 GMT

admin: 管理者だけのページ

ログインしていない curl から、管理者用のページがそのまま見えます。 POST /mypage/profile も、どこから送っても 200 と updated が返りました。セキュリティヘッダーも付いていません。

starter を足しただけ:全URLがログイン必須になる

依存関係に spring-boot-starter-security を足して、コードは何も変えずに起動しました。起動ログに次の行が出ます。

2026-10-17T22:17:56.440+09:00  WARN 1737 --- [           main] .s.a.UserDetailsServiceAutoConfiguration : 

Using generated security password: eb1fcf52-7173-441c-af92-0bbbe4ca759f

This generated password is for development use only. Your security configuration must be updated before running your application in production.

ユーザー名 user と、このランダムなパスワードでログインできる開発用のユーザーが1人作られます。 パスワードは起動のたびに変わります。Spring Boot の公式リファレンスによると、このメッセージは WARN で出るため、ログの設定で org.springframework.boot.security.autoconfigure の WARN を止めると表示されなくなります。

この状態で、さきほどと同じ curl を送った結果です。

$ curl -i http://localhost:25101/public
HTTP/1.1 401 
Set-Cookie: JSESSIONID=E51BAC378F199C92CF0D68C0A89CCEBB; Path=/; HttpOnly
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sat, 17 Oct 2026 13:18:03 GMT

/public も含めて、すべてのURLが 401 になりました。 ブラウザのように Accept: text/html を付けると、401 ではなくログイン画面への転送(302)になります。

$ curl -i -H 'Accept: text/html' http://localhost:25101/admin
HTTP/1.1 302 
Set-Cookie: JSESSIONID=B017BFA8F9EBB2231D90F921B66B34E7; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:25101/login
Content-Length: 0
Date: Sat, 17 Oct 2026 13:18:03 GMT

転送先の /login には、Spring Security が自動で作るログイン画面(見出しは「Please sign in」)が出ます。curl -u user:(生成されたパスワード) を付けると /mypage は 200 で mypage: user が返りました。

starter を足しただけで付いたもの・変わったものをまとめます。

見えたもの 意味
401 と WWW-Authenticate: Basic ログインしていない API 呼び出しを止めた(Basic 認証を求める)
302 と Location: /login ブラウザからのアクセスはログイン画面へ回した
Set-Cookie: JSESSIONID=…; HttpOnly ログイン状態を覚えるセッションを用意した
X-Frame-Options: DENY 他のサイトの枠(iframe)に表示させない
X-Content-Type-Options: nosniff ファイルの種類をブラウザに推測させない
Cache-Control: no-cache, no-store … ログイン後の画面をブラウザやプロキシに残させない
X-XSS-Protection: 0 ブラウザに以前あった XSS フィルターを切る(公式の推奨)

公式リファレンスの既定ヘッダーには Strict-Transport-Security もありますが、これは HTTPS のリクエストにだけ付きます。今回は HTTP で呼んだので付いていません。

設定して必要な所だけ開ける

最後に、次の設定を書きました(ユーザーは後半の「DB認証」の節のとおり PostgreSQL から読みます。user は一般ユーザー、admin は管理者です)。

@Bean
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/", "/public", "/css/**", "/js/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults())
        .httpBasic(Customizer.withDefaults())
        .logout(logout -> logout.logoutSuccessUrl("/public"));
    return http.build();
}

同じURLを、だれで呼ぶかを変えて送った結果です。

リクエスト 結果 返ってきた本文・ヘッダー
GET /public(ログインなし) 200 public: だれでも見られるページ
GET /mypage(ログインなし) 401 WWW-Authenticate: Basic realm="Realm"
GET /admin(user・一般ユーザー) 403 本文なし
GET /admin(admin・管理者) 200 admin: 管理者だけのページ
GET /mypage(パスワード違い) 401 WWW-Authenticate: Basic realm="Realm"

/admin に一般ユーザーで入ったときの応答です。ログインはできているので 401 ではなく 403 になります。

$ curl -i -u user:password http://localhost:25102/admin
HTTP/1.1 403 
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sat, 17 Oct 2026 13:19:43 GMT

考え方:まず全部閉じて、必要な所だけ開ける

3段階の結果から分かる Spring Security の基本の考え方です。

1

starter を入れると、全部閉じる

全URLがログイン必須になる。開け忘れても「見えない」側に倒れるので、漏れより安全

2

誰でも見てよいURLだけを開ける

requestMatchers("/public").permitAll()。トップ・ログイン画面・CSS・JS など

3

権限が要るURLにロールを付ける

requestMatchers("/admin/**").hasRole("ADMIN")。一般ユーザーは 403 になる

4

残りは最後にまとめて「ログイン必須」

anyRequest().authenticated() を最後に書く。新しいURLは自動でここに入る

Spring Securityでできること一覧

ログインの方式、権限による出し分け、パスワードの保存、攻撃対策まで、Webアプリの安全に関わる部分をひととおり持っています。 公式リファレンスの機能の章(認証・認可・攻撃からの保護・連携)から、入門でよく使うものを並べました。

できること 主な書き方・部品 既定の状態
フォームログイン・ログアウト formLogin()・logout() starter だけで有効
Basic 認証 httpBasic() starter だけで有効
JWT・OAuth2・SAML でのログイン oauth2ResourceServer()・oauth2Login()・saml2Login() 依存を足して設定する
URLごとの認可 authorizeHttpRequests()・hasRole() 全URLがログイン必須
メソッドごとの認可 @EnableMethodSecurity と @PreAuthorize 設定で有効にする
パスワードのハッシュ化 PasswordEncoder(BCrypt など) Bean を用意する(標準は bcrypt)
CSRF対策 csrf()・csrf.spa() 有効
セッション管理 sessionManagement() セッション固定攻撃への対策が有効
セキュリティヘッダー・CORS headers()・cors() ヘッダーは有効、CORS は設定する

それぞれを詳しく扱った記事です。

Spring Securityの主要部品の早見表

ここからは、設定コードを書くときに出てくる部品です。

部品 役割 この記事で書くもの
SecurityFilterChain リクエストに適用するフィルタの並び(設定の本体) @Beanで定義
HttpSecurity SecurityFilterChainを組み立てるビルダー 認可ルール・ログイン方式・CSRF
UserDetailsService ユーザー名からユーザー情報を取り出す JdbcUserDetailsManager(DB)
PasswordEncoder パスワードのハッシュ化と照合 BCryptPasswordEncoder/DelegatingPasswordEncoder
AuthenticationEntryPoint 未ログイン時の応答(ログイン画面へ転送、401など) API用に401を返す設定
AccessDeniedHandler ログイン済みだが権限不足のときの応答(403) 既定のまま、または独自画面
CsrfFilter POSTなどの更新系リクエストでCSRFトークンを検証 既定で有効、SPA・APIでの扱い

バージョンと、書き方が変わったポイント

Spring Boot 4.1.1 の依存管理では、Spring Security は 7.1.1 です。 Spring Boot 3.x 系では Spring Security 6.x 系が使われます。書き方の主な変化は次のとおりです。

項目 以前の書き方 現在の書き方
設定クラス WebSecurityConfigurerAdapterを継承(Security 6.0で削除) SecurityFilterChainを@Beanで返す
設定のつなぎ方 .and()でつなぐ(Security 7で削除) ラムダ式(Lambda DSL)で各項目を書く
URLごとの認可 authorizeRequests()(Security 7で削除) authorizeHttpRequests()
URLの指定 antMatchers()/mvcMatchers()(6.0で削除) requestMatchers()
SPA向けCSRF Cookie用の設定を個別に組み合わせる csrf.spa()(Security 7.0で追加)

検索で見つかる記事の書き方が、使っているバージョンと合っているかを最初に確かめると、つまずきが大きく減ります。 Spring Security 7.1.1 の HttpSecurity に authorizeRequests() はなく、各設定から HttpSecurity に戻る .and() も無くなっています。

依存関係(Gradle)

Spring Boot 4 では、Web の起点となるスターター名が spring-boot-starter-webmvc になりました(旧名 spring-boot-starter-web は Spring Boot 4.1.1 のソースで「spring-boot-starter-webmvc に置き換え予定(deprecated)」と説明されています)。テストで @WithMockUser などを使うには spring-boot-starter-security-test を追加します。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
}

自分で SecurityFilterChain の Bean を定義すると、Spring Boot の既定の設定(全URLログイン必須)は使われなくなります。 さらに UserDetailsService などの Bean を定義すると、起動ログの生成パスワードのユーザーも作られなくなります。実際に、DB のユーザーを読む設定で起動したときは Using generated security password の行は出ず、Global AuthenticationManager configured with UserDetailsService bean with name users と出ました。

SecurityFilterChainの仕組み

Spring Securityは、Servletのフィルタとしてリクエストの前に割り込みます。 公式リファレンスの構成を図にすると次のようになります。

flowchart TD
    A[ブラウザ・curl] --> B[DelegatingFilterProxy]
    B --> C[FilterChainProxy]
    C -->|最初に一致した1つ| D[SecurityFilterChain<br>CSRF→認証→認可]
    D --> E[Controller]
    D -.->|拒否| F[401・403]
登場人物 何をするか
DelegatingFilterProxy Servletコンテナと、Springが管理するBeanのフィルタをつなぐ
FilterChainProxy 複数のSecurityFilterChainから、最初に一致した1つだけを選んで実行する
SecurityFilterChain 選ばれたチェーンのフィルタを順に実行する
ExceptionTranslationFilter 認証エラーはAuthenticationEntryPointへ、権限エラーはAccessDeniedHandlerへ振り分ける
AuthorizationFilter authorizeHttpRequestsのルールで許可・拒否を決める

フィルタは「CSRF対策 → 認証 → 認可」の順に並びます。 ログレベルを DEBUG にすると、起動時に実際の並びが出ます。

logging.level.org.springframework.security=DEBUG

後で出てくる「API用」と「画面用」の2つのチェーンを定義して起動したときのログです(時刻などを省き、読みやすいように改行しています)。

DEBUG o.s.s.web.DefaultSecurityFilterChain : Will secure Or [PathPattern [/api/**]] with filters:
  DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter,
  HeaderWriterFilter, LogoutFilter, BasicAuthenticationFilter, RequestCacheAwareFilter,
  SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter,
  ExceptionTranslationFilter, AuthorizationFilter
DEBUG o.s.s.web.DefaultSecurityFilterChain : Will secure any request with filters:
  DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter,
  HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter,
  DefaultResourcesFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter,
  BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter,
  AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter

画面用のチェーンでは、CsrfFilter(CSRF対策)が UsernamePasswordAuthenticationFilter(フォームログイン)・BasicAuthenticationFilter(Basic 認証)より前に、AuthorizationFilter(認可)が最後に並んでいます。csrf.disable() にした API 用のチェーンには CsrfFilter がありません。

最小構成のSecurityFilterChain

まずは「トップと静的ファイルは誰でも、管理画面はADMINだけ、それ以外はログイン必須」という形から始めます。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain webChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/css/**", "/js/**", "/login", "/error").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")              // 独自のログイン画面(省略すると標準画面)
                .defaultSuccessUrl("/mypage", true)
            )
            .logout(logout -> logout.logoutSuccessUrl("/"));
        return http.build();
    }
}

認可ルールを書くときのポイントは次の4つです。

ポイント 説明
上から順に評価される 具体的なURLを先に、anyRequest()は最後に書く
hasRole("ADMIN")はROLE_ADMINを見る DBの権限はROLE_付きで保存する。付けない場合はhasAuthority("ADMIN")
独自ログイン画面はpermitAll()にする 忘れるとログイン画面自体にログインが必要になり、転送が繰り返される
/errorをpermitAll()にする 既定では Boot の /error もログイン必須。開けないと 403 や 500 が 401 に見えることがある(CSRFの節で実例)

APIと画面でチェーンを分ける

securityMatcherと@Orderを使うと、URLごとに別のチェーンを定義できます。 FilterChainProxyは最初に一致したチェーンだけを使うため、範囲の狭いチェーンに小さい@Orderを付けます。

@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults())
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .csrf(csrf -> csrf.disable())   // ブラウザ以外から呼ばれるステートレスAPIの場合
        .exceptionHandling(e -> e
            .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));
    return http.build();
}

@Bean
@Order(2)
SecurityFilterChain webChain(HttpSecurity http) throws Exception { /* 上の画面用の設定 */ }

API 用のチェーンで、ログインなしとログインありで /api/hello を呼んだ結果です。HttpStatusEntryPoint にしたので、401 に WWW-Authenticate ヘッダーが付かず、ブラウザの Basic 認証のダイアログも出ません。

$ curl -i http://localhost:25102/api/hello
HTTP/1.1 401 
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sat, 17 Oct 2026 13:19:50 GMT

$ curl -i -u user:password http://localhost:25102/api/hello
HTTP/1.1 200 
(ヘッダーは同じ)
Content-Type: text/plain;charset=UTF-8

{"user":"user"}

STATELESS にしたので、API 用のチェーンの応答には Set-Cookie: JSESSIONID が付きません(画面用のチェーンの 401 には付いていました)。

DB認証:JdbcUserDetailsManagerでログインする

DBに保存したユーザーでログインするには、UserDetailsServiceの実装をBeanとして用意します。 Spring Securityには、標準のテーブル構成で使えるJdbcUserDetailsManagerが用意されています。

テーブルを作る

公式リファレンスの標準スキーマは、ユーザー表usersと権限表authoritiesの2つです。公式の定義はvarchar_ignorecaseという型を使っています(公式の例は H2 で動かしています)。PostgreSQL 16.15 でこの型を使うと ERROR: type "varchar_ignorecase" does not exist になるため、PostgreSQLではvarcharに置き換えます。 公式リファレンスも、使うデータベースに合わせてスキーマを調整するよう書いています。

-- schema.sql(PostgreSQL向けに型を置き換えたもの)
create table users(
  username varchar(50) not null primary key,
  password varchar(500) not null,
  enabled boolean not null
);
create table authorities (
  username varchar(50) not null,
  authority varchar(50) not null,
  constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

このDDLをPostgreSQL 16.15で実行し、公式リファレンスに載っているpasswordのハッシュ値でユーザーを登録して、JdbcUserDetailsManagerが既定で発行するものと同じ形のSELECT(select username,password,enabled from users where username = ?)を流した結果です。

 username |                               password                               | enabled 
----------+----------------------------------------------------------------------+---------
 user     | {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW | t
(1 row)

 username | authority 
----------+-----------
 user     | ROLE_USER
(1 row)

同じ権限を2回登録すると、ユニークインデックスで弾かれます。

ERROR:  duplicate key value violates unique constraint "ix_auth_username"
DETAIL:  Key (username, authority)=(user, ROLE_USER) already exists.

Beanを定義する

@Bean
UserDetailsManager users(DataSource dataSource) {
    return new JdbcUserDetailsManager(dataSource);
}

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

ユーザー登録画面から登録するときは、必ずPasswordEncoderでハッシュ化してから保存します。

@Service
public class SignupService {
    private final UserDetailsManager users;
    private final PasswordEncoder encoder;

    public SignupService(UserDetailsManager users, PasswordEncoder encoder) {
        this.users = users;
        this.encoder = encoder;
    }

    public void signup(String username, String rawPassword, String... roles) {
        users.createUser(User.withUsername(username)
            .password(encoder.encode(rawPassword))   // {bcrypt}$2a$10$... の形で保存される
            .roles(roles)                            // "ADMIN" → authorities に ROLE_ADMIN が入る
            .build());
    }
}

起動時にこの signup("admin", "admin-pass", "ADMIN", "USER") を呼んでから、テーブルを見た結果です。roles() に渡した名前に ROLE_ が付いて保存されています。

 username |                               password                               | authority  
----------+----------------------------------------------------------------------+------------
 admin    | {bcrypt}$2a$10$rON6cPF5QFLgJ3vxINgoRuSnc.aPDAtikc.MYpzafZAKn5Bdz50hO | ROLE_ADMIN
 admin    | {bcrypt}$2a$10$rON6cPF5QFLgJ3vxINgoRuSnc.aPDAtikc.MYpzafZAKn5Bdz50hO | ROLE_USER
 user     | {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW | ROLE_USER
(3 rows)

この admin で /admin が 200、user で 403 になったのが、前半の「設定して必要な所だけ開ける」の結果です。

既存のテーブルを使うとき:UserDetailsServiceを自作する

すでに独自の会員テーブルがある場合は、UserDetailsServiceを実装します。やることは「ユーザー名で1件探し、UserDetailsに詰めて返す」だけです。

@Service
public class AccountUserDetailsService implements UserDetailsService {
    private final AccountRepository accounts;

    public AccountUserDetailsService(AccountRepository accounts) {
        this.accounts = accounts;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        Account a = accounts.findByLoginId(username)
            .orElseThrow(() -> new UsernameNotFoundException("not found"));
        return User.withUsername(a.loginId())
            .password(a.passwordHash())
            .authorities(a.roles().toArray(String[]::new))   // 例: "ROLE_USER"
            .disabled(!a.active())
            .build();
    }
}

パスワードのハッシュ化:BCryptPasswordEncoder

パスワードは元に戻せない形(ハッシュ)で保存し、ログイン時は入力値を同じ方法で照合します。 Spring SecurityのPasswordEncoderFactories.createDelegatingPasswordEncoder()は、新しいパスワードをbcryptでハッシュ化し、{bcrypt}のようなアルゴリズム名の接頭辞つきで保存します。将来アルゴリズムを変えても、接頭辞を見て以前のハッシュも照合できる仕組みです。

書き方 保存される形 使いどころ
createDelegatingPasswordEncoder() {bcrypt}$2a$10$... 新規アプリの標準
new BCryptPasswordEncoder() $2a$10$...(接頭辞なし) bcryptだけで運用すると決めている場合
{noop}password 平文 学習用のサンプルのみ

bcryptの性質を実際に動かして確認する

Spring Security 7.1.1 の BCryptPasswordEncoder と DelegatingPasswordEncoder をそのまま呼んで確かめました。

PasswordEncoder delegating = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String h1 = delegating.encode("password");
String h2 = delegating.encode("password");
System.out.println("hash1: " + h1);
System.out.println("hash2: " + h2);
System.out.println("同じ文字列でもハッシュは別: " + !h1.equals(h2));
System.out.println("正しいパスワード: " + delegating.matches("password", h1));
System.out.println("誤ったパスワード: " + delegating.matches("passw0rd", h1));

// Spring Security 公式ドキュメント掲載のハッシュ("password" を bcrypt したもの)
String doc = "{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW";
System.out.println("公式ドキュメントのハッシュと一致: " + delegating.matches("password", doc));

BCryptPasswordEncoder plain = new BCryptPasswordEncoder();
System.out.println("接頭辞なし: " + plain.encode("password"));
System.out.println("平文と照合: " + plain.matches("password", "password"));

for (int strength : new int[] {10, 12, 14}) {
    BCryptPasswordEncoder enc = new BCryptPasswordEncoder(strength);
    String h = enc.encode("password");
    long t = System.nanoTime();
    enc.matches("password", h);
    System.out.printf("strength=%d: 照合 %.3f 秒%n", strength, (System.nanoTime() - t) / 1e9);
}

実行結果(2コアのLinuxで実行。ハッシュ値と時間は実行ごと・環境ごとに変わります):

hash1: {bcrypt}$2a$10$ytFJKJ.aFEeE1XkECO8LFeZr4gUU8wSNQH0VSFR4qouc11gwhtUeK
hash2: {bcrypt}$2a$10$8CwwyfCHmjVk0zE.um/1He.zE8EolGxt/HtNc32DWF9XwqkJFa/7y
同じ文字列でもハッシュは別: true
正しいパスワード: true
誤ったパスワード: false
公式ドキュメントのハッシュと一致: true
接頭辞なし: $2a$10$m00JwAiN3uW4lN67xvrFo.7wWDq9XymX6y2CfQ3ofwJnrYYQ9IfH2
WARNING: Encoded password does not look like BCrypt
平文と照合: false
strength=10: 照合 0.088 秒
strength=12: 照合 0.323 秒
strength=14: 照合 1.289 秒

わかることは次の4点です。

  • 同じパスワードでも毎回ハッシュが変わる(ランダムなソルトが含まれるため)。だから「ハッシュ同士を文字列比較」ではなくmatches()で照合します
  • $2a$10$の10が強度(コスト)で、1増えるごとに計算量が約2倍になります。今回の環境では12で約0.3秒、14で約1.3秒でした
  • DBに平文が入っていると、BCryptPasswordEncoder は例外ではなく警告ログ(Encoded password does not look like BCrypt)を出して false を返します。ログインできない原因を探すときは、このログを見ます
  • 公式リファレンスは「検証に約1秒かかる程度」に調整することをすすめています。new BCryptPasswordEncoder(12)のように強度を指定し、本番と同じ性能のサーバーで測って決めます(既定値は10)

CSRFの扱い

CSRF(クロスサイト・リクエスト・フォージェリ)対策は既定で有効です。 CSRFとは、ログイン中のユーザーのブラウザを悪用して、別サイトから意図しない更新リクエストを送らせる攻撃です。Spring Security 7.1.1 の CsrfFilter は、GET・HEAD・TRACE・OPTIONS 以外(POST・PUT・DELETE・PATCH など)のリクエストにトークンを要求します。

アプリの形 設定 ポイント
サーバー側で画面を作る(Thymeleafなど) 既定のまま th:actionを使うフォームにはトークンが自動で入る
SPA(ブラウザのJavaScriptからAPIを呼ぶ) csrf(csrf -> csrf.spa())(7.0以降) XSRF-TOKENCookieを読み、X-XSRF-TOKENヘッダーで送る
ブラウザ以外から呼ばれるステートレスAPI csrf(csrf -> csrf.disable()) Cookieのセッションを使わない場合に限る
一部のURLだけ対象外(外部からのWebhookなど) csrf.ignoringRequestMatchers("/hooks/**") 範囲を最小限にする

トークンあり・なしでPOSTした結果

フォームログインでログインしたセッションで、POST /mypage/profile を送りました。

送り方 結果
ログイン画面の _csrf を付けずに POST 403
ログイン画面の <input name="_csrf" type="hidden" value="…"> の値を付けて POST 200(updated)
Basic 認証(セッションなし)でトークンを付けずに POST、/error は開けていない 401(403 ではない)
同じ送り方で、/error を permitAll() にした設定 403

3行目が入門でつまずきやすい所です。 DEBUG ログ(時刻などを省いています)を見ると、CsrfFilter が Invalid CSRF token found で 403 を返したあと、エラー画面の GET /error に転送され、その /error がログイン必須のため 401 に変わっていました。

DEBUG o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:25103/mypage/profile
DEBUG o.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code
DEBUG o.s.security.web.FilterChainProxy        : Securing GET /error
DEBUG o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext

公式リファレンスの Getting Started も、既定の動きとして「Boot の /error を含むすべてのエンドポイントで認証を求める」と書いています。「POST だけ 401 になる」ときは CSRF トークンを疑い、/error を permitAll() にすると本当のステータス(403)が見えます。

401と403の違い

401は「あなたが誰かわからない(未ログイン)」、403は「誰かはわかるが権限がない」です。 Spring SecurityではExceptionTranslationFilterがこの2つを振り分けます。前半の実行結果と合わせて整理します。

状況 処理する部品 応答(実行結果)
未ログインでブラウザから保護URLへ AuthenticationEntryPoint 302(Location: /login)
未ログインで curl・API から 同上 401とWWW-Authenticate: Basic
未ログインでHttpStatusEntryPointのAPIへ 同上 401(WWW-Authenticateなし)
ログイン済みだがロール不足 AccessDeniedHandler 403
CSRFトークンなし・不一致 AccessDeniedHandler 403(/errorがログイン必須だと401に見える)

APIでは、ログイン画面へのリダイレクトではなく401を返すほうがクライアントで扱いやすくなります。前述のHttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)がその設定です。

よくあるエラーと直し方

症状・メッセージ 主な原因 直し方
Given that there is no default password encoder configured, each password must have a password encoding prefix.(7.1.1で実際に出た文。公式リファレンスのトラブルシューティングにはThere is no PasswordEncoder mapped for the id "null"と載っている) DelegatingPasswordEncoderを使っているのに、DBのハッシュに{bcrypt}などの接頭辞がない 既存ハッシュの先頭に{bcrypt}を付ける、またはBCryptPasswordEncoderを直接使う
ログにEncoded password does not look like BCrypt BCryptPasswordEncoder直指定なのにDBが平文や別形式 登録時にencode()してから保存する
POSTだけ403 CSRFトークンが送られていない フォームはth:action、SPAはcsrf.spa()、ステートレスAPIは無効化を検討
POSTだけ401(Basic 認証のとき) CSRFの403のあと、/errorがログイン必須で401に変わった /errorをpermitAll()にして本当の原因を見る
ログインできるのに/adminが403 DBの権限がADMINで、hasRole("ADMIN")はROLE_ADMINを探す 権限をROLE_ADMINで保存、またはhasAuthority("ADMIN")
ログイン画面へのリダイレクトが繰り返される 独自の/loginがpermitAll()になっていない ログイン画面と、その画面が使うCSS・JSを許可する
CSSや画像が読み込まれない 静的ファイルも認証対象になっている requestMatchers("/css/**", "/js/**").permitAll()
自作のチェーンが使われない 先に別のチェーンに一致している securityMatcherの範囲と@Orderの順番を見直す

1行目と5行目は、PostgreSQL に「接頭辞のないハッシュ」と「ADMIN(ROLE_なし)の権限」のユーザーを入れて、実際に起こしたものです。接頭辞のないハッシュのユーザーで Basic 認証をすると、サーバーのログには java.lang.IllegalArgumentException: Given that there is no default password encoder configured, … が出て、クライアントには 401 が返りました(/error を permitAll() にした設定では 500)。

よくある質問

Q. Spring Securityとは何ですか? A. Spring で作ったアプリに、認証(だれか)・認可(何をしてよいか)・攻撃対策を加えるフレームワークです。リクエストがコントローラーに届く前に、入口のフィルターでまとめて確かめます。詳しくは「Spring Securityは何のためにあるのか」の節で説明しています。

Q. Spring Securityは必要ですか?無いと何が困りますか? A. ログインがあるアプリや、人によって見せる内容が違うアプリでは、使うほうが安全です。無いと、各コントローラーにログインの確認を書くことになり、1か所の書き忘れでそのURLが誰でも見られます。実際に、入れる前のアプリでは /admin がログインなしで 200 を返しました。

Q. Spring Securityで何ができますか? A. フォーム・Basic・JWT・OAuth2・SAML でのログイン、URLやメソッドごとの認可、パスワードのハッシュ化、CSRF対策、セッション管理、セキュリティヘッダー、CORS などです。一覧は「Spring Securityでできること一覧」の表にまとめています。

Q. starter を入れたらログイン画面が出て、何も見られなくなりました。 A. 既定で全URLがログイン必須になるためです。起動ログの Using generated security password の値とユーザー名 user でログインできます。開けたいURLは SecurityFilterChain の Bean で permitAll() にします。

Q. WebSecurityConfigurerAdapterを使った記事のコードは動きますか? A. Spring Security 6.0で削除されたため、Spring Boot 3以降では使えません。SecurityFilterChainを返す@Beanメソッドに書き換えます。 .and()でつなぐ書き方もSpring Security 7で削除されたので、ラムダ式の形にそろえます。

Q. InMemoryUserDetailsManagerとJdbcUserDetailsManagerはどう使い分けますか? A. InMemoryはアプリ起動時にコードで定義したユーザーだけを使う方式で、学習や動作確認に向いています。ユーザーを追加・変更するアプリでは、JdbcUserDetailsManagerか自作のUserDetailsServiceでDBから読みます。

Q. REST APIならCSRFは無効にしてよいですか? A. 公式リファレンスは、ブラウザからのアクセスを受けないバックエンドなら無効化する選択があるとしています。ブラウザのJavaScriptからCookieのセッションで呼ぶSPAの場合は、csrf.spa()で有効なまま使います。

Q. BCryptの強度はいくつにすればよいですか? A. 公式は「検証に約1秒」を目安に調整するよう示しています。ログインが集中する時間帯のCPU負荷とのバランスもあるため、本番相当の環境で測って決めます。既定値は10で、今回の2コアの環境では12で約0.3秒、14で約1.3秒でした。

まとめ

  • Spring Securityとは、誰が入れて何をしてよいかを、アプリの入口でまとめて確かめる仕組み。役割は認証・認可・攻撃対策の3つ
  • starter を足しただけで全URLがログイン必須になり、curl には 401、ブラウザにはログイン画面への 302 が返る。セキュリティヘッダーも付く
  • 考え方は「まず全部閉じて、必要な所だけ開ける」。permitAll() → hasRole() → 最後に anyRequest().authenticated()
  • SecurityFilterChainはBeanで定義し、ラムダ式で書く。.and()とauthorizeRequests()は Security 7 で削除、csrf.spa()が追加
  • DB認証はJdbcUserDetailsManager(標準テーブル)か、自作UserDetailsService(既存テーブル)
  • パスワードはDelegatingPasswordEncoder(bcrypt)でハッシュ化し、強度は約1秒を目安に調整
  • POSTだけ403ならCSRFトークン、Basic 認証で401に見えるときは/errorの許可を確かめる

参考・出典

確認日はいずれも 2026年10月17日です。

  • Spring Security 公式リファレンス「Spring Security」(7.1.1):https://docs.spring.io/spring-security/reference/index.html
  • Spring Security 公式リファレンス「Hello Spring Security(Servlet の Getting Started)」:https://docs.spring.io/spring-security/reference/servlet/getting-started.html
  • Spring Security 公式リファレンス「Security HTTP Response Headers」:https://docs.spring.io/spring-security/reference/features/exploits/headers.html
  • Spring Security 公式リファレンス「Password Storage」:https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html
  • Spring Security 公式リファレンス「Cross Site Request Forgery (CSRF)」(Servlet):https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html
  • Spring Security 公式リファレンス「JDBC Authentication」:https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html
  • Spring Security 公式リファレンス「Architecture」:https://docs.spring.io/spring-security/reference/servlet/architecture.html
  • Spring Security 6.5 公式リファレンス「Configuration Migrations(7.0 に向けた準備)」:https://docs.spring.io/spring-security/reference/6.5/migration-7/configuration.html
  • Spring Boot 公式リファレンス「Spring Security」(4.1.1):https://docs.spring.io/spring-boot/reference/web/spring-security.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Securityとは|何のためにあるかと入門・DB認証まで」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?