今回は、Spring Securityで一般ユーザーと管理者ユーザーを用意し、管理者ユーザーのみアクセスできる画面の制御を実装してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
また、以下の構成のuser_passテーブルが作成されていること。
参考ソース
Spring Securityの実装は、下記記事の実装も参照のこと。
サンプルプログラムの作成
作成したサンプルプログラムの構成は、以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
Spring Securityの設定ファイルの内容は以下の通りで、管理者ユーザー向けのアクセスパス「/has_admin_auth」を、管理者権限(ADMIN)をもつユーザーのみアクセス可能にしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.firewall.DefaultHttpFirewall; @Configuration @EnableWebSecurity public class DemoSecurityConfig extends WebSecurityConfigurerAdapter { /** * Spring-Security用のユーザーアカウント情報を * 取得・設定するサービスへのアクセス */ @Autowired private UserPassAccountService userDetailsService; @Override public void configure(WebSecurity web) { //org.springframework.security.web.firewall.RequestRejectedException: //The request was rejected because the URL contained a potentially malicious String ";" //というエラーログがコンソールに出力されるため、下記を追加 DefaultHttpFirewall firewall = new DefaultHttpFirewall(); web.httpFirewall(firewall); //セキュリティ設定を無視するリクエスト設定 //静的リソースに対するアクセスの場合は、Spring Securityのフィルタを通過しないように設定する web.ignoring().antMatchers("/css/**"); } /** * SpringSecurityによる認証を設定 * @param http HttpSecurityオブジェクト * @throws Exception 例外 */ @Override protected void configure(HttpSecurity http) throws Exception { //初期表示画面を表示する際にログイン画面を表示する http.formLogin() //ログイン画面は常にアクセス可能とする .loginPage("/login").permitAll() //ログインに成功したら検索画面に遷移する .defaultSuccessUrl("/") .and() //ログイン画面のcssファイルとしても共通のdemo.cssを利用するため、 //src/main/resources/static/cssフォルダ下は常にアクセス可能とする .authorizeRequests().antMatchers("/css/**").permitAll() .and() //かつ //管理者ユーザー向けのアクセスパスは、管理者権限をもつユーザーのみアクセス可能にする //それ以外はログインした全てのユーザーがアクセス可能にする .authorizeRequests().mvcMatchers("/has_admin_auth").hasAuthority("ADMIN") .anyRequest().authenticated() .and() //かつ //ログアウト時はログイン画面に遷移する .logout().logoutSuccessUrl("/login").permitAll() .and() //かつ //エラー発生時はエラー画面に遷移する .exceptionHandling().accessDeniedPage("/toError"); } /** * 認証するユーザー情報をデータベースからロードする処理 * @param auth 認証マネージャー生成ツール * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //認証するユーザー情報をデータベースからロードする //その際、パスワードはBCryptで暗号化した値を利用する auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } /** * パスワードをBCryptで暗号化するクラス * @return パスワードをBCryptで暗号化するクラスオブジェクト */ @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } } |
また、コントローラクラスの内容は以下の通りで、ログイン画面に遷移する際に、一般ユーザーと管理者ユーザーを登録する仕様になっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class DemoController { /** * Spring-Security用のユーザーアカウント情報を * 取得・設定するサービスへのアクセス */ @Autowired private UserPassAccountService userDetailsService; /** * パスワードをBCryptで暗号化するクラスへのアクセス */ @Autowired private PasswordEncoder passwordEncoder; /** * ログイン画面に遷移する * @return ログイン画面へのパス */ @GetMapping("/login") public String login(){ //ユーザーパスワードデータテーブル(user_pass)へユーザー情報を登録する。 //その際、パスワードはBCryptで暗号化する userDetailsService.registerUser("user" , passwordEncoder.encode("pass"), "USER"); userDetailsService.registerUser("admin" , passwordEncoder.encode("pass"), "ADMIN"); //ログイン画面に遷移する return "login"; } /** * 初期表示画面に遷移する * @return 初期表示画面へのパス */ @RequestMapping("/") public String index(){ return "index"; } /** * 一般ユーザーの画面に遷移する * @return 一般ユーザーの画面へのパス */ @GetMapping("/has_user_auth") public String has_user_auth(){ return "user"; } /** * 管理者ユーザーの画面に遷移する * @return 管理者ユーザーの画面へのパス */ @GetMapping("/has_admin_auth") public String has_admin_auth(){ return "admin"; } /** * エラー画面に遷移する * @return エラー画面へのパス */ @RequestMapping("/toError") public String toError(){ return "error"; } } |
このサンプルプログラムを実行すると、一般ユーザーと管理者ユーザーは、user_passテーブルに、以下のように登録される。
さらに、user_passテーブルへのアクセスに関連する部分の内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package com.example.demo; import lombok.Data; @Data public class UserPass { /** * 指定したユーザー名・パスワード・権限をもつUserPassオブジェクトを作成する * @param name ユーザー名 * @param pass パスワード * @param auth 権限 */ public UserPass(String name, String pass, String auth){ this.name = name; this.pass = pass; this.auth = auth; } /** ユーザー名 */ private String name; /** パスワード */ private String pass; /** 権限 */ private String auth; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | package com.example.demo; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; public class UserPassAccount implements UserDetails { /** UserPassオブジェクト */ private UserPass userPass; /** * Spring-Security用のユーザーアカウント情報(UserDetails)を作成する * @param userPass UserPassオブジェクト */ public UserPassAccount(UserPass userPass){ this.userPass = userPass; } /** * ユーザー権限情報を取得する * @return ユーザー権限情報 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return AuthorityUtils.createAuthorityList(userPass.getAuth()); } /** * パスワードを取得する * @return パスワード */ @Override public String getPassword() { return userPass.getPass(); } /** * ユーザー名を取得する * @return ユーザー名 */ @Override public String getUsername() { return userPass.getName(); } /** * アカウントが期限切れでないかを取得する * @return アカウントが期限切れでないか */ @Override public boolean isAccountNonExpired() { return true; } /** * アカウントがロックされていないかを取得する * @return アカウントがロックされていないか */ @Override public boolean isAccountNonLocked() { return true; } /** * アカウントが認証期限切れでないかを取得する * @return アカウントが認証期限切れでないか */ @Override public boolean isCredentialsNonExpired() { return true; } /** * アカウントが利用可能かを取得する * @return アカウントが利用可能か */ @Override public boolean isEnabled() { return true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service public class UserPassAccountService implements UserDetailsService { /** * ユーザーパスワードデータテーブル(user_pass)へアクセスするマッパー */ @Autowired private UserPassMapper userPassMapper; /** * 指定したユーザー名をもつSpring-Security用のユーザーアカウント情報を取得する * @param username ユーザー名 * @return 指定したユーザー名をもつSpring-Security用のユーザーアカウント情報 * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (StringUtils.isEmpty(username)) { throw new UsernameNotFoundException("ユーザー名を入力してください"); } //指定したユーザー名をもつUserPassオブジェクトを取得する UserPass userPass = userPassMapper.findByName(username); if(userPass == null){ throw new UsernameNotFoundException("ユーザーが見つかりません"); } //指定したユーザー名をもつSpring-Security用のユーザーアカウント情報を取得する return new UserPassAccount(userPass); } /** * 指定したユーザー名・パスワードをもつレコードをユーザーパスワードデータテーブル(user_pass)に登録する * @param username ユーザー名 * @param password パスワード * @param auth 権限 */ @Transactional public void registerUser(String username, String password, String auth){ if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password) || StringUtils.isEmpty(auth)){ return; } //指定したユーザー名をもつUserPassオブジェクトを取得する UserPass userPass = userPassMapper.findByName(username); //UserPassオブジェクトが無ければ追加・あれば更新する if(userPass == null){ userPass = new UserPass(username, password, auth); userPassMapper.create(userPass); }else{ userPassMapper.update(userPass); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package com.example.demo; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserPassMapper { /** * ユーザーパスワードデータテーブル(user_pass)からユーザー名をキーにデータを取得する * @param name ユーザー名 * @return ユーザー名をもつデータ */ UserPass findByName(String name); /** * 指定したユーザーパスワードデータテーブル(user_pass)のデータを追加する * @param userPass ユーザーパスワードデータテーブル(user_pass)の追加データ */ void create(UserPass userPass); /** * 指定したユーザーパスワードデータテーブル(user_pass)のデータを更新する * @param userPass ユーザーパスワードデータテーブル(user_pass)の更新データ */ void update(UserPass userPass); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.UserPassMapper"> <resultMap id="userPassResultMap" type="com.example.demo.UserPass" > <result column="name" property="name" jdbcType="VARCHAR" /> <result column="pass" property="pass" jdbcType="VARCHAR" /> <result column="auth" property="auth" jdbcType="VARCHAR" /> </resultMap> <select id="findByName" resultMap="userPassResultMap"> SELECT name, pass, auth FROM USER_PASS WHERE name = #{name} </select> <insert id="create" parameterType="com.example.demo.UserPass"> INSERT INTO USER_PASS ( name, pass, auth ) VALUES (#{name}, #{pass}, #{auth}) </insert> <update id="update" parameterType="com.example.demo.UserPass"> UPDATE USER_PASS SET pass = #{pass}, auth = #{auth} WHERE name = #{name} </update> </mapper> |
また、HTMLファイルの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <link th:href="@{/css/demo.css}" rel="stylesheet" type="text/css" /> <title>ログイン画面</title> </head> <body> <div th:if="${param.error}" class="errorMessage"> ユーザー名またはパスワードが誤っています。 </div> <form method="post" th:action="@{/login}"> <table border="0"> <tr> <td align="left" valign="top">ユーザー名:</td> <td> <input type="text" id="username" name="username" /> </td> </tr> <tr> <td align="left" valign="top">パスワード:</td> <td> <input type="password" id="password" name="password" /> </td> </tr> </table> <br/><br/> <input type="submit" value="ログイン" /> </form> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>index page</title> </head> <body> <form method="get" th:action="@{/has_user_auth}"> <input type="submit" value="一般ユーザーの画面へ" /> </form> <br/><br/> <form method="get" th:action="@{/has_admin_auth}"> <input type="submit" value="管理者ユーザーの画面へ" /> </form> <br/><br/><br/><br/> <form method="post" th:action="@{/logout}"> <button type="submit">ログアウト</button> </form> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>index page</title> </head> <body> ここは一般ユーザーの画面です。<br/><br/> <input type="button" value="戻る" onclick="history.back();" /> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>index page</title> </head> <body> ここは管理者ユーザーの画面です。<br/><br/> <input type="button" value="戻る" onclick="history.back();" /> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 | <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>エラー画面</title> </head> <body> 管理者ページへのアクセス権限はありません。 <br/><br/> <input type="button" value="戻る" onclick="history.back();" /> </body> </html> |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-security-auth/demo
サンプルプログラムの実行結果
サンプルプログラムの実行結果は、以下の通り。
1) Spring Bootアプリケーションを起動し、「http://(サーバー名):(ポート番号)/」とアクセスし、一般ユーザーの「user」でログイン
2) 初期表示画面に遷移するので、「一般ユーザーの画面へ」ボタンを押下
3) 一般ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下
4) 初期表示画面に戻るので、「管理者ユーザーの画面へ」ボタンを押下
5) 管理者ユーザーの画面に遷移しようとするが権限が無く、下記エラー画面に遷移する。ここで「戻る」ボタンを押下
8) ログイン画面で管理者ユーザーの「admin」でログイン
9) 初期表示画面に遷移するので、「一般ユーザーの画面へ」ボタンを押下
10) 一般ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下
11) 初期表示画面に戻るので、「管理者ユーザーの画面へ」ボタンを押下
12) 管理者ユーザーの画面に遷移することが確認できる。ここで「戻る」ボタンを押下
要点まとめ
- Spring Securityによる権限制御を行うには、http.authorizeRequests()オブジェクトのmvcMatchersメソッドでアクセスパスを指定し、hasAuthorityメソッドで権限設定を行えばよい。