🦔

Spring Boot にて Spring Security を使用した直後の認証の仕組みをソースコードとともに確認する

2023/01/17に公開約17,500字

はじめに

フルスタックフレームワークである 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 以外にも、
開発者体験とアプリケーションの品質を高めるための機能が満載です。
(日本語訳のドキュメントだとこちら)

本当に機能が豊富でまさにフルスタックフレームワークといったところです。

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 を検索することで、
01-zenn-spring-security
ローカルにダウンロードされたライブラリ内部の定義を確認できます。
02-zenn-spring-security

アプリケーションを起動すると、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 されています。
03-zenn-spring-security

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 を生成している箇所を見つけました。

04-zenn-spring-security

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 を確認してみます。

05-zenn-spring-security

しっかり、 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 のドキュメントにあるように、
InMemoryUserDetailsManagerUserDetailsService を実装することにより、
メモリ上で認証情報を保存し、ユーザー名とパスワードベースの認証を実現しているようです。

つまり、 UserDetailsServiceAutoConfiguration にて、property で設定した値か初期値をもとに、
メモリ上で認証を行う InMemoryUserDetailsManager をインスタンス化したものをDIコンテナに登録することで、ユーザーとパスワードを初期設定しているということです。


おわりに

プロダクションレベルで使うのであれば、認証情報がインメモリで揮発するのでは使えないですし、
認証周りに任意の処理を filter で差し込む、認可に関する設定を追加するといったカスタマイズも必要となるでしょう。

今回確認した、Spring Boot が設定してくれていた内容を参考に、
次回は Spring Security の設定のカスタマイズ方法とその結果のソースコードリーディングを実施します。

つづき

https://zenn.dev/kiyotatakeshi/articles/cf198ab5e6c735

Discussion

ログインするとコメントできます