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