Spring Boot にて Spring Security を使用した直後の認証の仕組みをソースコードとともに確認する
はじめに
フルスタックフレームワークである Spring でアプリケーションを実装している場合、
Spring Security を使用することで、アプリケーションに認証、認可、一般的な攻撃に対する保護を追加できます。
Spring Security が設定してくれている内容と、フレームワーク内部の実装を掘り下げることで、
セキュリティを意識する上で必要な実装についての理解を深められればと思います。
サンプルコード をもとに Spring Security の導入、設定のカスタマイズの検証を行います。
(が、長くなってしまったので今回は導入直後に起きていることのみを確かめます)
なお、Spring Boot のバージョンは 3.0.1
を使用するため、 BOM による dependency management で以下のバージョンが使用されています。
ライブラリ | バージョン | Maven central URL |
---|---|---|
spring-boot-starter-web |
3.0.1 |
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web/3.0.1 |
spring-boot-starter-security |
3.0.1 |
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security/3.0.1 |
spring-core |
6.0.3 |
https://mvnrepository.com/artifact/org.springframework/spring-core/6.0.3 |
spring-security-web |
6.0.1 |
https://mvnrepository.com/artifact/org.springframework.security/spring-security-web/6.0.1 |
まずは、Spring および Spring Boot を使用するメリットをおさらいします。
Spring を使うメリット
Spring が提供する機能のうちコアとなるものは、DI(=IoC)コンテナ です。
そのため、誤解を恐れず Spring を使用するメリットを一言で表すと、
「アプリケーション内でのインスタンスの生成知識をDIコンテナにて一括で管理することで、
実装の差し替えを容易にしたり、テスト容易性(Testability)を高めること」 です。
具体的には、役割ごとにアプリケーションのレイヤーを分割し、
別のレイヤーへの依存関係を DI により解決する(interface を constructor injection する)
ことにより、
Profile に応じて開発環境は Mock を使用するといった環境に合わせた振る舞いの変更や、各レイヤーに閉じて Testability の高い品質の高いコードを記述できるようになります。
なおレイヤーの分割方法には、
- シンプルな CRUD アプリケーションに用いられる
controller
,service
,repository
- DDD, Onion Architecture を意識した
presentation
,usecase
,infrastructure
,domain
といった分類があります。
Spring Boot を使うメリット
Spring でのアプリケーション開発では Spring Boot を使用することが一般的です。
設定に関するコードを減らして、スピーディーにアプリケーションの開発、実装が可能になるからです。
この辺りは先人のスライド(「初めてのひとのためのSpring/Spring Boot #jjug」) が大変参考になります(Spring Boot 登場前を知らない世代なので...)。
Auto-configuration という仕組みで、
インスタンス(=Bean) をDIコンテナに登録する(=Bean定義)ことで、
開発者はライブラリを使用するにあたっての設定の記述をしなくて良くなります。
(特定のカスタマイズをしたい場合は Java-based Container Configuration の記述は必要です)
Spring Boot を使用すると、Auto-configuration 以外にも、
開発者体験とアプリケーションの品質を高めるための機能が満載です。
(日本語訳のドキュメントだとこちら)
- BOM による互換性のあるライブラリを使用する(dependency management)
-
設定の外部化
-
application.yaml
への設定の外だし、環境ごとの切り替え
-
- ログ出力の設定のカスタマイズが容易
- JSON を使用するための設定が Bean として登録済み
-
テストサポート
- 最小限のレイヤーのみをDIコンテナに用意してテストできる(Auto-configured Tests)
- kotlin のサポート
- コンテナイメージの作成
本当に機能が豊富でまさにフルスタックフレームワークといったところです。
Spring および Spring Boot を使用するメリットを自分なりに無理やり二言でまとめると、
「DIコンテナを活用することで、レイヤーごとに分離性とテスト容易性の高いコードを記述できること。さらに豊富なライブラリを容易に組み合わせて、優れた開発者体験で記載できること」 です。
(その分、機能が豊富なのでドキュメントを調べる時間が長くなりますが)
Spring Security の導入
では、Spring Security を導入します。
サンプルコードのブランチ apply-spring-security までの作業です。
org.springframework.boot:spring-boot-starter-security
を依存関係(build.gradle.kts
)に加えることで、
Auto-configuration により、Spring Security に関するBean定義が行われ、Spring Security が適用された状態になります。
Understanding Auto-configured Beans を参考にすると、
You can browse the source code of spring-boot-autoconfigure to see the @AutoConfiguration classes that Spring provides (see the META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file).
Spring Security の servlet 関係のBean定義は以下のクラスが追加されていそうです。
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
IDE(IntelliJ)で確認する場合は、ファイル名検索で AutoConfiguration.imports
を検索することで、
ローカルにダウンロードされたライブラリ内部の定義を確認できます。
アプリケーションを起動すると、Basic認証が適用されています。
Using generated security password: 378aa85f-37ce-48a7-aa19-2ae7020f492a
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2023-01-17T21:00:44.520+09:00 INFO 63734 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@b3e86d5, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@72b43104, org.springframework.security.web.context.SecurityContextHolderFilter@39ffda4a, org.springframework.security.web.header.HeaderWriterFilter@2d85fb64, org.springframework.security.web.csrf.CsrfFilter@7c1447b5, org.springframework.security.web.authentication.logout.LogoutFilter@12b5736c, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@73a8e994, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@d5bb1c4, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@e111c7c, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@609e57da, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@56e9a474, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@38cedb7d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4d0e54e0, org.springframework.security.web.access.ExceptionTranslationFilter@6e0e5dec, org.springframework.security.web.access.intercept.AuthorizationFilter@33a55bd8]
2023-01-17T21:00:44.541+09:00 INFO 63734 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9080 (http) with context path ''
ブラウザにて http://localhost:9080/public
にアクセスしても http://localhost:9080/login
に redirect されています。
curl コマンドでリクエストしても 401 Unauthorized
です。
$ curl -I --request GET 'localhost:9080/public'
HTTP/1.1 401
先程の起動時のログにあったパスワードを使用すると、Basic認証を通過できコンテンツにアクセスできるようになりました。
$ encoded_credential=$(echo -n "user:378aa85f-37ce-48a7-aa19-2ae7020f492a" | base64)
$ curl --location --request GET 'localhost:9080/public' \
--header "Authorization: Basic $encoded_credential"
hello public world
ログイン画面が表示されている仕組み
まずは、ログイン画面が表示されている仕組みを追いかけてみます。
Please sign in
という文言で Project and Libraries
で検索をかけることで、ライブラリ内で HTML を生成している箇所を見つけました。
org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java
のようです。
こちらの filter は org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java
にて登録されています。
つまり、FilterChain 内にて、Security Filter の1つとして処理されます。
put(DefaultLoginPageGeneratingFilter.class, order.next());
Spring Boot が自動的に設定してくれていることはドキュメントに列挙されていたので、そこからわかることも確かめてみます。
Enables Spring Security’s default configuration, which creates a servlet Filter as a bean named springSecurityFilterChain. This bean is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the login form, and so on) within your application.
springSecurityFilterChain
という Bean がセキュリティを担当するとあるので、
(Java-based Container Configurationで)Bean定義されている箇所に break point を貼り、デバック実行して登録されている filter を確認してみます。
しっかり、 DefaultLoginPageGeneratingFilter
も登録されていました。
> this.securityFilterChains.get(0).getFilters()
result = {ArrayList@6842} size = 15
0 = {DisableEncodeUrlFilter@6845}
1 = {WebAsyncManagerIntegrationFilter@6846}
2 = {SecurityContextHolderFilter@6847}
3 = {HeaderWriterFilter@6848}
4 = {CsrfFilter@6849}
5 = {LogoutFilter@6850}
6 = {UsernamePasswordAuthenticationFilter@6851}
7 = {DefaultLoginPageGeneratingFilter@6852}
8 = {DefaultLogoutPageGeneratingFilter@6853}
9 = {BasicAuthenticationFilter@6854}
10 = {RequestCacheAwareFilter@6855}
11 = {SecurityContextHolderAwareRequestFilter@6856}
12 = {AnonymousAuthenticationFilter@6857}
13 = {ExceptionTranslationFilter@6858}
14 = {AuthorizationFilter@6859}
別の確認方法としては、 @EnableWebSecurity(debug = true)
とすることで、
ログの出力を増やすことができるようで、
@SpringBootApplication
@EnableWebSecurity(debug = true)
class SampleApplication
fun main(args: Array<String>) {
runApplication<SampleApplication>(*args)
}
リクエストのたびに適用されている filter を確認ができるので、
開発時の調査に便利そうです。
************************************************************
Request received for GET '/public':
org.apache.catalina.connector.RequestFacade@7df6baa5
servletPath:/public
pathInfo:null
headers:
user-agent: PostmanRuntime/7.30.0
accept: */*
postman-token: 9008beaa-611f-4718-8bc9-fccef19f9b95
host: localhost:9080
accept-encoding: gzip, deflate, br
connection: keep-alive
cookie: JSESSIONID=5B54EB7D07537197E38233256CBB18EF
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
ExceptionTranslationFilter
AuthorizationFilter
]
次に Auto-configuration から分かりそうなことを追ってみると、
org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfiguration.java
で読み込まれている
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
org/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration.java
に servlet application のセキュリティの設定をするためのBean定義が記載されていました。
@Configuration class securing servlet applications.
まさに Spring Boot を使うメリットとして紹介した以下の箇所のことが起きています。
Auto-configuration という仕組みで、
インスタンス(=Bean) をDIコンテナに登録する(=Bean定義)ことで、
開発者はライブラリを使用するにあたっての設定の記述をしなくて良くなります。
SpringBootWebSecurityConfiguration
クラスの以下のBean定義により、
Form ログインとBasic認証が設定されているようです。
/**
* The default configuration for web security. It relies on Spring Security's
* content-negotiation strategy to determine what sort of authentication to use. If
* the user specifies their own {@link SecurityFilterChain} bean, this will back-off
* completely and the users should specify all the bits that they want to configure as
* part of the custom security configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().anyRequest().authenticated();
http.formLogin();
http.httpBasic();
return http.build();
}
}
初期設定の認証情報はどこから来ているのか
次の疑問は、起動時に表示されていた自動生成されているユーザーとパスワードがどこから来ているかです。
Spring Boot が自動的に設定してくれていることを列挙したドキュメントに怪しい記述があります。
Creates a UserDetailsService bean with a username of user and a randomly generated password that is logged to the console.
Auto-configuration にも UserDetailsServiceAutoConfiguration
があります。
org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java
を見てみると、 SecurityProperties
を引数に、
in-momory に認証情報を保持するBean定義が記載されています(ドキュメントの記載だとこの辺り)。
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
なお、 org/springframework/boot/autoconfigure/security/SecurityProperties.java
は、
設定の外部化(external-config)の方法として、
application.yaml
に記載の設定値を relaxed-binding で Bean にマッピングしています。
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
このクラス内で static class として初期値のユーザー名である user
と、
パスワードが UUID で自動生成されています。
public static class User {
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
application.yaml
にてインメモリで扱うユーザの情報を記載してデバック実行すると、
spring:
security:
user:
# https://docs.spring.io/spring-boot/docs/3.0.1/reference/htmlsingle/#application-properties.security.spring.security.user.name
name: test
password: 1qazxsw2
SecurityProperties.java
の setter に処理が入ってきて、
認証時にメモリ上で管理される情報も書き換わることが確認できます。
In-Memory Authentication のドキュメントにあるように、
InMemoryUserDetailsManager
が UserDetailsService
を実装することにより、
メモリ上で認証情報を保存し、ユーザー名とパスワードベースの認証を実現しているようです。
つまり、 UserDetailsServiceAutoConfiguration
にて、property で設定した値か初期値をもとに、
メモリ上で認証を行う InMemoryUserDetailsManager
をインスタンス化したものをDIコンテナに登録することで、ユーザーとパスワードを初期設定しているということです。
おわりに
プロダクションレベルで使うのであれば、認証情報がインメモリで揮発するのでは使えないですし、
認証周りに任意の処理を filter で差し込む、認可に関する設定を追加するといったカスタマイズも必要となるでしょう。
今回確認した、Spring Boot が設定してくれていた内容を参考に、
次回は Spring Security の設定のカスタマイズ方法とその結果のソースコードリーディングを実施します。
つづき
Discussion