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中。