UsernamePasswordAuthenticationFilter應該是我們最關注的Filter,因為它實現了我們最常用的基於用戶名和密碼的認證邏輯。
先看一下一個常用的form-login配置:
1 <form-login login-page="/login" 2 username-parameter="ssoId" 3 password-parameter="password" 4 authentication-failure-url ="/loginfailure" 5 default-target-url="/loginsuccess"/> 6 <logout invalidate-session="true"/>
在這里可以自定義表單中對應的用戶名密碼的name,已經登錄登錄成功或失敗后跳轉的url地址以及登錄表單的action。
UsernamePasswordAuthenticationFilter繼承虛擬類AbstractAuthenticationProcessingFilter。
AbstractAuthenticationProcessingFilter要求設置一個authenticationManager,authenticationManager的實現類將實際處理請求的認證。AbstractAuthenticationProcessingFilter將攔截符合過濾規則的request,並試圖執行認證。子類必須實現 attemptAuthentication 方法,這個方法執行具體的認證。
認證處理:如果認證成功,將會把返回的Authentication對象存放在SecurityContext;然后setAuthenticationSuccessHandler(AuthenticationSuccessHandler)
方法將會調用;這里處理認證成功后跳轉url的邏輯;可以重新實現AuthenticationSuccessHandler的onAuthenticationSuccess方法,實現自己的邏輯,比如需要返回json格式數據時,就可以在這里重新相關邏輯。如果認證失敗,默認會返回401代碼給客戶端,當然也可以在<form-login>節點中配置失敗后跳轉的url,還可以重寫AuthenticationFailureHandler的onAuthenticationFailure方法實現自己的邏輯。
一個典型的自定義配置如下:
1 <beans:bean id="restfulUsernamePasswordAuthenticationFilter" 2 class="com.kingdee.core.config.RestfulUsernamePasswordAuthenticationFilter"> 3 <beans:property name="authenticationManager" ref="authenticationManager" /> 4 <beans:property name="authenticationSuccessHandler" ref="restfulAuthenticationSuccessHandler" /> 5 <beans:property name="authenticationFailureHandler" ref="restfulAuthenticationFailureHandler" /> 6 <beans:property name="loginUrl" value="/login/restful" /> 7 </beans:bean>
下面先看一下authentication-manager的配置,這個配置實現自定義UserDetail,需要重新實現一個繼承UserDetailsService接口的類。
1 <authentication-manager alias="authenticationManager"> 2 <authentication-provider user-service-ref="customUserDetailsService"> 3 <password-encoder ref="bcryptEncoder"/> 4 </authentication-provider> 5 </authentication-manager>
我們看到authentication-manager節點有一個子節點authentication-provider,而authentication-provider有一個屬性user-service-ref,user-service-ref的值就是我們要實現的自定義類。
整個調用過程大致如下:
繼承虛擬類AbstractAuthenticationProcessingFilter的UsernamePasswordAuthenticationFilter實現了attemptAuthentication方法
1 public Authentication attemptAuthentication(HttpServletRequest request, 2 HttpServletResponse response) throws AuthenticationException { 3 if (postOnly && !request.getMethod().equals("POST")) { 4 throw new AuthenticationServiceException( 5 "Authentication method not supported: " + request.getMethod()); 6 } 7 8 String username = obtainUsername(request); 9 String password = obtainPassword(request); 10 11 if (username == null) { 12 username = ""; 13 } 14 15 if (password == null) { 16 password = ""; 17 } 18 19 username = username.trim(); 20 21 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( 22 username, password); 23 24 // Allow subclasses to set the "details" property 25 setDetails(request, authRequest); 26 27 return this.getAuthenticationManager().authenticate(authRequest); 28 }
這個方法的最后this.getAuthenticationManager().authenticate(authRequest)是實現自接口Authentication,而實現這個接口的類中有一個叫ProviderManager的,它有一個成員變量List<AuthenticationProvider>,對應於我們配置文件中的authentication-provider,這里也說明是可以配置多個authentication-provider的。我們只使用一個我們需要的。我們需要關注的是AbstractUserDetailsAuthenticationProvider這個虛擬類,它實現了我們所需要的authenticate方法:
1 public Authentication authenticate(Authentication authentication) 2 throws AuthenticationException { 3 Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, 4 messages.getMessage( 5 "AbstractUserDetailsAuthenticationProvider.onlySupports", 6 "Only UsernamePasswordAuthenticationToken is supported")); 7 8 // Determine username 9 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" 10 : authentication.getName(); 11 12 boolean cacheWasUsed = true; 13 UserDetails user = this.userCache.getUserFromCache(username); 14 15 if (user == null) { 16 cacheWasUsed = false; 17 18 try { 19 user = retrieveUser(username, 20 (UsernamePasswordAuthenticationToken) authentication); 21 } 22 catch (UsernameNotFoundException notFound) { 23 logger.debug("User '" + username + "' not found"); 24 25 if (hideUserNotFoundExceptions) { 26 throw new BadCredentialsException(messages.getMessage( 27 "AbstractUserDetailsAuthenticationProvider.badCredentials", 28 "Bad credentials")); 29 } 30 else { 31 throw notFound; 32 } 33 } 34 35 Assert.notNull(user, 36 "retrieveUser returned null - a violation of the interface contract"); 37 } 38 39 try { 40 preAuthenticationChecks.check(user); 41 additionalAuthenticationChecks(user, 42 (UsernamePasswordAuthenticationToken) authentication); 43 } 44 catch (AuthenticationException exception) { 45 if (cacheWasUsed) { 46 // There was a problem, so try again after checking 47 // we're using latest data (i.e. not from the cache) 48 cacheWasUsed = false; 49 user = retrieveUser(username, 50 (UsernamePasswordAuthenticationToken) authentication); 51 preAuthenticationChecks.check(user); 52 additionalAuthenticationChecks(user, 53 (UsernamePasswordAuthenticationToken) authentication); 54 } 55 else { 56 throw exception; 57 } 58 } 59 60 postAuthenticationChecks.check(user); 61 62 if (!cacheWasUsed) { 63 this.userCache.putUserInCache(user); 64 } 65 66 Object principalToReturn = user; 67 68 if (forcePrincipalAsString) { 69 principalToReturn = user.getUsername(); 70 } 71 72 return createSuccessAuthentication(principalToReturn, authentication, user); 73 }
從代碼中可以看到,它會先從cache中取user(這與配置有關,這里我們不涉及),如果沒有,在執行retrieveUser方法。代碼中還可以看到,UsernameNotFoundException默認是被轉換成BadCredentialsException的。
它的子類DaoAuthenticationProvider重寫了retrieveUser方法:
1 protected final UserDetails retrieveUser(String username, 2 UsernamePasswordAuthenticationToken authentication) 3 throws AuthenticationException { 4 UserDetails loadedUser; 5 6 try { 7 loadedUser = this.getUserDetailsService().loadUserByUsername(username); 8 } 9 catch (UsernameNotFoundException notFound) { 10 if (authentication.getCredentials() != null) { 11 String presentedPassword = authentication.getCredentials().toString(); 12 passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, 13 presentedPassword, null); 14 } 15 throw notFound; 16 } 17 catch (Exception repositoryProblem) { 18 throw new InternalAuthenticationServiceException( 19 repositoryProblem.getMessage(), repositoryProblem); 20 } 21 22 if (loadedUser == null) { 23 throw new InternalAuthenticationServiceException( 24 "UserDetailsService returned null, which is an interface contract violation"); 25 } 26 return loadedUser; 27 }
在代碼第7行可以看到,UserDetails從UserDetailsService().loadUserByUsername(username)中獲得的。我們已經配置了userService方法,所以只要在配置類中重寫loadUserByUsername(username)方法就可以了。這里需要注意的是我們重寫的方法需要返回一個實現了UserDetails接口的對象,而org.springframework.security.core.userdetails.User就是我們經常實際返回的對象。
它的一個構造方法如下:
1 public User(String username, String password, boolean enabled, 2 boolean accountNonExpired, boolean credentialsNonExpired, 3 boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { 4 5 if (((username == null) || "".equals(username)) || (password == null)) { 6 throw new IllegalArgumentException( 7 "Cannot pass null or empty values to constructor"); 8 } 9 10 this.username = username; 11 this.password = password; 12 this.enabled = enabled; 13 this.accountNonExpired = accountNonExpired; 14 this.credentialsNonExpired = credentialsNonExpired; 15 this.accountNonLocked = accountNonLocked; 16 this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); 17 }
我們根據自己的需要,從數據庫中取得user和user對應的權限,構造一個org.springframework.security.core.userdetails.User返回即可。
這里只是重新實現了User的認證方法,如果想在SecurityContext中添加用戶的其他信息,如email,address等,可以新指定一個authentication-provider的實現類,可以實現復用DaoAuthenticationProvider的大部分代碼,只需要添加authentication.setDetails的相關代碼即可。雖然UsernamePasswordAuthenticationFilter的注釋是在setDetails(request, authRequest);方法中實現添加自定義的details,但也可以根據實際情況修改。甚至可以不用在這里修改,直接把需要的信息放在httpSession中。
這些博客都是重在理解SpringSecurity的工作過程,有助於重寫一些自己的邏輯,但不涉及重寫的具體實現(這些實現很多是可以在網上找到的)。
