Spring Boot整合Spring Security(六)基于表单的认证流程

阅前提示

此文章基于Spring Security 6.0

基于表单的认证流程

鉴权

Spring Boot整合Spring Security(六)基于表单的认证流程_第1张图片
1、当一个未认证用户请求一个不在白名单里的接口
2、服务端FilterSecurityInterceptor拒绝了这个未认证的请求,并抛出AccessDeniedException
3、根据配置的LoginUrl或者AuthenticationEntryPoint,服务端向客户端响应请求,重定向到登录界面
4、客户端请求登录界面
5、服务端返回登录界面
tips:如果是前后端分离项目,跟4,5没关系

认证

Spring Boot整合Spring Security(六)基于表单的认证流程_第2张图片
当用户提交的请求路径匹配到post方法的/login(或者自定义)时UsernamePasswordAuthenticationFilter过滤器被触发
1、在UsernamePasswordAuthenticationFilter中组装一个authenticated为false的UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken中的属性

  • Collection authorities 权限集合
  • Object details 可以放一些权限信息什么的
  • boolean authenticated = false 默认false的是否认证通过
  • Object principal 用户信息(只要是继承了UserDetails的基本都能算是用户信息类,所以这里是Object)
  • Object credentials 一般是密码。在类中实现了CredentialsContainer接口,认证完后将密码擦除
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,password);

public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
   	return new UsernamePasswordAuthenticationToken(principal, credentials);
   }
   
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
   	super(null);
   	this.principal = principal;
   	this.credentials = credentials;
   	setAuthenticated(false);
   }

2、从Spring获取一个AuthenticationManager实例,调用authenticate方法,验证用户名和密码是否正确,至于这个authenticate用到的是哪个,就看用户名密码存在哪里了(内存和数据库)
3、如果认证失败了清除SecurityContextHolder,调用RememberMeService的内容,如果没有配置RememberMeService,那么不调用,然后返回登录失败的信息
4、如果认证成功了,将存储session,保存认证信息,如果有配置RememberMeService,调用RememberMeService,ApplicationEventPublisher publishes an InteractiveAuthenticationSuccessEvent,最后调用AuthenticationSuccessHandler的方法,重定向到之前RequestCache里的URL。如果没有,就重定向到主页

如何认证

在Spring Security 6.0中,AuthenticationManager默认实现类是ProviderManager,这个类的作用就是,选出基于你的配置,可以提供authenticate方法的类,如果不出意外的话,基本就是DaoAuthenticationProvider了,在这个类中,有两个关键方法

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	/**
	 * The plaintext password used to perform
	 * {@link PasswordEncoder#matches(CharSequence, String)} on when the user is not found
	 * to avoid SEC-2056.
	 */
	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

	private PasswordEncoder passwordEncoder;

	/**
	 * The password used to perform {@link PasswordEncoder#matches(CharSequence, String)}
	 * on when the user is not found to avoid SEC-2056. This is necessary, because some
	 * {@link PasswordEncoder} implementations will short circuit if the password is not
	 * in a valid format.
	 */
	private volatile String userNotFoundEncodedPassword;

	private UserDetailsService userDetailsService;

	private UserDetailsPasswordService userDetailsPasswordService;

	public DaoAuthenticationProvider() {
		setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
	}

	@Override
	@SuppressWarnings("deprecation")
	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"));
		}
	}

	@Override
	protected void doAfterPropertiesSet() {
		Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
	}

	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			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);
		}
	}

	@Override
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		boolean upgradeEncoding = this.userDetailsPasswordService != null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user);
	}

	private void prepareTimingAttackProtection() {
		if (this.userNotFoundEncodedPassword == null) {
			this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
		}
	}

	private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
		if (authentication.getCredentials() != null) {
			String presentedPassword = authentication.getCredentials().toString();
			this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
		}
	}

	/**
	 * Sets the PasswordEncoder instance to be used to encode and validate passwords. If
	 * not set, the password will be compared using
	 * {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
	 * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
	 * types.
	 */
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
		this.passwordEncoder = passwordEncoder;
		this.userNotFoundEncodedPassword = null;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return this.passwordEncoder;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	protected UserDetailsService getUserDetailsService() {
		return this.userDetailsService;
	}

	public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
		this.userDetailsPasswordService = userDetailsPasswordService;
	}

}

UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
这两个方法分别是从UserdetailsService中根据用户名取出一个UserDetail以及进行密码校验。
在密码校验通过后返回一个认证通过的Authentication,否则就抛出密码错误

在SpringBoot启动的时候,如果没有配置UserDetailsService的bean,那么就不会调用这个类(会在控制台打印密码,默认用户名user),如果配置了UserDetailsService但没有配置加密类的bean,就会报错(Spring Security 5.0是这样的,不知道6.0有没有默认的加密类,还没试过)

你可能感兴趣的:(spring,spring,boot,java)