Spring Bootで(というかSpring Securityのような気もするが)
ユーザ認証を実装する方法について。
とりあえずハードコードで、という方法は見つかるのだが
という場合の方法が見つからず苦労したので記録しておく。
以下の作業が必要。
WebSecurityConfigurerAdapterを継承して設定するUserDetailsServiceを継承して認証ロジックをカスタマイズするbuild.gradleのdependenciesにspring-boot-starter-securityを追加する。
(バージョンは適切なものを選ぶ)
dependencies {
    :
    compile 'org.springframework.boot:spring-boot-starter-security:1.2.2.RELEASE'
}
ルートパッケージに以下のようなクラスを作成する。
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/css/**", "/webjars/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // Required to use GET method for logout
                .permitAll();
    }
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService)
            .passwordEncoder(new StandardPasswordEncoder());
    }
}
ポイント:
UserDetailsServiceImplクラスはこれから作成するクラス(後述)。.antMatchers("/", "/css/**", "/webjars/**")の部分は認証なしでアクセスできるパスの指定。必要に応じて変更する必要あり。.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))の指定は、ログアウトのリンクをformでなくアンカー(GET)で作る場合に必要。これがないと、/logoutにアクセスした時にCSRFトークンのチェックに引っ掛かる。
.passwordEncoder(new StandardPasswordEncoder())の指定は、パスワードの暗号化方法を指定している。
デフォルトではログインに使用するusernameだけしかセッションに保管されないのだが、実際のuserテーブルと関連を持つテーブルはuser.idなどで参照させると思う。
これを取得するためにいちいちSELECTするのもおかしな話なので認証とともにUserクラスをセッションに持たせるようにしたい。
つまり、以下のようにコントローラのアクションでPrincipalパラメータを持たせたら
@RequestMapping
public String index(Principal principal, TodoForm form, Model model) {
そこからユーザ情報が取り出せるようにしたい。
Authentication authentication = (Authentication) principal;
User user = (User) authentication.getPrincipal();
model.addAttribute("allTodos", todoService.findAll(user.getId()));
そこで、既に名前だけ登場しているUserDetailsServiceImplをserviceパッケージ内に作る。
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        User user = null;
        if (null == username || "".equals(username)) {
            throw new UsernameNotFoundException("Username is empty");
        } else {
            User domainUser = userRepository.findByUsername(username);
            if (domainUser == null) {
                throw new UsernameNotFoundException("User not found for name: " + username);
            } else {
                List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                if (domainUser.getRoles() != null) {
                    for (Role role : domainUser.getRoles()) {
                        authorities.add(
                                new SimpleGrantedAuthority(role.getName()));
                    }
                }
                user = new User(username,
                        domainUser.getPassword(),
                        domainUser.isEnabled(),
                        true, true, true, authorities);
                user.setId(domainUser.getId());
                user.setCreatedAt(domainUser.getCreatedAt());
                user.setUpdatedAt(domainUser.getUpdatedAt());
                user.setRoles(domainUser.getRoles());
            }
        }
        return user;
    }
}
ここで登場するUserクラスはプロジェクト内で定義しているクラスだが、org.springframework.security.core.userdetails.Userを継承しているのがポイント。
@Entity
public class User extends org.springframework.security.core.userdetails.User {
    @Id
    @GeneratedValue
    private Long id;
    @Column(nullable = false, unique = true)
    private String username;
    @Column(nullable = false)
    private String password;
    private boolean enabled;
    @Column(nullable = false)
    private Long createdAt;
    @Column(nullable = false)
    private Long updatedAt;
    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(name = "role_user", joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles;
    public User() {
        super("INVALID", "INVALID", false, false, false, false, new ArrayList<GrantedAuthority>());
    }
    public User(String username, String password, boolean enabled, boolean accountNonExpired,
                boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        setUsername(username);
        setPassword(password);
        setEnabled(enabled);
    }
    // getter/setterは省略
}