LoginSignup
10
10

More than 1 year has passed since last update.

Spring Security 使い方メモ Run-As

Last updated at Posted at 2017-06-10

Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita を参照してください。

基礎・仕組み的な話
認証・認可の話
Remember-Me の話
CSRF の話
セッション管理の話
レスポンスヘッダーの話
メソッドセキュリティの話
CORS の話
ACL の話
テストの話
MVC, Boot との連携の話

番外編
Spring Security にできること・できないこと

Hello World

実装

MyRunAsService.java
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() メソッドは、ログイン中のユーザに付与された権限を全て表示する
RunAsTestServlet.java
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 にアクセスがあったら、 MyRunAsServicesecured() メソッドを実行する
  • それ以外のパスにリクエストがあったら、 nonSecured() メソッドを実行する
  • 各メソッドの実行前後でも、ログインユーザの権限を表示させる

namespace

applicationContext.xml
<?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

MyGlobalMethodSecurityConfig.java
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;
    }
}
MySpringSecurityConfig.java
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 の実装クラスを用意する

applicationContext.xml
    <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 に適用する

applicationContext.xml
    <sec:global-method-security secured-annotations="enabled" run-as-manager-ref="runAsManager" />
  • RunAsManager<global-method-security>run-as-manager-ref 属性に指定する
  • また、 secured-annotations を有効にしておく(@Secured アノテーションが使えるようになる)
MyGlobalMethodSecurityConfig.java
@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 に対応する設定は、 @EnableGlobalMethodSecuritysecuredEnabled で指定

RunAsImplAuthenticationProvider を用意する

applicationContext.xml
    <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 の並びに追加する
  • RunAsImplAuthenticationProviderkey プロパティには、 RunAsManagerImpl で設定したのと同じ値を設定しておく

追加する権限を定義する

MyRunAsService.java
    @Secured({"ROLE_USER", "RUN_AS_HOGE", "RUN_AS_FUGA"})
    public void secured() {
        this.printAuthorities("secured");
    }
  • 最後に権限の追加を定義する部分
  • @Secured アノテーションの value で、次の値を設定する
    1. アノテート対象を実行するために必要となる権限(ここでは ROLE_USER
      • @Secured 自体はメソッドセキュリティのためのアノテーションなので、このメソッドを実行するために必要となる定義を指定しなければならない
      • secured-annotations="enabled"@Secured を有効にすると、 MethodSecurityIntercpetor が使用する VoterRoleVoterAuthenticatedVoter になる
      • なので、これらの Voter に合わせた形で指定しなければならない(式ベースではない)
        • RoleVoter は、 ROLE_ で始まる値が指定された場合に、ユーザが同様の権限(ロール)を与えられているかチェックする
        • AuthenticatedVoter定数で指定された3つのいずれか を指定する
    2. Run-As で付加する権限(ここでは RUN_AS_HOGERUN_AS_FUGA
      • 付加する権限は RUN_AS_ で始まる形にする
      • 実際にユーザに付加される権限は、 ROLE_ が頭についた形になる(RUN_AS_HOGEROLE_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 がやること

RunAsImplAuthenticationProviderAuthentication オブジェクトの実体が RunAsUserToken の場合に認証処理をサポートする。

この RunAsUserTokenRunAsManagerImpl が生成する Authentication オブジェクトで、元の Authentication が持っていた情報に RUN_AS_ で指定した権限が追加された Authentication オブジェクトとなっている。

この RunAsUserToken には、 RunAsManagerImpl を生成するときに指定した key が設定されている。

applicationContext.xml
    <bean id="runAsManager"
          class="org.springframework.security.access.intercept.RunAsManagerImpl">
        <property name="key" value="foo" /> ★これ
    </bean>

この keyRunAsImplAuthenticationProvider にも設定している。

applicationContext.xml
    <bean id="runAsAuthenticationProvider"
          class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
        <property name="key" value="foo" /> ★同じやつを設定している
    </bean>

RunAsImplAuthenticationProvider は、 RunAsUserToken に設定された key と、自分自身に設定された key を比較し、もし異なる場合は BadCredentialsException をスローするようになっている。

いつ呼ばれる?

自分が確認した限りだと、普通に使っているだけだと RunAsImplAuthenticationProvider の処理が実行されることはない。

次の条件が満たされたときに実行されることは確認できた。

  1. MethodSecurityInterceptoralwaysReauthenticatetrue になっている
  2. ProviderManagereraseCredentialsAfterAuthenticationfalse になっている(Form ログインが有効の場合)
  3. 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_ が固定でついたりして自由度が低い

こちらの記事 を参考に、もうちょっとスマートに使えるようにする。

実装

RunAs.java
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 で追加する権限を定義するための専用のアノテーションを自作する
MyRunAsManagerImpl.java
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 でメソッドがアノテートされている場合にかぎり、処理を行うようにする
    • 権限の置き換えを行わない場合は、 nullreturn すればいい
  • メソッドが @RunAs でアノテートされている場合は、 value を取得して新しい権限を追加した RunAsUserToken インスタンスを取得して return する

namespace

applicationContext.xml
<?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

MyGlobalMethodSecurityConfig.java
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 のほうと同じ変更を加える
MyRunAsService.java
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

参考

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10