Spring Security 3.1 自定义 authentication provider
2014-05-07 17:30
369 查看
前言
在使用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的配置:
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给它,下面看看其实现:
Java代码
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:
Java代码
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:
Java代码
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
Java代码
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
Java代码
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个类,就实现了对验证参数的扩展了。。。
相关文章推荐
- spring security 3.1中基于数据库自定义验证授权功能实现
- Spring Security 3.1 自定义实例之登陆
- Spring Security 3.1 自定义 authentication provider
- spring security 3.1中基于数据库自定义验证授权功能实现
- 转:Spring Security 3.1 自定义实例之登陆
- Spring Security 3.1 自定义 authentication provider
- Spring Security 3.1自定义登录
- Spring Security3.1登陆验证
- 简单说说Spring Security 使用(附加验证码登录,自定义认证)
- spring security之自定义登录页面
- Spring security 自定义登录与权限控制
- spring security起步三:自定义登录配置与form-login属性详解
- spring security 4.1 中自定义登录界面和扩展login controller
- Spring Security4实战与原理分析视频课程( 扩展+自定义)
- 【Spring Security】四、自定义页面
- Spring Security学习——自定义数据源的用户权限验证
- spring security 3.1注册后自动登录
- Spring Security3.1登陆验证
- Spring Security3.1 最新配置实例
- Spring Security 入门(3-11)Spring Security 的使用-自定义登录验证和回调地址