SpringSecurity-UsernamePasswordAuthenticationFilter的作用


  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的工作過程,有助於重寫一些自己的邏輯,但不涉及重寫的具體實現(這些實現很多是可以在網上找到的)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM