Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita を参照してください。
基礎・仕組み的な話
認証・認可の話
Remember-Me の話
CSRF の話
セッション管理の話
レスポンスヘッダーの話
メソッドセキュリティの話
CORS の話
ACL の話
テストの話
MVC, Boot との連携の話
番外編
Spring Security にできること・できないこと
Hello World
実装
package sample.spring.security.service;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
public class MyRunAsService {
@Secured({"ROLE_USER", "RUN_AS_HOGE", "RUN_AS_FUGA"})
public void secured() {
this.printAuthorities("secured");
}
public void nonSecured() {
this.printAuthorities("nonSecured");
}
public void printAuthorities(String tag) {
System.out.println("[" + tag + "]");
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println("Authentication.class = " + auth.getClass());
auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.forEach(System.out::println);
}
}
-
secured()
メソッドは@Secured
でアノテートしている -
nonSecured()
メソッドはアノテーションなし -
printAuthorities()
メソッドは、ログイン中のユーザに付与された権限を全て表示する
package sample.spring.security.servlet;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.service.MyRunAsService;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/run-as/*")
public class RunAsTestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
MyRunAsService service = context.getBean(MyRunAsService.class);
service.printAuthorities("メソッド呼び出し前");
if ("/secured".equals(req.getPathInfo())) {
service.secured();
} else {
service.nonSecured();
}
service.printAuthorities("メソッド呼び出し後");
}
}
-
/run-as/secured
にアクセスがあったら、MyRunAsService
のsecured()
メソッドを実行する - それ以外のパスにリクエストがあったら、
nonSecured()
メソッドを実行する - 各メソッドの実行前後でも、ログインユーザの権限を表示させる
namespace
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<bean class="sample.spring.security.service.MyRunAsService" />
<bean id="runAsManager"
class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="foo" />
</bean>
<bean id="runAsAuthenticationProvider"
class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="foo" />
</bean>
<sec:global-method-security secured-annotations="enabled" run-as-manager-ref="runAsManager" />
<sec:http>
<sec:intercept-url pattern="/login" access="permitAll" />
<sec:intercept-url pattern="/**" access="isAuthenticated()" />
<sec:form-login />
<sec:logout />
</sec:http>
<sec:authentication-manager>
<sec:authentication-provider>
<sec:user-service>
<sec:user name="hoge" password="hoge" authorities="ROLE_USER" />
</sec:user-service>
</sec:authentication-provider>
<sec:authentication-provider ref="runAsAuthenticationProvider" />
</sec:authentication-manager>
</beans>
Java Configuration
package sample.spring.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.intercept.RunAsManager;
import org.springframework.security.access.intercept.RunAsManagerImpl;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled=true)
public class MyGlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
public static final String RUN_AS_KEY = "foo";
@Override
protected RunAsManager runAsManager() {
RunAsManagerImpl runAsManager = new RunAsManagerImpl();
runAsManager.setKey(RUN_AS_KEY);
return runAsManager;
}
}
package sample.spring.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.security.access.intercept.RunAsImplAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import sample.spring.security.service.MyRunAsService;
@EnableWebSecurity
@Import(MyGlobalMethodSecurityConfig.class)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
RunAsImplAuthenticationProvider provider = new RunAsImplAuthenticationProvider();
provider.setKey(MyGlobalMethodSecurityConfig.RUN_AS_KEY);
auth.authenticationProvider(provider)
.inMemoryAuthentication()
.withUser("hoge")
.password("hoge")
.authorities("ROLE_USER");
}
@Bean
public MyRunAsService myRunAsService() {
return new MyRunAsService();
}
}
動作確認
/run-as/secured
にアクセス
[メソッド呼び出し前]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
[secured]
Authentication.class = class org.springframework.security.access.intercept.RunAsUserToken
ROLE_RUN_AS_HOGE
ROLE_RUN_AS_FUGA
ROLE_USER
[メソッド呼び出し後]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
/run-as/nonSecured
にアクセス
[メソッド呼び出し前]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
[nonSecured]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
[メソッド呼び出し後]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
-
secured()
メソッドのときだけ、ROLE_RUN_AS_HOGE
,ROLE_RUN_AS_FUGA
の権限が付与されている - また、
Authentication
の実体がRunAsUserToken
に切り替わっている
説明
- Run-As の仕組みを使うと、特定のセキュアオブジェクトが実行されている間だけ、任意の権限を一時的に追加することができる
RunAsManager の実装クラスを用意する
<bean id="runAsManager"
class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="foo" />
</bean>
- Run-As の具体的な処理は
RunAsManager
が行う - Spring Security はデフォルト実装として
RunAsManagerImpl
を用意しているので、これを用意する - プロパティ
key
に、任意の文字列を指定しておく(詳細は後述)
RunAsManager を SecurityInterceptor に適用する
<sec:global-method-security secured-annotations="enabled" run-as-manager-ref="runAsManager" />
-
RunAsManager
を<global-method-security>
のrun-as-manager-ref
属性に指定する - また、
secured-annotations
を有効にしておく(@Secured
アノテーションが使えるようになる)
@Configuration
@EnableGlobalMethodSecurity(securedEnabled=true)
public class MyGlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
public static final String RUN_AS_KEY = "foo";
@Override
protected RunAsManager runAsManager() {
RunAsManagerImpl runAsManager = new RunAsManagerImpl();
runAsManager.setKey(RUN_AS_KEY);
return runAsManager;
}
}
- Java Configuration の場合は、
GlobalMethodSecurityConfiguration
を継承したクラスを設定クラスとして用意し、runAsManager()
メソッドをオーバーライドする-
secured-annotations
に対応する設定は、@EnableGlobalMethodSecurity
のsecuredEnabled
で指定
-
RunAsImplAuthenticationProvider を用意する
<bean id="runAsAuthenticationProvider"
class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="foo" />
</bean>
<sec:authentication-manager>
...
<sec:authentication-provider ref="runAsAuthenticationProvider" />
</sec:authentication-manager>
- つづいて
RunAsImplAuthenticationProvider
を定義し、AuthenticationProvider
の並びに追加する -
RunAsImplAuthenticationProvider
のkey
プロパティには、RunAsManagerImpl
で設定したのと同じ値を設定しておく
追加する権限を定義する
@Secured({"ROLE_USER", "RUN_AS_HOGE", "RUN_AS_FUGA"})
public void secured() {
this.printAuthorities("secured");
}
- 最後に権限の追加を定義する部分
-
@Secured
アノテーションのvalue
で、次の値を設定する- アノテート対象を実行するために必要となる権限(ここでは
ROLE_USER
)-
@Secured
自体はメソッドセキュリティのためのアノテーションなので、このメソッドを実行するために必要となる定義を指定しなければならない -
secured-annotations="enabled"
で@Secured
を有効にすると、MethodSecurityIntercpetor
が使用するVoter
はRoleVoter
とAuthenticatedVoter
になる - なので、これらの
Voter
に合わせた形で指定しなければならない(式ベースではない)-
RoleVoter
は、ROLE_
で始まる値が指定された場合に、ユーザが同様の権限(ロール)を与えられているかチェックする -
AuthenticatedVoter
は 定数で指定された3つのいずれか を指定する
-
-
- Run-As で付加する権限(ここでは
RUN_AS_HOGE
とRUN_AS_FUGA
)- 付加する権限は
RUN_AS_
で始まる形にする - 実際にユーザに付加される権限は、
ROLE_
が頭についた形になる(RUN_AS_HOGE
はROLE_RUN_AS_HOGE
になる)
- 付加する権限は
- アノテート対象を実行するために必要となる権限(ここでは
使いどころは?
公式リファレンスには、リモートの Web サービスを呼び出すときに便利、みたいなことが書かれていたが、正直意味は分からなかった。
these run-as replacements are particularly useful when calling remote web services
(翻訳)
Run-As の置き換えは、特にリモートの Web サービスを呼ぶときに便利です。
34.1 Overview | 34. Run-As Authentication Replacement
こっちのページだと、アプリのバックエンドでちょっと特殊な権限が必要になるときに、その特殊な権限を常にユーザに付与するわけにはいかない場合に、一時的に付与したいときに使えるとか説明されている。
ただ、それは権限の割り振りの設計が間違ってね? という気がしなくもない。
まぁ、知っておいたら何かのときに使える日が来るかもしれない。
RunAsImplAuthenticationProvider の価値は?
RunAsImplAuthenticationProvider がやること
RunAsImplAuthenticationProvider
は Authentication
オブジェクトの実体が RunAsUserToken
の場合に認証処理をサポートする。
この RunAsUserToken
は RunAsManagerImpl
が生成する Authentication
オブジェクトで、元の Authentication
が持っていた情報に RUN_AS_
で指定した権限が追加された Authentication
オブジェクトとなっている。
この RunAsUserToken
には、 RunAsManagerImpl
を生成するときに指定した key
が設定されている。
<bean id="runAsManager"
class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="foo" /> ★これ
</bean>
この key
は RunAsImplAuthenticationProvider
にも設定している。
<bean id="runAsAuthenticationProvider"
class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="foo" /> ★同じやつを設定している
</bean>
RunAsImplAuthenticationProvider
は、 RunAsUserToken
に設定された key
と、自分自身に設定された key
を比較し、もし異なる場合は BadCredentialsException
をスローするようになっている。
いつ呼ばれる?
自分が確認した限りだと、普通に使っているだけだと RunAsImplAuthenticationProvider
の処理が実行されることはない。
次の条件が満たされたときに実行されることは確認できた。
-
MethodSecurityInterceptor
のalwaysReauthenticate
がtrue
になっている -
ProviderManager
のeraseCredentialsAfterAuthentication
がfalse
になっている(Form ログインが有効の場合) - Run-As で権限を書き換えているメソッドの中で、別のメソッドセキュリティの処理対象となるメソッドが呼ばれた
1 は、これが true
になっていると MethodSecurityInterceptor
が実行されるときに毎回 AuthenticationManager
の認証処理が呼ばれるようになる。逆に false
だと認証処理が呼ばれないので、 RunAsImplAuthenticationProvider
の処理も呼ばれなくなる(デフォルトは false
)。
2 は、これが false
になっていないと Form ログインした時点でパスワードが Authentication
オブジェクトから削除されてしまう。その状態で MethodSecurityInterceptor
での AuthenticationManager
の認証処理が実行されると、パスワードが取得できずに認証エラーになってしまう。
3 は、 RunAsImplAuthenticationProvider
の処理が実行されるのが Authentication
オブジェクトの実体が RunAsUserToken
の場合のみなので、 Run-As が実行された後に呼ばれたメソッドだけが RunAsImplAuthenticationProvider
の処理対象になることを意味している。
こんな設定にわざわざすることってあるのだろうか?
意味はある?
RunAsImplAuthenticationProvider
は、 key
の比較だけを行っているが、このチェックにはどういう意味があるのだろうか?
リファレンスでは、悪意あるコードがどうのこうのいう文章があったが、英語力がなくて何言っているのか分からず。。。
To ensure malicious code does not create a RunAsUserToken and present it for guaranteed acceptance by the RunAsImplAuthenticationProvider, the hash of a key is stored in all generated tokens. The RunAsManagerImpl and RunAsImplAuthenticationProvider is created in the bean context with the same key:
RunAsUserToken
を攻撃者が偽造できることがあるということだろうか?
そんなことできる時点で、任意のコードが実行できてしまっている気がするが、どうなのだろう。。。
結論
わからん。
でも自分の気付いていないところで実は重要なチェックの働きをしているかもしれないので、念のために設定はしておいたほうがいいのかもしれない。
もう少しスマートにする
問題点
@Secured({"ROLE_USER", "RUN_AS_HOGE", "RUN_AS_FUGA"})
public void secured() {
this.printAuthorities("secured");
}
- 権限の指定(
ROLE_USER
)と Run-As の設定(RUN_AS_FUGA
)が1つの配列上で指定されていて、分かりにくい - 名前が
RUN_AS_
でないといけなかったり、その後付与される権限がROLE_
が固定でついたりして自由度が低い
こちらの記事 を参考に、もうちょっとスマートに使えるようにする。
実装
package sample.spring.security.runas;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RunAs {
String[] value();
}
- Run-As で追加する権限を定義するための専用のアノテーションを自作する
package sample.spring.security.runas;
import org.springframework.aop.framework.ReflectiveMethodInvocation;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.intercept.RunAsManagerImpl;
import org.springframework.security.access.intercept.RunAsUserToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class MyRunAsManagerImpl extends RunAsManagerImpl {
private static final Authentication NO_REPLACE = null;
@Override
public Authentication buildRunAs(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attributes) {
if (!this.supports(secureObject)) {
return NO_REPLACE;
}
String[] runAsAuthorities = this.getRunAsAuthorities(secureObject);
return new RunAsUserToken(
this.getKey(),
authentication.getPrincipal(),
authentication.getCredentials(),
this.createNewAuthorities(authentication, runAsAuthorities),
authentication.getClass()
);
}
private boolean supports(Object secureObject) {
return (secureObject instanceof ReflectiveMethodInvocation)
&& ((ReflectiveMethodInvocation) secureObject).getMethod().isAnnotationPresent(RunAs.class);
}
private String[] getRunAsAuthorities(Object secureObject) {
ReflectiveMethodInvocation invocation = (ReflectiveMethodInvocation) secureObject;
Method method = invocation.getMethod();
RunAs runAs = method.getAnnotation(RunAs.class);
return runAs.value();
}
private List<GrantedAuthority> createNewAuthorities(Authentication authentication, String[] runAsAuthorities) {
List<GrantedAuthority> newAuthorities = new ArrayList<>(authentication.getAuthorities());
for (String runAsAuthority : runAsAuthorities) {
newAuthorities.add(new SimpleGrantedAuthority(runAsAuthority));
}
return newAuthorities;
}
}
- デフォルト実装の
RunAsManagerImpl
を継承し、buildRunAs()
メソッドをオーバーライドする - セキュアオブジェクトが
ReflectiveMethodInvocation
の場合で、かつ@RunAs
でメソッドがアノテートされている場合にかぎり、処理を行うようにする- 権限の置き換えを行わない場合は、
null
をreturn
すればいい
- 権限の置き換えを行わない場合は、
- メソッドが
@RunAs
でアノテートされている場合は、value
を取得して新しい権限を追加したRunAsUserToken
インスタンスを取得してreturn
する
namespace
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
...>
<bean class="sample.spring.security.service.MyRunAsService" />
<bean id="runAsManager"
class="sample.spring.security.runas.MyRunAsManagerImpl">
<property name="key" value="foo" />
</bean>
<bean id="runAsAuthenticationProvider"
class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="foo" />
</bean>
<sec:global-method-security pre-post-annotations="enabled" run-as-manager-ref="runAsManager" />
<sec:http>
...
</sec:http>
<sec:authentication-manager>
...
<sec:authentication-provider ref="runAsAuthenticationProvider" />
</sec:authentication-manager>
</beans>
-
RunAsManager
の実装クラスをMyRunAsManagerImpl
に変更 -
<global-method-security>
のsecured-annotations
を除去し、代わりにpre-post-annotations
を有効に
Java Configuration
package sample.spring.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.intercept.RunAsManager;
import org.springframework.security.access.intercept.RunAsManagerImpl;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import sample.spring.security.runas.MyRunAsManagerImpl;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MyGlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
public static final String RUN_AS_KEY = "foo";
@Override
protected RunAsManager runAsManager() {
RunAsManagerImpl runAsManager = new MyRunAsManagerImpl();
runAsManager.setKey(RUN_AS_KEY);
return runAsManager;
}
}
- namespace のほうと同じ変更を加える
package sample.spring.security.service;
import org.springframework.security.access.prepost.PreAuthorize;
import sample.spring.security.runas.RunAs;
...
public class MyRunAsService {
@PreAuthorize("hasRole('ROLE_USER')")
@RunAs({"HOGE", "FUGA"})
public void secured() {
this.printAuthorities("secured");
}
...
}
-
@Secured
の代わりに@PreAuthorize
と@RunAs
でメソッドをアノテートする
動作確認
/run-as/secured
にアクセス
[メソッド呼び出し前]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
[secured]
Authentication.class = class org.springframework.security.access.intercept.RunAsUserToken
ROLE_USER
HOGE
FUGA
[メソッド呼び出し後]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
/run-as/nonSecured
にアクセス
[メソッド呼び出し前]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
[nonSecured]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
[メソッド呼び出し後]
Authentication.class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
ROLE_USER
参考
- Spring Security Reference
- TERASOLUNA Server Framework for Java (5.x) Development Guideline — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.3.0.RELEASE documentation
- Spring Security Run-As example using annotations and namespace configuration - DZone DevOps
- Spring Security – Run-As Authentication | Baeldung
- That Java Thing: Spring Security Run-As example using annotations and namespace configuration