1M Views
June 28, 22
スライド概要
JSUG 勉強会の資料です。
Java、Spring、IntelliJ IDEA
最新の6.0で学ぶ! 初めてのひとのための Spring Security (株)クレディセゾン 多田真敏 2022年6月28日(2023年1月改訂) JSUG勉強会 1
このセッションについて ▸ Spring Securityの最新情報に基づいて、 基礎から丁寧に解説します ▸ 対象者 ▸ [必須] この資料レベルのSpringのDIコンテナ・Spring MVC ・Spring JDBC・Spring Bootの知識がある方 ▸ Spring Securityを使うのが初めての方 or Spring Securityを使ったことはあるけど知識をアップデートしたい方 ▸ 以下はスコープ外とします ▸ OAuth、OpenID Connect、SAML、Kotlin、WebFlux 2
使用しているバージョン ▸ JDK 17 ▸ Spring Security 6.0 ▸ Spring Boot 3.0 将来のバージョンでは、 内容が異なる可能性があります 3
最新情報のアップデートだけ知りたい方 ▸ 👇のブログを読んでください https://qiita.com/suke_masa/items/908805dd45df08ba28d8 4
自己紹介 ▸ 多田真敏 (@suke̲masa) ▸ 元・研修トレーナーの Javaプログラマー ▸ Springとは2015年からの お付き合い ▸ IntelliJ IDEAが好き ▸ 日本Springユーザ会(JSUG) スタッフ 5
目次 ① Spring Securityって何? ② 認証・認可設定の記述方法 ③ Thymeleafとの連携 ④ DBからユーザー情報を取得する ⑤ [上級者向け] アーキテクチャーを知る ⑥ [参考資料] JUnitテストとの連携 6
目次 ① Spring Securityって何? ② 認証・認可設定の記述方法 ③ Thymeleafとの連携 ④ DBからユーザー情報を取得する ⑤ [上級者向け] アーキテクチャーを知る ⑥ [参考資料] JUnitテストとの連携 7
Spring Securityって何? ▸ Springのサブプロジェクトの1つ ▸ 名前の通り、セキュリティ(特に認証認可)を 担当する ▸ 主にサーブレットフィルターで 処理を実現している 8
Spring Securityの歴史 2004 Ben Alex氏が「Acegi Security」としてリリース 2008 Springプロジェクトの傘下となり、Spring Security 2.0としてリリース 2009 Spring Security 3.0リリース 2015 Spring Security 4.0リリース 2017 Spring Security 5.0リリース。OAuth 2.0機能が導入される 2021 Spring Security 5.5リリース。以降、半年に一度のリリースサイクルとなる 2022 Spring Security 5.7リリース(5月)。WebSecurityCon gurerAdapterが非推奨に 2022 Spring Security 5.8および6.0リリース 9 fi ※Wikipediaを参考に作成
リリースサイクル ▸ Spring Security 5.5以降から、 半年に1回のリリースサイクルとなった ▸ 毎年5月と11月にリリース ▸ 機能追加・非推奨化・削除のスピードが 速くなった ▸ 常に最新情報をキャッチアップしておくことや、 自動テストを整備しておくことが重要 10
目次 ① Spring Securityって何? ② 認証・認可設定の記述方法 ③ Thymeleafとの連携 ④ DBからユーザー情報を取得する ⑤ [上級者向け] アーキテクチャーを知る ⑥ [参考資料] JUnitテストとの連携 11
今回のサンプルアプリ ▸ ユーザーが2人 ▸ 一般ユーザー(ロールはROLE̲GENERAL) ▸ 管理者ユーザー(ロールはROLE̲ADMIN) ▸ ソースコードはGitHubにあります ▸ これからデモします 12
ログイン画面
▸ パラメーター名が決まっている(設定で変更も可能)
▸ ユーザー名は "username"
▸ パスワードは "password"
<form method="post" action="index.html" th:action="@{/login}">
ユーザー名: <input type="text"
name="username"><br>
パスワード: <input type="password"
name="password"><br>
<button type="submit">ログイン</button>
</form>
13
必要なライブラリ <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ... </dependencies> 14
セキュリティ設定クラスのひな形 @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // ここに設定を書く(後述) return http.build(); } } 15
WebSecurityCon gurerAdapterは非推奨に ▸ Spring Security 5.7で非推奨に → 6.0で削除(Issueはこちら) ▸ 今のうちに新しい書き方に移行しましょう! 非推奨!! fi 16
フォーム認証の設定
@Configuration @EnableWebSecurity @EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.formLogin(login -> login
// フォーム認証の設定記述開始
.loginProcessingUrl("/login")
// ユーザー名・パスワードの送信先URL
.loginPage("/login")
// ログイン画面のURL
.defaultSuccessUrl("/")
// ログイン成功後のリダイレクト先URL
.failureUrl("/login?error")
// ログイン失敗後のリダイレクト先URL
.permitAll()
// ログイン画面は未ログインでもアクセス可能
);
return http.build();
}
}
17
ログアウトの設定
@Configuration @EnableWebSecurity @EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.formLogin(login -> login
// 前ページ参照
).logout(logout -> logout
.logoutSuccessUrl("/")
// ログアウトの設定記述開始
// ログアウト成功後のリダイレクト先URL
);
return http.build();
}
}
18
URLごとの認可設定
@Configuration @EnableWebSecurity @EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.formLogin(login -> login
// 前々ページ参照
).logout(logout -> logout
// 前ページ参照
).authorizeHttpRequests(authz -> authz
// URLごとの認可設定記述開始
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
.permitAll()
// "/css/**"などはログイン無しでもアクセス可能
.requestMatchers("/")
.permitAll()
// "/"はログイン無しでもアクセス可能
.requestMatchers("/general")
.hasRole("GENERAL") // "/general"はROLE_GENERALのみアクセス可能
.requestMatchers("/admin")
.hasRole("ADMIN")
// "/admin"はROLE_ADMINのみアクセス可能
.anyRequest().authenticated()
// 他のURLはログイン後のみアクセス可能
);
return http.build();
}
}
19
authorizeRequests()は非推奨に ▸ AuthorizationManagerが AccessDecisionManagerを置き換える ことによるもの(詳細は後述。Issueはこちら) ▸ authorizeHttpRequests()を利用した場合 → AuthorizationFilter + AuthorizationManagerが使われる ▸ authorizeRequests()を利用した場合 → FilterSecurityInterceptor + AccessDecisionManagerが使われる 20
mvcMatchers()・antMatchers()は削除された ▸ 代わりにrequestMatchers()を使う 21
WebSecurity#ignoring()は❌ ▸ 指定したURLに対してSpring Securityが 何もしなくなるので、安全でない可能性がある ▸ 使ってると次のようなログが出る ▸ You are asking Spring Security to ignore Mvc [pattern='/ css/**']. This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead. ▸ たぶん将来的には非推奨になるのでは? ▸ 代わりにpermitAll()を使う 22
他にも設定できること ▸ セッション管理 ▸ CSRF対策 ▸ イベントの発火 ▸ AOPによるメソッドの認可 ▸ Session Fixation対策 ▸ 多重ログイン防止 ▸ ・・・など 23
より詳細な設定は ▸ Spring徹底入門を読みましょう ▸ 👇の記事と併せて読んでください 令和時代に「Spring入門」 「Spring徹底入門」を読むとき気 をつけるべきN個のこと https://www.amazon.co.jp/dp/B01IEWNLBU/ 24
目次 ① Spring Securityって何? ② 認証・認可設定の記述方法 ③ Thymeleafとの連携 ④ DBからユーザー情報を取得する ⑤ [上級者向け] アーキテクチャーを知る ⑥ [参考資料] JUnitテストとの連携 25
必要なライブラリ <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity6</artifactId> </dependency> ... ThymeleafとSpring Securityの </dependencies> 連携ライブラリ 26
thymeleaf-extras-springsecurity6
▸ できること
▸ 認証済みか否か、権限があるか否かで
HTML要素の表示/非表示を切り替えられる
▸ ユーザー情報を画面に表示できる
▸ IDEで補完するためには下記の記述が必要
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
...
27
表示/非表示の切り替え
▸ 認証済みならp要素を出力する
SpEL
(詳しくは書籍参照)
<p sec:authorize="authenticated">...</p>
▸ ROLE̲ADMINならp要素を出力する
<p sec:authorize="hasRole('ADMIN')">...</p>
▸ /adminへのアクセス権があればp要素を出力する
<p sec:authorize-url="/admin">...</p>
28
ユーザー情報の表示 <span sec:authentication="principal.loginUser.name"> ... </span> Authentication.getPrincipal().getLoginUser().getName() (後述) 29
目次 ① Spring Securityって何? ② 認証・認可設定の記述方法 ③ Thymeleafとの連携 ④ DBからユーザー情報を取得する ⑤ [上級者向け] アーキテクチャーを知る ⑥ [参考資料] JUnitテストとの連携 30
今回のDBの仕様 -- ロール CREATE TABLE roles( id INTEGER PRIMARY KEY, name VARCHAR(32) NOT NULL -- ロールのID -- ロールの名前 ); -- ユーザー CREATE TABLE login_user( id INTEGER PRIMARY KEY, -- ユーザーのID name VARCHAR(128) NOT NULL, -- ユーザーの表示名 email VARCHAR(256) NOT NULL, -- メールアドレス(ログイン時に利用) password VARCHAR(128) NOT NULL -- ハッシュ化済みのパスワード ); -- ユーザーとロールの対応付け CREATE TABLE user_role( user_id INTEGER, -- ユーザーのID role_id INTEGER, ユーザーとロールは 多対多 -- ロールのID CONSTRAINT pk_user_role PRIMARY KEY (user_id, role_id), CONSTRAINT fk_user_role_user_id FOREIGN KEY (user_id) REFERENCES login_user(id), CONSTRAINT fk_user_role_role_id FOREIGN KEY (role_id) REFERENCES roles(id) ); 31
今回のDBの仕様 INSERT INTO roles(id, name) VALUES(1, 'ROLE_GENERAL'); INSERT INTO roles(id, name) VALUES(2, 'ROLE_ADMIN'); -- password = "general" INSERT INTO login_user(id, name, email, password) VALUES(1, '一般太郎', '[email protected]', '$2a$10$6fPXYK.C9rCWUBifuqBIB.GRNU.nQtBpdzkkKis8ETaKVKxNo/ltO'); -- password = "admin" INSERT INTO login_user(id, name, email, password) VALUES(2, '管理太郎', '[email protected]', '$2a$10$SJTWvNl16fCU7DaXtWC0DeN/A8IOakpCkWWNZ/FKRV2CHvWElQwMS'); INSERT INTO user_role(user_id, role_id) VALUES(1, 1); INSERT INTO user_role(user_id, role_id) VALUES(2, 1); INSERT INTO user_role(user_id, role_id) VALUES(2, 2); 32
パスワードはハッシュ化しておく ▸ DBにパスワードを平文で保存していると、 万が一DBの内容が漏洩したときに大変 → ハッシュ化が必要 (復号化できないので"暗号化"ではない) ▸ INSERT文を書く前に、 BCryptPasswordEncoder#encode() でハッシュ化したパスワードを取得しておく BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); System.out.println(encoder.encode("元のパスワード")); 33
DBを使った認証の仕組み ①送信 ユーザー名 [email protected] ユーザー名 パスワード ●●●●●●●● パスワード ログイン User Details Service (Bean) DB ②ユーザー名 で検索 ユーザー名 ③結果 (平文) ④UserDetails を作成 ログイン画面 Password Encoder (Bean) ⑤ハッシュ化したもの 同士を比較 パスワード (ハッシュ化) User Details ユーザー名 パスワード (ハッシュ化) 34
PasswordEncoderをBean定義する ▸ Bean定義すれば、Spring Securityが DIコンテナから取得して使ってくれる @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { ... } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } 35
DBに対応したユーザークラスの作成 public record LoginUser(String email, String name, String password, List<String> roleList) { } JDK 17なら recordを使いましょう! 36
UserDetailsの作成
public class LoginUserDetails
implements UserDetails
{
private final LoginUser loginUser;
private final Collection<? extends GrantedAuthority> authorities;
public LoginUserDetails(LoginUser loginUser) {
this.loginUser = loginUser;
this.authorities = loginUser.roleList()
.stream()
.map(role -> new SimpleGrantedAuthority(role))
.toList();
}
public LoginUser getLoginUser() { return loginUser; }
@Override
// ハッシュ化済みのパスワードを返す
public String getPassword() { return loginUser.password(); }
@Override
// ログインで利用するユーザー名を返す
public String getUsername() { return loginUser.email(); }
// 次ページに続く
37
UserDetailsの作成 // 前ページから続き @Override // ロールのコレクションを返す public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override // ユーザーが期限切れでなければtrueを返す public boolean isAccountNonExpired() { return true; } @Override // ユーザーがロックされていなければtrueを返す public boolean isAccountNonLocked() { return true; } @Override // ユーザーのパスワードが期限切れでなければtrueを返す public boolean isCredentialsNonExpired() { return true; } @Override // ユーザーが有効であればtrueを返す public boolean isEnabled() { return true; } } 38
DBからユーザーを検索するクラスの作成
@Repository
public class LoginUserRepository {
ORマッパーは何でもOK
private static final String SQL_FIND_BY_EMAIL = "SELECT ...";
private static final ResultSetExtractor<LoginUser> LOGIN_USER_EXTRACTOR
= (rs) -> { ...; return new LoginUser(...); };
private final NamedParameterJdbcTemplate jdbcTemplate;
public LoginUserRepository(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
ユーザー名で検索
}
public Optional<LoginUser> findByEmail(String email) {
MapSqlParameterSource params = new MapSqlParameterSource("email", email);
LoginUser loginUser = jdbcTemplate.query(
SQL_FIND_BY_EMAIL, params, LOGIN_USER_EXTRACTOR);
return Optional.ofNullable(loginUser);
}
}
39
UserDetailsServiceの作成&Bean定義
▸ Bean定義すれば、Spring Securityが
DIコンテナから取得して使ってくれる
@Service
public class LoginUserDetailsService implements
private final LoginUserRepository repo;
UserDetailsService
{
public LoginUserDetailsService(LoginUserRepository repo) {
this.repo = repo;
}
ユーザー名で検索して、
@Override
UserDetailsを返す
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
Optional<LoginUser> userOp = repo.findByEmail(email);
return userOp.map(user -> new LoginUserDetails(user))
.orElseThrow(() -> new UsernameNotFoundException("not found"));
}
}
40
DaoAuthenticationProvider ▸ AuthenticationProvider実装クラスの1つ ▸ 詳細は後述 ▸ UserDetailsServiceを使って、 DBからユーザー情報を取得して認証を行う 41
目次 ① Spring Securityって何? ② 認証・認可設定の記述方法 ③ Thymeleafとの連携 ④ DBからユーザー情報を取得する ⑤ [上級者向け] アーキテクチャーを知る ⑥ [参考資料] JUnitテストとの連携 42
認証情報の構造 HttpSession SecurityContext getAuthentication() Authentication getPrincipal() UserDetails 43
認証情報の取得 // スレッドローカルからSecurityContextを取得 SecurityContext securityContext = SecurityContextHolder.getContext(); // SecurityContextからAuthenticationを取得 Authentication authentication = securityContext.getAuthentication(); // AuthenticationからUserDetailsを取得(キャストが必要) // 未ログイン時はgetPrincipal()がStringを返すので注意=ClassCastExceptionの可能性あり LoginUserDetails loginUserDetails = (LoginUserDetails) authentication.getPrincipal(); // コントローラーメソッドの引数で取得 @GetMapping("/foo") public String foo(Authentication authentication) { ... } // 未ログイン時は引数がnullになるので注意 @GetMapping("/bar") public String bar(@AuthenticationPrincipal LoginUserDetails userDetails) { ... } 44
Filter Chain ▸ 認証認可処理は何重ものサーブレットフィルターで 実現されている サーブレットコンテナ springSecurityFilterChain (サーブレットフィルター) springSecurityFilterChain (Bean) リクエスト レスポンス Filter 1 (Bean) ・・・ 次ページで詳しく説明 Filter N (Bean) Dispatcher Servlet Controller Controller Controller (Bean) (Bean) (Bean) DIコンテナ 45
主なフィルター この順に実行される SecurityContext PersistenceFilter LogoutFilter HttpSessionからSecurityContextを取り出し て、スレッドローカルに保存する。レスポンス時は スレッドローカルをクリアする ログアウト処理を行う UsernamePassword フォーム認証を行う AuthenticationFilter ExceptionTranslation 認証例外や認可例外を処理する Filter AuthorizationFilter 認可処理を行う 46
認証処理 UsernamePassword AuthenticationFilter ProviderManager (AuthenticationManager実装クラス) Authentication Provider 1 認証実行 ・・・・・・ 1つでも認証が成功すれば 全体としても認証OKとなる Authentication Provider N 認証実行 47
独自の認証方法を使いたい時 ▸ AuthenticationProvider実装クラスを作成 +Bean定義して、そこに認証ロジックを記述するだけでOK ▸ 例はこちら ▸ この場合、UserDetailsServiceなどの作成は不要 @Component public class MyAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // ここに認証ロジックを記述 } } 48
認可処理 AuthorizationFilter RequestMatcherDelegating AuthorizationManager (AuthorizationManager実装クラス) Authorization Manager 1 認可実行 ・・・・・・ 1つでも認可が成功すれば 全体としても認可OKとなる Authorization Manager N 認可実行 49
AccessDecisionManagerは将来非推奨に ▸ 従来はFilterSecurityInterceptorから AccessDecisionManagerを呼び出して 認可処理を行っていた ▸ AuthorizationFilter + AuthorizationManagerは 👆を置き換えるもの ▸ 対応するIssueはこちら 50
目次 ① Spring Securityって何? ② 認証・認可設定の記述方法 ③ Thymeleafとの連携 ④ DBからユーザー情報を取得する ⑤ [上級者向け] アーキテクチャーを知る ⑥ [参考資料] JUnitテストとの連携 51
Springとテスト ▸ 先に👇の情報を見ておきましょう ▸ 今こそ知りたいSpring Test ▸ 動画はこちら ▸ スライドはこちら 52
必要なライブラリ <dependencies> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> ... </dependencies> 53
セキュリティを考慮したコントローラーの単体テスト @WebMvcTest(includeFilters = @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, classes = {LoginUserDetailsService.class, LoginUserRepository.class, SecurityConfig.class} )) @AutoConfigureJdbc public class HelloControllerTest { @Autowired MockMvc mvc; UserDetailsServiceのBean IDと ユーザー名を指定 @Test @WithUserDetails(value = "[email protected]", userDetailsServiceBeanName = "loginUserDetailsService") public void 管理者で管理者画面にアクセスできる() throws Exception { mvc.perform(get("/admin").accept(MediaType.TEXT_HTML)) .andExpect(status().isOk()) .andExpect(view().name("admin")); } // 続く 54
セキュリティを考慮したコントローラーの単体テスト // 続き @Test @WithUserDetails(value = "[email protected]", userDetailsServiceBeanName = "loginUserDetailsService") public void 一般ユーザーで管理者画面にアクセスすると403() throws Exception { mvc.perform(get("/admin").accept(MediaType.TEXT_HTML)) .andExpect(status().isForbidden()); } 匿名ユーザー(未ログイン) で実行 @Test @WithAnonymousUser public void 未ログインで管理者画面にアクセスするとログイン画面にリダイレクトされる() throws Exception { mvc.perform(get("/admin").accept(MediaType.TEXT_HTML)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrlPattern("**/login")); } } 55
以下、ためになる情報たち 56
Spring徹底入門 ▸ 👇の記事と併せて読んでください 令和時代に「Spring入門」 「Spring徹底入門」を読むとき 気をつけるべきN個のこと https://www.amazon.co.jp/dp/B01IEWNLBU/ 57
公式ドキュメント ▸ 英語 → https://docs.spring.io/spring-security/ reference/5.7.0/index.html ▸ 日本語 → https://spring.pleiades.io/springsecurity/reference/5.7.0/ ▸ 「What's New」で各バージョンの新機能を 必ずチェックしましょう ▸ しかし、非推奨になったり削除されたりした機能のことは 書いていない😭 58
公式ブログ ▸ https://spring.io/blog/ ▸ 新機能のみならず、非推奨になったものも解説されているので とても有用 ▸ 例: Spring Security without the WebSecurityCon gurerAdapter ▸ 英語のみですが頑張って読みましょう ▸ Twitterで公式アカウントやコミッターさんを フォローしてると重要なブログが流れてきます ▸ @SpringSecurity @SpringCentral @snicoll がオススメ fi 59
Thanks! 60