LoginSignup
32
31

More than 5 years have passed since last update.

Spring SecurityのhasRole(role)に要注意

Posted at

はじめに

前回の記事の続きで、ユーザの情報をデータベースなどから取得する方法を実装しようと考えました。データベースから直接データを呼び出す、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());
    }
32
31
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
31