埼玉在住エンジニアのナレッジ帳

webのエンジニアをやっており、日頃の開発で詰まったことについて残していきたいと思っています。https://ikujip.jpの開発も行っているため、そこで使った知識なども載せられればと思います。

Spring Securityを使ったログイン機能 (3)ログイン処理の実装

ti-tomo-knowledge.hatenablog.com

にて、アカウント情報の登録まで行いましたが、
次は実際にログイン機能を実装してみます。
※認証失敗時など、細かい処理まで書くと長くなってしまうので、今回は認証が必ず成功するパターンで書きます。

WebSecurityConfigurerAdapterを継承しているSpring Sercurityの設定クラスの中で、
「HttpSecurity http」を引数に持つconfigureメソッドの中に以下を追加してください。

http.formLogin()
    .loginProcessingUrl("/login") // 認証処理を起動させるパス
    .loginPage("/loginForm") // ログインフォームのパス
    .failureUrl("/loginForm/?error") // ログイン処理失敗時の遷移先
    .defaultSuccessUrl("/") // 認証成功時の遷移先
    .usernameParameter("email").passwordParameter("password"); // ユーザ名(今回はメールアドレスだけど)とパラメータ
 
http.logout()
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout**")) // ログアウト処理を起動させるパス
    .logoutSuccessUrl("/"); // ログアウト完了時のパス

また、同一クラスの中で以下のAuthenticationConfigurationクラスも追加してください。

@Configuration
protected static class AuthenticationConfiguration
        extends GlobalAuthenticationConfigurerAdapter {
    @Autowired
    JpaUserDetailsServiceImpl userDetailsService;
 
    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(new BCryptPasswordEncoder());
    }
}

このクラスは認証処理時に自動で呼ばれるクラスです。
やっていることは入力されたパスワードに対してBCryptでハッシュ化し、
入力値が正しいか認証を行っています。

この中で呼ばれているJpaUserDetailsServiceImplクラスについてですが、
以下のようにUserDetailsServiceクラスを継承する形で定義しています。

package test.package;
 
import test.package.model.Account;
import test.package.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
 
@Component
public class JpaUserDetailsServiceImpl implements UserDetailsService {
 
    @Autowired
    private AccountRepository accountRepository;
 
    @Override
    public UserDetails loadUserByUsername(String email)
            throws UsernameNotFoundException {
 
        Account account = accountRepository.findByEmail(email);
        return account;
    }
}

findByEmailを実行するために、
AccountRepository内に下記を追加しておいてください。
Account findByEmail(String email);
さて、JpaUserDetailsServiceImplクラスのloadUserByUsernameでは、UserDetails形式で値を返すという宣言をしています。
よって通常のEntityクラスで値を返すことはできないので、accountRepositoryで取得するオブジェクト(Accountオブジェクト)は、UserDetails形式になるようにします。
実際の定義は以下のようになります。

package test.model;
 
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Tolerate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
import javax.persistence.*;
import java.util.Collection;
import java.util.Date;
 
@Entity
@Table(name = "accounts")
@Data
public class Account implements UserDetails {
    @Id
    @GeneratedValue
    private int id;
 
    @Column(nullable=false, unique = true)
    private String email;
 
    @Column(nullable=false, length=60)
    private String password;
 
    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#getAuthorities()
    */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
 
    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#getUsername()
    */
    @Override
    public String getUsername() {
        return this.email;
    }
 
    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#isAccountNonExpired()
    */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#isAccountNonLocked()
    */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
}

上記のようにEntityアノテーションを付けたクラスを定義し、UserDetailsインターフェースを継承しています。

UserDetailsはすでにSpring Security内のクラスですが、中身を見てみると以下のようになっています。

package org.springframework.security.core.userdetails;
 
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
 
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
 
    String getPassword();
 
    String getUsername();
 
    boolean isAccountNonExpired();
 
    boolean isAccountNonLocked();
 
    boolean isCredentialsNonExpired();
 
    boolean isEnabled();
}

Serializableを継承しており、7つの抽象メソッドが定義されています。
なので、このインターフェースを継承したEntityクラスで、抽象メソッドをオーバーライドして実装する必要があります。
とりあえずこれでログイン処理は上手くいったので進めてみます。