Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( 番外編 )( Spring Security でいろいろ試してみる )
概要
今回ログイン画面を作成する上で Spring Security について書籍や Web でいろいろ調べたので、忘れないうちに最初に書いていた機能以外にも試してみたいと思います。
- 今回の手順で確認できるのは以下の内容です。
- ログイン画面は表示せず URL のみで認証する ( パラメータで渡された値で認証してOKなら認証後の画面へ )
- ログイン画面で remember-me Cookie が有効ならログイン画面を表示せず自動ログインする
参照したサイト・書籍
Spring3入門
Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ
- 作者: 長谷川裕一,大野渉,土岐孝平
- 出版社/メーカー: 技術評論社
- 発売日: 2012/11/02
- メディア: 大型本
- 購入: 8人 クリック: 115回
- この商品を含むブログ (14件) を見る
- 「6.4.2 Controllerメソッドの引数」を参照しました。
目次
- ログイン画面は表示せず URL のみで認証する ( パラメータで渡された値で認証してOKなら認証後の画面へ )
- ログイン画面の URL にアクセス時に有効な remember-me Cookie があればログイン画面を表示せず自動ログインする
手順
ログイン画面は表示せず URL のみで認証する
動作確認のために、ユーザ名を書いたパラメータだけを付けた URL を用意しておいて、その URL でアクセスすればパラメータに書いたユーザでログインした状態になるようにしたいことがあります。単にログイン画面を表示して ID、パスワードを入力するのが面倒だと思っているだけなのですが。。。
Spring Security でこれができるか試してみます。URL は /urllogin?user=[メールアドレス] にします。
1.0.x-make-urllogin ブランチの作成
- IntelliJ IDEA で 1.0.x-make-urllogin ブランチを作成します。
LoginController クラスの変更
LoginController クラスに /urllogin に対応するメソッドを追加します。
- src/main/java/ksbysample/webapp/lending/web の下の LoginController.java を リンク先のその1の内容 に変更します。
Bean を @Autowired アノテーションでインジェクションするとエディタで赤の下線が表示されるのを無効にする
@SuppressWarnings("SpringJavaAutowiringInspection")
アノテーションを付加すると赤の下線は表示されなくなりますが、アノテーションを付けて回避することに意義が見いだせないのでチェック自体を無効にします。何のためのチェックか全く分からないんですよね。。。
src/main/java/ksbysample/webapp/lending/config の下の WebSecurityConfig.java を リンク先のその1の内容 に変更します。
userDetailsService フィールドに赤の下線が表示されますので、カーソルを移動した後 Alt+Enter を押します。コンテキストメニューが表示されたら「Inspection 'Autowiring for Bean Class' options」->「Disable inspection」を選択します。
userDetailsService フィールドに赤の下線が表示されなくなります。
WebSecurityConfig クラスの変更
/urllogin へのアクセスを認証不要に設定します。
- src/main/java/ksbysample/webapp/lending/config の下の WebSecurityConfig.java を リンク先のその2の内容 に変更します。
LendingUserDetailsService の変更
- 今回の機能とは関係ありませんが、気になった点があったので修正します。src/main/java/ksbysample/webapp/lending/security の下の LendingUserDetailsService.java を リンク先の内容 に変更します。
動作確認
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動して http://localhost:8080/urllogin?user=tanaka.taro@sample.com にアクセスします。自動的にログインして「ログイン成功!」の画面が表示されます。
ブラウザから http://localhost:8080/logout にアクセスしてログアウトします。
ブラウザから http://localhost:8080/urllogin?user=test にアクセスします。今度は存在しないメールアドレスなので、UsernameNotFoundException が発生してログイン画面が表示されす。
Ctrl+F2 を押して Tomcat を停止します。
commit、Push、Pull Request、マージ
- commit、GitHub へ Push、1.0.x-make-urllogin -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-make-urllogin ブランチを削除、をします。
ログイン画面の URL にアクセス時に有効な remember-me Cookie があればログイン画面を表示せず自動ログインする
実装方針
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter の doFilter メソッドの中に remember-me Cookie がある時の認証処理があるので、ここから必要な処理を持ってくればよいはずです。その方向で実装してみます。
1.0.x-autologin-rememeberme ブランチの作成
- IntelliJ IDEA で 1.0.x-autologin-rememeberme ブランチを作成します。
WebSecurityConfig クラスの変更
- Remember Me 機能に使用する key の文字列を LoginController クラスから利用できるようにします。src/main/java/ksbysample/webapp/lending/config の下の WebSecurityConfig.java を リンク先のその3の内容 に変更します。
LoginController クラスの変更
- src/main/java/ksbysample/webapp/lending/web の下の LoginController.java を リンク先のその2の内容 に変更します。
動作確認
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動して http://localhost:8080/logout にアクセスし、remember-me Cookie が存在しても一旦削除します。
ブラウザから http://localhost:8080/ にアクセスし、ログイン画面を表示します。ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックして「ログイン」ボタンをクリックします。「ログイン成功!」の画面が表示されます。これで remember-me Cookie が生成されました。
ブラウザを一旦終了させてから再度起動します。
ブラウザから http://localhost:8080/ にアクセスします。有効な remember-me Cookie が存在するので、ログイン画面は表示されず「ログイン成功!」の画面が表示されます。
再度 http://localhost:8080/logout にアクセスし、remember-me Cookie を削除します。
ブラウザを一旦終了させてから再度起動します。
ブラウザから http://localhost:8080/ にアクセスします。今度は remember-me Cookie が存在しないのでログイン画面が表示されます。
Ctrl+F2 を押して Tomcat を停止します。
commit、Push、Pull Request、マージ
- commit、GitHub へ Push、1.0.x-autologin-rememeberme -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-autologin-rememeberme ブランチを削除、をします。
ソースコード
LoginController.java
■その1
package ksbysample.webapp.lending.web; import ksbysample.webapp.lending.config.WebSecurityConfig; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.dao.UserRoleDao; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.entity.UserRole; import ksbysample.webapp.lending.security.LendingUserDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Controller @RequestMapping("/") public class LoginController { @Autowired private UserInfoDao userInfoDao; @Autowired private UserRoleDao userRoleDao; @Autowired private MessageSource messageSource; @RequestMapping public String index() { return "login"; } @RequestMapping("/encode") @ResponseBody public String encode(@RequestParam String password) { return new BCryptPasswordEncoder().encode(password); } @RequestMapping("/loginsuccess") public String loginsuccess() { return "loginsuccess"; } @RequestMapping("/urllogin") public String urllogin(@RequestParam String user , HttpServletRequest request) { // user パラメータで指定されたメールアドレスのユーザが user_info テーブルに存在するかチェックする UserInfo userInfo = userInfoDao.selectByMailAddress(user); if (userInfo == null) { throw new UsernameNotFoundException( messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound" , null, LocaleContextHolder.getLocale())); } // user_role テーブルから設定されている権限を読み込む Set<SimpleGrantedAuthority> authorities = new HashSet<>(); List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId()); if (userRoleList != null) { authorities.addAll( userRoleList.stream() .map(userRole -> new SimpleGrantedAuthority(userRole.getRole())) .collect(Collectors.toList())); } // UsernamePasswordAuthenticationToken を生成して SecurityContext にセットする LendingUserDetails lendingUserDetails = new LendingUserDetails(userInfo, authorities); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(lendingUserDetails, null, authorities); // 下の2行は org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter の // setDetails メソッドを見て実装しています AuthenticationDetailsSource<HttpServletRequest,?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); token.setDetails(authenticationDetailsSource.buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(token); return "redirect:" + WebSecurityConfig.DEFAULT_SUCCESS_URL; } }
- urllogin メソッドを追加します。
■その2
package ksbysample.webapp.lending.web; import ksbysample.webapp.lending.config.WebSecurityConfig; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.dao.UserRoleDao; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.entity.UserRole; import ksbysample.webapp.lending.security.LendingUserDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Controller @RequestMapping("/") public class LoginController { @Autowired private UserInfoDao userInfoDao; @Autowired private UserRoleDao userRoleDao; @Autowired private MessageSource messageSource; @Autowired private UserDetailsService userDetailsService; @RequestMapping public String index(HttpServletRequest request, HttpServletResponse response) { // 有効な remember-me Cookie が存在する場合にはログイン画面を表示させず自動ログインさせる TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices(WebSecurityConfig.REMEMBERME_KEY, userDetailsService); rememberMeServices.setCookieName("remember-me"); Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); return "redirect:" + WebSecurityConfig.DEFAULT_SUCCESS_URL; } return "login"; } @RequestMapping("/encode") @ResponseBody public String encode(@RequestParam String password) { return new BCryptPasswordEncoder().encode(password); } @RequestMapping("/loginsuccess") public String loginsuccess() { return "loginsuccess"; } @RequestMapping("/urllogin") public String urllogin(@RequestParam String user , HttpServletRequest request) { // user パラメータで指定されたメールアドレスのユーザが user_info テーブルに存在するかチェックする UserInfo userInfo = userInfoDao.selectByMailAddress(user); if (userInfo == null) { throw new UsernameNotFoundException( messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound" , null, LocaleContextHolder.getLocale())); } // user_role テーブルから設定されている権限を読み込む Set<SimpleGrantedAuthority> authorities = new HashSet<>(); List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId()); if (userRoleList != null) { authorities.addAll( userRoleList.stream() .map(userRole -> new SimpleGrantedAuthority(userRole.getRole())) .collect(Collectors.toList())); } // UsernamePasswordAuthenticationToken を生成して SecurityContext にセットする LendingUserDetails lendingUserDetails = new LendingUserDetails(userInfo, authorities); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(lendingUserDetails, null, authorities); // 下の2行は org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter の // setDetails メソッドを見て実装しています AuthenticationDetailsSource<HttpServletRequest,?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); token.setDetails(authenticationDetailsSource.buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(token); return "redirect:" + WebSecurityConfig.DEFAULT_SUCCESS_URL; } }
private UserDetailsService userDetailsService;
を追加します。- index メソッドの引数に
HttpServletRequest request, HttpServletResponse response
を追加します。 - index メソッドに remember-me Cookie の有無を確認して自動ログインするための処理を追加します。
WebSecurityConfig.java
■その1
@Configuration @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public static final String DEFAULT_SUCCESS_URL = "/loginsuccess"; @Autowired private UserDetailsService userDetailsService;
- userDetailsService に付加していた
@SuppressWarnings("SpringJavaAutowiringInspection")
アノテーションを削除します。
■その2
package ksbysample.webapp.lending.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public static final String DEFAULT_SUCCESS_URL = "/loginsuccess"; @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します // .antMatchers("/country/**").permitAll() .antMatchers("/fonts/**").permitAll() .antMatchers("/html/**").permitAll() .antMatchers("/encode").permitAll() .antMatchers("/urllogin").permitAll() .anyRequest().authenticated(); http.formLogin() .loginPage("/") .loginProcessingUrl("/login") .defaultSuccessUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL) .failureUrl("/") .usernameParameter("id") .passwordParameter("password") .permitAll() .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/") .deleteCookies("JSESSIONID") .deleteCookies("remember-me") .invalidateHttpSession(true) .permitAll() .and() .rememberMe() .key("ksbysample-webapp-lending") .tokenValiditySeconds(60 * 60 * 24 * 30); } @Bean public AuthenticationProvider daoAuhthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); return daoAuthenticationProvider; } @Autowired public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuhthenticationProvider()) .userDetailsService(userDetailsService); } }
public static final String DEFAULT_SUCCESS_URL = "/loginsuccess";
を追加します。- configure メソッド内の以下の点を変更します。
.antMatchers("/urllogin").permitAll()
を追加します。.defaultSuccessUrl("/loginsuccess")
→.defaultSuccessUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL)
へ変更します。
■その3
package ksbysample.webapp.lending.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public static final String DEFAULT_SUCCESS_URL = "/loginsuccess"; public static final String REMEMBERME_KEY = "ksbysample-webapp-lending"; @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します // .antMatchers("/country/**").permitAll() .antMatchers("/fonts/**").permitAll() .antMatchers("/html/**").permitAll() .antMatchers("/encode").permitAll() .antMatchers("/urllogin").permitAll() .anyRequest().authenticated(); http.formLogin() .loginPage("/") .loginProcessingUrl("/login") .defaultSuccessUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL) .failureUrl("/") .usernameParameter("id") .passwordParameter("password") .permitAll() .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/") .deleteCookies("JSESSIONID") .deleteCookies("remember-me") .invalidateHttpSession(true) .permitAll() .and() .rememberMe() .key(REMEMBERME_KEY) .tokenValiditySeconds(60 * 60 * 24 * 30); } @Bean public AuthenticationProvider daoAuhthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); return daoAuthenticationProvider; } @Autowired public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuhthenticationProvider()) .userDetailsService(userDetailsService); } }
public static final String REMEMBERME_KEY = "ksbysample-webapp-lending";
を追加します。.key("ksbysample-webapp-lending")
→.key(REMEMBERME_KEY)
へ変更します。
LendingUserDetailsService.java
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfo userInfo = userInfoDao.selectByMailAddress(username); if (userInfo == null) { throw new UsernameNotFoundException( messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound" , null, LocaleContextHolder.getLocale())); } Set<SimpleGrantedAuthority> authorities = new HashSet<>(); List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId()); if (userRoleList != null) { authorities.addAll( userRoleList.stream() .map(userRole -> new SimpleGrantedAuthority(userRole.getRole())) .collect(Collectors.toList())); } return new LendingUserDetails(userInfo, authorities); }
messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound", new Object[]{}, LocaleContextHolder.getLocale()));
の第2引数をnew Object[]{}
→null
へ変更します。userRoleList.stream().map(userRole -> new SimpleGrantedAuthority(userRole.getRole()))
で.map(...)
の前で改行します。
履歴
2015/08/04
初版発行。