Spring Boot で複数のログイン画面を使う

会員用のログイン画面と管理画面用のログイン画面を分けるみたいな場合に、 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 実装だけが使われるっぽい