Spring Security で JWT を使用し認可の判定を行う
はじめに
これまで、 Spring Security の挙動をソースコードを追いながら見ていきました。
今回の記事では、サンプルコード をもとに Spring Security が認可の判定を行う流れと、
認証時に JWT を発行し、以降のリクエストに JWT を付与することで認証は行わずに、 JWT に含まれる権限でアクセス制御をする方法を確認していきます(そのためにカスタムで定義した filter を差し込みます)。
今回の作業ブランチは use-jwt です。
(差分は こちらの merge commit より確認できます)
Spring Security が設定してくれている内容と、フレームワーク内部の実装を掘り下げることで、
セキュリティを意識する上で必要な実装についての理解を深められればと思います。
ライブラリ | バージョン | 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 Security が認可の判定を行う流れ
SecurityFilterChain
をカスタマイズしたものをBean定義することで、リクエストのパスごとに権限を見てアクセスの制御が行なえます。
デフォルトの設定では認可の設定が入っているかを確認してみます。
以前の記事で確認したように、 org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
が Auto-configuration でDIコンテナに登録されていますが、
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
そこから @Import
で読み込まれ、DIコンテナに登録されている org/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration.java
に、
デフォルトの SecurityFilterChain
の定義があります。
以下の定義により、 spring-boot-starter-security
の導入後に、
Basic認証が適用され、ログイン画面が表示されます。
ただし、リソースへのアクセスについては、
http.authorizeHttpRequests().anyRequest().authenticated();
の設定が示すように、認可の判定はなく認証されたユーザがすべてのリソースにアクセスできるようになっています。
class SpringBootWebSecurityConfiguration {
/**
* 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();
}
}
これをオーバーライドしたものをBean定義することで、
パスベースで権限を見てアクセスできるかを判定するように認可を適用できます。
今回の設定だと、パスベースで以下のようにリソースへのアクセス制御ができています。
- ユーザの登録(
/register
)と公開リソース(/public
)は認証無しでアクセス可能 -
/private
は権限ADMIN
,USER
を持つ認証済みユーザのみアクセス可能 -
/roles
,/customers
は権限の追加やユーザへの付与を行うパスとしているため、ADMIN
を持つ認証済みユーザのみアクセス可能 -
/me
は認証済みユーザのみアクセス可能(権限は不要)
@Bean
@Throws(Exception::class)
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain? {
http
.authorizeHttpRequests {
it.requestMatchers("/register", "/public").permitAll()
.requestMatchers("/private").hasAnyRole("ADMIN", "USER")
.requestMatchers("/roles", "/customers/**").hasRole("ADMIN")
.requestMatchers("/me").authenticated()
}
.formLogin()
.and().httpBasic()
// TODO: enable
.and().csrf().disable()
return http.build()
}
では、どのように認可の判定が行われているかを追ってみます。
AuthorizationManager
(ドキュメント) が AuthorizationFilter
から呼び出されアクセス制御を行っています。
AuthorizationManagers are called by the AuthorizationFilter and are responsible for making final access control decisions. The AuthorizationManager interface contains two methods:
AuthorizationFilter
(ドキュメント) は FilterChainProxy
の中の1つの filter として組み込まれ、認可を提供しています。
The AuthorizationFilter provides authorization for HttpServletRequests. It is inserted into the FilterChainProxy as one of the Security Filters.
以前の記事で Spring Security により適用されている filter の一覧を確認したように、
AuthorizationFilter
は一番最後に適用される filter です。
> 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}
AuthorizationFilter
のドキュメントの図を確認すると流れがわかりやすいです。
- 順次 filter の処理を適用し
AuthorizationFilter
が処理される -
AuthorizationFilter
はSecurityContextHolder
から認証を取得 -
AuthorizationManager
(これは interface) に処理が渡され、実装であるRequestMatcherDelegatingAuthorizationManager
が認可を判定
という流れになります。
実際にデバック実行して確認してみます。
サンプルコードの README.md
を実行してテストデータを用意します。
(DDL,DML を流している Makefile はこちら)
$ docker compose up -d
$ make init-db show-sample-data
検証なので、ログレベルを TRACE
にしておきます。
logging.level.org.springframework.security: TRACE
この状態で、認証および認可が必要なエンドポイントにリクエストします。
/private
は権限ADMIN
,USER
を持つ認証済みユーザのみアクセス可能
$ admin_encoded_credential=$(echo -n "admin:1qazxsw2" | base64)
$ curl --location --request GET 'localhost:9080/private' --header "Authorization: Basic $admin_encoded_credential"
hello public world
org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java
の check
メソッドに breakpoint を張ると入ってきました。
stacktrace をみると、呼び出し元が AuthorizationFilter
であることがわかります。
なお、ログレベルを TRACE
にしていることで
filter が順に呼ばれていることが確認でき、 this.logger.isTraceEnabled()
の箇所も true となりログ出力されます。
2023-01-26T12:44:07.953+09:00 TRACE 53949 --- [nio-9080-exec-1] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (17/18)
2023-01-26T12:44:07.953+09:00 TRACE 53949 --- [nio-9080-exec-1] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (18/18)
2023-01-26T12:47:24.594+09:00 TRACE 53949 --- [nio-9080-exec-1] estMatcherDelegatingAuthorizationManager : Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@7f3e9de6]
SecurityFilterChain
をオーバーライドしてパスごとの権限を設定したものが RequestMatcherEntry
として渡されています。
2023-01-26T13:01:54.571+09:00 TRACE 53949 --- [nio-9080-exec-3] estMatcherDelegatingAuthorizationManager : Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@348845c5] using AuthorityAuthorizationManager[authorities=[ROLE_ADMIN, ROLE_USER]]
その後、org/springframework/security/authorization/AuthorityAuthorizationManager.java
が呼ばれ、認証したユーザが権限を持っているかを判定しています。
private boolean isAuthorized(Authentication authentication) {
Set<String> authorities = AuthorityUtils.authorityListToSet(this.authorities);
for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
if (authorities.contains(grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
AuthorizationFilter
-> AuthorizationManager
-> RequestMatcherDelegatingAuthorizationManager
-> AuthorityAuthorizationManager
と処理されていくことを確認できました。
JWT に含まれる権限情報で認可を判定する
以前の記事でも参照した、パスワードの保存に関するメカニズムの進化の歴史を記載したドキュメント に以下の記載があります。
Because adaptive one-way functions are intentionally resource intensive, validating a username and password for every request can significantly degrade the performance of an application. There is nothing Spring Security (or any other library) can do to speed up the validation of the password, since security is gained by making the validation resource intensive. Users are encouraged to exchange the long term credentials (that is, username and password) for a short term credential (such as a session, and OAuth Token, and so on). The short term credential can be validated quickly without any loss in security.
パスワードを検証するためのハッシュ化は、Brute-force attack への対策としてあえて時間がかかるものにしてあり、
ユーザが都度認証を実施するとシステムのパフォーマンスの低下に繋がります。
そのため、ユーザーが長期のクレデンシャル(ユーザー名とパスワード)ではなく、
短期のクレデンシャル(セッション、JWT など)を使用できるようにするのが良いとのことです。
これを実現するために、認証時に JWT を発行し、以降のリクエストに JWT を付与することで認証は行わずに、 JWT に含まれる権限でアクセス制御できるようにします。
Spring Security にはJWT をチェックする機能が用意されています。(今回は spring-boot-starter-oauth2-resource-server
を使用)
making さんにコメントにて教えて頂きました!
そのため、これから紹介する方法は独自の処理を定義した filter を差し込む方法の参考までに残しておきますが、 JWT の検証は Spring Security が用意しているものを使用しましょう!
Spring Security が用意している JWT のサポートの紹介は後半に記載しています!
以下のように SecurityFilterChain
のBean定義にて自分で定義した filter を差し込むようにします。
-
Basic認証を使用する場合は Header の形式が有効化を確認する filter
.addFilterBefore(AuthorizationHeaderValidationFilter(), BasicAuthenticationFilter::class.java)
-
Basic認証の前に JWT を検証する filter
.addFilterBefore(JWTValidationFilter(), BasicAuthenticationFilter::class.java)
-
Basic認証か JWT の検証の後にユーザの権限をログ出力する filter
.addFilterAfter(LoggingAuthoritiesFilter(), BasicAuthenticationFilter::class.java)
-
Basic認証後に JWT を発行する filter
.addFilterAfter(JWTGenerationFilter(), LoggingAuthoritiesFilter::class.java)
@Configuration
class SecurityConfig {
@Bean
@Throws(Exception::class)
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain? {
http
.authorizeHttpRequests {
it.requestMatchers("/register", "/public").permitAll()
.requestMatchers("/private").hasAnyRole("ADMIN", "USER")
.requestMatchers("/roles", "/customers/**").hasRole("ADMIN")
.requestMatchers("/me").authenticated()
}
.formLogin()
.and().httpBasic()
.and().csrf().disable()
.addFilterBefore(AuthorizationHeaderValidationFilter(), BasicAuthenticationFilter::class.java)
.addFilterBefore(JWTValidationFilter(), BasicAuthenticationFilter::class.java)
.addFilterAfter(LoggingAuthoritiesFilter(), BasicAuthenticationFilter::class.java)
.addFilterAfter(JWTGenerationFilter(), LoggingAuthoritiesFilter::class.java)
return http.build()
}
JWT を生成する filter からみていきます。
Basic認証後に認証情報を取得し、 JWT を生成します。
JWT の生成のライブラリは jwtk
を使用しています。
この filter の処理を行うのは /me
にリクエストが来たときだけにします。
class JWTGenerationFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
val authentication = SecurityContextHolder.getContext().authentication
if (null != authentication) {
val jwt = generateJWT(authentication)
response.setHeader(HttpHeaders.AUTHORIZATION, jwt)
}
filterChain.doFilter(request, response)
}
/**
* only exec filter `/me`
*/
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
return request.servletPath != "/me"
}
private fun generateJWT(authentication: Authentication): String {
val key = Keys.hmacShaKeyFor(SecurityConstants.JWT_KEY.toByteArray(StandardCharsets.UTF_8))
val now = Date()
val eightHoursAfter = Date(now.time + (1000 * 60 * 60 * 8))
return Jwts.builder()
.setIssuer("Spring Security Sample")
.setSubject("JWT")
.claim("username", authentication.name)
.claim("roles", authentication.authorities
.map { it.authority }
.toSet()
)
.setIssuedAt(now)
.setExpiration(eightHoursAfter)
.signWith(key)
.compact()
}
次に JWT を検証する filter を見ていきます。
こちらはリクエストの Authorization Header
に Bearer
から始まる値が指定されていた時に、JWT を取り出して検証します。
JWT が有効なものであれば、そこから権限情報を取り出して、SecurityContextHolder
に保持します。
class JWTValidationFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(
req: HttpServletRequest,
res: HttpServletResponse,
filterChain: FilterChain,
) {
val header = req.getHeader(HttpHeaders.AUTHORIZATION)
// Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTcHJpbmcgU2VjdXJpdHkgU2FtcGxlIiwic3ViIjoiSldUIiwidXNlcm5hbWUiOiJtaWtlQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiUk9MRV9URVNUIiwiUk9MRV9VU0VSIl0sImlhdCI6MTY3NDQ4NjUzNSwiZXhwIjoxNjc0NTE2NTM1fQ.IK4T9hIDikFQuI4hQIhm_z4sih_yYK7GEtPO9nwDEmE
if (null != header) {
if (StringUtils.startsWithIgnoreCase(header, "Bearer")) {
val base64Token = header.substring(7)
try {
val key = Keys.hmacShaKeyFor(SecurityConstants.JWT_KEY.toByteArray((StandardCharsets.UTF_8)))
val claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(base64Token)
.body
val roles = claims["roles"] as ArrayList<*>
val auth: Authentication = UsernamePasswordAuthenticationToken(
claims["username"].toString(),
null,
AuthorityUtils.commaSeparatedStringToAuthorityList(roles.joinToString(", "))
)
SecurityContextHolder.getContext().authentication = auth
}
catch (e: ExpiredJwtException) {
throw BadCredentialsException("Token is expired!\n${e.printStackTrace()}")
}
catch (e: Exception) {
throw BadCredentialsException("Invalid Token received!\n${e.printStackTrace()}")
}
}
}
filterChain.doFilter(req, res)
}
/**
* only not exec filter `/me`
*/
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
return request.servletPath == "/me"
}
org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java
は Authorization Header に Basic
の付与がないと、
ProviderManager
以降が呼び出され DB にユーザを取得しに行く箇所の処理には入らないです。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
if (authRequest == null) {
this.logger.trace("Did not process authentication request since failed to find "
+ "username and password in Basic Authorization header");
chain.doFilter(request, response);
return;
}
※ログレベルを TRACE
にして確認したもの
2023-01-26T13:54:15.875+09:00 TRACE 53949 --- [nio-9080-exec-9] o.s.security.web.FilterChainProxy : Invoking BasicAuthenticationFilter (11/18)
2023-01-26T13:54:15.876+09:00 TRACE 53949 --- [nio-9080-exec-9] o.s.s.w.a.www.BasicAuthenticationFilter : Did not process authentication request since failed to find username and password in Basic Authorization header
2023-01-26T13:54:15.876+09:00 TRACE 53949 --- [nio-9080-exec-9] o.s.security.web.FilterChainProxy : Invoking LoggingAuthoritiesFilter (12/18)
つまり Authorization Header
に Basic
を付与せずに Bearer
を付与してリクエストした場合は JWT の検証を行い、
認可権限の判定に使用する認証情報を保持するようにするのです。
残りのカスタムの filter は重要ではないので、ソースコードへのリンクを付与し説明は割愛します。
Basic認証を使用する場合は Header の形式が有効化を確認する filter
Basic認証か JWT の検証の後にユーザの権限をログ出力する filter
Basic認証を使用してアクセスする場合と、認証後に発行された JWT を使用してアクセスする場合の簡単なパフォーマンスの比較をしてみます。
Postman の collection を import してリクエストしてみます。
Basic認証を使用してアクセスする場合は DB まで取りに行く必要があるので 126ms
でした。
一方 JWT を検証する場合は 14ms
でした。
この簡易的な結果からも、以下のことが確認できました。
ユーザーが長期のクレデンシャル(ユーザー名とパスワード)ではなく、
短期のクレデンシャル(セッション、JWT など)を使用できるようにするのが良い
セキュリティの要件に合わせて、 JWT を使用しても問題ない場合は、
都度 DB までユーザを取得して認証を行わない方法も使用できそうです。
※今回は JWT の有効期限を8hとしています
Spring Scurity の JWT サポートを使用する
making さんにコメントにて教えて頂いた spring-security-oauth2-resource-server
を使用して JWT を使った認証を行います。
同じくコメントにて教えて頂いた以下のハンズオンも大変分かりやすいです。
branch use-spring-security-oauth2-resource-server に checkout することで動かして確認できます。
OAuth 2.0 Resource Server JWT のドキュメントを参考に、
spring-boot-starter-oauth2-resource-server
を使用することで、 OAuth 2.0 の Resource Server としての必要な機能を実装できます。
ただし、今回の例では Authorization Server は存在せず、 self-signed JWT を使用します(認証と JWT の発行元が Resourse Server 自身)。
アプリを起動し JWT を発行後に、JWT をリクエストに付与( Authorization Header
に Bearer
で指定)したときの処理を追ってみます。
ドキュメントの How JWT Authentication Works に記載の図の流れで JWT を decode して認証に成功したら SecurityContextHolder
に情報が保持されます。
まず、適用されている filter を確認すると、
BearerTokenAuthenticationFilter
が BasicAuthenticationFilter
の前に差し込まれています。
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter
HeaderWriterFilter
LogoutFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
AuthorizationFilter
]
org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java
に breakpoint を貼った状態で、 JWT を付与したリクエストをすると、
BearerTokenAuthenticationToken
-> AuthenticationManager
-> JwtAuthenticationProvider
と呼び出されていることが確認できます。
DIコンテナに登録しておいた jwtDecoder が使われ、
リクエストの JWT を検証して org/springframework/security/oauth2/jwt/Jwt.java
にマッピングしてくれます。
private Jwt getJwt(BearerTokenAuthenticationToken bearer) {
try {
return this.jwtDecoder.decode(bearer.getToken());
}
その後は、
org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java
が JWT 内の claim
の scope
を見て
private Collection<String> getAuthorities(Jwt jwt) {
String claimName = getAuthoritiesClaimName(jwt);
if (claimName == null) {
this.logger.trace("Returning no authorities since could not find any claims that might contain scopes");
return Collections.emptyList();
}
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Looking for scopes in claim %s", claimName));
}
Object authorities = jwt.getClaim(claimName);
// 略
Collection<GrantedAuthority>
に変換してくれます。
これにより、 JWT に含まれている認可の情報を判定に使用できます。
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String authority : getAuthorities(jwt)) {
grantedAuthorities.add(new SimpleGrantedAuthority(this.authorityPrefix + authority));
}
return grantedAuthorities;
}
なお、JwtAuthenticationConverter
の変換はデフォルトだと、
SCOPE_
の prefix がつくため、カスタマイズしたものをBean定義しています。
※JWT 作成時に scope
に ROLE_
の prefix を付与しているため、SCOPE_
の prefix を取り除くようにカスタマイズしました。
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
// prefix を取り除く
grantedAuthoritiesConverter.setAuthorityPrefix("")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
BearerTokenAuthenticationFilter
が呼び出されるところから、最後の filter である AuthorizationFilter
が呼び出し終わるまでのログ(log level=TRACE)が以下のものです。
BasicAuthenticationFilter
内で DB からユーザを取得して認証を行っていないので、
先程 Postman で検証した時同様に、レスポンスは早くなっています。
2023-01-28T00:56:41.398+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking BearerTokenAuthenticationFilter (6/14)
2023-01-28T00:56:41.407+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.s.authentication.ProviderManager : Authenticating request with JwtAuthenticationProvider (1/2)
2023-01-28T01:20:56.141+09:00 TRACE 69125 --- [nio-9080-exec-3] s.o.s.r.a.JwtGrantedAuthoritiesConverter : Looking for scopes in claim scope
2023-01-28T01:23:45.387+09:00 DEBUG 69125 --- [nio-9080-exec-3] o.s.s.o.s.r.a.JwtAuthenticationProvider : Authenticated token
2023-01-28T01:24:08.556+09:00 DEBUG 69125 --- [nio-9080-exec-3] .s.r.w.a.BearerTokenAuthenticationFilter : Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@55e006a8, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ADMIN, ROLE_USER]]
2023-01-28T01:24:08.556+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking AuthorizationHeaderValidationFilter (7/14)
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking BasicAuthenticationFilter (8/14)
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.s.w.a.www.BasicAuthenticationFilter : Did not process authentication request since failed to find username and password in Basic Authorization header
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking LoggingAuthoritiesFilter (9/14)
2023-01-28T01:24:08.557+09:00 INFO 69125 --- [nio-9080-exec-3] c.e.z.security.LoggingAuthoritiesFilter : User "admin@example.com" is successfully authenticated and has the authorities [ROLE_ADMIN, ROLE_USER]
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (10/14)
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.s.w.s.HttpSessionRequestCache : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (11/14)
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (12/14)
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (13/14)
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (14/14)
2023-01-28T01:24:08.557+09:00 TRACE 69125 --- [nio-9080-exec-3] estMatcherDelegatingAuthorizationManager : Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@12699b6b]
2023-01-28T01:24:08.562+09:00 TRACE 69125 --- [nio-9080-exec-3] estMatcherDelegatingAuthorizationManager : Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@12699b6b] using AuthorityAuthorizationManager[authorities=[ROLE_ADMIN, ROLE_USER]]
2023-01-28T01:24:08.566+09:00 TRACE 69125 --- [nio-9080-exec-3] o.s.s.w.a.AnonymousAuthenticationFilter : Did not set SecurityContextHolder since already authenticated JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@55e006a8, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ADMIN, ROLE_USER]]
おわりに
Spring Security の認可の流れと JWT を使用した認可の判定を追加しました。
独自の処理を実現する filter を任意の場所に組み込めるので、
要件に合わせた柔軟なカスタマイズが可能となることがわかりました。
これまで数回の記事で見てきたように、
Spring Security のフレームワークとしての決まりごとに乗っかることで、
アプリケーションにとって必要なセキュリティをフルスクラッチで作ることなく、
それでいて高いカスタマイズ性を持って実現可能なのは魅力的でした。
今後、別の言語やフレームワークで認証周りを実装する時に、
Spring Security の思想や考慮しているセキュリティ要件が大変参考になると感じました。
Discussion
JWTをチェックする機能はSpring Securityで用意されていますが、ご存知でしょうか?
以下のチュートリアルがわかりやすいです。存じ上げていなかったです!
ドキュメントを確認し、紹介いただいたチュートリアルをやってみましたが、
spring-security-oauth2-resource-server
を使用することで、BearerTokenAuthenticationFilter
がBasicAuthenticationFilter
の前に差し込まれて、今回実現したかったことがまさにできました!
記事とサンプルコードは後ほど修正します! -> 修正しました!
情報ありがとうございました。