Spring Security 3.1 自定义 authentication provider

来源:http://www.xeclipse.com/?p=1359


前言

在使用Spring Security的时候,遇到一个比较特殊的情况,需要根据用户名、邮箱等多个条件去验证用户或者使用第三方的验证服务来进行用户名和密码的判断,这样SS(Spring Security,一下简称SS)内置的authentication provider和user detail service就不能用了,花了一些时间去寻找其他的办法。

前置条件

  • Spring MVC 结构的Web项目
  • Spring Security
  • 使用第三方的Service验证用户名密码(并非数据库或者OpenID等SS已经支持的服务)

需求

  • 根据用户输入的用户名和密码验证登录

问题分析

在尝试了修改Filter、替换SS内置的Filter之后,发现了一个比较简单的方法,这里简单的讲讲思路。

先看看SS验证用户名和密码的过程

DelegatingFilterProxy(Security filter chain):

  1.   ConcurrentSessionFilter
  2.   SecurityContextPersistenceFilter
  3.   LogoutFilter
  4.   UsernamePasswordAuthenticationFilter
  5.   RequestCacheAwareFilter
  6.   SecurityContextHolderAwareRequestFilter
  7.   RememberMeAuthenticationFilter
  8.   AnonymousAuthenticationFilter
  9.   SessionManagementFilter
  10.   ExceptionTranslationFilter
  11.   FilterSecurityInterceptor

这些都是内置的Filter,请求会从上往下依次过滤。这里由于我们主要关心用户名和密码的验证,所以就要从UsernamePasswordAuthenticationFilter下手了。(UsernamePasswordAuthenticationFilter需要AuthenticationManager去进行验证。)

在SS的配置文件里面可以看到,如何给SS传递用户验证信息数据源(设置AuthenticationManager):

1
2
3
4
5
6
7
< authentication-manager >
     < authentication-provider >
         < user-service >
             < user name = "admin" authorities = "ROLE_USER" password = "admin" />
         user-service >
     authentication-provider >
authentication-manager >

当然这里是一个最简单的配置,跟踪一下代码就会发现:

  • 内置的AuthenticationManager为org.springframework.security.authentication.ProviderManager
  • 默认的AuthenticationProvider为org.springframework.security.authentication.dao.DaoAuthenticationProvider
  • 再往下看,这个authentication provider使用org.springframework.security.core.userdetails.UserDetailsService去进行验证
  • 到这里用过SS的都清楚了,只需要实现一个UserDetailService,写写下面这个方法就OK了:
1
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
到这里问题就出来了
这个方法只有一个参数,怎么才能传递多个参数呢?

一个可用的解决方案

重新自定义一个authentication provider,替换掉默认的DaoAuthenticationProvider.
先看看XML的配置:
1
2
3
4
5
6
7
8
9
< authentication-manager alias = "authenticationManager" >
     < authentication-provider ref = "loginAuthenticationProvider" >
     authentication-provider >
authentication-manager >
 
< bean id = "loginAuthenticationProvider"
     class = "com.XXX.examples.security.LoginAuthenticationProvider" >
     < property name = "userDetailsService" ref = "loginUserDetailService" > property >
bean >
这里我们依然仿照DaoAuthenticationProvider,传递一个UserDetailService给它,下面看看其实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
public class LoginAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
{
 
     // ~ Instance fields
     // ================================================================================================
 
     private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();
 
     private SaltSource saltSource;
 
     private LoginUserDetailsService userDetailsService;
 
     // ~ Methods
     // ========================================================================================================
 
     protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                   UsernamePasswordAuthenticationToken authentication)
             throws AuthenticationException
     {
         Object salt = null ;
 
         if ( this .saltSource != null )
         {
             salt = this .saltSource.getSalt(userDetails);
         }
 
         if (authentication.getCredentials() == null )
         {
             logger.debug( "Authentication failed: no credentials provided" );
 
             throw new BadCredentialsException( "Bad credentials:" + userDetails);
         }
 
         String presentedPassword = authentication.getCredentials().toString();
 
         if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt))
         {
             logger.debug( "Authentication failed: password does not match stored value" );
 
             throw new BadCredentialsException( "Bad credentials:" + userDetails);
         }
     }
 
     protected void doAfterPropertiesSet() throws Exception
     {
         Assert.notNull( this .userDetailsService, "A UserDetailsService must be set" );
     }
 
     protected PasswordEncoder getPasswordEncoder()
     {
         return passwordEncoder;
     }
 
     protected SaltSource getSaltSource()
     {
         return saltSource;
     }
 
     protected LoginUserDetailsService getUserDetailsService()
     {
         return userDetailsService;
     }
 
     protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
             throws AuthenticationException
     {
         UserDetails loadedUser;
 
         try
         {
             String password = (String) authentication.getCredentials();
             loadedUser = getUserDetailsService().loadUserByUsername(username, password); //区别在这里
         }
         catch (UsernameNotFoundException notFound)
         {
             throw notFound;
         }
         catch (Exception repositoryProblem)
         {
             throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
         }
 
         if (loadedUser == null )
         {
             throw new AuthenticationServiceException(
                                                      "UserDetailsService returned null, which is an interface contract violation" );
         }
         return loadedUser;
     }
 
     /**
      * Sets the PasswordEncoder instance to be used to encode and validate
      * passwords. If not set, the password will be compared as plain text.
      *

      * For systems which are already using salted password which are encoded
      * with a previous release, the encoder should be of type
      * {@code org.springframework.security.authentication.encoding.PasswordEncoder}
      * . Otherwise, the recommended approach is to use
      * {@code org.springframework.security.crypto.password.PasswordEncoder}.
      *
      * @param passwordEncoder
      *            must be an instance of one of the {@code PasswordEncoder}
      *            types.
      */
     public void setPasswordEncoder(Object passwordEncoder)
     {
         Assert.notNull(passwordEncoder, "passwordEncoder cannot be null" );
 
         if (passwordEncoder instanceof PasswordEncoder)
         {
             this .passwordEncoder = (PasswordEncoder) passwordEncoder;
             return ;
         }
 
         if (passwordEncoder instanceof org.springframework.security.crypto.password.PasswordEncoder)
         {
             final org.springframework.security.crypto.password.PasswordEncoder delegate = (org.springframework.security.crypto.password.PasswordEncoder) passwordEncoder;
             this .passwordEncoder = new PasswordEncoder()
             {
                 private void checkSalt(Object salt)
                 {
                     Assert.isNull(salt, "Salt value must be null when used with crypto module PasswordEncoder" );
                 }
 
                 public String encodePassword(String rawPass, Object salt)
                 {
                     checkSalt(salt);
                     return delegate.encode(rawPass);
                 }
 
                 public boolean isPasswordValid(String encPass, String rawPass, Object salt)
                 {
                     checkSalt(salt);
                     return delegate.matches(rawPass, encPass);
                 }
             };
 
             return ;
         }
 
         throw new IllegalArgumentException( "passwordEncoder must be a PasswordEncoder instance" );
     }
 
     /**
      * The source of salts to use when decoding passwords. null is
      * a valid value, meaning the DaoAuthenticationProvider will
      * present null to the relevant PasswordEncoder.
      *

      * Instead, it is recommended that you use an encoder which uses a random
      * salt and combines it with the password field. This is the default
      * approach taken in the
      * {@code org.springframework.security.crypto.password} package.
      *
      * @param saltSource
      *            to use when attempting to decode passwords via the
      *            PasswordEncoder
      */
     public void setSaltSource(SaltSource saltSource)
     {
         this .saltSource = saltSource;
     }
 
     public void setUserDetailsService(LoginUserDetailsService userDetailsService)
     {
         this .userDetailsService = userDetailsService;
     }
}
代码跟DaoAuthenticationProvider几乎一样,只是我们替换了UserDetailService,使用自定义的一个新的LoginUserDetailService:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface LoginUserDetailsService
{
     /**
      * 功能描述:根据用户米密码验证用户信息
      *

      * 前置条件:
      *

      * 方法影响:
      *

      * Author , 2012-9-26
      *
      * @since server 2.0
      * @param username
      * @param password
      * @return
      * @throws UsernameNotFoundException
      */
     UserDetails loadUserByUsername(String username, String password) throws UsernameNotFoundException;
}
一个简单的实现LoginUserDetailsServiceImpl:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class LoginUserDetailsServiceImpl implements LoginUserDetailsService
{
 
     private UserAccountService userAccountService;
 
     /**
      *
      */
     public LoginUserDetailsServiceImpl()
     {
     }
 
     /**
      * getter method
      *
      * @see LoginUserDetailsServiceImpl#userAccountService
      * @return the userAccountService
      */
     public UserAccountService getUserAccountService()
     {
         return userAccountService;
     }
 
     /**
      * 功能描述:查找登录的用户
      *

      * 前置条件:
      *

      * 方法影响:
      *

      * Author , 2012-9-26
      *
      * @since server 2.0
      * @param username
      * @return
      */
     public UserDetails loadUserByUsername(String username, String password) throws UsernameNotFoundException
     {
         boolean result = userAccountService.validate(username, password);
         if (!result)
         {
             return null ;
         }
 
         List authorities = new ArrayList();
         GrantedAuthorityImpl grantedAuthorityImpl = new GrantedAuthorityImpl();
         authorities.add(grantedAuthorityImpl);
         LoginUserDetailsImpl user = new LoginUserDetailsImpl(username, password, authorities);
         grantedAuthorityImpl.setDelegate(user);
     }
 
     /**
      * setter method
      *
      * @see LoginUserDetailsServiceImpl#userAccountService
      * @param userAccountService
      *            the userAccountService to set
      */
     public void setUserAccountService(UserAccountService userAccountService)
     {
         this .userAccountService = userAccountService;
     }
 
}
其他相关的代码:
GrantedAuthorityImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class GrantedAuthorityImpl implements GrantedAuthority
{
 
     /**
      * ROLE USER 权限
      */
     private static final String ROLE_USER = "ROLE_USER" ;
 
     /**
      * Serial version UID
      */
     private static final long serialVersionUID = 1L;
 
     private UserDetails delegate;
 
     public GrantedAuthorityImpl(UserDetails user)
     {
         this .delegate = user;
     }
 
     /**
      *
      */
     public GrantedAuthorityImpl()
     {
     }
 
     public String getAuthority()
     {
         return ROLE_USER;
     }
 
     /**
      * setter method
      *
      * @see GrantedAuthorityImpl#delegate
      * @param delegate
      *            the delegate to set
      */
     public void setDelegate(UserDetails delegate)
     {
         this .delegate = delegate;
     }
 
     /**
      * getter method
      *
      * @see GrantedAuthorityImpl#delegate
      * @return the delegate
      */
     public UserDetails getDelegate()
     {
         return delegate;
     }
 
}
LoginUserDetailsImpl
1
2

你可能感兴趣的:(Spring)