Edited at

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

More than 1 year has passed since last update.

基礎・仕組み的な話

認証・認可の話

Remember-Me の話

CSRF の話

セッション管理の話

レスポンスヘッダーの話

CORS の話

Run-As の話

ACL の話

テストの話

MVC, Boot との連携の話

番外編

Spring Security にできること・できないこと


Spring Security では、 URL 指定によるアクセス制御以外にもメソッドレベルでのアクセス制御をサポートしている。

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

http://docs.spring.io/spring-security/site/docs/4.2.1.RELEASE/reference/htmlsingle/#request-matching


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. セキュリティチェックをすり抜けること