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的工作过程,有助于重写一些自己的逻辑,但不涉及重写的具体实现(这些实现很多是可以在网上找到的)。

https://www.cnblogs.com/zsxneil/p/6622564.html

原文地址:https://www.cnblogs.com/sjqq/p/10132974.html