前面使用过滤器的方式实现了带图形验证码的验证功能,属于Servlet层面,简单、易理解。其实,Spring Security还提供了一种更优雅的实现图形验证码的方式,即自定义认证。
1.认识AuthenticationProvider
在学习Spring Security的自定义认证之前,有必要了解Spring Security是如何灵活集成多种认证技术的。
我们所面对的系统中的用户,在Spring Security中被称为主体(principal)。主体包含了所有能够经过验证而获得系统访问权限的用户、设备或其他系统。主体的概念实际上来自 Java Security,Spring Security通过一层包装将其定义为一个Authentication。
public interface Authentication extends Principal, Serializable {
/**
* 获取主体权限列表
*/
Collection extends GrantedAuthority> 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中流动。
AuthenticationProvider被Spring 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 extends Authentication> 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
* UsernameNotFoundException
s 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
* UsernameNotFoundException
s 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,并实现retrieveUser和additionalAuthenticationChecks两个抽象方法即可自定义核心认证过程,灵活性非常高。
@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中,我们可以得到的参数是来自UserDetailsService的UserDetails,以及根据用户提交的账号信息封装而来的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);
}
}
想要应用自定义的 AuthenticationProvider 和 AuthenticationDetailsSource,还需在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) -> {
});
}
}