【Spring Security系列】Spring Security使用自定义认证实现图形验证码

前面使用过滤器的方式实现了带图形验证码的验证功能,属于Servlet层面,简单、易理解。其实,Spring Security还提供了一种更优雅的实现图形验证码的方式,即自定义认证。

1.认识AuthenticationProvider

在学习Spring Security的自定义认证之前,有必要了解Spring Security是如何灵活集成多种认证技术的。

我们所面对的系统中的用户,在Spring Security中被称为主体(principal)。主体包含了所有能够经过验证而获得系统访问权限的用户、设备或其他系统。主体的概念实际上来自 Java Security,Spring Security通过一层包装将其定义为一个Authentication。

public interface Authentication extends Principal, Serializable {
	/**
	 * 获取主体权限列表
	 */
	Collection getAuthorities();

	/**
	 * 获取主体凭证,通常为用户密码
	 */
	Object getCredentials();

	/**
	 * 获取主体携带的详细信息
	 */
	Object getDetails();

	/**
	 * 获取主体,通常为一个用户名
	 */
	Object getPrincipal();

	/**
	 * 主体是否认证成功
	 */
	boolean isAuthenticated();

	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication中包含主体权限列表、主体凭据、主体详细信息,以及主体是否验证成功等信息。由于大部分场景下身份验证都是基于用户名和密码进行的,所以Spring Security提供了一个 UsernamePasswordAuthenticationToken用于代指这一类证明(例如,用SSH KEY也可以登录,但它不属于用户名和密码登录这个范畴,如有必要,也可以自定义提供)。在前面使用的表单登录中,每一个登录用户都被包装为一UsernamePasswordAuthenticationToken,从而在Spring Security的各个AuthenticationProvider中流动。

AuthenticationProviderSpring Security定义为一个验证过程。

public interface AuthenticationProvider {
	/**
	 * 验证过程,验证成功返回一个验证完成的Authentication
	 */
	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	/**
	 * 是否支持当前的authentication类型
	 */
	boolean supports(Class authentication);
}

一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}
}

2.自定义AuthenticationProvider

Spring Security提供了多种常见的认证技术,包括但不限于以下几种:

  • HTTP层面的认证技术,包括HTTP基本认证和HTTP摘要认证两种。
  • 基于LDAP的认证技术(Lightweight Directory Access Protocol,轻量目录访问协议)。
  • 聚焦于证明用户身份的OpenID认证技术。
  • 聚焦于授权的OAuth认证技术。
  • 系统内维护的用户名和密码认证技术。

其中,使用最为广泛的是由系统维护的用户名和密码认证技术,通常会涉及数据库访问。为了更好地按需定制,Spring Security 并没有直接糅合整个认证过程,而是提供了一个抽象的AuthenticationProvider

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {

	protected final Log logger = LogFactory.getLog(getClass());

	// ~ Instance fields
	// ================================================================================================

	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private UserCache userCache = new NullUserCache();
	private boolean forcePrincipalAsString = false;
	protected boolean hideUserNotFoundExceptions = true;
	private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
	private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

	// ~ Methods
	// ========================================================================================================

	/**
	 * Allows subclasses to perform any additional checks of a returned (or cached)
	 * UserDetails for a given authentication request. Generally a subclass
	 * will at least compare the {@link Authentication#getCredentials()} with a
	 * {@link UserDetails#getPassword()}. If custom logic is needed to compare additional
	 * properties of UserDetails and/or
	 * UsernamePasswordAuthenticationToken, these should also appear in this
	 * method.
	 *
	 * @param userDetails as retrieved from the
	 * {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)} or
	 * UserCache
	 * @param authentication the current request that needs to be authenticated
	 *
	 * @throws AuthenticationException AuthenticationException if the credentials could
	 * not be validated (generally a BadCredentialsException, an
	 * AuthenticationServiceException)
	 */
	protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;

	public final void afterPropertiesSet() throws Exception {
		Assert.notNull(this.userCache, "A user cache must be set");
		Assert.notNull(this.messages, "A message source must be set");
		doAfterPropertiesSet();
	}

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// 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);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	/**
	 * Creates a successful {@link Authentication} object.
	 * 

* Protected so subclasses can override. *

*

* Subclasses will usually store the original credentials the user supplied (not * salted or encoded passwords) in the returned Authentication object. *

* * @param principal that should be the principal in the returned object (defined by * the {@link #isForcePrincipalAsString()} method) * @param authentication that was presented to the provider for validation * @param user that was loaded by the implementation * * @return the successful authentication token */ 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(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } protected void doAfterPropertiesSet() throws Exception { } public UserCache getUserCache() { return userCache; } public boolean isForcePrincipalAsString() { return forcePrincipalAsString; } public boolean isHideUserNotFoundExceptions() { return hideUserNotFoundExceptions; } /** * Allows subclasses to actually retrieve the UserDetails from an * implementation-specific location, with the option of throwing an * AuthenticationException immediately if the presented credentials are * incorrect (this is especially useful if it is necessary to bind to a resource as * the user in order to obtain or generate a UserDetails). *

* Subclasses are not required to perform any caching, as the * AbstractUserDetailsAuthenticationProvider will by default cache the * UserDetails. The caching of UserDetails does present * additional complexity as this means subsequent requests that rely on the cache will * need to still have their credentials validated, even if the correctness of * credentials was assured by subclasses adopting a binding-based strategy in this * method. Accordingly it is important that subclasses either disable caching (if they * want to ensure that this method is the only method that is capable of * authenticating a request, as no UserDetails will ever be cached) or * ensure subclasses implement * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} * to compare the credentials of a cached UserDetails with subsequent * authentication requests. *

*

* Most of the time subclasses will not perform credentials inspection in this method, * instead performing it in * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} * so that code related to credentials validation need not be duplicated across two * methods. *

* * @param username The username to retrieve * @param authentication The authentication request, which subclasses may * need to perform a binding-based retrieval of the UserDetails * * @return the user information (never null - instead an exception should * the thrown) * * @throws AuthenticationException if the credentials could not be validated * (generally a BadCredentialsException, an * AuthenticationServiceException or * UsernameNotFoundException) */ protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; public void setForcePrincipalAsString(boolean forcePrincipalAsString) { this.forcePrincipalAsString = forcePrincipalAsString; } /** * By default the AbstractUserDetailsAuthenticationProvider throws a * BadCredentialsException if a username is not found or the password is * incorrect. Setting this property to false will cause * UsernameNotFoundExceptions to be thrown instead for the former. Note * this is considered less secure than throwing BadCredentialsException * for both exceptions. * * @param hideUserNotFoundExceptions set to false if you wish * UsernameNotFoundExceptions to be thrown instead of the non-specific * BadCredentialsException (defaults to true) */ public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) { this.hideUserNotFoundExceptions = hideUserNotFoundExceptions; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } public void setUserCache(UserCache userCache) { this.userCache = userCache; } public boolean supports(Class authentication) { return (UsernamePasswordAuthenticationToken.class .isAssignableFrom(authentication)); } protected UserDetailsChecker getPreAuthenticationChecks() { return preAuthenticationChecks; } /** * Sets the policy will be used to verify the status of the loaded * UserDetails before validation of the credentials takes place. * * @param preAuthenticationChecks strategy to be invoked prior to authentication. */ public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) { this.preAuthenticationChecks = preAuthenticationChecks; } protected UserDetailsChecker getPostAuthenticationChecks() { return postAuthenticationChecks; } public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) { this.postAuthenticationChecks = postAuthenticationChecks; } public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { this.authoritiesMapper = authoritiesMapper; } private class DefaultPreAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isAccountNonLocked()) { logger.debug("User account is locked"); throw new LockedException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); } if (!user.isEnabled()) { logger.debug("User account is disabled"); throw new DisabledException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); } if (!user.isAccountNonExpired()) { logger.debug("User account is expired"); throw new AccountExpiredException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } } private class DefaultPostAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isCredentialsNonExpired()) { logger.debug("User account credentials have expired"); throw new CredentialsExpiredException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired")); } } } }

AbstractUserDetailsAuthenticationProvider中实现了基本的认证流程,通过继承AbstractUserDetailsAuthenticationProvider,并实现retrieveUseradditionalAuthenticationChecks两个抽象方法即可自定义核心认证过程,灵活性非常高。

@Component
public class MyAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 编写更多校验逻辑
        // 校验密码
    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        return userDetailsService.loadUserByUsername(username);
    }
}

Spring Security 同样提供一个继承自 AbstractUserDetailsAuthenticationProvider AuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	// ~ Static fields/initializers
	// =====================================================================================

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

	// ~ Instance fields
	// ================================================================================================

	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());
	}

	// ~ Methods
	// ========================================================================================================

	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

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

	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 passwordEncoder;
	}

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

	protected UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

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

DaoAuthenticationProvider的用户信息来源于UserDetailsService,并且整合了密码编码的实现,在前面博客的表单认证就是由DaoAuthenticationProvider提供的。

3.实现图形验证码的AuthenticationProvider

前面我们已经基本了解了Spring Security的认证流程,现在重新回到自定义认证实现图形验证码登录这个具体案例中。由于只是在常规的认证之上增加了图形验证码的校验,其他流程并没有变化,所以只需继承DaoAuthenticationProvider并稍作增添即可。

@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 实现图形验证码的校验逻辑

        // 调用父类方法完成密码校验
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

在验证流程中添加新的逻辑后似乎有些问题。在additionalAuthenticationChecks中,我们可以得到的参数是来自UserDetailsServiceUserDetails,以及根据用户提交的账号信息封装而来的UsernamePasswordAuthenticationToken,而图形验证码的校验必须要有HttpServletRequest对象,因为用户提交的验证码和session存储的验证码都需要从用户的请求中获取,这是否意味着这种实现方式不可行呢?并非如此,Authentication实际上还可以携带账号信息之外的数据。

如果这个数据可以利用,那么难题自然就迎刃而解了。前面提到过,一次完整的认证可以包含多个AuthenticationProvider,这些AuthenticationProvider都是由ProviderManager管理的,而ProviderManager是由UsernamePasswordAuthenticationFilter 调用的。也就是说,所有的AuthenticationProvider包含的Authentication都来源于UsernamePasswordAuthenticationFilter

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	private boolean postOnly = true;

	// ~ Constructors
	// ===================================================================================================

	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}

	// ~ Methods
	// ========================================================================================================

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

	/**
	 * Enables subclasses to override the composition of the password, such as by
	 * including additional values and a separator.
	 * 

* This might be used for example if a postcode/zipcode was required in addition to * the password. A delimiter such as a pipe (|) should be used to separate the * password and extended value(s). The AuthenticationDao will need to * generate the expected password in a corresponding manner. *

* * @param request so that request attributes can be retrieved * * @return the password that will be presented in the Authentication * request token to the AuthenticationManager */ @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(passwordParameter); } /** * Enables subclasses to override the composition of the username, such as by * including additional values and a separator. * * @param request so that request attributes can be retrieved * * @return the username that will be presented in the Authentication * request token to the AuthenticationManager */ @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(usernameParameter); } /** * Provided so that subclasses may configure what is put into the authentication * request's details property. * * @param request that an authentication request is being created for * @param authRequest the authentication request object that should have its details * set */ protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * Sets the parameter name which will be used to obtain the username from the login * request. * * @param usernameParameter the parameter name. Defaults to "username". */ public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } /** * Sets the parameter name which will be used to obtain the password from the login * request.. * * @param passwordParameter the parameter name. Defaults to "password". */ public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } /** * Defines whether only HTTP POST requests will be allowed by this filter. If set to * true, and an authentication request is received which is not a POST request, an * exception will be raised immediately and authentication will not be attempted. The * unsuccessfulAuthentication() method will be called as if handling a failed * authentication. *

* Defaults to true but may be overridden by subclasses. */ public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return usernameParameter; } public final String getPasswordParameter() { return passwordParameter; } }

AbstractAuthenticationProcessingFilter本身并没有设置用户详细信息的流程,而且是通过标准接口 AuthenticationDetailsSource构建的,这意味着它是一个允许定制的特性。

public interface AuthenticationDetailsSource {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Called by a class when it wishes a new authentication details instance to be
	 * created.
	 *
	 * @param context the request object, which may be used by the authentication details
	 * object
	 *
	 * @return a fully-configured authentication details instance
	 */
	T buildDetails(C context);
}

UsernamePasswordAuthenticationFilter中使用的AuthenticationDetailsSource是一个标准的Web认证源,携带的是用户的sessionId和IP地址。

public class WebAuthenticationDetailsSource implements
		AuthenticationDetailsSource {

	// ~ Methods
	// ========================================================================================================

	/**
	 * @param context the {@code HttpServletRequest} object.
	 * @return the {@code WebAuthenticationDetails} containing information about the
	 * current request
	 */
	public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
		return new WebAuthenticationDetails(context);
	}
}
public class WebAuthenticationDetails implements Serializable {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================

	private final String remoteAddress;
	private final String sessionId;

	// ~ Constructors
	// ===================================================================================================

	/**
	 * Records the remote address and will also set the session Id if a session already
	 * exists (it won't create one).
	 *
	 * @param request that the authentication request was received from
	 */
	public WebAuthenticationDetails(HttpServletRequest request) {
		this.remoteAddress = request.getRemoteAddr();

		HttpSession session = request.getSession(false);
		this.sessionId = (session != null) ? session.getId() : null;
	}

	/**
	 * Constructor to add Jackson2 serialize/deserialize support
	 *
	 * @param remoteAddress remote address of current request
	 * @param sessionId session id
	 */
	private WebAuthenticationDetails(final String remoteAddress, final String sessionId) {
		this.remoteAddress = remoteAddress;
		this.sessionId = sessionId;
	}

	// ~ Methods
	// ========================================================================================================

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof WebAuthenticationDetails) {
			WebAuthenticationDetails rhs = (WebAuthenticationDetails) obj;

			if ((remoteAddress == null) && (rhs.getRemoteAddress() != null)) {
				return false;
			}

			if ((remoteAddress != null) && (rhs.getRemoteAddress() == null)) {
				return false;
			}

			if (remoteAddress != null) {
				if (!remoteAddress.equals(rhs.getRemoteAddress())) {
					return false;
				}
			}

			if ((sessionId == null) && (rhs.getSessionId() != null)) {
				return false;
			}

			if ((sessionId != null) && (rhs.getSessionId() == null)) {
				return false;
			}

			if (sessionId != null) {
				if (!sessionId.equals(rhs.getSessionId())) {
					return false;
				}
			}

			return true;
		}

		return false;
	}

	/**
	 * Indicates the TCP/IP address the authentication request was received from.
	 *
	 * @return the address
	 */
	public String getRemoteAddress() {
		return remoteAddress;
	}

	/**
	 * Indicates the HttpSession id the authentication request was received
	 * from.
	 *
	 * @return the session ID
	 */
	public String getSessionId() {
		return sessionId;
	}

	@Override
	public int hashCode() {
		int code = 7654;

		if (this.remoteAddress != null) {
			code = code * (this.remoteAddress.hashCode() % 7);
		}

		if (this.sessionId != null) {
			code = code * (this.sessionId.hashCode() % 7);
		}

		return code;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(super.toString()).append(": ");
		sb.append("RemoteIpAddress: ").append(this.getRemoteAddress()).append("; ");
		sb.append("SessionId: ").append(this.getSessionId());

		return sb.toString();
	}
}

有了HttpServletRequest之后,一切都将变得非常顺畅。基于图形验证码的场景,我们可以继承WebAuthenticationDetails,并扩展需要的信息。

public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
    private String imageCode;

    private String saveImageCode;

    private boolean imageCodeIsRight;

    public boolean isImageCodeIsRight() {
        return imageCodeIsRight;
    }

    public MyWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        setImageCode(request.getParameter("captcha"));

        HttpSession session = request.getSession();
        setSaveImageCode((String) session.getAttribute("captcha"));
        if (!StringUtils.isEmpty(saveImageCode)) {
            session.removeAttribute("captcha");

            if (!StringUtils.isEmpty(imageCode) && !StringUtils.isEmpty(saveImageCode) && imageCode.equals(saveImageCode)) {
                imageCodeIsRight = true;
            }
        }
    }

    public String getImageCode() {
        return imageCode;
    }

    public void setImageCode(String imageCode) {
        this.imageCode = imageCode;
    }

    public String getSaveImageCode() {
        return saveImageCode;
    }

    public void setSaveImageCode(String saveImageCode) {
        this.saveImageCode = saveImageCode;
    }

    public void setImageCodeIsRight(boolean imageCodeIsRight) {
        this.imageCodeIsRight = imageCodeIsRight;
    }
}

将它提供给一个自定义的AuthenticationDetailsSource

@Component
public class MyWebAuthenticationDetailsSource implements AuthenticationDetailsSource {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new MyWebAuthenticationDetails(context);
    }
}

接下来实现我们自定义的AuthenticationProvider

@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 实现图形验证码的校验逻辑
        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
        if (!details.isImageCodeIsRight()) {
            throw new VertifyCodeException();
        }
        // 调用父类方法完成密码校验
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

想要应用自定义的 AuthenticationProviderAuthenticationDetailsSource,还需在WebSecurityConfig中完成剩余的配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationDetailsSource myWebAuthenticationDetailsSource;

    @Autowired
    private AuthenticationProvider authenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/api/**").hasAuthority("ROLE_ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .antMatchers("app/api/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .csrf().disable()
            .formLogin()
                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
                .loginPage("/myLogin.html")
                .loginProcessingUrl("/auth/form").permitAll()
                .failureHandler((request, response, exception) -> {

                });
    }
}

 

 

 

【Spring Security系列】Spring Security使用自定义认证实现图形验证码_第1张图片

 

 

你可能感兴趣的:(Spring,Security)