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 のメソッドセキュリティオプションを利用すべきです。
メソッド実行前にチェック処理を入れる
実装
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!!";
}
}
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
<?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
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
でログインしたとき。
説明
<sec:global-method-security pre-post-annotations="enabled" />
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
- メソッドセキュリティを有効にするには、
- namespace の場合は
<global-method-security>
タグを追加しpre-post-annotations="enabled"
を設定する - Java Configuration の場合は
@EnableGlobalMethodSecurity
を追加しprePostEnabled=true
を設定する
- namespace の場合は
@PreAuthorize("hasAuthority('ADMIN')")
public String getMessage() {
return "Hello Method Security!!";
}
- セキュリティを適用したいメソッドに
@PreAuthorize
アノテーションをつける -
value
には<intercept-url>
のaccess
と同じように式ベースのアクセス制御を指定する
メソッドの後にチェック処理を入れる
実装
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;
}
}
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
/method-security?parameter=fuga
にアクセス
parameter = fuga
説明
@PostAuthorize("returnObject == 'hoge'")
public String getMessage(String parameter) {
System.out.println("parameter = " + parameter);
return parameter;
}
-
@PostAuthorize
でアノテートすると、メソッドの実行後にチェックが行われる -
value
には@PreAuthorize
同様、式ベースのアクセス制御を指定する -
returnObject
という名前で、メソッドの戻り値を参照できる
メソッドの引数を式で参照する
実装
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;
}
}
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
にアクセス
/method-security?strValue=aaa&intValue=1
にアクセス
説明
@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ファイル解析」の順番で適用され、最初に見つかった名前が使用される。
参考
- 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
-
セキュリティチェックをすり抜けること ↩