Spring Security Oauth2 中优雅的扩展自定义(短信验证码)登录方式

方案引入

跟踪Spring Security的登录逻辑发现,帐号密码的验证是在tokenGranter中完成的。帐号密码方式对应的是 org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter
而Spring Security找到对应的tokenGranter是通过登录时的一个表单参数grant_type来找到的,那是不是可以通过扩展一个TokenGranter来达成想要的效果呢

Spring Security默认是同时支持多重grant_type的(根据客户端的配制决定特定客户端支持特定的grant_type),而AuthorizationServerConfigurerAdapter的配制中tokenGranter又只能设置一个,那么Spring Security是怎么实现多个的呢?经过折腾发现了一个org.springframework.security.oauth2.provider.CompositeTokenGranter,原来是通过它来实现的

接下来研究如何向CompositeTokenGranter中增加自定义的TokenGranter,结果发现Spring Security在创建CompositeTokenGranter的时候已经把内置的TokenGranter写死,没法通过它的机制扩展。唯一的方法就是直接使用CompositeTokenGranter。那么我们还是想要内置的TokenGranter也一起工作怎么办?最后无奈的选择把创建内置TokenGranter的代码copy出来并修改,能用…直接上代码

以下基于spring-security-oauth2 + redis认证服务器改造

认证配置

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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package cn.appblog.security.oauth2.config;

import cn.appblog.security.oauth2.enhancer.UserTokenEnhancer;
import cn.appblog.security.oauth2.granter.SMSCodeTokenGranter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.PostConstruct;
import java.util.*;

/**
* @author yezhou
* @version 1.0
* @date 2019/8/15 14:11
*/
@Slf4j
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfigure extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserDetailsService userDetailsService; // 这是提供根据用户名查用户的方式给spring使用的

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private TokenStore tokenStore;

/**
之前有个 public void configure(ClientDetailsServiceConfigurer clients) 配制客户端的方法, 但是因为直接使用 CompositeTokenGranter, 所以此方法不生效了
就在这里配制, 同时使用这样的配制方式, 后面可以改成从库里获取, 自己实现一个 ClientDetailsService 就行
由于之前的 Builder方式只能在 ClientDetailsServiceConfigurer 中使用, 所以这里暂时先这样了, 后期可改为从库里获取
*/
@Bean
public ClientDetailsService clientDetailsService() {
log.info("OAuth2AuthorizationServerConfigure.clientDetailsService()");
BaseClientDetails result = new BaseClientDetails();
result.setClientId("client_sms");
List<String> authorizedGrantTypes = new ArrayList<>();
authorizedGrantTypes.add("refresh_token");
authorizedGrantTypes.add("sms_code");
result.setAuthorizedGrantTypes(authorizedGrantTypes); // 这个 client 支持的 grant_type
//result.setClientSecret("$2a$10$9s0p62wfKh7WT64a/VYFpOAk19GsrHh5C7Ty9.wPRWX40cjq7Rmu."); // 该密码是用 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 搞出来的, 明文是 123456
result.setClientSecret(passwordEncoder.encode("123456"));
List<String> scopes = new ArrayList<>();
scopes.add("select");
result.setScope(scopes);
result.setAuthorities(AuthorityUtils.createAuthorityList("client"));

Map<String, ClientDetails> clientDetails = new HashMap<String, ClientDetails>();
clientDetails.put(result.getClientId(), result);

InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService();
clientDetailsService.setClientDetailsStore(clientDetails);
return clientDetailsService;
}

private AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices(); //使用默认
}

private OAuth2RequestFactory requestFactory() {
return new DefaultOAuth2RequestFactory(clientDetailsService()); //使用默认
}

/**
* 这是从spring 的代码中 copy出来的,默认的几个 TokenGranter, 我们自定义的就加到这里就行了,目前我还没有加
*/
private List<TokenGranter> getDefaultTokenGranters() {
log.info("OAuth2AuthorizationServerConfigure.getDefaultTokenGranters");
ClientDetailsService clientDetails = clientDetailsService();
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();

List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices,
authorizationCodeServices, clientDetails, requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails,
requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(
new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager,
tokenServices, clientDetails, requestFactory));
}
tokenGranters.add(new SMSCodeTokenGranter(tokenServices, clientDetails, requestFactory));
return tokenGranters;
}

/**
* 通过 tokenGranter 塞进去的就是它了
*/
private TokenGranter tokenGranter() {
log.info("OAuth2AuthorizationServerConfigure.tokenGranter");
TokenGranter tokenGranter = new TokenGranter() {
private CompositeTokenGranter delegate;

@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (delegate == null) {
delegate = new CompositeTokenGranter(getDefaultTokenGranters());
}
return delegate.grant(grantType, tokenRequest);
}
};
return tokenGranter;
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
log.info("OAuth2AuthorizationServerConfigure.configure");
//endpoints.tokenStore(tokenStore())
endpoints.tokenStore(tokenStore)
// .accessTokenConverter(accessTokenConverter())
.tokenGranter(tokenGranter())
// .tokenEnhancer(tokenEnhancerChain) //设置 tokenGranter 后该配制失效, 需要在 tokenServices() 中设置
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService) //refresh_token 需要配制它, 否则会 UserDetailsService is required
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}

public TokenEnhancer tokenEnhancer() {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
//tokenEnhancerChain.setTokenEnhancers(Arrays.asList(new UserTokenEnhancer(), accessTokenConverter())); // CustomTokenEnhancer 是我自定义一些数据放到token里用的
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(new UserTokenEnhancer())); // CustomTokenEnhancer 是我自定义一些数据放到token里用的
return tokenEnhancerChain;
}

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//允许表单认证
oauthServer.allowFormAuthenticationForClients();
// .checkTokenAccess("permitAll()"); // 允许check_token, 因为用了JWT, 客户端可以验证签名, 生产中可以不用
}

// public TokenStore tokenStore() {
// return new JwtTokenStore(accessTokenConverter());
// }

// public JwtAccessTokenConverter accessTokenConverter() {
// JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("authorizationKey.jks"), "123456".toCharArray());
// converter.setKeyPair(keyStoreKeyFactory.getKeyPair("klw"));
// return converter;
// }

@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
//defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setTokenStore(tokenStore);
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenEnhancer(tokenEnhancer()); // 如果没有设置它, JWT就会失效
return defaultTokenServices;
}
}

到这里,我们就可以愉快的自己扩展TokenGranter了,参考ResourceOwnerPasswordTokenGranter,并把自定义的TokenGranter添加到getDefaultTokenGranters()返回的list中

增加短信验证码的TokenGranter

参考org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter的代码

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
package cn.appblog.security.oauth2.granter;

import cn.appblog.security.oauth2.service.UserAuthDetailsService;
import cn.appblog.security.oauth2.utils.ApplicationContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author yezhou
* @version 1.0
* @date 2019/8/15 13:59
*/

@Slf4j
public class SMSCodeTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "sms_code";

private UserAuthDetailsService authUserDetailsService;

public SMSCodeTokenGranter(AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}

@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client,
TokenRequest tokenRequest) {

Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String userMobileNo = parameters.get("username"); //客户端提交的用户名
String smsCode = parameters.get("sms_code"); //客户端提交的验证码

// 从库里查用户
// UserDetails user = 从库里查找用户的代码略;
// if (user == null) {
// throw new InvalidGrantException("用户不存在");
// }
authUserDetailsService = (UserAuthDetailsService) ApplicationContextUtil.getBean("UserAuthDetailsService");
UserDetails user = authUserDetailsService.loadUserByUsername(userMobileNo);

//验证用户状态(是否警用等),代码略

// 验证验证码
//String smsCodeCached = 获取服务中保存的用户验证码, 代码略. 一般我们是在生成好后放到缓存中
String smsCodeCached = "888888";
if (StringUtils.isBlank(smsCodeCached)) {
throw new InvalidGrantException("用户没有发送验证码");
}
if (!smsCode.equals(smsCodeCached)) {
throw new InvalidGrantException("验证码不正确");
} else {
log.info("验证通过: [userMobileNo: {}, smsCode: {}]", userMobileNo, smsCode);
//验证通过后从缓存中移除验证码, 代码略
}

Authentication userAuth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
// 关于user.getAuthorities(): 我们的自定义用户实体已实现
// org.springframework.security.core.userdetails.UserDetails 接口的, 所以有 user.getAuthorities()
// 当然该参数传null也行
((AbstractAuthenticationToken) userAuth).setDetails(parameters);

OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}

SMSCodeTokenGranter加入到CompositeTokenGranter需要的List<TokenGranter>中,继续修改OAuth2AuthorizationServerConfig类,在getDefaultTokenGranters方法中加入:

1
tokenGranters.add(new SMSCodeTokenGranter(tokenServices, clientDetails, requestFactory));

getDefaultTokenGranters的完整代码:

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
/**
* 这是从spring 的代码中 copy出来的,默认的几个 TokenGranter, 我们自定义的就加到这里就行了,目前我还没有加
*/
private List<TokenGranter> getDefaultTokenGranters() {
log.info("OAuth2AuthorizationServerConfigure.getDefaultTokenGranters");
ClientDetailsService clientDetails = clientDetailsService();
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();

List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices,
authorizationCodeServices, clientDetails, requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails,
requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(
new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager,
tokenServices, clientDetails, requestFactory));
}
tokenGranters.add(new SMSCodeTokenGranter(tokenServices, clientDetails, requestFactory));
return tokenGranters;
}

校验测试

访问:http://localhost:9003/oauth/token?username=15000778868&sms_code=888888&grant_type=sms_code&client_id=client_sms&client_secret=123456

1
2
3
4
5
6
7
8
{
"access_token":"1e419d602ab94cb58e7dbc8359690ddd",
"token_type":"bearer",
"refresh_token":"97fb819885cb4ed3a5de3755e84b11f9",
"expires_in":43199,
"scope":"select",
"client_id":"client_sms"
}

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :