Spring Securityの設定方法についての備忘です。
※本記事は、Spring Security 5.7以前の書き方で行っています。5.7以降での書き方との差分はこちらの記事に記載しています。
構成
構成は下記のとおりです
※このあと記述するコードの部分のみを抜粋しております。
Spring Security の設定を行うConfigクラスの作成
✓ ポイント
-
WebSecurityConfigurerAdapter
クラスを継承してSpringSecurityの設定を行うクラスを自作する。 -
@EnableWebSecurity
アノテーションをクラスにつけることで、WebSecurity関連の設定であることを示す。 -
configure(HttpSecurity http)
のメソッドをオーバーライドして、URLごとにセキュリティを決める。
package com.example.config;
import文 省略
@EnableWebSecurity // WebSecurity関連の設定であること示すアノテーション
public class SecurityConfig extends WebSecurityConfigurerAdapter{ // WebSecurityConfigurerAdapterクラスを継承する
// config(HttpSecurity http)をオーバーライドして、主にURLごとに異なるセキュリティ設定を行う
@Override
protected void config(HttpSecurity http)throws Exception{
http.authorizeRequests()
// URLごとに、認証の要・不要の定義を行う
.antMatchers("/login/**", "/img/**", "/css/**", "/js/**").permitAll() // ログイン画面、imageフォルダ、cssフォルダ、jsフォルダは認証が不要である
.anyRequest().authenticated() //上記以外は認証が必要である
// ログイン関連の設定
.and()
.formLogin() //認証方式は、フォーム認証であること
.usernameParameter("name") // usernameのパラメータを独自定義(デフォルトは、username)
.passwordParameter("password") // passwordのパラメータを独自定義(デフォルトはpassword)
.loginProcessingUrl("/login").permitAll() // ここで指定したURLがリクエストされるとログイン認証の処理を行う
.loginPage("/login") // ログインページを定義(これで自作したページをログインページとして使用できる)
.failureUrl("/login?failed") // ログインに失敗したときのURL(デフォルトは?error)
.defaultSuccessUrl("/top") // ログインに成功したときのURL
// ログアウト関連の定義
.and()
.logout()
.logoutUrl("/logout") // ログアウトURLの定義
.logoutSuccessUrl("/login") // ログアウト成功後の画面(今回はログインページに遷移するよう定義)
;
}
}
entityの作成
User情報を格納しているテーブルに対応するentityクラスを作成。
ここでは、usersテーブルに対応するuserクラスを作成する。
package com.example.entity;
import文省略
public class User {
public String id;
public String name;
public String password;
public String authority;
getterやsetter等は省略
}
UserDetailsの作成
UserDetailsとは、ユーザー名、パスワード、権限などの情報を保持するもの
● Springが用意している標準の実装で問題ない場合
┗:Userクラスを継承してUserDetailsを作成する
● Springが用意している標準の実装では要件が満たせない場合
┗:UserDetailsインターフェイスを実装してUserDetailsを作成する
Userクラスを継承してUserDetailsを作成する
✓ ポイント
-
User(org.springframework.security.core.userdetails.User)
を継承することでUserDetailsの作成の際の記述を減らせる
package com.example.authentication;
// これを継承することで、UserDetails作成の際に記述量を減らせる
import org.springframework.security.core.userdetails.User;
その他inport文省略
public class CustomUserDetails extends User {
// username,password,authoritiesを引数に取るコンストラクタを用意
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
}
UserDetailsインターフェイスを実装してUserDetailsを作成する
✓ ポイント
-
UserDetails(org.springframework.security.core.userdetails.UserDetails)
を実装することでUserクラスを継承して作成するときよりも細やかなカスタマイズができる。
package com.example.authentication;
public class CustomUserDetails implements UserDetails {
// 先ほど作成したentity.Userをフィールドに持ってくる
private final User user;
public CustomUserDetails(User user) {
super();
this.user = user;
}
public User getUser() {
return user;
}
// 権限を返す
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.createAuthorityList("ROLE_" + user.getAuthority());
}
// passwordを返す
@Override
public String getPassword() {
return user.getPassword();
}
// nameを返す
@Override
public String getUsername() {
return user.getName();
}
// ユーザーのアカウントの有効期限が切れているかどうかを示す。今回は有効期限は設定しないので、デフォルトでtrueに設定。
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return true;
}
// ユーザーのアカウントがロックされているかかどうかを示す。今回はロックは設定しないので、デフォルトでtrueに設定。
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
}
// ユーザーの資格情報(パスワード)の有効期限が切れているかどうかを示す。今回は資格情報の有効期限は設定しないので、デフォルトでtrueに設定。
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
// ユーザーが有効か無効かを示す。今回は有効か無効かは設定しないので、デフォルトでtrueに設定。
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
@Override
public String toString() {
return "CustomUserDetails [user=" + user + "]";
}
}
UserDetailsServiceの作成
UserDetailsServiceとは、認証に必要な情報をキーにして取得するDAOである。
一番ポピュラー(?)なやり方は、UserDetailsService(org.springframework.security.core.userdetails.UserDetailsService)
インターフェイスを実装するやり方であるので、ここでもそのやり方を記載する。
✓ ポイント
-
UserDetailsService
インターフェイスのloadUserByUsername(String username)
を実装する。ここではusernameをクレデンシャルとし、DBにusernameで検索することで認証を行う。 - 認証に成功した場合は、先ほど作成した
UserDetails
クラスをnewしてreturnする。 - 認証に失敗した際は、
UsernameNotFoundException
を投げる。
package com.example.authentication;
@Service
public class CustomUserDetailsService implements UserDetailsService{
// DBにアクセスするリポジトリを作成する。コードは後述する。
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByName(username); // usernameをクレデンシャルとし、DBにusernameで検索する。
if(user == null) {
throw new UsernameNotFoundException("user not found");
}
CustomUserDetails customUserDetails = new CustomUserDetails(user); // 先ほど作成したUserDetailsクラスをnewしてreturnする
return customUserDetails;
}
}
✓ 追加ポイント
UserDetailsService
インターフェイスでは、認証に必要な情報を"名前"(username)として実装している。つまり、「usernameの情報だけに基づいて検索」するためのものとして存在している。しかし多くの場合は、パスワードもクレデンシャルとして検索したいだろう。そういう場合はUserDetailsService
インターフェイスを用いるのは適さなく、AuthenticationProvider
を直接実装する方がよい。
https://spring.pleiades.io/spring-security/site/docs/5.2.7.RELEASE/reference/html/overall-architecture.html
※UserDetailsServiceインターフェイスを用いらずに実装したものをこちら 【Spring Boot】UserDetailsService を使用せずに Spring Security で認証を行う。に記載しました。
※しかし、usernameが正、passwordが不正の場合、loadUserByUsername(String username)
を用いて実装していても、認証不可となってはじかれる。ではどこでパスワードの整合性をチェックしているのかというと、DaoAuthenticationProvider
クラスの
additionalAuthenticationChecks
メソッドにてチェック処理を行っているらしい。DaoAuthenticationProvider
がUserを取得後にチェック処理を行っているようだ。
※参考
UserDetailsServiceは誤解されている
UserDetailsServiceのloadUserByUsernameの存在意義がよくわからないです
Spring Bootで、ユーザ名とパスワードを指定した認証処理の実装方法
usernameでレコードを取得して、その後どのようにしてパスワードの整合性を見ているのか
DBにアクセスするリポジトリの作成
今回はMyBatisを用いて実装する。実装内容は、usernameに基づいて検索するだけの簡単なものである。
package com.example.repository;
import文省略
@Mapper
public interface UserRepository {
/**
* nameからuserを特定
* @return User
*/
public User findByName(String name);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.repository.UserRepository">
<select id="findByName" parameterType="String" resultType="com.example.entity.User">
SELECT id, name, password, authority
FROM users
WHERE name = #{name}
</select>
</mapper>
Spring Security の設定を行うConfigクラスにパスワードのエンコード方式を設定
先ほど作成したConfigクラスにパスワードのエンコード方式を追加で設定する。
✓ ポイント
-
configure(AuthenticationManagerBuilder auth)
のメソッドをオーバーライドして、エンコード方式を設定。
package com.example.config;
import文 省略
@EnableWebSecurity // WebSecurity関連の設定であること示すアノテーション
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void config(HttpSecurity http)throws Exception{
http.authorizeRequests()
// URLごとに、認証の要・不要の定義を行う
.antMatchers("/login/**", "/img/**", "/css/**", "/js/**").permitAll() // ログイン画面、imageフォルダ、cssフォルダ、jsフォルダは認証が不要である
.anyRequest().authenticated() //上記以外は認証が必要である
// ログイン関連の設定
.and()
.formLogin() //認証方式は、フォーム認証であること
.usernameParameter("name") // usernameのパラメータを独自定義(デフォルトは、username)
.passwordParameter("password") // passwordのパラメータを独自定義(デフォルトはpassword)
.loginPage("/login") // ログインページを定義(これで自作したページをログインページとして使用できる)
.failureUrl("/login?failed") // ログインに失敗したときのURL(デフォルトは?error)
.defaultSuccessUrl("/top") // ログインに成功したときのURL
// ログアウト関連の定義
.and()
.logout()
.logoutUrl("/logout") // ログアウトURLの定義
.logoutSuccessUrl("/login") // ログアウト成功後の画面(今回はログインページに遷移するよう定義)
;
}
// ▼ ここが追加した部分 ▼
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 今回は模擬なので、何もしないパスワードエンコーダーであるNoOpPasswordEncoderを用いる。
auth.userDetailsService(customUserDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
終わりに
Spring Securityは裏でどういう仕組みで動いているのかわかりづらいものだと思うので、インプットした内容を備忘のためにここに記載しました。
上記の実装は5.7以降の書き方ではありません。5.7以降での書き方との差分はこちらに掲載しております。
間違っている部分もあるかと思いますので、その際はご指摘頂ければと思います。