spring-security学习(八)——QQ登录(下篇)

前言

上篇博客篇幅较长,较为详细的介绍了第三方登录的问题,以QQ为例的目的其实是因为QQ的QAuth2协议流程比较规范和完善。但是絮叨的东西比较零散,如果要成体系的学习可能需要反复看几遍,这篇博客是上篇博客的一个延续,在完成QQ第三方登录之后,如果用户原本没有在系统中注册,则会引发spring-social中默认的singup的默认跳转,本篇博客就是总结如何解决这个问题

问题原因

上篇博客在介绍完QQ登录之后,最后会发现并没有很顺畅的跳转到我们系统的主页,而是发现跳转到了signin的路径。这个路径是spring-security中默认的注册路径。为什么会发生这个跳转,其实原因比较简单,是SocialAuthenticationFilter中捕获了SocialAuthenticationProvider中抛出的BadCredentialsException然后做了跳转,在源码中可以找到佐证的内容

SocialAuthenticationFilter中做认证的源码如下

//以下代码位于:
//org.springframework.social.security.SocialAuthenticationFilter#doAuthentication
//看名字就知道是做认证的
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
    
    
	try {
    
    
		if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
		token.setDetails(authenticationDetailsSource.buildDetails(request));
		Authentication success = getAuthenticationManager().authenticate(token);
		Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
		updateConnections(authService, token, success);			
		return success;
	} catch (BadCredentialsException e) {
    
    //捕获BadCredentialsException,进行处理
		// connection unknown, register new user?
		if (signupUrl != null) {
    
    //如果signupUrl不为空,则表示需要跳到注册
			// store ConnectionData in session and redirect to register page
            //将从第三方获取到的用户信息存入到会话中
			sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
            //然后抛出需要跳转的异常
			throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
		}
		throw e;
	}
}

SocialAuthenticationProvider中做认证的代码如下

//以下代码位于:
//org.springframework.social.security.SocialAuthenticationProvider#authenticate
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
    //判断认证的authentication类型是否是SocialAuthenticationToken
	Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
    //判断是否已经认证了
	Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
    //获取token中的providerId(服务商ID)
	SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
	String providerId = authToken.getProviderId();
    //这里的connection可以认为就是第三方服务商返回的用户数据。
	Connection<?> connection = authToken.getConnection();

    //这里要根据第三方的用户id,去本地数据库中查找用户信息,如果用户信息不存在,则会抛出BadCredentialsException
    //进而引发signup的跳转
	String userId = toUserId(connection);
	if (userId == null) {
    
    
		throw new BadCredentialsException("Unknown access token");
	}
	
    //取出用户信息,返回userDetails
	UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
	if (userDetails == null) {
    
    
		throw new UsernameNotFoundException("Unknown connected account id");
	}

    //根据userDetails构建认证成功的token。
	return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
}

//以下代码位于:
//org.springframework.social.security.SocialAuthenticationProvider#toUserId
//主要用于通过usersConnectionRepository根据第三方用户id查找本地原有的用户信息
protected String toUserId(Connection<?> connection) {
    
    
	List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
	// only if a single userId is connected to this providerUserId
	return (userIds.size() == 1) ? userIds.iterator().next() : null;
}

可以看到关键的一步——toUserId,这一步是根据第三方服务商返回的用户信息中的用户id(通常是openId)去本地数据库中获取用户信息,如果没有获取到用户信息,则抛出了BadCredentialsException进而引发了signup的跳转。

**从业务表现上来说,我们虽然拿到了第三方的用户信息,但是这个用户信息目前在我们自己的系统中并不存在,则spring-social默认引导新用户去注册。**当然,我们可以定制处理这个流程,这个后面再说。至少现在,问题根源通过源码找到了。

配置自己的注册页

上面问题分析之后已经知道,spring-social为我们自定义了一个注册页,如果我们想自定义注册页,只需要在将社交登录的配置加入到spring-security的时候设置相关属性即可。

在我们之前配置的SpringSocialConfigurer中加入配置即可。

@Bean
public SpringSocialConfigurer selfSocialSecurityConfig(){
    
    
    String processFilterUrl = securityProperties.getSocial().getProcessFilterUrl();
    SelfSpringSocialConfig selfSpringSocialConfig = new SelfSpringSocialConfig(processFilterUrl);

    //指定第三方用户信息认证不存在的注册页
    selfSpringSocialConfig.signupUrl(securityProperties.getBrowser().getSiguUpPage());
    return selfSpringSocialConfig;
}

一个简陋的注册页

在这里插入图片描述

**需要说明的是,这是因为社交用户,本身我们系统中就没有该社交用户的数据,spring social默认为该用户需要注册和绑定,才会引发跳转。我们需要让用户自己决定注册还是绑定。在绑定和注册的时候,我们需要将注册完成的用户信息与第三方用户信息做一个关联,这才是完整的流程。**只需要记住一点,我们现在虽然能顺利拿到用户的第三方信息,但是我们本地还没有数据。

我们提供的注册接口,这里不给出具体实现(毕竟注册千人前面),只是说明需要做的事情

@PostMapping("/register")
public void userRegister(@RequestBody User user) {
    
    
    //TODO:这里要完成用户注册,同时将用户信息存入到我们自己的数据表
    //TODO:还需要将用户信息与第三方用户信息做一个关联
    //TODO:如果是绑定,请求也会走到这里,也可以完成上述操作。
}

现在还有两个问题:

1、如果用户需要在注册页面显示第三方的用户信息,告知用户具体在与那一个社交账号绑定,该如何实现

2、在注册的时候,我们本地系统会产生一个唯一的用户id,如何将这个用户id传给已经存于会话中的第三方认证的用户信息。

两个问题汇成一个——如何更新关联第三方登录之后的AuthenticationToken信息。

操作已认证的第三方用户信息

上一小节中,其实是如果社交用户在我们本系统中没有用户数据的时候,我们的系统需要帮助其完成注册,而注册了用户需要将会话中的用户与user_connection表中的用户进行关联与更新,需要通过spring-social为我们封装好的工具类——ProviderSignUpUtils

只是我们需要配置一下

/**
 * autor:liman
 * createtime:2021/7/14
 * comment: 社交登录的配置类
 */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
    
    

    @Autowired
    private DataSource dataSource;
    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private ConnectionFactoryLocator connectionFactoryLocator;


    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
    
    
        //第二个参数是一个,这个参数会被传递进来,这个是根据条件自动查找相关的ConnectionFactory ,如果是微信登录,则查找微信的ConnectionFactory
        //第三个是加解密
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        repository.setTablePrefix("self_");
        return repository;
    }

    @Bean
    public SpringSocialConfigurer selfSocialSecurityConfig(){
    
    
        String processFilterUrl = securityProperties.getSocial().getProcessFilterUrl();
        SelfSpringSocialConfig selfSpringSocialConfig = new SelfSpringSocialConfig(processFilterUrl);

        //指定第三方用户信息认证不存在的注册页
        selfSpringSocialConfig.signupUrl(securityProperties.getBrowser().getSiguUpPage());
        return selfSpringSocialConfig;
    }

    //这里配置ProviderSignInUtils
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
    
    
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator));
    }
}

在上述代码的尾端,配置了ProviderSignInUtils这个配置类也是第三方认证登录开发的核心配置类之一。有了这个工具类之后,我们可以直接读取第三方认证登录时,会话中的认证信息了。

针对上一个标题结尾的两个问题的解决方案

1、提供获取第三方用户信息的接口

具体如下:

@Autowired
private ProviderSignInUtils providerSignInUtils;

//可以根据自己的喜好定义其存放位置
@GetMapping("/socialuserinfo")
public BaseResponse getSocialUserInfo(HttpServletRequest request){
    
    
    BaseResponse result = new BaseResponse(StatusCode.Success);
    log.info("开始获取会话中的第三方用户信息");
    //利用providerSignInUtils,从会话中读取第三方用户信息
    Connection<?> connectionFromSession = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
    SocialUserInfo socialUserInfo = new SocialUserInfo();
    socialUserInfo.setProviderId(connectionFromSession.getKey().getProviderId());
    socialUserInfo.setProviderUserId(connectionFromSession.getKey().getProviderUserId());
    socialUserInfo.setNickName(connectionFromSession.getDisplayName());
    socialUserInfo.setHeadImg(connectionFromSession.getImageUrl());
    result.setData(socialUserInfo);
    return result;
}

/**
 * autor:liman
 * createtime:2021/7/18
 * comment: 第三方用户信息的实体
 */
@Data
@ToString
public class SocialUserInfo {
    
    

    private String providerId;

    private String providerUserId;

    private String nickName;

    private String headImg;

}

2、将注册后的用户信息绑定到会话

在之前的register请求中,加入如下一行代码即可。

@Autowired
private ProviderSignInUtils providerSignInUtils;

@PostMapping("/register")
public void userRegister(@RequestBody User user, HttpServletRequest request) {
    
    
    //TODO:这里要完成用户注册,同时将用户信息存入到我们自己的数据表
    //TODO:还需要将用户信息与第三方用户信息做一个关联
    //TODO:如果是绑定,请求也会走到这里,也可以完成上述操作。

    //利用providerSignInUtils,将注册之后的用户信息,关联到会话中
    providerSignInUtils.doPostSignUp(user.getId(),new ServletWebRequest(request));

}

顺便说一句,从会话中读取第三方用户信息是知道了,但是……还记得第三方的用户信息是何时存入进去的么?

在文章开头已经提到,是在SocialAuthenticationFilter中处理BadCredentialsException的时候存入会话的,在开头的源码注释中,加上了相关备注。

静默注册

上面算是解决了用户授权之后的用户绑定,但是还需要用户手动绑定和注册,如果想实现用户的静默注册和跳转,这个又该如何操作。在QQ第三方登录篇幅的最后,还是简单介绍一下实现静默注册关联的方案。

向源码要方案

文章开篇介绍过org.springframework.social.security.SocialAuthenticationProvider#authenticate的源码,其中调用了一个toUserId方法,这个方法会去数据库中查询关联的本地用户信息,如果不存在则抛出BadCredentialsException,这也是引发自定义signup跳转的罪魁祸首。于是我们就需要继续深入到toUserId方法中去探索

//org.springframework.social.security.SocialAuthenticationProvider#toUserId
protected String toUserId(Connection<?> connection) {
    
    
    //由于我们使用的是JdbcUsersConnectionRepository,这行会实际执行JdbcUsersConnectionRepository中的对应方法
	List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
	// only if a single userId is connected to this providerUserId
	return (userIds.size() == 1) ? userIds.iterator().next() : null;
}

进入到

//以下代码位于:
//org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository#findUserIdsWithConnection
public List<String> findUserIdsWithConnection(Connection<?> connection) {
    
    
	ConnectionKey key = connection.getKey();
	List<String> localUserIds = jdbcTemplate.queryForList("select userId from " + tablePrefix + "UserConnection where providerId = ? and providerUserId = ?", String.class, key.getProviderId(), key.getProviderUserId());		
    //如果没有找到本地系统中的userId,其实还有一个connectionSignUp操作。
    //这个就是根据第三方用户信息静默注册的逻辑,如果不为空,则调用connectionSignUp中的execute方法,
    //设置新的newUserId,然后新的newUserId加入到会话中。
	if (localUserIds.size() == 0 && connectionSignUp != null) {
    
    
		String newUserId = connectionSignUp.execute(connection);
		if (newUserId != null)
		{
    
    
			createConnectionRepository(newUserId).addConnection(connection);
			return Arrays.asList(newUserId);
		}
	}
	return localUserIds;
}

因此,解决方案就非常简单了,我们只需要自定义实现connectionSignUp接口,并将其交给SpringSocialConfigure管理就可以了。

/**
 * autor:liman
 * createtime:2021/7/18
 * comment: 自定义第三方静默注册的逻辑(简单实例)
 */
@Component
public class SelfConnectionSignUp implements ConnectionSignUp {
    
    
    
//    @Autowired
//    private MyUserDetailService userDetailService;

    /**
     * 根据第三方用户信息静默注册的逻辑
     * @param connection
     * @return
     */
    @Override
    public String execute(Connection<?> connection) {
    
    
        //根据第三方用户信息创建用户,并返回静默注册之后的唯一本地用户标示
        String newUserId = connection.getKey().getProviderUserId();//这里简单处理的,直接以第三方用户id为唯一标示
        return newUserId;
    }
}

交给SpringSocialConfig管理即可(这里只截取了部分代码)

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
    
    

    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
    
    
        //第二个参数是一个,这个参数会被传递进来,这个是根据条件自动查找相关的ConnectionFactory ,如果是微信登录,则查找微信的ConnectionFactory
        //第三个是加解密
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        repository.setTablePrefix("self_");
        if(connectionSignUp!=null){
    
    
            repository.setConnectionSignUp(connectionSignUp);
        }
        return repository;
    }
}

总结

至此,QQ第三方登录中所有的细节,均已根据某课网的课程总结完成,非常零散,而且在测试的时候需要有可用的QQ互联的相关配置,因此在测试学习的时候,遇到很大阻力,个人觉得这应该算是spring-security中最为复杂的部分了。

还是附上源码地址吧——参考其中的spring-security开头的项目

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/118875577
今日推荐