【Spring Security】DBによる認証
はじめに
過去記事でインメモリによる認証を用いて、Spring Secutityの認証の流れなどを説明しています。 この記事ではインメモリをDBに置き換えた場合の例を示すのみで、認証の仕組み等には詳しく触れません。 仕組みを知りたい方は過去記事を参照してください。
またDBはMySQL、DB処理にはSpring Data JPAを使用します。 こちらも詳しくは触れませんので、こちらなどを参照してください。
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
...
}
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: user
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
今回データとして使用するのは、以下の user テーブルになります。 使用しているDBにテーブルの作成とデータ登録をしておいてください。
- テーブル定義(user)
項目名 型 PK 備考 id INT ◯ 自動インクリメント username varchar ユニーク password varchar BCryptでハッシュ化 role varchar ADMIN, USERのどちらかを設定 email varchar - 登録データ
id username password role email 1 admin $2a$08$h0qbchHHG86Yd6xb8VImjuuaIAnfls7.9RLwcxJpVYfnsEiHDXTZG ADMIN admin@example.com 2 user $2a$08$h0qbchHHG86Yd6xb8VImjuuaIAnfls7.9RLwcxJpVYfnsEiHDXTZG USER user@example.com passwordはどちらも
"password"
をBCryptでハッシュ化した値
DB認証
UserDetails
ユーザー情報としてUserDetails
インターフェースを実装するUser
クラスを作成します。
今回はSpring Data JPAを使用するため、Entityとして実装していきます。
@Entity
@Table(name = "user")
public class User implements UserDetails {
@Id private int id;
private String username;
private String password;
private String email;
private String role;
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
return authorities;
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return true;
}
public boolean isAccountNonLocked() {
return true;
}
}
今回は、権限の判定をRoleで行う想定のため、getAuthorities()
でROLE_XXX
となるように値を返しています。
RoleとAuthorityの違いについては過去記事を参照してください。
isAccountNonExpired()
などのユーザーの有効性を判定するための項目は、今回使用しないため明示的にtrue
を返しています。
必要な場合は、テーブルに該当項目を追加して値を返してください。
UserDetailsService
次にUserDetailsService
を実装するUserService
クラスを作成します。
UserDetailsService
の役割は、username
からユーザー情報(User
)を取得することです。
今回は user テーブルからデータを取得する必要があるため、事前に以下のようなUserRepository
を作成しておきます。
@Repository
public interface UserRepository extends JpaRepository<User, Integer>{
public Optional<User> findByUsername(String username);
}
public class UserService implements UserDetailsService {
@Autowired private UserRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = repository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException(username + " not found."));
return user;
}
}
Config
最後にConfigとして認証の設定を行います。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService users() {
return new UserService();
}
@Bean
public SecurityFilterChain secutiryFilterChain(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/fuga").hasRole("USER")
.requestMatchers("/piyo").hasRole("ADMIN")
.anyRequest().authenticated());
return http.build();
}
}
今回パスワードはBCryptでハッシュ化するため、PasswordEncoder
にはBCryptPasswordEncoder
を使用します。
ユーザーの参照部分には先ほど定義したUserService
を使用します。
動作確認用に、securityFilterChain()
で定義したパスの処理を作成します。
@RestController
public class TestController {
@GetMapping("hoge")
public String getHoge() {
return "hoge";
}
@GetMapping("fuga")
public String getFuga() {
return "fuga";
}
@GetMapping("piyo")
public String getPiyo() {
return "piyo";
}
}
今回認証としてBasic認証を採用しているため、リクエスト送信時にAuthorization
ヘッダーにusername:password
をBASE64でエンコードした値を設定します
(パスワードは平文)。
Authorization: Basic YWRtaW46cGFzc3dvcmQ=
結果として以下のHTTPステータスが返ってくることを確認します。
admin | user | |
---|---|---|
/hoge | 200 | 200 |
/fuga | 403 | 200 |
/piyo | 200 | 403 |
前述していますが、基本的には過去記事のインメモリの認証からユーザー参照の部分をDBに置き換えただけです。
UserDetails
やUserDetailsService
の役割がわかっていれば、それほど難しくはないと思います。
JdbcUserDetailsManager
上記ではUserDetailsService
を独自に実装しましたが、あらかじめ実装されているJdbcUserDetailsManager
を使用することができます。
ただし、JdbcUserDetailsManager(JdbcDaoImpl)
の実装にデータ設計を合わせる必要があります。
というのも、あらかじめ使用するSQLが内部で定義されています。
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, GroupManager {
//...
@Override
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username);
}
//...
}
public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware {
//...
public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled "
+ "from users "
+ "where username = ?";
//...
public JdbcDaoImpl() {
this.usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;
this.authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;
this.groupAuthoritiesByUsernameQuery = DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY;
}
//...
public String getUsersByUsernameQuery() {
return this.usersByUsernameQuery;
}
//...
}
上記の通り、JdbcUserDetailsManager
のloadUsersByUsername()
では、JdbcDaoImpl
のgetUsersByUsernameQuery()
で取得したSQLを実行します。
これを辿ると、JdbcDaoImpl
のDEF_USERS_BY_USERNAME_QUERY
で定義されたSQLであることがわかります。
JdbcUserDetailsManager
及びJdbcDaoImpl
には他にもユーザー操作に関する様々なSQLが内部で定義されています。
実例等は割愛しますが、使用する場合はこのSQLに従ってデータ構造を作成する必要があります。
おそらく使用頻度はそれほど高くないとは思います。
JdbcUserDetailsManager
を使用する場合は、以下のようにDatasource
をコンストラクタに渡します。
@Bean
public UserDetailsService userService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}