Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita を参照してください。
基礎・仕組み的な話
認証・認可の話
Remember-Me の話
CSRF の話
セッション管理の話
レスポンスヘッダーの話
メソッドセキュリティの話
Run-As の話
ACL の話
テストの話
MVC, Boot との連携の話
番外編
Spring Security にできること・できないこと
CORS とは
@tomoyukilabs さんの CORSまとめ - Qiita が勉強になりました。
雑にまとめると、
- ブラウザは、あるオリジンから読み込まれたリソースから別のオリジンにアクセスできないようになっている
- 同一オリジンポリシーというらしい
- サーバー側で、「このオリジンからのリクエストなら別オリジンであっても許可する」みたいな設定をすることで、クライアントはオリジンを超えてリクエストを送信できるようになる
- それが CORS (Cross-Origin Resource Sharing)
みたいな感じ。
「オリジン」が何なのかは オリジンの定義 | 同一オリジンポリシー - Web セキュリティ | MDN とかを参照。
Hello World
クロスオリジンでアクセスするページの準備
以下の html を GitHub 上に配置し、静的ページとしてブラウザから開けるようにしておく。
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>cors test</title>
</head>
<body>
<div>
<select id="contextPath">
<option value="namespace" selected>namespace</option>
<option value="java-config">java-config</option>
</select>
</div>
<div>
<input id="headerName" placeholder="Header-Name" />
<input id="headerValue" placeholder="Header-Value" />
</div>
<div>
<input id="responseHeaderName" placeholder="responseHeaderName" />
</div>
<button type="button" id="sendButton">Send Request</button>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script>
$(function() {
$('#sendButton').on('click', function() {
var headers = {};
var headerName = $('#headerName').val();
var headerValue = $('#headerValue').val();
if (headerName) {
headers[headerName] = headerValue;
}
var contextPath = $('#contextPath').val();
$.ajax({
url: 'http://localhost:8080/' + contextPath + '/cors',
type: 'GET',
dataType: 'text',
headers: headers
})
.done(function(text, status, jqXhr) {
console.log("text = " + text);
var responseHeaderName = $('#responseHeaderName').val();
if (responseHeaderName) {
console.log("[ResponseHeader] " + responseHeaderName + " : " + jqXhr.getResponseHeader(responseHeaderName));
}
})
.fail(function() {
console.error(arguments);
});
});
});
</script>
</body>
</html>
localhost
に対して Ajax でアクセスするようにしている。
サーバー側の実装
package sample.spring.security.servlet;
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("/cors")
public class CorsServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Original-Header", "CORS!!");
try (PrintWriter writer = resp.getWriter()) {
writer.println("Hello CORS!!");
}
}
}
適当なヘッダーとテキストのボディを返すだけの実装。
まずは CORS の設定なしでアクセスしてみる
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: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="" />
</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.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;
@EnableWebSecurity
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 {
auth.eraseCredentials(false)
.inMemoryAuthentication()
.withUser("hoge")
.password("hoge")
.roles();
}
}
CORS 用の設定などは一切していない状態。
先ほどの検証用ページ で Send Request
ボタンをクリックすると、次のようなエラーが発生する。
Spring Security の認証チェックに阻まれて、ログインページにリダイレクトさせられている。
また、 CORS の仕様で定義されている Access-Control-Allow-Origin
のヘッダーがレスポンスにないため、アクセスできません、と言っている。
Spring Security の設定を変更してアクセスできるようにする
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 id="corsConfiguration" class="org.springframework.web.cors.CorsConfiguration">
<property name="allowedOrigins">
<list>
<value>http://opengl-8080.github.io</value>
</list>
</property>
<property name="allowedMethods">
<list>
<value>GET</value>
</list>
</property>
</bean>
<bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
<property name="corsConfigurations">
<map>
<entry key="/cors" value-ref="corsConfiguration" />
</map>
</property>
</bean>
<sec:http>
<sec:intercept-url pattern="/login" access="permitAll" />
<sec:intercept-url pattern="/cors" access="permitAll" />
<sec:intercept-url pattern="/**" access="isAuthenticated()" />
<sec:form-login />
<sec:logout />
<sec:cors configuration-source-ref="corsSource" />
</sec:http>
...
</beans>
Java Configuration
package sample.spring.security;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/cors").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.cors()
.configurationSource(this.corsConfigurationSource());
}
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedMethod("GET");
corsConfiguration.addAllowedOrigin("http://opengl-8080.github.io");
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
corsSource.registerCorsConfiguration("/cors", corsConfiguration);
return corsSource;
}
...
}
もう一度実行する。
今度はアクセスが成功してサーバーが返したテキストを出力できている。
説明
CORS を有効にする
<bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
...
</bean>
<sec:http>
...
<sec:cors configuration-source-ref="corsSource" />
</sec:http>
-
<http>
タグの下に<cors>
を追加することで CORS の制御が有効になる - このとき、
configuration-source-ref
属性に CORS の細かい設定を定義したCorsConfigurationSource
の Bean を指定する
@Override
public void configure(HttpSecurity http) throws Exception {
http...
.and()
.cors()
.configurationSource(this.corsConfigurationSource());
}
private CorsConfigurationSource corsConfigurationSource() {
...
}
- Java Configuration の場合は
.cors().configurationSource(CorsConfigurationSource)
で指定する
CORS の設定
<bean id="corsConfiguration" class="org.springframework.web.cors.CorsConfiguration">
<property name="allowedOrigins">
<list>
<value>http://opengl-8080.github.io</value>
</list>
</property>
<property name="allowedMethods">
<list>
<value>GET</value>
</list>
</property>
</bean>
<bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
<property name="corsConfigurations">
<map>
<entry key="/cors" value-ref="corsConfiguration" />
</map>
</property>
</bean>
- CORS の設定には2つのクラスを使用する
-
CorsConfigurationSource
とCorsConfiguration
-
-
CorsConfiguration
にどの HTTP メソッドを許可するか、どのオリジンからのアクセスを許可するか、といった詳細なルールを設定する- 上記設定の場合は、
allowedOrigins
プロパティで許可するオリジンのリストを、allowedMethods
で許可する HTTP メソッドを指定している -
CorsConfiguration
は、デフォルトだとすべてのリクエストを許可しない設定になっているので、何かしら許可の設定をする必要がある
- 上記設定の場合は、
-
CorsConfigurationSource
は複数のCorsConfiguration
を保持し、現在のリクエストに対してどのCorsConfiguration
を適用すべきかを判断する- 上記設定では、実装クラスとして
UrlBasedCorsConfigurationSource
を使用している -
UrlBasedCorsConfigurationSource
は、どのCorsConfiguration
を適用すべきかどうかをパスパターンと紐づけることで管理している - 上記設定の場合は、
/cors
にアクセスが来た場合にcorsConfiguration
Bean で設定したルールを適用するようにしている - パスパターンは Ant 形式で指定できる
- 上記設定では、実装クラスとして
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedMethod("GET");
corsConfiguration.addAllowedOrigin("http://opengl-8080.github.io");
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
corsSource.registerCorsConfiguration("/cors", corsConfiguration);
return corsSource;
}
- Java Configuration の場合は
addAllowedMethod(String)
やregisterCorsConfiguration(String, CorsConfiguration)
など、1つずつ設定できるメソッドが利用できる- (XML でも利用できなくはないが、記述が煩雑になるだけなので普通は使わないと思う)
preflight request へ対応させる
CORS の仕様では、デフォルトで許可されている HTTP メソッドや設定可能なヘッダーは限られている。
デフォルトで許可されていないヘッダーを送りたい、いった場合は、 prefilight request という「許可されているか確認する」ための事前リクエストを送ることになっている。
このリクエストに対してサーバーが「OKよ」と返した場合に限り、クライアントは本番のリクエストを送信する。
まずは許可されていない HTTP ヘッダーを送信してみる
Hoge
という許可されていないヘッダーを載せてリクエストを送信してみる。
むっちゃ怒られた。
preflight request で許可されていないヘッダーがあったのでアクセスできなかった、と言っている。
リクエストとレスポンスのヘッダーを確認してみる。
リクエストヘッダーに、 Access-Control-Request-Headers: hoge
とあり、 hoge
というヘッダーは許可されるか確認している。
しかし、レスポンスには hoge
は許可しているよ、ということを表すヘッダーがない。
このため、ブラウザは hoge
は許可されないヘッダーだと判断して処理を中断している。
Hoge ヘッダーを許可する
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 id="corsConfiguration" class="org.springframework.web.cors.CorsConfiguration">
<property name="allowedOrigins">
<list>
<value>http://opengl-8080.github.io</value>
</list>
</property>
<property name="allowedMethods">
<list>
<value>GET</value>
</list>
</property>
<property name="allowedHeaders"> ★追加
<list>
<value>Hoge</value>
</list>
</property>
</bean>
...
</beans>
Java Configuration
package sample.spring.security;
...
@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
...
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
...
corsConfiguration.addAllowedHeader("Hoge"); ★追加
...
}
...
}
CorsConfiguration
の allowedHeader
に Hoge
を追加している。
再度動作検証する。
今度はちゃんとリクエストが通った。
HTTP リクエストの様子を確認する。
先に OPTIONS
メソッドによる preflight request が実行され、その後本番の GET
によるリクエストが送信されている。
OPTIONS
の方のヘッダーを見てみる。
レスポンスヘッダーに Access-Control-Allow-Headers: hoge
が追加され、 hoge
ヘッダーが許可されていることが示されている。
Spring Security は、 CorsConfiguration
に設定した許可のルールに従い、自動的に preflight request への回答を行ってくれるようになっている。
クライアントが任意のレスポンスヘッダーにアクセスする場合もデフォルトでは制限があるが、 exposedHeaders を設定することで許可することができる。