Spring Securityの追加ログインフィールド
1. 前書き
この記事では、Spring Security xadding an extra field to the standard login formを使用したカスタム認証シナリオを実装します。
フレームワークの多様性とそれを使用できる柔軟な方法を示すために、2 different approachesに焦点を当てます。
Our first approachは、既存のコアSpringSecurity実装の再利用に焦点を当てたシンプルなソリューションになります。
Our second approachは、よりカスタムなソリューションであり、高度なユースケースにより適している可能性があります。
previous articles on Spring Security loginで説明されている概念に基づいて構築します。
2. Mavenセットアップ
Spring Bootスターターを使用してプロジェクトをブートストラップし、必要なすべての依存関係を取り込みます。
使用するセットアップには、親宣言、Webスターター、およびセキュリティスターターが必要です。 thymeleafも含まれます:
org.springframework.boot
spring-boot-starter-parent
2.0.0.M7
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.thymeleaf.extras
thymeleaf-extras-springsecurity4
Spring Bootセキュリティスターターの最新バージョンはover at Maven Centralにあります。
3. 簡単なプロジェクト設定
最初のアプローチでは、SpringSecurityが提供する実装の再利用に焦点を当てます。 特に、DaoAuthenticationProviderとUsernamePasswordTokenは「すぐに使用できる」状態で存在するため、再利用します。
主なコンポーネントは次のとおりです。
-
SimpleAuthenticationFilter –はUsernamePasswordAuthenticationFilterの拡張です
-
SimpleUserDetailsService –UserDetailsServiceの実装
-
User –は、Spring Securityによって提供されるUserクラスの拡張であり、追加のdomainフィールドを宣言します。
-
SecurityConfig –は、SimpleAuthenticationFilterをフィルターチェーンに挿入し、セキュリティルールを宣言し、依存関係を結び付けるSpringSecurity構成です。
-
login.html–は、username、password、およびdomainを収集するログインページです。
3.1. シンプルな認証フィルター
SimpleAuthenticationFilterでは、the domain and username fields are extracted from the request。 これらの値を連結し、それらを使用してUsernamePasswordAuthenticationTokenのインスタンスを作成します。
認証用のThe token is then passed along to the AuthenticationProvider:
public class SimpleAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
UsernamePasswordAuthenticationToken authRequest
= getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager()
.authenticate(authRequest);
}
private UsernamePasswordAuthenticationToken getAuthRequest(
HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
String usernameDomain = String.format("%s%s%s", username.trim(),
String.valueOf(Character.LINE_SEPARATOR), domain);
return new UsernamePasswordAuthenticationToken(
usernameDomain, password);
}
// other methods
}
3.2. 単純なUserDetailsサービス
UserDetailsServiceコントラクトは、loadUserByUsername.Our implementation extracts the username and domain.と呼ばれる単一のメソッドを定義します。次に、値がUserRepositoryに渡され、Userが取得されます。
public class SimpleUserDetailsService implements UserDetailsService {
// ...
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String[] usernameAndDomain = StringUtils.split(
username, String.valueOf(Character.LINE_SEPARATOR));
if (usernameAndDomain == null || usernameAndDomain.length != 2) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
usernameAndDomain[0], usernameAndDomain[1]));
}
return user;
}
}
3.3. Spring Securityの構成
addFilterBeforeを呼び出すwe insert our SimpleAuthenticationFilter into the filter chain before the defaultのため、セットアップは標準のSpringSecurity構成とは異なります。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").authenticated()
.and()
.formLogin().loginPage("/login")
.and()
.logout()
.logoutUrl("/logout");
}
SimpleUserDetailsServiceで構成しているため、提供されているDaoAuthenticationProviderを使用できます。 our SimpleUserDetailsService knows how to parse out our username and domain fieldsを思い出して、認証時に使用する適切なUserを返します。
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
SimpleAuthenticationFilterを使用しているため、失敗したログイン試行が適切に処理されるように、独自のAuthenticationFailureHandlerを構成します。
public SimpleAuthenticationFilter authenticationFilter() throws Exception {
SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
3.4. ログインページ
使用するログインページは、SimpleAuthenticationFilter:によって抽出される追加のdomainフィールドを収集します
アプリケーションを実行してhttp://localhost:8081のコンテキストにアクセスすると、保護されたページにアクセスするためのリンクが表示されます。 リンクをクリックすると、ログインページが表示されます。 予想どおり、we see the additional domain field:
3.5. 概要
最初の例では、ユーザー名フィールドを「偽造」することで、DaoAuthenticationProviderとUsernamePasswordAuthenticationTokenを再利用できました。
その結果、add support for an extra login field with a minimal amount of configuration and additional codeを実行できました。
4. カスタムプロジェクトのセットアップ
2番目のアプローチは最初のアプローチに非常に似ていますが、重要なユースケースにより適している場合があります。
2番目のアプローチの主要なコンポーネントは次のとおりです。
-
CustomAuthenticationFilter –はUsernamePasswordAuthenticationFilterの拡張です
-
CustomUserDetailsService –loadUserbyUsernameAndDomainメソッドを宣言するカスタムインターフェイス
-
CustomUserDetailsServiceImpl –はCustomUserDetailsServiceの実装です
-
CustomUserDetailsAuthenticationProvider –はAbstractUserDetailsAuthenticationProviderの拡張です
-
CustomAuthenticationToken –はUsernamePasswordAuthenticationTokenの拡張です
-
User –は、Spring Securityによって提供されるUserクラスの拡張であり、追加のdomainフィールドを宣言します。
-
SecurityConfig –は、CustomAuthenticationFilterをフィルターチェーンに挿入し、セキュリティルールを宣言し、依存関係を結び付けるSpringSecurity構成です。
-
login.html–username、password、およびdomainを収集するログインページ
4.1. カスタム認証フィルター
CustomAuthenticationFilterでは、extract the username, password, and domain fields from the requestです。 これらの値は、認証のためにAuthenticationProviderに渡されるCustomAuthenticationTokenのインスタンスを作成するために使用されます。
public class CustomAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain";
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
CustomAuthenticationToken authRequest = getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
return new CustomAuthenticationToken(username, password, domain);
}
4.2. カスタムUserDetailsサービス
CustomUserDetailsServiceコントラクトは、loadUserByUsernameAndDomain.と呼ばれる単一のメソッドを定義します
作成するCustomUserDetailsServiceImplクラスは、コントラクトを実装し、CustomUserRepositoryに委任して、Userを取得します。
public UserDetails loadUserByUsernameAndDomain(String username, String domain)
throws UsernameNotFoundException {
if (StringUtils.isAnyBlank(username, domain)) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(username, domain);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
username, domain));
}
return user;
}
4.3. カスタムUserDetailsAuthenticationProvider
CustomUserDetailsAuthenticationProviderはAbstractUserDetailsAuthenticationProviderを拡張し、CustomUserDetailServiceに委任してUserを取得します。 The most important feature of this class is the implementation of the retrieveUser method。
カスタムフィールドにアクセスするには、認証トークンをCustomAuthenticationTokenにキャストする必要があることに注意してください。
@Override
protected UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
UserDetails loadedUser;
try {
loadedUser = this.userDetailsService
.loadUserByUsernameAndDomain(auth.getPrincipal()
.toString(), auth.getDomain());
} catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials()
.toString();
passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
}
throw notFound;
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
// ...
return loadedUser;
}
4.4. 概要
2番目のアプローチは、最初に示した単純なアプローチとほぼ同じです。 独自のAuthenticationProviderとCustomAuthenticationTokenを実装することで、ユーザー名フィールドをカスタム解析ロジックに適合させる必要がなくなりました。
5. 結論
この記事では、追加のログインフィールドを使用するフォームログインをSpringSecurityに実装しました。 これは2つの異なる方法で行いました。
-
単純なアプローチでは、記述する必要のあるコードの量を最小限に抑えました。 カスタム解析ロジックでreuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the usernameを実行できました
-
よりカスタマイズされたアプローチでは、extending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationTokenによるカスタムフィールドサポートを提供しました
いつものように、すべてのソースコードはover on GitHubで見つけることができます。