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