Spring Security-26-社交账号登录重构

上文中实现了用户名密码登录与短信验证码登录的重构,还剩最后一种登录方式,就是第三方社交账号的登录,下面再把第三方登录重构一下

在App里面使用第三方登录和之前的浏览器里面的第三方登录在流程上有一些区别, 在App的环境中,使用第三方用户访问的不是我们服务中的某个路径, 而是服务提供商提供的一个SDK, 不同的服务商的SDK都是不一样的,所以他们的 SDK 走的授权模式也不一样,一般是简化模式和授权码模式这两种

简化模式

流程图如下:
image

简化模式中,第三方服务提供商直接会返回 openId 和 AccessToken ,到这儿的时候 App 就可以用 openId 去获取第三方社交账号的用户信息了,但是并不能访问我们后台提供的那些REST服务, 因为这里拿到的 token 是第三方社交账号的token,如果要访问我们自己的服务的话,还需要一个我们系统里生成的 token, 就跟前面的用户名密码登录之后的返回是一样的,最终返回一个我们系统的 Token ,简单来说,就是需要一个用第三方的 OpenId 换我们系统的 Token 的服务

这个和我们之前自己写的短信验证码登录的方式其实是一样的,只不过这里是要去验证 openId ,那传过来的 openId 从数据库里面查一下对应的用户信息, 如果存在的话,就生成一个 token 返回给用户

代码实现

跟之前的短信验证码的登录是一样的, OpenIdAuthenticationToken OpenIdAuthenticationFilter OpenIdAuthenticationProvider

把之前短信的直接复制过来改改就能用, OpenIdAuthenticationToken 如下:

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
public class OpenIdAuthenticationToken  extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

/**
* openId
*/
private final Object principal;

/**
* 服务提供商的id
*/
private String providerId;

/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public OpenIdAuthenticationToken(Object principal, String providerId) {
super(null);
this.principal = principal;
this.providerId = providerId;
setAuthenticated(false);
}

/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param authorities
*/
public OpenIdAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}


@Override
public Object getCredentials() {
return null;
}

@Override
public Object getPrincipal() {
return this.principal;
}

public String getProviderId() {
return providerId;
}

@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}

super.setAuthenticated(false);
}

@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}

然后是过滤器 OpenIdAuthenticationFilter

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
public class OpenIdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

/**
* 请求中的openId
*/
private String openIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_OPENID;

/**
* 请求中的providerId
*/
private String providerIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_PROVIDERID;

/**
* 只支持post请求
*/
private boolean postOnly = true;

public OpenIdAuthenticationFilter() {
super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_OPENID, "POST"));
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String openId = obtainOpenId(request);
String providerId = obtainProviderId(request);

if (openId == null) {
openId = "";
}

if (providerId == null) {
providerId = "";
}

openId = openId.trim();
providerId = providerId.trim();

// 创建token对象
OpenIdAuthenticationToken openIdAuthenticationToken = new OpenIdAuthenticationToken(openId, providerId);

// 设置一些请求的详细信息
setDetails(request, openIdAuthenticationToken);

return this.getAuthenticationManager().authenticate(openIdAuthenticationToken);
}

protected void setDetails(HttpServletRequest request,
OpenIdAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

protected String obtainOpenId(HttpServletRequest request) {
return request.getParameter(openIdParameter);
}

protected String obtainProviderId(HttpServletRequest request) {
return request.getParameter(providerIdParameter);
}

public void setOpenIdParameter(String openIdParameter) {
Assert.hasText(openIdParameter, "Username parameter must not be empty or null");
this.openIdParameter = openIdParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getOpenIdParameter() {
return openIdParameter;
}

public String getProviderIdParameter() {
return providerIdParameter;
}

public void setProviderIdParameter(String providerIdParameter) {
this.providerIdParameter = providerIdParameter;
}

}

最后是具体的验证 OpenIdAuthenticationProvider

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
@Data
public class OpenIdAuthenticationProvider implements AuthenticationProvider {

private SocialUserDetailsService userDetailsService;

private UsersConnectionRepository usersConnectionRepository;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;

// 通过 providerId 和 openId 去 usersConnection 表里面查询
Set<String> providerUserIds = new HashSet<>();
providerUserIds.add((String) authenticationToken.getPrincipal());
Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authenticationToken.getProviderId(), providerUserIds);

if(CollectionUtils.isEmpty(userIds) || userIds.size() != 1) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}

// 通过id查询用户信息
String userId = userIds.iterator().next();
UserDetails user = userDetailsService.loadUserByUserId(userId);

if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}

// 组装认证成功后的token对象
OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());

authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}

@Override
public boolean supports(Class<?> authentication) {
return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
}
}

搞定之后还需要一个配置类, 作用是使这个filter生效, 新建 OpenIdAuthenticationSecurityConfig

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
@Component
public class OpenIdAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;

@Autowired
private SocialUserDetailsService userDetailsService;

@Autowired
private UsersConnectionRepository usersConnectionRepository;

@Override
public void configure(HttpSecurity http) throws Exception {

// 过滤器的配置
OpenIdAuthenticationFilter openIdAuthenticationFilter = new OpenIdAuthenticationFilter();
// 设置AuthenticationManager
openIdAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置登录成功失败的处理
openIdAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
openIdAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

// Provider的配置
OpenIdAuthenticationProvider openIdAuthenticationProvider = new OpenIdAuthenticationProvider();
openIdAuthenticationProvider.setUserDetailsService(userDetailsService);
openIdAuthenticationProvider.setUsersConnectionRepository(usersConnectionRepository);

http.authenticationProvider(openIdAuthenticationProvider)
.addFilterAfter(openIdAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

}
}

最后加到资源服务器的配置中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableResourceServer
public class MyResourcesServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig;

@Override
public void configure(HttpSecurity http) throws Exception {
// ... 省略部分代码

http
// ... 省略部分代码
.and()
.apply(openIdAuthenticationSecurityConfig)

// ... 省略部分代码
;
}
}

测试

最后启动项目,做一下测试,如图:
image

通过传进去的 openId, 成功获取到了Token

授权码模式

流程图如下:
image

这个模式,app请求qq或微信去获取授权码,然后将授权码交给我们自己的第三方client,由第三方client带着授权码去qq或微信申请令牌,然后发放令牌给第三方应用,然后读取用户数据.然后第三方应用重新生成自己的令牌返回给app. 流程大致就是这样

测试

首先把项目的依赖转到web上面,然后请求第三方登录, 在代码中打个断点,如下:
image

然后这步获取到code的时候停掉项目,切换到app的依赖上去,然后再重新请求他的回调,这时候会去获取到第三方的token,然后做个重定向,因为在 web 环境中,授权成功之后就是重定向的,但是在 app 环境中,应该是返回给App一个token, 所以这里要把第三方授权成功之后的动作修改一下,让他走我们自己的成功处理器

core 项目中新建 SocialAuthenticationFilterPostProcessor

1
2
3
4
5
6
7
8
public interface SocialAuthenticationFilterPostProcessor {

/**
* SocialAuthenticationFilter 请求处理
* @param socialAuthenticationFilter
*/
void process(SocialAuthenticationFilter socialAuthenticationFilter);
}

然后 app 项目中,需要一个实现类, AppSocialAuthenticationFilterPostProcessor:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {

@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Override
public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
socialAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
}
}

这里就是让第三方认证成功之后,走我们自己的成功处理, 浏览器的配置中不需要去实现,让他走默认的重定向就可以

MySpringSocialConfigurer 需要做一些修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class MySpringSocialConfigurer extends SpringSocialConfigurer {

private String filterProcessesUrl;

private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

public MySpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}

@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
if (socialAuthenticationFilterPostProcessor != null) {
socialAuthenticationFilterPostProcessor.process(filter);
}
return (T) filter;
}

}

最后,总的配置 SocialConfig 中,注入进去,赋值给 MySpringSocialConfigurer 里面的 SocialAuthenticationFilterPostProcessor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableSocial
@Order(10)
public class SocialConfig extends SocialConfigurerAdapter {

@Autowired(required = false)
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

@Bean
public SpringSocialConfigurer securitySocialConfigurer(){
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
MySpringSocialConfigurer mySpringSocialConfigurer = new MySpringSocialConfigurer(filterProcessesUrl);
mySpringSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
mySpringSocialConfigurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return mySpringSocialConfigurer;
}

}

最后是测试, 还是刚才那个流程,先切到web环境中请求code,然后切回app ,请求刚才的回调地址

本文代码传送门