前言
在使用Spring Security的时候,遇到一个比较特殊的情况,需要根据用户名、邮箱等多个条件去验证用户或者使用第三方的验证服务来进行用户名和密码的判断,这样SS(Spring Security,一下简称SS)内置的authentication provider和user detail service就不能用了,花了一些时间去寻找其他的办法。
前置条件
- Spring MVC 结构的Web项目
- Spring Security
- 使用第三方的Service验证用户名密码(并非数据库或者OpenID等SS已经支持的服务)
需求
- 根据用户输入的用户名和密码验证登录
问题分析
在尝试了修改Filter、替换SS内置的Filter之后,发现了一个比较简单的方法,这里简单的讲讲思路。
先看看SS验证用户名和密码的过程 :
DelegatingFilterProxy(Security filter chain):
- ConcurrentSessionFilter
- SecurityContextPersistenceFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
这些都是内置的Filter,请求会从上往下依次过滤。这里由于我们主要关心用户名和密码的验证,所以就要从UsernamePasswordAuthenticationFilter 下手了。(UsernamePasswordAuthenticationFilter需要AuthenticationManager去进行验证。)
在SS的配置文件里面可以看到,如何给SS传递用户验证信息数据源(设置AuthenticationManager):
<authentication-manager> <authentication-provider> <user-service> <user name="admin" authorities="ROLE_USER" password="admin" /> </user-service> </authentication-provider> </authentication-manager>
当然这里是一个最简单的配置,跟踪一下代码就会发现:
- 内置的AuthenticationManager为org.springframework.security.authentication.ProviderManager
- 默认的AuthenticationProvider为org.springframework.security.authentication.dao.DaoAuthenticationProvider
- 再往下看,这个authentication provider使用org.springframework.security.core.userdetails.UserDetailsService 去进行验证
- 到这里用过SS的都清楚了,只需要实现一个UserDetailService,写写下面这个方法就OK了:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
到这里问题就出来了 :
这个方法只有一个参数,怎么才能传递多个参数呢?
一个可用的解决方案
重新自定义一个authentication provider,替换掉默认的DaoAuthenticationProvider.
先看看XML的配置:
- <authentication-manager alias="authenticationManager">
- <authentication-provider ref="loginAuthenticationProvider">
- </authentication-provider>
- </authentication-manager>
- <bean id="loginAuthenticationProvider"
- class="com.XXX.examples.security.LoginAuthenticationProvider">
- <property name="userDetailsService" ref="loginUserDetailService"></property>
- </bean>
这里我们依然仿照DaoAuthenticationProvider,传递一个UserDetailService给它,下面看看其实现:
- public class LoginAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
- {
- // ~ Instance fields
- // ================================================================================================
- private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();
- private SaltSource saltSource;
- private LoginUserDetailsService userDetailsService;
- // ~ Methods
- // ========================================================================================================
- protected void additionalAuthenticationChecks(UserDetails userDetails,
- UsernamePasswordAuthenticationToken authentication)
- throws AuthenticationException
- {
- Object salt = null;
- if (this.saltSource != null)
- {
- salt = this.saltSource.getSalt(userDetails);
- }
- if (authentication.getCredentials() == null)
- {
- logger.debug("Authentication failed: no credentials provided");
- throw new BadCredentialsException("Bad credentials:" + userDetails);
- }
- String presentedPassword = authentication.getCredentials().toString();
- if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt))
- {
- logger.debug("Authentication failed: password does not match stored value");
- throw new BadCredentialsException("Bad credentials:" + userDetails);
- }
- }
- protected void doAfterPropertiesSet() throws Exception
- {
- Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
- }
- protected PasswordEncoder getPasswordEncoder()
- {
- return passwordEncoder;
- }
- protected SaltSource getSaltSource()
- {
- return saltSource;
- }
- protected LoginUserDetailsService getUserDetailsService()
- {
- return userDetailsService;
- }
- protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
- throws AuthenticationException
- {
- UserDetails loadedUser;
- try
- {
- String password = (String) authentication.getCredentials();
- loadedUser = getUserDetailsService().loadUserByUsername(username, password);//区别在这里
- }
- catch (UsernameNotFoundException notFound)
- {
- throw notFound;
- }
- catch (Exception repositoryProblem)
- {
- throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
- }
- if (loadedUser == null)
- {
- throw new AuthenticationServiceException(
- "UserDetailsService returned null, which is an interface contract violation");
- }
- return loadedUser;
- }
- /**
- * Sets the PasswordEncoder instance to be used to encode and validate
- * passwords. If not set, the password will be compared as plain text.
- * <p>
- * For systems which are already using salted password which are encoded
- * with a previous release, the encoder should be of type
- * {@code org.springframework.security.authentication.encoding.PasswordEncoder}
- * . Otherwise, the recommended approach is to use
- * {@code org.springframework.security.crypto.password.PasswordEncoder}.
- *
- * @param passwordEncoder
- * must be an instance of one of the {@code PasswordEncoder}
- * types.
- */
- public void setPasswordEncoder(Object passwordEncoder)
- {
- Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
- if (passwordEncoder instanceof PasswordEncoder)
- {
- this.passwordEncoder = (PasswordEncoder) passwordEncoder;
- return;
- }
- if (passwordEncoder instanceof org.springframework.security.crypto.password.PasswordEncoder)
- {
- final org.springframework.security.crypto.password.PasswordEncoder delegate = (org.springframework.security.crypto.password.PasswordEncoder) passwordEncoder;
- this.passwordEncoder = new PasswordEncoder()
- {
- private void checkSalt(Object salt)
- {
- Assert.isNull(salt, "Salt value must be null when used with crypto module PasswordEncoder");
- }
- public String encodePassword(String rawPass, Object salt)
- {
- checkSalt(salt);
- return delegate.encode(rawPass);
- }
- public boolean isPasswordValid(String encPass, String rawPass, Object salt)
- {
- checkSalt(salt);
- return delegate.matches(rawPass, encPass);
- }
- };
- return;
- }
- throw new IllegalArgumentException("passwordEncoder must be a PasswordEncoder instance");
- }
- /**
- * The source of salts to use when decoding passwords. <code>null</code> is
- * a valid value, meaning the <code>DaoAuthenticationProvider</code> will
- * present <code>null</code> to the relevant <code>PasswordEncoder</code>.
- * <p>
- * Instead, it is recommended that you use an encoder which uses a random
- * salt and combines it with the password field. This is the default
- * approach taken in the
- * {@code org.springframework.security.crypto.password} package.
- *
- * @param saltSource
- * to use when attempting to decode passwords via the
- * <code>PasswordEncoder</code>
- */
- public void setSaltSource(SaltSource saltSource)
- {
- this.saltSource = saltSource;
- }
- public void setUserDetailsService(LoginUserDetailsService userDetailsService)
- {
- this.userDetailsService = userDetailsService;
- }
- }
代码跟DaoAuthenticationProvider几乎一样,只是我们替换了UserDetailService,使用自定义的一个新的LoginUserDetailService:
- public interface LoginUserDetailsService
- {
- /**
- * 功能描述:根据用户米密码验证用户信息
- * <p>
- * 前置条件:
- * <p>
- * 方法影响:
- * <p>
- * Author , 2012-9-26
- *
- * @since server 2.0
- * @param username
- * @param password
- * @return
- * @throws UsernameNotFoundException
- */
- UserDetails loadUserByUsername(String username, String password) throws UsernameNotFoundException;
- }
一个简单的实现LoginUserDetailsServiceImpl:
- public class LoginUserDetailsServiceImpl implements LoginUserDetailsService
- {
- private UserAccountService userAccountService;
- /**
- *
- */
- public LoginUserDetailsServiceImpl()
- {
- }
- /**
- * getter method
- *
- * @see LoginUserDetailsServiceImpl#userAccountService
- * @return the userAccountService
- */
- public UserAccountService getUserAccountService()
- {
- return userAccountService;
- }
- /**
- * 功能描述:查找登录的用户
- * <p>
- * 前置条件:
- * <p>
- * 方法影响:
- * <p>
- * Author , 2012-9-26
- *
- * @since server 2.0
- * @param username
- * @return
- */
- public UserDetails loadUserByUsername(String username, String password) throws UsernameNotFoundException
- {
- boolean result = userAccountService.validate(username, password);
- if (!result)
- {
- return null;
- }
- LoginUserDetailsImpl user = new LoginUserDetailsImpl(username, password);
- return user;
- }
- /**
- * setter method
- *
- * @see LoginUserDetailsServiceImpl#userAccountService
- * @param userAccountService
- * the userAccountService to set
- */
- public void setUserAccountService(UserAccountService userAccountService)
- {
- this.userAccountService = userAccountService;
- }
- }
其他相关的代码:
GrantedAuthorityImpl
- public class GrantedAuthorityImpl implements GrantedAuthority
- {
- /**
- * ROLE USER 权限
- */
- private static final String ROLE_USER = "ROLE_USER";
- /**
- * Serial version UID
- */
- private static final long serialVersionUID = 1L;
- private UserDetailsService delegate;
- public GrantedAuthorityImpl(UserDetailsService user)
- {
- this.delegate = user;
- }
- public String getAuthority()
- {
- return ROLE_USER;
- }
- }
LoginUserDetailsImpl
- public class LoginUserDetailsImpl extends User implements UserDetails
- {
- /**
- *
- */
- private static final long serialVersionUID = -5424897749887458053L;
- /**
- * 邮箱
- */
- private String mail;
- /**
- * @param username
- * @param password
- * @param enabled
- * @param accountNonExpired
- * @param credentialsNonExpired
- * @param accountNonLocked
- * @param authorities
- */
- public LoginUserDetailsImpl(String username, String password, boolean enabled, boolean accountNonExpired,
- boolean credentialsNonExpired, boolean accountNonLocked,
- Collection<? extends GrantedAuthority> authorities)
- {
- super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
- }
- /**
- * @param username
- * @param password
- * @param authorities
- */
- public LoginUserDetailsImpl(String username, String password, Collection<? extends GrantedAuthority> authorities)
- {
- super(username, password, authorities);
- }
- /**
- * @param username
- * @param password
- * @param authorities
- */
- public LoginUserDetailsImpl(String username, String password)
- {
- super(username, password, new ArrayList<GrantedAuthority>());
- }
- /**
- * getter method
- * @see LoginUserDetailsImpl#mail
- * @return the mail
- */
- public String getMail()
- {
- return mail;
- }
- /**
- * setter method
- * @see LoginUserDetailsImpl#mail
- * @param mail the mail to set
- */
- public void setMail(String mail)
- {
- this.mail = mail;
- }
- @Override
- public String toString()
- {
- return super.toString() + "; Mail: " + mail;
- }
- }
至于
<bean id="userAccountService" class="com.XXX.UserAccountService"/>
它的实现就随你的意吧,这里是模仿第三方的一个实现。
小结
为了自定义一个authenticationProvider,我们还需要自定义一个UserDetailsService,只需要这2个类,就实现了对验证参数的扩展了。。。