一直对这块了解的不是很透彻,网上看到一位牛人写的一篇文章,分析的很到位,拿来借鉴一下:
原文网址:http://www.evget.com/zh-CN/Info/catalog/7179.html
简单分析一下 Spring Acegi 的源代码实现:
代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//这里检验是不是符合 ServletRequest/SevletResponse 的要求
if (!(request instanceof HttpServletRequest)) {
throw new ServletException("Can only process HttpServletRequest");
}
if (!(response instanceof HttpServletResponse)) {
throw new ServletException("Can only process HttpServletResponse");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
//根据 HttpServletRequest和HttpServletResponse 来进行验证
if (requiresAuthentication(httpRequest, httpResponse)) {
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
//这里定义 Acegi中的Authentication 对象来持有相关的用户验证信息
Authentication authResult;
try {
onPreAuthentication(httpRequest, httpResponse);
//这里的具体验证过程委托给子类完成,比如 AuthenticationProcessingFilter 来完成基于 Web 页面的用户验证
authResult = attemptAuthentication(httpRequest);
} catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(httpRequest, httpResponse, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//完成验证后的后续工作,比如跳转到相应的页面
successfulAuthentication(httpRequest, httpResponse, authResult);
return;
}
chain.doFilter(request, response);
}
在 AuthenticationProcessingFilter 中的具体验证过程是这样的:
代码
public Authentication attemptAuthentication(HttpServletRequest request)
throws AuthenticationException {
//这里从 HttpServletRequest 中得到用户验证的用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
//这里根据得到的用户名和密码去构造一个 Authentication 对象提供给 AuthenticationManager 进行验证,里面包含了用户的用户名和密码信息
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Place the last username attempted into HttpSession for views
request.getSession().setAttribute(ACEGI_SECURITY_LAST_USERNAME_KEY, username);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//这里启动 AuthenticationManager 进行验证过程
return this.getAuthenticationManager().authenticate(authRequest);
}
在 Acegi 框架中,进行验证管理的主要类是 AuthenticationManager,我们看看它是怎样进行验证管理的 - 验证的调用入口是 authenticate 在 AbstractAuthenticationManager 的实现中:
//这是进行验证的函数,返回一个 Authentication 对象来记录验证的结果,其中包含了用户的验证信息,权限配置等,同时这个 Authentication 会以后被授权模块使用
代码
//如果验证失败,那么在验证过程中会直接抛出异常
public final Authentication authenticate(Authentication authRequest)
throws AuthenticationException {
try {//这里是实际的验证处理,我们下面使用 ProviderManager 来说明具体的验证过程,传入的参数 authRequest 里面已经包含了从 HttpServletRequest 中得到的用户输入的用户名和密码
Authentication authResult = doAuthentication(authRequest);
copyDetails(authRequest, authResult);
return authResult;
} catch (AuthenticationException e) {
e.setAuthentication(authRequest);
throw e;
}
}
在 ProviderManager 中进行实际的验证工作,假设这里使用数据库来存取用户信息:
代码
public Authentication doAuthentication(Authentication authentication)
throws AuthenticationException {
//这里取得配置好的 provider 链的迭代器,在配置的时候可以配置多个 provider,这里我们配置的是 DaoAuthenticationProvider 来说明, 它使用数据库来保存用户的用户名和密码信息。
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());
//这个 result 包含了验证中得到的结果信息
Authentication result = null;
try {//这里是 provider 进行验证处理的过程
result = provider.authenticate(authentication);
sessionController.checkAuthenticationAllowed(result);
} catch (AuthenticationException ae) {
lastException = ae;
result = null;
}
if (result != null) {
sessionController.registerSuccessfulAuthentication(result);
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}"));
}
// 这里发布事件来通知上下文的监听器
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) {
publishEvent(event);
} else {
if (logger.isDebugEnabled()) {
logger.debug("No event was found for the exception " + lastException.getClass().getName());
}
}
// Throw the exception
throw lastException;
}
我们下面看看在 DaoAuthenticationProvider 是怎样从数据库中取出对应的验证信息进行用户验证的,在它的基类 AbstractUserDetailsAuthenticationProvider 定义了验证的处理模板:
代码
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 这里取得用户输入的用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
// 如果配置了缓存,从缓存中去取以前存入的用户验证信息 - 这里是 UserDetail,是服务器端存在数据库里的用户信息,这样就不用每次都去数据库中取了
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
//没有取到,设置标志位,下面会把这次取到的服务器端用户信息存入缓存中去
if (user == null) {
cacheWasUsed = false;
try {//这里是调用 UserDetailService 去取用户数据库里信息的地方
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 {//这里是验证过程,在 retrieveUser 中从数据库中得到用户的信息,在 additionalAuthenticationChecks 中进行对比用户输入和服务器端的用户信息
//如果验证通过,那么构造一个 Authentication 对象来让以后的授权使用,如果验证不通过,直接抛出异常结束鉴权过程
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (ie not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} else {
throw exception;
}
}
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();
}
//最后返回 Authentication 记录了验证结果供以后的授权使用
return createSuccessAuthentication(principalToReturn, authentication, user);
}
//这是是调用 UserDetailService 去加载服务器端用户信息的地方,从什么地方加载要看设置,这里我们假设由 JdbcDaoImp 来从数据中进行加载
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
//这里调用 UserDetailService 去从数据库中加载用户验证信息,同时返回从数据库中返回的信息,这些信息放到了 UserDetails 对象中去了
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
} catch (DataAccessException repositoryProblem) {
throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new AuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
下面我们重点分析一下 JdbcDaoImp 这个类来看看具体是怎样从数据库中得到用户信息的:
代码
public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService {
//~ Static fields/initializers ==============================================================
//这里是预定义好的对查询语句,对应于默认的数据库表结构,也可以自己定义查询语句对应特定的用户数据库验证表的设计
public static final String DEF_USERS_BY_USERNAME_QUERY =
"SELECT username,password,enabled FROM users WHERE username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
"SELECT username,authority FROM authorities WHERE username = ?";
//~ Instance fields ===============================================================
//这里使用 Spring JDBC 来进行数据库操作
protected MappingSqlQuery authoritiesByUsernameMapping;
protected MappingSqlQuery usersByUsernameMapping;
private String authoritiesByUsernameQuery;
private String rolePrefix = "";
private String usersByUsernameQuery;
private boolean usernameBasedPrimaryKey = true;
//~ Constructors ===============================================================
//在初始化函数中把查询语句设置为预定义的 SQL 语句
public JdbcDaoImpl() {
usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;
authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;
}
//~ Methods ==============================================================
protected void addCustomAuthorities(String username, List authorities) {}
public String getAuthoritiesByUsernameQuery() {
return authoritiesByUsernameQuery;
}
public String getRolePrefix() {
return rolePrefix;
}
public String getUsersByUsernameQuery() {
return usersByUsernameQuery;
}
protected void initDao() throws ApplicationContextException {
initMappingSqlQueries();
}
/**
* Extension point to allow other MappingSqlQuery objects to be substituted in a subclass
*/
protected void initMappingSqlQueries() {
this.usersByUsernameMapping = new UsersByUsernameMapping(getDataSource());
this.authoritiesByUsernameMapping = new AuthoritiesByUsernameMapping(getDataSource());
}
public boolean isUsernameBasedPrimaryKey() {
return usernameBasedPrimaryKey;
}
//这里是取得数据库用户信息的具体过程
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
//根据用户名在用户表中得到用户信息,包括用户名,密码和用户是否有效的信息
List users = usersByUsernameMapping.execute(username);
if (users.size() == 0) {
throw new UsernameNotFoundException("User not found");
}
//取集合中的第一个作为有效的用户对象
UserDetails user = (UserDetails) users.get(0); // contains no GrantedAuthority[]
//这里在权限表中去取得用户的权限信息,同样的返回一个权限集合对应于这个用户
List dbAuths = authoritiesByUsernameMapping.execute(user.getUsername());
addCustomAuthorities(user.getUsername(), dbAuths);
if (dbAuths.size() == 0) {
throw new UsernameNotFoundException("User has no GrantedAuthority");
}
//这里根据得到的权限集合来配置返回的 User 对象供以后使用
GrantedAuthority[] arrayAuths = (GrantedAuthority[]) dbAuths.toArray(new GrantedAuthority[dbAuths.size()]);
String returnUsername = user.getUsername();
if (!usernameBasedPrimaryKey) {
returnUsername = username;
}
return new User(returnUsername, user.getPassword(), user.isEnabled(), true, true, true, arrayAuths);
}
public void setAuthoritiesByUsernameQuery(String queryString) {
authoritiesByUsernameQuery = queryString;
}
public void setRolePrefix(String rolePrefix) {
this.rolePrefix = rolePrefix;
}
public void setUsernameBasedPrimaryKey(boolean usernameBasedPrimaryKey) {
this.usernameBasedPrimaryKey = usernameBasedPrimaryKey;
}
public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
this.usersByUsernameQuery = usersByUsernameQueryString;
}
//~ Inner Classes ==============================================================
/**
* 这里是调用 Spring JDBC 的数据库操作,具体可以参考对 JDBC 的分析,这个类的作用是把数据库查询得到的记录集合转换为对象集合 - 一个很简单的 O/R 实现
*/
protected class AuthoritiesByUsernameMapping extends MappingSqlQuery {
protected AuthoritiesByUsernameMapping(DataSource ds) {
super(ds, authoritiesByUsernameQuery);
declareParameter(new SqlParameter(Types.VARCHAR));
compile();
}
protected Object mapRow(ResultSet rs, int rownum)
throws SQLException {
String roleName = rolePrefix + rs.getString(2);
GrantedAuthorityImpl authority = new GrantedAuthorityImpl(roleName);
return authority;
}
}
/**
* Query object to look up a user.
*/
protected class UsersByUsernameMapping extends MappingSqlQuery {
protected UsersByUsernameMapping(DataSource ds) {
super(ds, usersByUsernameQuery);
declareParameter(new SqlParameter(Types.VARCHAR));
compile();
}
protected Object mapRow(ResultSet rs, int rownum)
throws SQLException {
String username = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
UserDetails user = new User(username, password, enabled, true, true, true,
new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")});
return user;
}
}
}
从数据库中得到用户信息后,就是一个比对用户输入的信息和这个数据库用户信息的比对过程,这个比对过程在 DaoAuthenticationProvider:
代码
//这个 UserDetail 是从数据库中查询到的,这个 authentication 是从用户输入中得到的
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) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"),
includeDetailsObject ? userDetails : null);
}
//这里取得用户输入的密码
String presentedPassword = authentication.getCredentials() == null ? "" : authentication.getCredentials().toString();
//这里判断用户输入的密码是不是和数据库里的密码相同,这里可以使用 passwordEncoder 来对数据库里的密码加解密
// 如果不相同,抛出异常,如果相同则鉴权成功
if (!passwordEncoder.isPasswordValid(
userDetails.getPassword(), presentedPassword, salt)) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"),
includeDetailsObject ? userDetails : null);
}
}
上面分析了整个 Acegi 进行验证的过程,从 AuthenticationProcessingFilter 中拦截Http请求得到用户输入的用户名和密码,这些用户输入的验证信息会被放到 Authentication 对象中持有并传递给 AuthenticatioManager 来对比在服务端的用户信息来完成整个鉴权。这个鉴权完成以后会把有效的用户信息放在一个 Authentication 中供以后的授权模块使用。在具体的鉴权过程中,使用了我们配置好的各种 Provider 以及对应的 UserDetailService 和 Encoder 类来完成相应的获取服务器端用户数据以及与用户输入的验证信息的比对工作。