Spring Security 3.1 自定义 authentication provider
本文同步发表在 http://www.xeclipse.com/?p=1359
前言
在使用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-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>
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; } }
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; }
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; } }
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; } }
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个类,就实现了对验证参数的扩展了。。。
上一篇: Oracle 基本数据类型-----(RowID详解)
下一篇: oracle 连接错误总结
推荐阅读
-
自定义Spring Security的身份验证失败处理方法
-
Spring Security框架进阶、自定义登录
-
struts2中Spring Security 自定义登陆页面的实现
-
spring security 3.1 登陆成功无法跳转问题
-
spring security 3.1中条用js方法登陆后无法回调
-
spring security3.1实践
-
spring security3.1实践
-
Spring Security和自定义filter的冲突导致多执行的解决方案
-
Spring Security之默认的过滤器链及自定义Filter操作
-
Spring Security验证流程剖析及自定义验证方法