はじめに
前回の記事の続きで、ユーザの情報をデータベースなどから取得する方法を実装しようと考えました。データベースから直接データを呼び出す、jdbcAuthentication()というメソッドもあるのですが、ユーザ認証のためにユーザ情報を取得するUserDetailsServiceを使った実装を行おうと思います。
そこで思わぬ罠にはまってしまったので共有させて頂きます。
SecurityConfigの変更
前回のinMemoryAuthentication()を仕様している部分をUserDetailsServiceを設定するように変更します。userDetailsServiceは@Autowiredで自動的にインスタンス化されます。(もちろんそのために他でクラスを定義しておく必要があります)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
/*auth.inMemoryAuthentication()
.withUser("user").password("password").roles(Role.USER.name())
.and()
.withUser("admin").password("admin").roles(Role.USER.name(), Role.ADMIN.name());*/
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin").hasRole(Role.ADMIN.name())
.antMatchers("/").hasRole(Role.USER.name())
.and()
.formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
}
}
#UserDetailsServiceの実現
UserDetailsServiceの実現には、loadUserByUsername(String username)メソッドを実現する必要があります。つまり、ユーザネームを元にユーザ情報を返す必要があるということです。ユーザ情報はUserDetailsで返されます。
UserDetailsインターフェースは以下のようになります
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
...
全てを実現したクラスを作成してそれをloadUserByUsernameで返しても良いのですが、UserDetailsインターフェースを実現したUserクラスが利用できます。今回はLoginUserというUserを継承したクラスを作成します。UserProfileはこのアプリケーション独自のユーザーの詳細情報と見て下さい。
public class LoginUser extends User {
private static final long serialVersionUID = 5039583223116504886L;
private UserProfile userProfile;
public LoginUser(UserProfile userProfile, String[] roles) {
super(userProfile.getUserName(), userProfile.getPassword(),
AuthorityUtils.createAuthorityList(roles));
this.userProfile = userProfile;
}
public UserProfile getUserProfile() {
return userProfile;
}
}
public class UserProfile {
private String username;
private String password;
private Role[] roles;
private String description;
public UserProfile(String username, String password, Role[] roles, String description) {
this.username = username;
this.password = password;
this.roles = roles;
this.description = description;
}
public String getUserName() {
return username;
}
public String getPassword() {
return password;
}
public Role[] getRoles() {
return roles;
}
public String getDescription() {
return description;
}
}
#hasRole(role)に注意!!
LoginUserクラスには、ロールを文字列で指定します。一見以下で良さそうです。
@Override
public UserDetails loadUserByUsername(String username) {
. . . .
String[] roles = { Role.USER.name() };
return new LoginUser(profile, roles);
}
しかしこれでは動きません。HttpSecurityでは.antMatchers("/").hasRole(Role.USER.name())と
設定しているのに何故駄目なのでしょうか?
これは、hasRole()で指定した場合、"ROLE_"が自動的に付加されるためです。これに気づくまでかなり時間かかりました。。。以下のサイトでも ”分かるかいこんなん(泣)。” と書かれています。
ROLE_を付加すれば無事動きます
@Override
public UserDetails loadUserByUsername(String username) {
. . . .
String[] roles = { "ROLE_USER" };
return new LoginUser(profile, roles);
}
ログイン後にUserDetailsの取得
ログイン後にUserDetailsの取得は、@AuthenticaationPrincipalを使うことで便利に取得できます。
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView showHomePage(@AuthenticationPrincipal LoginUser user) {
return new ModelAndView("index", "userProfile", user.getUserProfile());
}
これは、以下の同じ意味になります
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView showHomePage() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser user = (LoginUser) authentication.getPrincipal();
return new ModelAndView("index", "userProfile", user.getUserProfile());
}