簡單分析一下Spring Acegi的源代碼實現:
Servlet.Filter的實現AuthenticationProcessingFilter啟動Web頁面的驗證過程 - 在AbstractProcessingFilter定義了整個驗證過程的模板:
Java代碼
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中的具體驗證過程是這樣的:
Java代碼
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會以後被授權模塊使用
Java代碼
//如果驗證失敗,那麼在驗證過程中會直接拋出異常
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中進行實際的驗證工作,假設這裡使用數據庫來存取用戶信息:
Java代碼
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定義了驗證的 處理模板:
Java代碼
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這個類來看看具體是怎樣從數據庫中得到用戶信息 的:
Java代碼
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:
Java代碼
//這個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類來完成相應的獲取服務器端用戶數據以及與用戶輸入的驗 證信息的比對工作。