本文同步发表在 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 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个类,就实现了对验证参数的扩展了。。。
分享到:
相关推荐
《Spring Security 3.1 实现验证码自定义登录详解》 在现代Web应用程序的安全管理中,Spring Security是一个不可或缺的框架,它提供了强大的访问控制和身份验证功能。在本文中,我们将深入探讨如何在Spring ...
<security:authentication-provider user-service-ref="userDetailsService"> <security:password-encoder hash="bcrypt" /> </security:authentication-provider> </security:authentication-manager> <!-- ...
在SpringSecurity 3.1版本中,相比3.0版本,配置上有一些变化,但其核心仍然是通过一系列过滤器来实现安全控制。本文将详细介绍SpringSecurity的体系结构、配置以及在非数据库环境下进行安全保护的基本步骤。 1. **...
1. **认证**:通过提供不同类型的Authentication Provider,如数据库、LDAP、Remember Me服务等,Spring Security实现了灵活的用户身份验证。 2. **授权**:基于Role的访问控制(RBAC)允许开发者定义角色和权限,...
4. `DefaultLoginPageGeneratingFilter`: 如果没有自定义登录页面,Spring Security将自动生成默认的登录页面。 5. `BasicAuthenticationFilter`: 处理HTTP Basic认证,适用于API或其他不希望展示登录页面的情况。 6...
Spring Security 参考 1 第一部分前言 15 1.入门 16 2.介绍 17 2.1什么是Spring Security? 17 2.2历史 19 2.3版本编号 20 2.4获得Spring安全 21 2.4.1使用Maven 21 Maven仓库 21 Spring框架 22 2.4.2 Gradle 23 ...
1.1. Spring Security是什么? 1.2. 历史 1.3. 发行版本号 1.4. 获得Spring Security 1.4.1. 项目模块 1.4.1.1. Core - spring-security-core.jar 1.4.1.2. Web - spring-security-web.jar 1.4.1.3. Config -...
- **示例代码**: 在`spring-security.xml`中配置`<authentication-manager>`和`<authentication-provider>`。 - **2.2 数据库表结构** - 介绍数据库表的设计原则,包括用户表(`users`)、角色表(`roles`)以及权限...
DAO Authentication Provider是一种强大的认证提供者,它允许开发者自定义数据访问层,从而实现高度定制化的认证逻辑。 ##### 8.1 Java Authentication and Authorization Service (JAAS) 提供者概述 JAAS是Java...
<security:authentication-provider user-service-ref="userService"> <security:password-encoder ref="passwordEncoder"/> </security:authentication-provider> </security:authentication-manager> ``` **...
### Spring Security 整合 CAS 全程详解 #### 一、引言 ##### 1.1 概述 ###### 1.1.1 单点登录介绍 单点登录(Single Sign-On,简称 SSO)是一种流行的解决方案,用于简化用户在多个相互信任的应用系统之间的身份...
在本教程中,我们将探讨如何逐步搭建 Spring Security 3.x 环境,特别针对 3.1 及以上版本的 Spring 框架。 **第一步:准备依赖库** 首先,你需要获取所有必要的 JAR 包,包括 Spring Security 的核心库和其他依赖...
**Acegi Security** 是一个为Spring框架设计的安全管理工具,它提供了丰富的安全服务,包括认证(Authentication)、授权(Authorization)以及会话管理(Session Management)等功能。在本章节中,我们将深入探讨Acegi的...
- **Spring Security**:基于 Spring 框架的安全插件,提供了强大的安全功能,支持多种 SSO 解决方案。 ##### 3.2 Java 实现案例 下面是一个简单的 Java 实现案例: ```java public class SimpleSSOAuthentication...
2.3.2. The JMS Provider ............................................................ 32 2.3.3. Anatomy of a JMS Message ............................................ 33 2.3.4. Message Selectors ..........