22
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Spring Security 使い方メモ メソッドセキュリティ

Last updated at Posted at 2017-06-10

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

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

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


Spring Security では、 URL 指定によるアクセス制御以外にもメソッドレベルでのアクセス制御をサポートしている。
そして、 URL によるアクセス制御だけに依存せず、サービス層でメソッドレベルのアクセス制御をすることを推奨している。

In practice we recommend that you use method security at your service layer, to control access to your application, and do not rely entirely on the use of security constraints defined at the web-application level.
【訳】
実際は、アプリケーションへのアクセス制御にはサービスレイヤでメソッドセキュリティを使用し、 Web アプリケーションレベルで定義されたセキュリティ制約の利用を完全に信頼しないことを推奨します。

URLs change and it is difficult to take account of all the possible URLs that an application might support and how requests might be manipulated.
【訳】
URL が変わったときに、アプリケーションがサポートする全ての URL とリクエストをどう扱うかについて考慮することは困難です。

Security defined at the service layer is much more robust and harder to bypass, so you should always take advantage of Spring Security’s method security options.
【訳】
サービスレイヤで定義されたセキュリティは、より堅牢でバイパス1が困難になります。
そのため、常に Spring Security のメソッドセキュリティオプションを利用すべきです。

メソッド実行前にチェック処理を入れる

実装

MyMethodSecurityService.java
package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;

public class MyMethodSecurityService {
    
    @PreAuthorize("hasAuthority('ADMIN')")
    public String execute() {
        return "Hello Method Security!!";
    }
}
MyMethodSecurityServlet.java
package sample.spring.security.servlet;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.service.MyMethodSecurityService;

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;
import java.io.PrintWriter;

@WebServlet("/method-security")
public class MyMethodSecurityServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        MyMethodSecurityService service = context.getBean(MyMethodSecurityService.class);

        PrintWriter writer = resp.getWriter();
        try {
            writer.println(service.getMessage());
        } catch (AccessDeniedException e) {
            writer.println(e.getMessage());
        } finally {
            writer.close();
        }
    }
}

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">
    
    <sec:global-method-security pre-post-annotations="enabled" />

    <bean class="sample.spring.security.service.MyMethodSecurityService" />

    <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="user" password="user" authorities="USER" />
                <sec:user name="admin" password="admin" authorities="ADMIN" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>

Java Configuration

MySpringSecurityConfig.java
package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.MyMethodSecurityService;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin();
    }
    
    @Bean
    public MyMethodSecurityService myMethodSecurityService() {
        return new MyMethodSecurityService();
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user")
                .password("user")
                .authorities("USER")
            .and()
                .withUser("admin")
                .password("admin")
                .authorities("ADMIN");
    }
}

動作確認

上が user でログインしたとき、下が admin でログインしたとき。

spring-security.jpg

説明

applicationContext.xml
    <sec:global-method-security pre-post-annotations="enabled" />
MySpringSecurityConfig.java
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
  • メソッドセキュリティを有効にするには、
    • namespace の場合は <global-method-security> タグを追加し pre-post-annotations="enabled" を設定する
    • Java Configuration の場合は @EnableGlobalMethodSecurity を追加し prePostEnabled=true を設定する
MyMethodSecurityService.java
    @PreAuthorize("hasAuthority('ADMIN')")
    public String getMessage() {
        return "Hello Method Security!!";
    }
  • セキュリティを適用したいメソッドに @PreAuthorize アノテーションをつける
  • value には <intercept-url>access と同じように式ベースのアクセス制御を指定する

メソッドの後にチェック処理を入れる

実装

MyMethodSecurityService.java
package sample.spring.security.service;

import org.springframework.security.access.prepost.PostAuthorize;

public class MyMethodSecurityService {
    
    @PostAuthorize("returnObject == 'hoge'")
    public String getMessage(String parameter) {
        System.out.println("parameter = " + parameter);
        return parameter;
    }
}
MyMethodSecurityServlet.java
package sample.spring.security.servlet;

...

@WebServlet("/method-security")
public class MyMethodSecurityServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        MyMethodSecurityService service = context.getBean(MyMethodSecurityService.class);

        PrintWriter writer = resp.getWriter();
        try {
            String parameter = req.getParameter("parameter");
            writer.println(service.getMessage(parameter));
        } catch (AccessDeniedException e) {
            writer.println(e.getMessage());
        } finally {
            writer.close();
        }
    }
}

設定とかは前のやつと同じ。

動作確認

/method-security?parameter=hoge にアクセス

サーバー出力
parameter = hoge

spring-security.jpg

/method-security?parameter=fuga にアクセス

サーバー出力
parameter = fuga

spring-security.jpg

説明

MyMethodSecurityService.java
    @PostAuthorize("returnObject == 'hoge'")
    public String getMessage(String parameter) {
        System.out.println("parameter = " + parameter);
        return parameter;
    }
  • @PostAuthorize でアノテートすると、メソッドの実行後にチェックが行われる
  • value には @PreAuthorize 同様、式ベースのアクセス制御を指定する
  • returnObject という名前で、メソッドの戻り値を参照できる

メソッドの引数を式で参照する

実装

MyMethodSecurityService.java
package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;

public class MyMethodSecurityService {

    @PreAuthorize("#strValue == 'aaa' and #intValue == 1")
    public String getMessage(String strValue, int intValue) {
        return "strValue=" + strValue + ", intValue=" + intValue;
    }
}
MyMethodSecurityServlet.java
package sample.spring.security.servlet;

...

@WebServlet("/method-security")
public class MyMethodSecurityServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        MyMethodSecurityService service = context.getBean(MyMethodSecurityService.class);

        PrintWriter writer = resp.getWriter();
        try {
            String strValue = req.getParameter("strValue");
            int intValue = Integer.parseInt(req.getParameter("intValue"));

            writer.println(service.getMessage(strValue, intValue));
        } catch (AccessDeniedException e) {
            writer.println(e.getMessage());
        } finally {
            writer.close();
        }
    }
}

動作確認

/method-security?strValue=abc&intValue=1 にアクセス

spring-security.jpg

/method-security?strValue=aaa&intValue=1 にアクセス

spring-security.jpg

説明

MyMethodSecurityService.java
    @PreAuthorize("#strValue == 'aaa' and #intValue == 1")
    public String getMessage(String strValue, int intValue) {
  • # を頭につけることで、パラメータを参照できる

メソッドのパラメータ名の解決方法

メソッドのパラメータ名は、 Java 7 以下だとリフレクションで取ることができない。
一方 Java 8 以上の場合は、コンパイル時に -parameters オプションを指定していればリフレクションでパラメータ名を取ることができる

Spring は、これらの状況に対応できるよう様々なパラメータ名の解決方法を用意している。

アノテーションで明示する

package sample.spring.security.service;

import org.springframework.security.access.method.P;
import org.springframework.security.access.prepost.PreAuthorize;

public class MyMethodSecurityService {
    
    @PreAuthorize("#strValue == 'aaa' and #intValue == 1")
    public String getMessage(@P("strValue") String strValue, @P("intValue") int intValue) {
        return "strValue=" + strValue + ", intValue=" + intValue;
    }
}

@P というアノテーションが用意されている。
これでパラメータをアノテートし、 value でパラメータ名を指定することで、パラメータ名をアノテーション経由で取得できるようになる。

実装クラスは AnnotationParameterNameDiscoverer

リフレクションで取得する

Java 8 以上で、コンパイル時に -parameters オプションを指定してコンパイルした場合に利用できる。

実装クラスは StandardReflectionParameterNameDiscoverer

class ファイルを解析してパラメータ名を取り出す

この機能を利用する場合は、コンパイル時にデバッグ情報を生成しておく必要がある。
具体的には、 -g オプションで最低限 vars を指定しておく必要がある。

ちなみに、 Gradle でコンパイルをしている場合はデフォルトでこのオプションが有効になっているので、特に気にすることなくこの機能を使うことができる。

実装クラスは LocalVariableTableParameterNameDiscoverer


上記機能は、「アノテーション」→「リフレクション」→「classファイル解析」の順番で適用され、最初に見つかった名前が使用される。

参考

  1. セキュリティチェックをすり抜けること

22
27
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
22
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?