1.在Acegi中是由认证管理器确定用户身份。一个认证管理器由借口AuthenticationManager实现。
/** * /**
* 尝试验证用户身份,如果验证成功,将返回一个含授权的完整的Authentication对象。
* 一个AuthenticationManager必须按照下面的规则处理异常:
* 如果账号是不被允许的必须抛出DisabledException,AuthenticationManager能检测到这个状态。
* 如果账号已经被锁定必须抛出LockedException,AuthenticationManager能够检测被锁定的账号。
* 如果取到的是非法的信任状(credential?)必须抛出BadCredentialsException。同时上述的2种异常也是可选
* 的,AuthenticationManager必须总是检测信任状(credential)。
* 异常必须被检测,如果有上述的异常出现须抛出异常。
* 如果一个账号是不被允许的或是被锁定的,认证请求会立即被拒绝,信任状的检测不会发生。
* 这防止了对不被允许账号和被锁定的账号的信任状检测。
*
* @param authentication the authentication request object
*
* @return a fully authenticated object including credentials
*
* @throws AuthenticationException if authentication fails
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
2.Authentication 继承了java.security.Principal,Principal实现了简单的主体(Principal)定义。
/** */ /**
* 通过AuthenticationManager设置Principal的授权信息。
* 注意:当Class状态为valid时,除非Value已经被可信任的AuthenticationManager设置t过,否则对Class
* 不起作用。(此句存疑 Note that classes should not rely on this value as being valid unless it has
* been set by AuthenticationManager)
* Authentication的实现类需要确保修改返回的数足不会影响Authentication Object的状态。(比如返回一个
* 数组的copy)。
*
* @return the authorities granted to the principal, or null if authentication has not been completed
*/
public GrantedAuthority[] getAuthorities();
/** */ /**
* 信任状证明Principal的身份是合法的。它经常是密码,但是也可以是任何与AuthenticationManager
* 相关的东西。
*
* @return the credentials that prove the identity of the Principal
*/
public Object getCredentials();
/** */ /**
* 储存认证请求的额外信息。有可能是IP地址,证件号码等。
*
* @return additional details about the authentication request, or null if not used
*/
public Object getDetails();
/** */ /**
* 返回一个已经通过验证的Principal(这通常是一个用户名)。
*
* @return the Principal being authenticated
*/
public Object getPrincipal();
/** */ /**
* AbstractSecurityInterceptor将认证令牌交给AuthenticationManager。如果认证成功AuthenticaionManager
* 会返回一个不变的认证令牌(返回true)。
* 返回"true"会改善性能,因为不再是每一个请求都需要调用AuthenticationManager。
* 由于安全上的考虑,实现这个接口返回true的时候需要十分的小心。除非他们不再变化或者有什么途径
* 确保属性在最初的初始化后不再变化。
*
* @return true if the token has been authenticated and the AbstractSecurityInterceptor
* does not need to represent the token for re-authentication to the AuthenticationManager
*/
public boolean isAuthenticated();
/** */ /**
* 详情参阅isAuthenticate()方法
* 参数为true的情况下,如果某个实现希望拒绝一个调用,那么将抛出一个IllegalArgumentException异常。
*
* @param isAuthenticated true if the token should be trusted (which may result in an exception) or
* false if the token should not be trusted
*
* @throws IllegalArgumentException if an attempt to make the authentication token trusted (by
* passing true as the argument) is rejected due to the implementation being immutable or
* implementing its own alternative approach to isAuthenticated()}
*/
public void setAuthenticated( boolean isAuthenticated)
throws IllegalArgumentException;
}
3.Acegi提供了一个能适应大多数情况的ProviderManager,实现了AuthenticationManager, ProviderManager继承抽象类AbstractAuthenticationManager:
/** */ /**
* 实现通过调用抽象方法doAuthentication()进行工作。
* 如果doAuthentication()方法抛出AuthenticationException异常,验证失败。
*
* @param authRequest the authentication request object
*
* @return a fully authenticated object including credentials
*
* @throws AuthenticationException if authentication fails
*/
public final Authentication authenticate(Authentication authRequest)
throws AuthenticationException {
try {
Authentication authResult = doAuthentication(authRequest);
copyDetails(authRequest, authResult);
return authResult;
} catch (AuthenticationException e) {
e.setAuthentication(authRequest);
throw e;
}
}
/** */ /**
* 在目标Authentication的detail没有被设置的情况下从源Authentication复制detail信息。
*
* @param source source authentication
* @param dest the destination authentication object
*/
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null )) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
/** */ /**
* 具体的实现通过覆写此方法提供认证服务,此方法的约束详见AuthenticationManager的authenticate()方法
*
* @param authentication the authentication request object
*
* @return a fully authenticated object including credentials
*
* @throws AuthenticationException if authentication fails
*/
protected abstract Authentication doAuthentication(Authentication authentication)
throws AuthenticationException;
}
private static final Log logger = LogFactory.getLog(ProviderManager. class );
// ~ Instance fields =============================================================
private ApplicationEventPublisher applicationEventPublisher;
private ConcurrentSessionController sessionController = new NullConcurrentSessionController();
private List providers;
protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
private Properties exceptionMappings;
// ~ Methods ==================================================================
public void afterPropertiesSet() throws Exception {
checkIfValidList( this .providers);
Assert.notNull( this .messages, " A message source must be set " );
if (exceptionMappings == null ) {
exceptionMappings = new Properties();
exceptionMappings.put(AccountExpiredException. class .getName(),
AuthenticationFailureExpiredEvent. class .getName());
exceptionMappings.put(AuthenticationServiceException. class .getName(),
AuthenticationFailureServiceExceptionEvent. class .getName());
exceptionMappings.put(LockedException. class .getName(), AuthenticationFailureLockedEvent. class .getName());
exceptionMappings.put(CredentialsExpiredException. class .getName(),
AuthenticationFailureCredentialsExpiredEvent. class .getName());
exceptionMappings.put(DisabledException. class .getName(), AuthenticationFailureDisabledEvent. class .getName());
exceptionMappings.put(BadCredentialsException. class .getName(),
AuthenticationFailureBadCredentialsEvent. class .getName());
exceptionMappings.put(UsernameNotFoundException. class .getName(),
AuthenticationFailureBadCredentialsEvent. class .getName());
exceptionMappings.put(ConcurrentLoginException. class .getName(),
AuthenticationFailureConcurrentLoginEvent. class .getName());
exceptionMappings.put(ProviderNotFoundException. class .getName(),
AuthenticationFailureProviderNotFoundEvent. class .getName());
exceptionMappings.put(ProxyUntrustedException. class .getName(),
AuthenticationFailureProxyUntrustedEvent. class .getName());
doAddExtraDefaultExceptionMappings(exceptionMappings);
}
}
private void checkIfValidList(List listToCheck) {
if ((listToCheck == null ) || (listToCheck.size() == 0 )) {
throw new IllegalArgumentException( " A list of AuthenticationManagers is required " );
}
}
/** */ /**
* 如果在启动期间没有exception被IoC容器注入,这个方法提供额外的异常对应。
*
* @param exceptionMappings the properties object, which already has entries in it
*/
protected void doAddExtraDefaultExceptionMappings(Properties exceptionMappings) {}
/** */ /**
* 尝试认证通过Authentication对象。
* AuthenticationProviders组将会接连尝试认证对象,直到其中的一个通过这个Authentication对象。
* 如果多个AuthenticationProvider通过了Authentication对象,那么只有第一个AuthenticationProvider
* 产生结果,后续的AuthenticationProvider将不会被尝试。
*
* @param authentication the authentication request object.
*
* @return a fully authenticated object including credentials.
*
* @throws AuthenticationException if authentication fails.
*/
public Authentication doAuthentication(Authentication authentication)
throws AuthenticationException {
Iterator iter = providers.iterator();
Class toTest = authentication.getClass();
AuthenticationException lastException = null ;
while (iter.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider) iter.next();
if (provider.supports(toTest)) {
logger.debug( " Authentication attempt using " + provider.getClass().getName());
Authentication result = null ;
try {
result = provider.authenticate(authentication);
sessionController.checkAuthenticationAllowed(result);
} catch (AuthenticationException ae) {
lastException = ae;
result = null ;
}
if (result != null ) {
sessionController.registerSuccessfulAuthentication(result);
applicationEventPublisher.publishEvent( new AuthenticationSuccessEvent(result));
return result;
}
}
}
if (lastException == null ) {
lastException = new ProviderNotFoundException(messages.getMessage( " ProviderManager.providerNotFound " ,
new Object[] {toTest.getName()} , " No AuthenticationProvider found for {0} " ));
}
// Publish the event
String className = exceptionMappings.getProperty(lastException.getClass().getName());
AbstractAuthenticationEvent event = null ;
if (className != null ) {
try {
Class clazz = getClass().getClassLoader().loadClass(className);
Constructor constructor = clazz.getConstructor( new Class[] {
Authentication. class , AuthenticationException. class
} );
Object obj = constructor.newInstance( new Object[] {authentication, lastException} );
Assert.isInstanceOf(AbstractAuthenticationEvent. class , obj, " Must be an AbstractAuthenticationEvent " );
event = (AbstractAuthenticationEvent) obj;
} catch (ClassNotFoundException ignored) {}
catch (NoSuchMethodException ignored) {}
catch (IllegalAccessException ignored) {}
catch (InstantiationException ignored) {}
catch (InvocationTargetException ignored) {}
}
if (event != null ) {
applicationEventPublisher.publishEvent(event);
} else {
if (logger.isDebugEnabled()) {
logger.debug( " No event was found for the exception " + lastException.getClass().getName());
}
}
// Throw the exception
throw lastException;
}
public List getProviders() {
return this .providers;
}
/** */ /**
* 返回设定的ConcurrentSessionController对象,如果对象没有被设置则返回一个
* NullConcurrentSessionController(默认初始化的对象)
*
* @return ConcurrentSessionController instance
*/
public ConcurrentSessionController getSessionController() {
return sessionController;
}
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this .applicationEventPublisher = applicationEventPublisher;
}
public void setMessageSource(MessageSource messageSource) {
this .messages = new MessageSourceAccessor(messageSource);
}
/** */ /**
* 设置AuthenticationProvider对象
*
* @param newList
*
* @throws IllegalArgumentException DOCUMENT ME!
*/
public void setProviders(List newList) {
checkIfValidList(newList);
Iterator iter = newList.iterator();
while (iter.hasNext()) {
Object currentObject = null ;
try {
currentObject = iter.next();
AuthenticationProvider attemptToCast = (AuthenticationProvider) currentObject;
} catch (ClassCastException cce) {
throw new IllegalArgumentException( " AuthenticationProvider " + currentObject.getClass().getName()
+ " must implement AuthenticationProvider " );
}
}
this .providers = newList;
}
/** */ /**
* 设置ConcurrentSessionController来限制用户的session数量。
* 默认设置为NullConcurrentSessionController
*
* @param sessionController ConcurrentSessionController
*/
public void setSessionController(ConcurrentSessionController sessionController) {
this .sessionController = sessionController;
}
}
4.AuthenticationManager不依靠自己实现身份验证,而是通过Iterator逐个遍历AuthenticationProvider的子类集合(如果使用Spring的话子类类型由配置文件注入),直到某个Provider成功验证Authentication。
Acegi提供的Provider实现包括:
AuthByAdapterProvider、CasAuthenticationProvider、DaoAuthenticationProvider、JaasAuthenticationProvider、PasswordDaoAuthenticationProvider、RemoteAuthenticationProvider、RunAsImplAuthenticationProvider、TestingAuthenticationProvider。
下面只分析DaoAuthenticationProvider的相关类,按照AuthenticationProvider-->AbstractUserDetailsAuthenticationProvider-->DaoAuthenticationProvider的顺序展开。
/** */ /**
* 和AuthenticationManager的同名方法实现同样的功能,详见AuthenticationManager
*
* @param authentication the authentication request object.
*
* @return a fully authenticated object including credentials. May return null if the
* AuthenticationProvider is unable to support authentication of the passed
* Authentication object. In such a case, the next AuthenticationProvider that
* supports the presented Authentication class will be tried.
*
* @throws AuthenticationException if authentication fails.
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException;
/** */ /**
* 如果这个AuthenticationProvider支持通过Authentication对象,则返回True。
* 返回True并不意味着AuthenticationProvider能认证当前的Authenctication。
* 他只是简单的声明支持通过认证。AuthenticationProvider仍旧能够通过authenticate()方法返回null,
* 使其它的Provider能够尝试认证这个Authentication。(存疑)
* 选择哪一个AuthenticatonProvider履行鉴定是由ProviderManager在运行时管理的。
*
* @param authentication DOCUMENT ME!
*
* @return true if the implementation can more closely evaluate the Authentication class
* presented
*/
public boolean supports(Class authentication);
}
protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false ;
protected boolean hideUserNotFoundExceptions = true ;
// ~ Methods ==================================================================
/** */ /**
* 允许子类对认证请求返回或缓存的UserDetails提供额外的校验。
* 通常情况下子类至少会对Authentication的getCredentials()方法和UserDetails的getPassword()方法进行
* 比照。
* 如果定制的逻辑需要比照额外的UserDetail属性或者UsernamePasswordAuthenticationToken,也应该在
* 这个方法中定制。
*
* @param userDetails as retrieved from the #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) {
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 " );
}
if ( ! user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage( " AbstractUserDetailsAuthenticationProvider.locked " ,
" User account is locked " ));
}
if ( ! user.isEnabled()) {
throw new DisabledException(messages.getMessage( " AbstractUserDetailsAuthenticationProvider.disabled " ,
" User is disabled " ));
}
if ( ! user.isAccountNonExpired()) {
throw new AccountExpiredException(messages.getMessage( " AbstractUserDetailsAuthenticationProvider.expired " ,
" User account has expired " ));
}
// This check must come here, as we don't want to tell users
// about account status unless they presented the correct credentials
try {
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} catch (AuthenticationException exception) {
// There was a problem, so try again after checking we're using latest data
cacheWasUsed = false ;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
if ( ! user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(messages.getMessage(
" AbstractUserDetailsAuthenticationProvider.credentialsExpired " , " User credentials have expired " ));
}
if ( ! cacheWasUsed) {
this .userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
/** */ /**
* 构建一个成功的Authentication对象。使用的保护类型所以子类可以覆写本方法。
* 子类往往会将用户提供的原始信任状(未经修改以及密码未被解密)储存在返回的Authentication
* 对象中。
*
* @param principal that should be the principal in the returned object (defined by the
* #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(), 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;
}
/** */ /**
* 允许子类由定义的实现位置获取UserDetails,如果呈上的信任状是非法的可以有选择的抛出
* AuthenticationExcepton异常(在有必要将用户与资源绑定来获取或产生UserDetail的情况下,这种处理
* 方式是十分有效的)。
* 子类没有必要去实现任何的缓存,因为AbstractUserDetailsAuthenticationProvider在默认的情况下
* 将会缓存UserDetails。
* UserDetails的缓存是十分复杂的,这意味着后续的认证请求即使是从缓存中得到回复也仍旧要进行
* 信任状的校验,即使信任状的校验被基类在这个方法中用binding-based策略实现。
* 因此在子类禁用缓存(如果想要保证这个方法是唯一能够进行认证的方法,没有UserDetails将会
* 被缓存)或是确认子类实现additionalAuthenticationChecks()方法来进行被缓存的UserDetails
* 对象的信任状和后续的认证请求的比照的情况下它是十分重要的。
* 在大多数情况下子类不必要在这个方法中实现信任状的校验,取而代之的是在
* additionalAuthenticationChecks()方法中实现。这样代码不用在2个方法中都实现信任状的校验。
*
* @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
* BadCredentialsExceptionan ,an AuthenticationServiceException or
* UsernameNotFoundException)
*/
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
public void setForcePrincipalAsString( boolean forcePrincipalAsString) {
this .forcePrincipalAsString = forcePrincipalAsString;
}
/** */ /**
* 在通常情况,如果用户名没有找到或是密码错误AbstractUserDetailsAuthenticationProvider将会
* 抛出一个BadCredentialsException异常。设置这个属性为false的话,程序将会抛出
* UsernameNotFoundException来取代BadCredentialsException异常。
* 需要注意的是:我们认为这不如抛出BadCredentialsException来的可靠。
*
* @param hideUserNotFoundExceptions set to false if you wish UsernameNotFoundExceptions
* 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));
}
}
private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();
private SaltSource saltSource;
private UserDetailsService userDetailsService;
// ~ Methods ==================================================================
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null ;
if ( this .saltSource != null ) {
salt = this .saltSource.getSalt(userDetails);
}
if ( ! passwordEncoder.isPasswordValid(userDetails.getPassword(), authentication.getCredentials().toString(), salt)) {
throw new BadCredentialsException(messages.getMessage(
" AbstractUserDetailsAuthenticationProvider.badCredentials " , " Bad credentials " ), userDetails);
}
}
protected void doAfterPropertiesSet() throws Exception {
Assert.notNull( this .userDetailsService, " An Authentication DAO must be set " );
}
public PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
public SaltSource getSaltSource() {
return saltSource;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this .getUserDetailsService().loadUserByUsername(username);
} catch (DataAccessException repositoryProblem) {
throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null ) {
throw new AuthenticationServiceException(
" AuthenticationDao returned null, which is an interface contract violation " );
}
return loadedUser;
}
/** */ /**
* 设置密码解密实例来进行密码的解密校验。如果没有设置,将会使用PlaintextPosswordEncoder
* 作为默认设置。
*
* @param passwordEncoder The passwordEncoder to use
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this .passwordEncoder = passwordEncoder;
}
/** */ /**
* (source of salts翻译不来)此方法解迷密码的时候使用。
* null是一个合法得值,这表明DaoAuthenticationProvider会把null返回给PasswordEncoder。
*
* @param saltSource to use when attempting to decode passwords via the PasswordEncoder
*/
public void setSaltSource(SaltSource saltSource) {
this .saltSource = saltSource;
}
public void setUserDetailsService(UserDetailsService authenticationDao) {
this .userDetailsService = authenticationDao;
}
}