SpringSecurity

UserDetailsServiceについての個人的なまとめ
◆UserDetailsServiceインターフェースのloadUserByUsernameメソッドをオーバーライドすることでDB認証が行える
→ ユーザー情報を取得するインターフェース。このインターフェースはDaoAuthenticationProviderによって利用されるユーザーDAOです。
◆一方で以下のような方法もある
UsernamePasswordAuthenticationFilterを継承して、AuthenticationProviderの実装クラスを作成した場合はAuthenticationProviderの実装クラスにてユーザ取得処理を実装する。
または、こんな感じでfileterからデフォルトのauthenticationManagerを利用する
// 認証の処理
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
// requestパラメータからユーザ情報を読み取る
LoginForm userForm = new ObjectMapper().readValue(req.getInputStream(), LoginForm.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
userForm.getUsername(),
userForm.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
AuthenticationManagerの実装クラス
(1)で認証処理を呼出す
static final class AuthenticationManagerDelegator implements AuthenticationManager {
private AuthenticationManagerBuilder delegateBuilder;
private AuthenticationManager delegate;
private final Object delegateMonitor = new Object();
AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder) {
Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
this.delegateBuilder = delegateBuilder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (this.delegate != null) {
(1) return this.delegate.authenticate(authentication);
}
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
this.delegate = this.delegateBuilder.getObject();
this.delegateBuilder = null;
}
}
return this.delegate.authenticate(authentication);
}
@Override
public String toString() {
return "AuthenticationManagerDelegator [delegate=" + this.delegate + "]";
}
}
AuthenticationManager(delegate)の実装クラスとなる。
(1)AuthenticationManager(delegate)の実装クラスではAuthenticationProvider#authenticateを呼出す
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private List<AuthenticationProvider> providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private AuthenticationManager parent;
省略
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
(1) result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
Providerの基底クラスでは以下のように呼び出される
(1)で自身のretrieveUserを呼出してuserを取得しているが、ここは実装クラス(DaoAuthenticationProvider)の処理が走る
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
(1) user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
DaoAuthenticationProviderではUserDetailsServiceのloadUserByUsernameメソッドを呼び出している。
最初の「UserDetailsServiceインターフェースのloadUserByUsernameメソッドをオーバーライドすることでDB認証が行える」でオーバーライドしているのはココのメソッド
(1)UserDetailsService#loadUserByUsernameの呼出し
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
(1) UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}

FilterがAuthenticationManagerを呼出すと、Providerの処理が呼び出される

UsernamePasswordAuthenticationToken
AbstractAuthenticationTokenを継承したAuthenticationの実装クラスです。
-
UsernamePasswordAuthenticationToken
- final Object principal
- Object credentials
-
AbstractAuthenticationToken
- final Collection<GrantedAuthority> authorities
- Object details
- boolean authenticated
UsernamePasswordAuthenticationFilter#attemptAuthentication
一度、リクエストのusernameとpasswordからUsernamePasswordAuthenticationTokenを作成して、
AuthenticationManagerに渡している。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
Providerでは以下のようにして返却する
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
結果の作成
return createSuccessAuthentication(principalToReturn, authentication, user);
・user = userの情報を取得する
→ ここはUserDetailsServiceのloadUserByUsernameの結果
・principalToReturnはuserの参照
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
Object principalToReturn = user;
・authenticationは引数の値なのでfilterで作成したやつ
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
最初はリクエストの情報をUsernamePasswordAuthenticationTokenとして作成し
今度はdbから取得した情報をUsernamePasswordAuthenticationTokenとして作成して
返却する

Providerのauthenticate
usernameはただゲットしているだけ
String username = determineUsername(authentication);
.....
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}
実装クラスにてuser取得
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
実装クラス
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
→ユーザー名のみ

デフォルトのProviderを利用した場合のチェック処理は以下で行われている
DaoAuthenticationProvider#additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}

WebSecurityConfigurerAdapterが非推奨になった

新しい書き方

Bean登録して使えるようになる
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
}

- The WebSecurityCustomizer is a callback interface that can be used to customize WebSecurity.
@Configuration
public class SecurityConfiguration {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
}
}
- WARNING: If you are configuring WebSecurity to ignore requests, consider using permitAll via HttpSecurity#authorizeHttpRequests instead. See the configure Javadoc for additional details.

インメモリ認証
まずコードを書きます
package com.volkruss.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
// ログイン後は/homeに遷移させる
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().defaultSuccessUrl("/home");
return http.build();
}
// misaka/mikotoでログインする
@Bean
public InMemoryUserDetailsManager userDetailsService(){
UserDetails user = User.withUsername("misaka")
.password(
PasswordEncoderFactories
.createDelegatingPasswordEncoder()
.encode("mikoto"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

2.6
/**
* Defines a filter chain which is capable of being matched against an
* {@code HttpServletRequest}. in order to decide whether it applies to that request.
* <p>
* Used to configure a {@code FilterChainProxy}.
*
* @author Luke Taylor
* @since 3.1
*/
public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
List<Filter> getFilters();
}
...
private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();
...
/**
* Creates the Spring Security Filter Chain
* @return the {@link Filter} that represents the security filter chain
* @throws Exception
*/
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
boolean hasFilterChain = !this.securityFilterChains.isEmpty();
Assert.state(!(hasConfigurers && hasFilterChain),
"Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.");
if (!hasConfigurers && !hasFilterChain) {
WebSecurityConfigurerAdapter adapter = this.objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
for (Filter filter : securityFilterChain.getFilters()) {
if (filter instanceof FilterSecurityInterceptor) {
this.webSecurity.securityInterceptor((FilterSecurityInterceptor) filter);
break;
}
}
}
for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
customizer.customize(this.webSecurity);
}
return this.webSecurity.build();
}
....
@Autowired(required = false)
void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
this.securityFilterChains = securityFilterChains;
}
....

SecurityFilterChain
[FilterChainProxy] → [SecurityFilterChain]
「springSecurityFilterChain」というのはDIコンテナの中に登録される、SpringSecurity用のFilterクラスで、Bean定義ファイルにhttpタグを定義すると自動的に設定される。
そのため、Bean定義ではFilterの名前をspringSecurityFilterChainにする必要がある。
なぜならば、DIコンテナに登録されるspringSecurityFilterChainという名前と同じにする必要があるから
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- filter-name/filter-class のペアについては、通常のサーブレットクラスの登録と同様
- filter-class
- フィルタを実装したクラス名
springSecurityFilterChainがurl-patternによって、全てのリクエストに対して適応される。
適応されるspringSecurityFilterChainはorg.springframework.web.filter.DelegatingFilterProxyです。
DelegatingFilterProxy
Filter インターフェースを実装する Spring 管理の Bean に委譲する標準サーブレットフィルターのプロキシ。web.xml の "targetBeanName" フィルター init-param をサポートし、Spring アプリケーションコンテキストでターゲット Bean の名前を指定します。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
委譲先のBeanを取得している
ここで取り出されるのがFilterChainProxyらしい
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
filter-nameの取得っぽい
@Nullable
protected String getFilterName() {
return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName);
}
FilterChainProxy は、 "springSecurityFilterChain" という Bean 名でコンテナに登録される。
このクラスは Spring Security の処理の入り口となる。
"標準サーブレットフィルターのプロキシ"と書てあるのは、web.xmlにfileterとして登録するからか。。
標準サーブレットフィルターとして動いて、springSecurityFilterChain(SpringのDIコンテナにある)を取得する→FilterChainProxyを取得。
FilterChainProxyがSecurityFilterChainを走査して呼出す
神記事

web.xmlにfileter-nameとfilter-classに必ずspringSecurityFilterChainという名前でDelegatingFilterProxyを登録する
DelegatingFilterProxyはDIコンテナからspringSecurityFilterChainという名前のBean(FilterChainProxy)を取得してくる
FilterChainProxyはSecurityFilterChainのリストを持ってて、マッチしたらdoFilterを呼ぶ

FilterChainProxy
Spring 管理のフィルター Bean のリストに Filter リクエストを委譲します。バージョン 2.0 以降、フィルターチェーンの内容を非常に細かく制御する必要がない限り、アプリケーションコンテキストで FilterChainProxy Bean を明示的に構成する必要はありません。ほとんどの場合、デフォルトの <security:http /> 名前空間構成オプションで適切にカバーされます。
FilterChainProxy は、アプリケーション web.xml ファイルに標準の Spring DelegatingFilterProxy 宣言を追加することにより、サーブレットコンテナーフィルターチェーンにリンクされます。
- Beanを明示的に書かなくても<security:http />でカバーされる
- DelegatingFilterProxyを適切に宣言すればリンクされる
FilterChainProxy は、設定されているフィルターで標準のフィルターライフサイクルメソッドを呼び出さないことに注意してください。他の Spring Bean の場合と同様に、Spring のアプリケーションコンテキストライフサイクルインターフェースを代替手段として使用することをお勧めします。
名前空間構成を使用して Web セキュリティを設定する方法を検討したとき、"springSecurityFilterChain" という名前の DelegatingFilterProxy を使用しました。これで、これがネームスペースによって作成された FilterChainProxy の名前であることがわかります。

SpringBootでは
Spring Bootは、簡単にSpring Securityを使えるようコンフィグレーションを定義してくれているため、spring-boot-security-starterパッケージを依存に追加するだけで、すぐに基本的な機能を利用可能になります。

WebSecurity は WebSecurityConfiguration によって作成され、Spring Security フィルターチェーン(springSecurityFilterChain)として知られる FilterChainProxy を作成します。springSecurityFilterChain は、DelegatingFilterProxy が委譲する Filter です。
WebSecurity のカスタマイズは、WebSecurityConfigurer の作成、WebSecurityConfigurerAdapter のオーバーライド、または WebSecurityCustomizer Bean の公開により行うことができます。

なんか、とても長くなってる・・・
/**
* Allows customization to the {@link WebSecurity}. In most instances users will use
* {@link EnableWebSecurity} and either create a {@link Configuration} that extends
* {@link WebSecurityConfigurerAdapter} or expose a {@link SecurityFilterChain} bean. Both
* will automatically be applied to the {@link WebSecurity} by the
* {@link EnableWebSecurity} annotation.
*
* @author Rob Winch
* @since 3.2
* @see WebSecurityConfigurerAdapter
* @see SecurityFilterChain
*/
public interface WebSecurityConfigurer<T extends SecurityBuilder<Filter>> extends SecurityConfigurer<Filter, T> {
}

じゃあSpringBootだとどうなっているのか
WebApplicationInitializer
ここでDelegatingFilterProxyをnewしている
private void insertSpringSecurityFilterChain(ServletContext servletContext) {
String filterName = DEFAULT_FILTER_NAME;
DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(filterName);
String contextAttribute = getWebApplicationContextAttribute();
if (contextAttribute != null) {
springSecurityFilterChain.setContextAttribute(contextAttribute);
}
registerFilter(servletContext, true, filterName, springSecurityFilterChain);
}
@EnableWebSecurityでインポートしているWebSecurityConfiguration.classを見ると
BeanとしてAbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAMEを登録していることがわかる
/**
* Creates the Spring Security Filter Chain
* @return the {@link Filter} that represents the security filter chain
* @throws Exception
*/
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
boolean hasFilterChain = !this.securityFilterChains.isEmpty();
Assert.state(!(hasConfigurers && hasFilterChain),
"Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.");
if (!hasConfigurers && !hasFilterChain) {
WebSecurityConfigurerAdapter adapter = this.objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
for (Filter filter : securityFilterChain.getFilters()) {
if (filter instanceof FilterSecurityInterceptor) {
this.webSecurity.securityInterceptor((FilterSecurityInterceptor) filter);
break;
}
}
}
for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
customizer.customize(this.webSecurity);
}
return this.webSecurity.build();
}

Configuring HttpSecurity
In Spring Security 5.4 we introduced the ability to configure HttpSecurity by creating a SecurityFilterChain bean.

◆UserDetails
- UserDetailsはAuthenticationのgetPrincipalから取得できる
- UserDetailsはユーザー名パスワード活動状態など定義している
- UserDetailsServiceを拡張することで、独自のUserDetailsを指定できる
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
実装クラス
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
UserDetailsオブジェクトを拡張する場合は extends Userとしてやるか、直接実装クラスを作るかどちらでも可能です

◆UserDetaialsSerivce
- UserDetailsオブジェクトの取得を目的としている
実装クラスとしては以下のようなものがある
- InMemoryUserDetailsManager
- UserDetailsManagerがextendsしている
- JdbcDaoImpl
解説にあるJDBC認証のJdbcUserDetailsManagerというのはJdbcDaoImplの拡張クラス
ユーザーの取得
/**
* Executes the SQL <tt>usersByUsernameQuery</tt> and returns a list of UserDetails
* objects. There should normally only be one matching user.
*/
@Override
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username);
}

サンプルで見た戻り値は、どちらもUserDetaialsSerivce
- InMemoryUserDetailsManager
- UserDetailsManager
上記にも書いたがJdbcUserDetailsManagerというのはJdbcDaoImplを継承しており、UserDetailsManagerの実装クラス

独自のUserDetailsを使いたい!
- JdbcDaoImplを継承して拡張クラスを作る
- UserDetailsServiceの実装クラスを作る
独自のUserDetailsを作りたい!
- Userクラスを拡張する
- UserDetailsを実装する
一例です

JDBC認証
- SpringBootでやってるので、普通にDataSourceをインジェクトすれば、application.yamlの定義が使える。
- レコードについては事前に登録している
- H2を使う場合は小文字にすることが大事
- DATABASE_TO_UPPER=false;
- 解説サイトで言ってるJDBC認証というのはデフォルトのUserDetaisの実装であるUserクラスが基準なのでフィールドも合わせる必要がある(テーブルのカラムとフィールド)
- 例えばenabledは必須項目
- 権限も検索されます
- パスワードはハッシュ化しておきます
小文字にしないと
enabledがテーブル定義にないと
権限テーブルがないと
パスワードのハッシュ化
既存のレコードの場合は
だから、そもそもusersテーブルなんか使いたくないよとか、定義変えたいよって場合UserDetailsServiceを実装する必要があります たぶん
usersテーブルも使いたくないし、カラム名も変えたい人は以下でも可能です
@Bean
public UserDetailsManager userDetailsService(){
String USERQUERY =
"select username,password,enabled from myusers where username = ?";
String AuthoritiesQuery =
"select username,role from myauthorities where username = ?";
JdbcUserDetailsManager users = new JdbcUserDetailsManager(this.dataSource);
// 独自のテーブルやカラム名を使いたい時
users.setUsersByUsernameQuery(USERQUERY);
users.setAuthoritiesByUsernameQuery(AuthoritiesQuery);
return users;
}

JdbcUserDetailsManager users = new JdbcUserDetailsManager(this.dataSource);
users.createUser(user); dbにインサートする処理
return users;
- JdbcUserDetailsManagerのcreateUserはdbにインサートします
- なので実装サンプルはコード上でUserDetailsを作成してデータベースに反映させている
@Override
public void createUser(final UserDetails user) {
validateUserDetails(user);
getJdbcTemplate().update(this.createUserSql, (ps) -> {
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setBoolean(3, user.isEnabled());
int paramCount = ps.getParameterMetaData().getParameterCount();
if (paramCount > 3) {
// NOTE: acc_locked, acc_expired and creds_expired are also to be inserted
ps.setBoolean(4, !user.isAccountNonLocked());
ps.setBoolean(5, !user.isAccountNonExpired());
ps.setBoolean(6, !user.isCredentialsNonExpired());
}
});
if (getEnableAuthorities()) {
insertUserAuthorities(user);
}
}
レコードが追加されている

JDBC認証を使う場合
users
authorities
テーブルが必要で、レコードがそれぞれに存在している必要があります
JdbcUserDetailsManagerクラスを見ればどんなSQLが流れるかわかります。
例えば以下
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, GroupManager {
public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
public static final String DEF_DELETE_USER_SQL = "delete from users where username = ?";
public static final String DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";
public static final String DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";
public static final String DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";
public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";
public static final String DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";
public static final String DEF_FIND_GROUPS_SQL = "select group_name from groups";
public static final String DEF_FIND_USERS_IN_GROUP_SQL = "select username from group_members gm, groups g "
+ "where gm.group_id = g.id and g.group_name = ?";
またパスワードはエンコードされている必要があります。
こんな感じでハッシュ化したパスワードをあらかじめテーブルのパスワードに設定しておけばログインできます

割とまとめた記事

-
Spring Security 5.7.0-M2 では、WebSecurityConfigurerAdapter を非推奨とする
-
コンポーネントベースのセキュリティ設定に移行
-
HttpSecurity#authorizeHttpRequestsというメソッドを使って認可ルールを定義
- ラムダを使うのがベストプラクティス
-
HttpSecurity#authorizeHttpRequestsを使用する理由
-
HttpSecurityをSecurityFilterChain@Beanを作成することで設定する
-
Spring Security 5.4では、WebSecurityCustomizerも導入された
@FunctionalInterface
public interface Customizer<T> {
/**
* Performs the customizations on the input argument.
* @param t the input argument
*/
void customize(T t);
/**
* Returns a {@link Customizer} that does not alter the input argument.
* @return a {@link Customizer} that does not alter the input argument.
*/
static <T> Customizer<T> withDefaults() {
return (t) -> {
};
}
}
- WebSecurityCustomizer は関数型インターフェースで、WebSecurityをカスタマイズするために使用する
- 今後はこのWebSecurityCustomizerを利用することが推奨される
- authorizeRequestsを使う代わりに、authorizeHttpRequestsを使う
- authorizeRequestsを改善している

JdbcUserDetailsManager
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
- AuthenticationManagerをインジェクションしている
- AuthenticationManagerはauthenticateメソッドを持った認証処理を担当している

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers(header -> {
header.frameOptions().disable();
});
http.authorizeHttpRequests(authorize -> {
authorize.antMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated();
});
http.formLogin(form -> {
form.defaultSuccessUrl("/home");
});
return http.build();
}
- 関数型インターフェースのWebSecurityCustomizerを使います
- HttpSecurity#authorizeHttpRequestsはとても改善されています

神

- SpringBootWebSecurityConfiguration
- SpringBoot用にSpringSecurityの設定を構築する
- HttpSecurity
- HttpSecurityはビルドクラス
- SecurityFilterChainを構築することが仕事
- FilterChainProxy
- FilterChainProxyはGenericFilterBeanである
- Spring IoCコンテナに注入されたSecurityFilterChainを全て管理する
- URLによって対応するSecurityFilterChainに処理を振り分ける
- /hoge/**はFilterA、/kamijo/**はFilterBのようなイメージ
- DelegatingFilterProxy
- DelegatingFilterProxyはSpringとServletを繋ぐ役割
SecurityFilterChainとは結局何か

上述のように様々にSpringBootWebSecurityConfigurationを利用することで様々なデフォルト設定をSpringBootに対して行っていました。
@BeanでSecurityFilterChainをすることで同じように設定できるようになった
例えばCSRF対策の設定を無効化した場合
@Bean(name="mySecurityFilterChain")
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
return http.build();
}
通常登録されるCsrfFilterというのが登録されなくなる
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
for (SecurityFilterChain chain : this.filterChains) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
this.filterChains.size()));
}
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
ここで入ってるfiltersが登録されなくなる
filters = {ArrayList@8032} size = 15
0 = {DisableEncodeUrlFilter@8052}
1 = {WebAsyncManagerIntegrationFilter@8053}
2 = {SecurityContextPersistenceFilter@8054}
3 = {HeaderWriterFilter@8055}
4 = {CsrfFilter@8056} // これがなくなる
5 = {LogoutFilter@8057}
6 = {UsernamePasswordAuthenticationFilter@8058}
7 = {DefaultLoginPageGeneratingFilter@8059}
8 = {DefaultLogoutPageGeneratingFilter@8060}
9 = {RequestCacheAwareFilter@8061}
10 = {SecurityContextHolderAwareRequestFilter@8062}
11 = {AnonymousAuthenticationFilter@8063}
12 = {SessionManagementFilter@8064}
13 = {ExceptionTranslationFilter@8065}
14 = {AuthorizationFilter@8066}

@Bean(name="mySecurityFilterChain")
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.build();
}
// ただBeanたけbuildした
0 = {DisableEncodeUrlFilter@8020}
1 = {WebAsyncManagerIntegrationFilter@8021}
2 = {SecurityContextPersistenceFilter@8022}
3 = {HeaderWriterFilter@8023}
4 = {CsrfFilter@8024}
5 = {LogoutFilter@8025}
6 = {RequestCacheAwareFilter@8026}
7 = {SecurityContextHolderAwareRequestFilter@8027}
8 = {AnonymousAuthenticationFilter@8028}
9 = {SessionManagementFilter@8029}
10 = {ExceptionTranslationFilter@8030}
色々設定
// ログイン後は/homeに遷移させる
@Bean(name="mySecurityFilterChain")
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers(header -> {
header.frameOptions().disable();
});
http.authorizeHttpRequests(authorize -> {
authorize.antMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated();
});
http.formLogin(form -> {
form.defaultSuccessUrl("/home");
});
return http.build();
}
filters = {ArrayList@8032} size = 14
0 = {DisableEncodeUrlFilter@8052}
1 = {WebAsyncManagerIntegrationFilter@8053}
2 = {SecurityContextPersistenceFilter@8054}
3 = {HeaderWriterFilter@8055}
4 = {LogoutFilter@8056}
5 = {UsernamePasswordAuthenticationFilter@8057}
6 = {DefaultLoginPageGeneratingFilter@8058}
7 = {DefaultLogoutPageGeneratingFilter@8059}
8 = {RequestCacheAwareFilter@8060}
9 = {SecurityContextHolderAwareRequestFilter@8061}
10 = {AnonymousAuthenticationFilter@8062}
11 = {SessionManagementFilter@8063}
12 = {ExceptionTranslationFilter@8064}
13 = {AuthorizationFilter@8065}
例えばログイン時にUsernamePasswordAuthenticationFilterが走るのは有名な話ですが
/loginのPOSTメソッドの時にマッチするようになっていることがわかります

SecurityFilterChainとは結局何か

認証オブジェクト取得
private Authentication getAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
}

まとめ

リクエストの認証是非
以下は落書きです
保持している対象
チェック処理
- forで走査
- AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
- Entryを取得
AuthorizationManager
◆未ログイン状態の時、AccessDeniedException例外を投げている
requestの書き換え
前
後
プロキシに戻った時
変更箇所
LoginUrlAuthenticationEntryPoint.java
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if (!this.useForward) {
// redirect to login page. Use https if forceHttps true
String redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
return;
}
String redirectUrl = null;
if (this.forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS. When that request is received,
// the forward to the login page will be used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl != null) {
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
return;
}
String loginForm = determineUrlToUseForThisRequest(request, response, authException);
logger.debug(LogMessage.format("Server side forward to: %s", loginForm));
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
buildRedirectUrlToLoginPageメソッド詳細
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) {
String loginForm = determineUrlToUseForThisRequest(request, response, authException);
if (UrlUtils.isAbsoluteUrl(loginForm)) {
return loginForm;
}
int serverPort = this.portResolver.getServerPort(request);
String scheme = request.getScheme();
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(loginForm);
if (this.forceHttps && "http".equals(scheme)) {
Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort);
if (httpsPort != null) {
// Overwrite scheme and port in the redirect URL
urlBuilder.setScheme("https");
urlBuilder.setPort(httpsPort);
}
else {
logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s",
serverPort));
}
}
return urlBuilder.getUrl();
}
/loginが設定される
://じゃないので処理が進む
そしてリダイレクト処理
これを使うのが安全?
@Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Redirecting to %s", redirectUrl));
}
response.sendRedirect(redirectUrl);
}
value

protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}