会員用のログイン画面と管理画面用のログイン画面を分けるみたいな場合に、 Spring Boot ( Spring Security )でどうやればいいのか手探りで調べたのでメモ。よくありそうな話ではありますが、意外と情報がなかった。
注意
remember me を有効にするとこの方法ではまともに動きません。 HttpSecurity
を複数用意するとだめっぽいです(そもそも formLogin
を複数用意するのは若干イレギュラー感もあり・・・)。 remember me を有効にしたい場合、ログイン成功時・失敗時の遷移先 URL を動的に切り替えるために以下のような手順が必要と思われます。
UsernamePasswordAuthenticationFilter
のカスタマイズAuthenticationEntryPoint
のカスタマイズ
・・・など。機会があればまたまとめるかもしれません。
ざっくりした要件
- 会員画面と管理画面は別々のログイン画面を用意する
- 未ログイン状態で会員画面にアクセスされた場合は会員用ログイン画面に、管理画面にアクセスされた場合は管理用ログイン画面にそれぞれ遷移する
- 会員はログイン状態でも管理画面にはアクセスできないし、逆も然り
URL 構成
こんな感じを目標とします。ちょっと微妙ですが・・・。
種別 | ログイン画面 URL | ログイン実行 URL | ログイン後 URL |
---|---|---|---|
会員用 | / または /index | /member/login | /member/top |
管理画面用 | /admin/index | /admin/login | /admin/top |
環境
- Spring Boot 1.3.0
- JDK 8u66
実装
Spring Boot の一般的な構成でプロジェクトを作ります。今回は STS メニューの Spring Starter Project から作成しました。 Thymeleaf と Security を使うようにしておきます。
application.yml
Basic 認証は要らないのでオフっておきます。
security: basic: enabled: false
Controller
ログイン画面表示用・ログイン後画面表示用のメソッドを定義しておきます。
@Controller public class IndexController { @RequestMapping("admin/index") String indexForAdmin() { return "admin/index"; } @RequestMapping("admin/top") String topForAdmin() { return "admin/top"; } @RequestMapping(value = {"", "index"}) String index() { return "index"; } @RequestMapping("member/top") String top() { return "member/top"; } }
画面
src/main/resources/templates
以下に、 Controller で指定した html を配置しておきます。
admin/index.html
admin/top.html
member/top.html
index.html
会員用ログイン画面の html はこんな感じです。管理画面用も th:action
の値が違うぐらいです。ここの値は後述の JavaConfig で指定します。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>ログイン</title> </head> <body> <p>会員用ログイン</p> <form method="post" th:action="@{/member/login}"> <p th:if="${param.error}">失敗</p> <input type="text" name="loginId" required="required" /> <input type="password" name="password" required="required" /> <input type="submit" value="ろぐいん" /> </form> </body> </html>
ログイン後トップ画面はなんでもいいです。
Security 用 JavaConfig クラス
configureGlobal
メソッドでインメモリのユーザーを用意しています。 /member/** には USER 権限のユーザーのみ、 /admin/** には ADMIN 権限のユーザーのみアクセスを許可します。
@EnableWebSecurity public class SecurityConfig { // 会員用設定 @Configuration @Order(1) public static class MemberConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/member/**") // ポイント? .authorizeRequests() .antMatchers("/member/**") .hasRole("USER") // USER 権限のみアクセス可 .anyRequest() .authenticated() .and() .formLogin() .loginProcessingUrl("/member/login") .loginPage("/index") .failureUrl("/index?error") .defaultSuccessUrl("/member/top") .usernameParameter("loginId") .passwordParameter("password") .and() .logout() .logoutUrl("/**/logout") .logoutSuccessUrl("/index") .deleteCookies("JSESSIONID") .and() .csrf() .disable(); } } // 管理画面用設定 @Configuration @Order(2) public static class AdminConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/admin/**") // ポイント? .authorizeRequests() .antMatchers("/admin/index") // ログイン画面は未ログインでもアクセス可 .permitAll() .antMatchers("/admin/**") .hasRole("ADMIN") // ADMIN 権限のみアクセス可 .anyRequest() .authenticated() .and() .formLogin() .loginProcessingUrl("/admin/login") .loginPage("/admin/index") .failureUrl("/admin/index?error") .defaultSuccessUrl("/admin/top") .usernameParameter("loginId") .passwordParameter("password") .and() .logout() .logoutUrl("/**/logout") .logoutSuccessUrl("/admin/index") .deleteCookies("JSESSIONID") .and() .csrf() .disable(); } } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { // インメモリのユーザーを用意 auth .inMemoryAuthentication() .withUser("user").password("pass").roles("USER"); auth .inMemoryAuthentication() .withUser("admin").password("pass").roles("ADMIN"); } }
とりあえずはこれで意図した動きになりました。が、いまいち信頼できる資料がなかったので、これでいいのかはちょっと自信ないです・・。まずそうな部分があったら指摘していただけると幸いです。
ポイントは以下でしょうか。
- 会員用・管理画面用でそれぞれ
WebSecurityConfigurerAdapter
の実装クラスを用意する- 一つの実装でやろうとするとうまく動かない(なぜかはよくわからない・・・)
WebSecurityConfigurerAdapter
の実装クラスに、@Order
でそれぞれ異なる順番を指定する- これをしないと起動時にはねられる
- 指定するのはどっちか片方だけでもよい
configure
メソッドの最初にHttpSecurity.antMatcher(String)
を必ず呼び出す- これをしないと、最初に読まれる
WebSecurityConfigurerAdapter
実装だけが使われるっぽい
- これをしないと、最初に読まれる