arcanum_jp’s blog

おっさんの日記

SpringBootでID/PASSによる認証を学ぶ

簡単なサンプルを自分で作って見て、SpringBootにおける認証処理、ユーザーごとの権限処理を学びます。以下はWebで結構な方が書いているので色々見たのを自分なりにメモしているものです。ソース自体はどこかの記事で見かけた物を流用したりしています。

 

まず、基本となる画面を作る

例としては、こんなURLでアクセスした場合の画面を作っていきます。

 /user            ユーザーがログインした時にしかアクセスできない領域
   /admin         管理者がログインした時にしかアクセスできない領域
   /etc              ログインしなくとも誰でも見れる領域
  /mylogin            ログイン用の画面

 はじめに、とりあえず上記のURLでアクセスできるものを作ります、

 pom.xmlは以下が必要になります。

spring-boot-starter-thymeleaf
spring-boot-starter-web

 

画面はこんな感じ

/user.html

<html>
<body>
	<h1>USER!! page</h1>
</body>
</html>

 

/admin.html

<html>
<body>
	<h1>ADMIN!! page</h1>
</body>
</html>

 

/etc.html

<html>
<body>
	<h1>etc...!! page</h1>
</body>
</html>

 
/mylogin.html

<html>
<body>
	<form id="login_form" method="post" action="@{'/login'}">
		 <label>login id</label>
		 <input type="text" id="login_id" name="login_id" />
		 <br>
		 <label>password</label>
		 <input type="password" id="login_password" name="login_password" />
		 <br>
		 <input id="login_button" type="submit" value="send" />
	</form>
</body>
</html>

 

とりあえず画面へのアクセスは1つのクラスにまとめておきました。(この設計が良いか悪いかは別とします。) 

PageController.java

@Controller
public class PageController {
	
	@RequestMapping(value="/user")
	public ModelAndView pageUser(ModelAndView mv) {
		mv.setViewName("user");
		return mv;
	}

	@RequestMapping(value="/admin")
	public ModelAndView pageAdmin(ModelAndView mv) {
		mv.setViewName("admin");
		return mv;
	}
	
	@RequestMapping(value="/etc")
	public ModelAndView pageEtc(ModelAndView mv) {
		mv.setViewName("etc");
		return mv;
	}
	
	@RequestMapping(value="/mylogin")
	public ModelAndView pageLogin(ModelAndView mv) {
		mv.setViewName("mylogin");
		return mv;
	}
}

 

 これでサクッと全ページアクセスできました。
f:id:arcanum_jp:20190519105103p:plain

認証処理を作る

次に、/admin, /userはログインしていないとアクセスができないようにします。認証処理をSpringBootで使うにはpom.xmlに次を追加すればいいようです。

spring-boot-starter-security

 
とりあえず追加してみて再起動してみます。あれ?御呼びでないページが出てきました。パスが/loginでこちらの期待するパスでもありません。SpringBootが出すデフォルトのログイン画面みたいですね。しかもどのページを開いてもこの画面になってしまいます。権限の設定などをしないとダメ見たいですね。
f:id:arcanum_jp:20190519105004p:plain
 

/user, /admin はログイン後に表示できるページという風にしたいので、未ログインの状態ではログイン画面に飛ぶようにしてみます。その場合、WebSecurityConfigurerAdapterのサブクラスを作って認証処理の設定を作り込むようです。認証処理はこのクラスがキモのようで、このクラスに色々と追加していくと認証が完成していくみたいです。その際、ログイン者の権限として"USER", "ADMIN"を作ります

MySecurityConfig.java

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 認可の設定
        http.authorizeRequests()
	    	.antMatchers("/user").hasAnyRole("USER", "ADMIN")
        	.antMatchers("/admin").hasAnyRole("ADMIN");

        // 認証処理を行うインスタンスを設定
       http.authenticationProvider(new MyAuthenticationProvider());

        // ログイン設定
        http.formLogin()
            .loginProcessingUrl("/login")   // 認証処理のパス
            .loginPage("/mylogin")            // ログインフォームのパス
            .failureHandler(new MyAuthenticationFailureHandler())       // 認証失敗時に呼ばれるハンドラクラス
            .defaultSuccessUrl("/user")     // 認証成功時の遷移先
            .usernameParameter("login_id").passwordParameter("login_password");  // ユーザー名、パスワードのパラメータ名
        
    }
}
||< 
 

<b>MyAuthenticationProvider.java</b>
>||
public class MyAuthenticationFailureHandler 
		implements AuthenticationFailureHandler {

	@Override
	public void onAuthenticationFailure(
			HttpServletRequest request, 
			HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {

		String errorId = "ERR_001";
		if(exception instanceof BadCredentialsException){
		    errorId = "LOGIN_001";
		}

		// ログイン画面にリダイレクトする
		response.sendRedirect("/mylogin?error=" + errorId);
		
	}
		
}


MyAuthenticationProvider.java

public class MyAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	private static final List<GrantedAuthority> AUTH_USER = AuthorityUtils.createAuthorityList("ROLE_USER");
	private static final List<GrantedAuthority> AUTH_ADMIN = AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN");
	
	@Override 
	protected void additionalAuthenticationChecks(
			UserDetails userDetails, 
			UsernamePasswordAuthenticationToken authentication
	) throws AuthenticationException  {
		// nothing to do
	}

	@Override
	protected UserDetails retrieveUser(
			String username, 
			UsernamePasswordAuthenticationToken authentication
	) throws AuthenticationException {
		String password = (String) authentication.getCredentials();

		// ユーザIDとパスワードをチェック
		// データベースのユーザー情報などに"ROLE_AUTH", "ROLE_ADMIN"などを保存しておき取得
		boolean isValid = true; //AuthApi.isValidUserIdAndPassword(username, password);
		if (!isValid) { 
			throw new UsernameNotFoundException(username); 
		}
		// とりあえずadminだったら管理者
		List<GrantedAuthority> auth = AUTH_USER;
		if (username.equals("admin")) {
			auth = AUTH_ADMIN;
		}

		// UserDetailsの実装(User)を生成し戻り値とする
		return new User(username, "dummy_pass", auth);

	}
}

注意点として、作ったロールですが、 MySecurityConfigでは"USER", "ADMIN"、MyAuthenticationProviderでは"ROLE_USER", "ROLE_ADMIN"と名前が異なっています。どうやらWebSecurityConfigurerAdapterでパスにロールを設定する際、hasRole( ), hasAnyRole( )を使った場合、勝手に"ROLE_"というプレフィクスをつけるようですね。紛らわしい


qiita.com