Closed40

SpringSecurity

さにらねさにらね

UserDetailsServiceについての個人的なまとめ

◆UserDetailsServiceインターフェースのloadUserByUsernameメソッドをオーバーライドすることでDB認証が行える
→ ユーザー情報を取得するインターフェース。このインターフェースはDaoAuthenticationProviderによって利用されるユーザーDAOです。

◆一方で以下のような方法もある
UsernamePasswordAuthenticationFilterを継承して、AuthenticationProviderの実装クラスを作成した場合はAuthenticationProviderの実装クラスにてユーザ取得処理を実装する。

または、こんな感じでfileterからデフォルトのauthenticationManagerを利用する

MyAuthenticationFilter
    // 認証の処理
    @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"));
	}
}	
さにらねさにらね

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.
さにらねさにらね

インメモリ認証

まずコードを書きます

SecurityConfiguration.java
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();

}

WebSecurityConfiguration.java
...
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を走査して呼出す

神記事
https://qiita.com/opengl-8080/items/c105152c9ca48509bd0c

https://java.keicode.com/lang/servlet-filter.php

https://kozake.hatenablog.com/entry/2016/09/13/220328

さにらねさにらね

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 宣言を追加することにより、サーブレットコンテナーフィルターチェーンにリンクされます。

https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/FilterChainProxy.html

  • Beanを明示的に書かなくても<security:http />でカバーされる
  • DelegatingFilterProxyを適切に宣言すればリンクされる

FilterChainProxy は、設定されているフィルターで標準のフィルターライフサイクルメソッドを呼び出さないことに注意してください。他の Spring Bean の場合と同様に、Spring のアプリケーションコンテキストライフサイクルインターフェースを代替手段として使用することをお勧めします。
名前空間構成を使用して Web セキュリティを設定する方法を検討したとき、"springSecurityFilterChain" という名前の DelegatingFilterProxy を使用しました。これで、これがネームスペースによって作成された FilterChainProxy の名前であることがわかります。

https://spring.pleiades.io/spring-security/site/docs/5.1.7.RELEASE/reference/html/web-app-security.html

さにらねさにらね

SpringBootでは

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

https://debug-life.net/entry/3189

さにらねさにらね

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

https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/WebSecurity.html

さにらねさにらね

なんか、とても長くなってる・・・

/**
 * 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している

AbstractSecurityWebApplicationInitializer
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();
	}
さにらねさにらね

◆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

JdbcUserDetailsManager.java
	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メソッドの時にマッチするようになっていることがわかります

さにらねさにらね

認証オブジェクト取得

	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の書き換え



プロキシに戻った時

変更箇所



https://tomcat.apache.org/tomcat-5.5-doc/catalina/docs/api/org/apache/tomcat/util/buf/MessageBytes.html

LoginUrlAuthenticationEntryPoint.java

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が設定される

://じゃないので処理が進む

そしてリダイレクト処理
これを使うのが安全?

DefaultRedirectStrategy
	@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

さにらねさにらね
ExceptionTranslationFilter.java
	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);
	}
このスクラップは2022/09/26にクローズされました