今回は、Spring Securityを利用してユーザー認証を行う際に、データベースから取得したログインユーザーを利用するように修正してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
また、Oracleデータベース上に、以下のuser_passテーブルが作成できていること
1 2 3 4 | create table user_pass ( name VARCHAR(40) PRIMARY KEY NOT NULL, pass VARCHAR(120) NOT NULL ); |
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
Spring Securityの設定を行うクラスの内容は以下の通りで、「configure(AuthenticationManagerBuilder auth)」メソッドでSpring-Security用のユーザーアカウント情報をデータベースからロードするサービスクラスの呼び出しを行うように変更すると共に、パスワードをBCryptで暗号化するクラスのオブジェクトを作成している。
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 | 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); } /** * 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().anyRequest().authenticated() .and() //かつ //ログアウト時はログイン画面に遷移する .logout().logoutSuccessUrl("/login").permitAll(); } /** * 認証するユーザー情報をデータベースからロードする処理 * @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(); } } |
また、Spring-Security用のサービスクラスの内容は以下の通りで、Spring-Security用のユーザーアカウント情報をデータベースからロードする処理と、ユーザーアカウント情報をデータベースに登録する処理を定義している。
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 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; 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 mapper; /** * 指定したユーザー名をもつ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 = mapper.findByName(username); if(userPass == null){ throw new UsernameNotFoundException("ユーザーが見つかりません"); } //指定したユーザー名をもつSpring-Security用のユーザーアカウント情報を取得する return new UserPassAccount(userPass, AuthorityUtils.createAuthorityList("USER")); } /** * 指定したユーザー名・パスワードをもつレコードをユーザーパスワードデータ * テーブル(user_pass)に登録する * @param username ユーザー名 * @param password パスワード */ @Transactional public void registerUser(String username, String password){ if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ return; } //指定したユーザー名をもつUserPassオブジェクトを取得する UserPass userPass = mapper.findByName(username); //UserPassオブジェクトが無ければ追加・あれば更新する if(userPass == null){ userPass = new UserPass(username, password); mapper.create(userPass); }else{ mapper.update(userPass); } } } |
また、Spring-Security用のユーザーアカウント情報を定義しているクラスの内容は以下の通り。
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 | package com.example.demo; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; public class UserPassAccount implements UserDetails { /** UserPassオブジェクト */ private UserPass userPass; /** ユーザー権限情報 */ private Collection<GrantedAuthority> authorities; /** * Spring-Security用のユーザーアカウント情報(UserDetails)を作成する * @param userPass UserPassオブジェクト * @param authorities ユーザー権限情報 */ public UserPassAccount(UserPass userPass, Collection<GrantedAuthority> authorities){ this.userPass = userPass; this.authorities = authorities; } /** * ユーザー権限情報を取得する * @return ユーザー権限情報 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } /** * パスワードを取得する * @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; } } |
さらに、user_passテーブルにアクセスする処理の内容は以下の通りで、MyBatisを利用するようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package com.example.demo; import lombok.Data; @Data public class UserPass { /** * 指定したユーザー名・パスワードをもつUserPassオブジェクトを作成する * @param name ユーザー名 * @param pass パスワード */ public UserPass(String name, String pass){ this.name = name; this.pass = pass; } /** ユーザー名 */ private String name; /** パスワード */ private String 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 | 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 | <?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" /> </resultMap> <select id="findByName" resultMap="userPassResultMap"> SELECT name, pass FROM USER_PASS WHERE name = #{name} </select> <insert id="create" parameterType="com.example.demo.UserPass"> INSERT INTO USER_PASS ( name, pass ) VALUES (#{name}, #{pass}) </insert> <update id="update" parameterType="com.example.demo.UserPass"> UPDATE USER_PASS SET pass = #{pass} WHERE name = #{name} </update> </mapper> |
また、コントローラクラスの内容は以下の通りで、ログイン画面を表示するメソッド内で、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 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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 | 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.ui.Model; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.validation.BindingResult; import org.springframework.web.bind.support.SessionStatus; import org.springframework.data.domain.Pageable; import java.util.ArrayList; import java.util.List; @Controller @SessionAttributes(types = {DemoForm.class, SearchForm.class}) public class DemoController { /** * Demoサービスクラスへのアクセス */ @Autowired private DemoService demoService; /** * Spring-Security用のユーザーアカウント情報を * 取得・設定するサービスへのアクセス */ @Autowired private UserPassAccountService userDetailsService; /** * パスワードをBCryptで暗号化するクラスへのアクセス */ @Autowired private PasswordEncoder passwordEncoder; /** * ユーザーデータテーブル(user_data)のデータを取得して返却する * @return ユーザーデータリスト */ @ModelAttribute("demoFormList") public List<DemoForm> userDataList(){ List<DemoForm> demoFormList = new ArrayList<>(); return demoFormList; } /** * 追加・更新用Formオブジェクトを初期化して返却する * @return 追加・更新用Formオブジェクト */ @ModelAttribute("demoForm") public DemoForm createDemoForm(){ DemoForm demoForm = new DemoForm(); return demoForm; } /** * 検索用Formオブジェクトを初期化して返却する * @return 検索用Formオブジェクト */ @ModelAttribute("searchForm") public SearchForm createSearchForm(){ SearchForm searchForm = new SearchForm(); return searchForm; } /** * ログイン用ユーザーのデータを登録する * @return ログイン画面へのパス */ @GetMapping("/login") public String login(){ //ユーザー名「user」、パスワード「pass」をユーザーパスワードデータテーブル(user_pass) //へ登録する。その際、パスワードはBCryptで暗号化する userDetailsService.registerUser( "user", passwordEncoder.encode("pass")); //ログイン画面に遷移する return "login"; } /** * 初期表示(検索)画面に遷移する * @return 検索画面へのパス */ @RequestMapping("/") public String index(){ return "search"; } /** * 検索処理を行い、一覧画面に遷移する * @param searchForm 検索用Formオブジェクト * @param result バインド結果 * @param model Modelオブジェクト * @return 一覧画面へのパス */ @PostMapping("/search") public String search(@Validated SearchForm searchForm , BindingResult result, Model model){ //検索用Formオブジェクトのチェック処理でエラーがある場合は、 //検索画面のままとする if(result.hasErrors()){ return "search"; } //現在ページ数を1ページ目に設定し、一覧画面に遷移する searchForm.setCurrentPageNum(1); return movePageInList(model, searchForm); } /** * 一覧画面で「先頭へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/firstPage") public String firstPage(SearchForm searchForm, Model model){ //現在ページ数を先頭ページに設定する searchForm.setCurrentPageNum(1); return movePageInList(model, searchForm); } /** * 一覧画面で「前へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/backPage") public String backPage(SearchForm searchForm, Model model){ //現在ページ数を前ページに設定する searchForm.setCurrentPageNum(searchForm.getCurrentPageNum() - 1); return movePageInList(model, searchForm); } /** * 一覧画面で「次へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/nextPage") public String nextPage(SearchForm searchForm, Model model){ //現在ページ数を次ページに設定する searchForm.setCurrentPageNum(searchForm.getCurrentPageNum() + 1); return movePageInList(model, searchForm); } /** * 一覧画面で「最後へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/lastPage") public String lastPage(SearchForm searchForm, Model model){ //現在ページ数を最終ページに設定する searchForm.setCurrentPageNum(demoService.getAllPageNum(searchForm)); return movePageInList(model, searchForm); } /** * 更新処理を行う画面に遷移する * @param id 更新対象のID * @param model Modelオブジェクト * @return 入力・更新画面へのパス */ @GetMapping("/update") public String update(@RequestParam("id") String id, Model model){ //更新対象のユーザーデータを取得 DemoForm demoForm = demoService.findById(id); //ユーザーデータを更新 model.addAttribute("demoForm", demoForm); return "input"; } /** * 削除確認画面に遷移する * @param id 更新対象のID * @param model Modelオブジェクト * @return 削除確認画面へのパス */ @GetMapping("/delete_confirm") public String delete_confirm(@RequestParam("id") String id, Model model){ //削除対象のユーザーデータを取得 DemoForm demoForm = demoService.findById(id); //ユーザーデータを更新 model.addAttribute("demoForm", demoForm); return "confirm_delete"; } /** * 削除処理を行う * @param demoForm 追加・更新用Formオブジェクト * @return 一覧画面の表示処理 */ @PostMapping(value = "/delete", params = "next") public String delete(DemoForm demoForm){ //指定したユーザーデータを削除 demoService.deleteById(demoForm.getId()); //一覧画面に遷移 return "redirect:/to_index"; } /** * 削除完了後に一覧画面に戻る * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面 */ @GetMapping("/to_index") public String toIndex(SearchForm searchForm, Model model){ //一覧画面に戻り、1ページ目のリストを表示する searchForm.setCurrentPageNum(1); return movePageInList(model, searchForm); } /** * 削除確認画面から一覧画面に戻る * @param model Modelオブジェクト * @param searchForm 検索用Formオブジェクト * @return 一覧画面 */ @PostMapping(value = "/delete", params = "back") public String confirmDeleteBack(Model model, SearchForm searchForm){ //一覧画面に戻る return movePageInList(model, searchForm); } /** * 追加処理を行う画面に遷移する * @param model Modelオブジェクト * @return 入力・更新画面へのパス */ @PostMapping(value = "/add", params = "next") public String add(Model model){ model.addAttribute("demoForm", new DemoForm()); return "input"; } /** * 追加処理を行う画面から検索画面に戻る * @return 検索画面へのパス */ @PostMapping(value = "/add", params = "back") public String addBack(){ return "search"; } /** * エラーチェックを行い、エラーが無ければ確認画面に遷移し、 * エラーがあれば入力画面のままとする * @param demoForm 追加・更新用Formオブジェクト * @param result バインド結果 * @return 確認画面または入力画面へのパス */ @PostMapping(value = "/confirm", params = "next") public String confirm(@Validated DemoForm demoForm, BindingResult result){ //追加・更新用Formオブジェクトのチェック処理でエラーがある場合は、 //入力画面のままとする if(result.hasErrors()){ return "input"; } //エラーが無ければ確認画面に遷移する return "confirm"; } /** * 一覧画面に戻る * @param model Modelオブジェクト * @param searchForm 検索用Formオブジェクト * @return 一覧画面の表示処理 */ @PostMapping(value = "/confirm", params = "back") public String confirmBack(Model model, SearchForm searchForm){ //一覧画面に戻る return movePageInList(model, searchForm); } /** * 完了画面に遷移する * @param demoForm 追加・更新用Formオブジェクト * @param result バインド結果 * @return 完了画面 */ @PostMapping(value = "/send", params = "next") public String send(DemoForm demoForm, BindingResult result){ //チェック処理を行い、エラーがなければ、更新・追加処理を行う if(result.hasErrors()){ return "input"; } demoService.createOrUpdate(demoForm); return "redirect:/complete"; } /** * 完了画面に遷移する * @param sessionStatus セッションステータス * @return 完了画面 */ @GetMapping("/complete") public String complete(SessionStatus sessionStatus){ //セッションオブジェクトを破棄 sessionStatus.setComplete(); return "complete"; } /** * 入力画面に戻る * @return 入力画面 */ @PostMapping(value = "/send", params = "back") public String sendBack(){ return "input"; } /** * 一覧画面に戻り、指定した現在ページのリストを表示する * @param model Modelオブジェクト * @param searchForm 検索用Formオブジェクト * @return 一覧画面の表示処理 */ private String movePageInList(Model model, SearchForm searchForm){ //現在ページ数, 総ページ数を設定 model.addAttribute("currentPageNum", searchForm.getCurrentPageNum()); model.addAttribute("allPageNum", demoService.getAllPageNum(searchForm)); //ページング用オブジェクトを生成し、現在ページのユーザーデータリストを取得 Pageable pageable = demoService.getPageable(searchForm.getCurrentPageNum()); List<DemoForm> demoFormList = demoService.demoFormList(searchForm, pageable); //ユーザーデータリストを更新 model.addAttribute("demoFormList", demoFormList); return "list"; } } |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-security-db/demo
サンプルプログラムの実行結果
サンプルプログラムの実行結果は、以下の通り。
1) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスすると、以下のログイン画面が表示される
2) このタイミングで、user_passテーブルを確認すると、名前が「user」で、パスワードが暗号化された値(passを暗号化している)で登録されていることが確認できる
3) データベースに登録したユーザーと違うユーザー名またはパスワードを入力し、「ログイン」ボタンを押下
4) 以下のように、ログインができず、ログイン画面にエラーメッセージが表示される
5) データベースに登録したユーザーと同じユーザー名・パスワードを入力し、「ログイン」ボタンを押下
6) 以下のように、ログインでき、検索画面が表示されることが確認できるので、「ログアウト」ボタンを押下
要点まとめ
- WebSecurityConfigurerAdapterクラスを継承したクラス内のconfigure(AuthenticationManagerBuilder auth)メソッドで、userDetailsServiceとpasswordEncoderを指定することで、データベースを利用したSpring-Security用の認証処理を行える。
- Spring-Security用のユーザーアカウント情報を利用して認証するサービスクラスは、UserDetailsServiceインタフェースを実装して作成する。
- Spring-Security用のユーザーアカウントは、UserDetailsインタフェースを実装して作成する。