Spring Security技术栈开发企业级认证与授权(十五)解决Spring Social集成QQ登录后的注册问题

上一篇文章主要完成了Spring Social集成QQ登录主要逻辑,但是最后还是遗留了一个问题,那就是授权登录后跳转到了/signup上,其实这是Spring Social注册逻辑,所以我们就一起用这节内容来共同探讨解决这个问题。

一、分析为什么会跳转到/signup上

为什么会跳转到/signup上,或者在上面情况下会跳转到/signup上呢?我们一起阅读源代码来查找原因。我们在此把社交登录的流程图贴到这里。

我们在封装好SocialAuthenticationToken以后,就会调用AuthenticationManager来调用SocialAuthenticationProvider来进行认证工作,我们一起来看具体的认证代码:

从上图的代码中可知,在认证过程中,打断点的那一步骤是拿到providerIdproviderUserId(其实就是openId)去数据库表UserConnection中去查询业务系统中的userId,因为我们业务系统中还没有这个授权登录的用户,所以这里返回的就是null,然后就直接抛出了BadCredentialsException异常,那么该异常最终在类SocialAuthenticationFilter中的doAuthentication方法中被捕获,代码如下:

该异常在这里被处理,这里有一个判断,判断signupUrl是否为null,其实它有一个默认值,那就是“/signup”,那么接下来的代码将从QQ服务器中获取的用户信息存储到了Session中,然后抛出了一个跳转的异常,然后该异常被捕获后,就会跳转到“/signup”上,然后我们并没有配置“/signup”免认证访问,所以就出现了如下图所示情况:

问题算是确定了,那么我们来分析一下场景:其实这个场景我们经常遇见,例如我们第一次使用QQ授权登录某网站,扫码后,一般都是跳转到了一个要求绑定本网站账户的页面上,并且也支持在该页面上注册账户,然后进行绑定,那么现在对于这种需要注册的场景,我们提供两种常见的解决方案:

  • 跳到注册绑定界面,要求用户注册或者绑定已有账户;
  • 默认为用户注册一个账户,保存到数据库中进行关联。

对于这两种解决方案,都是很常见的,那么我们来一一实现它。

二、用户自主注册

我们提供在lemon-security-browser项目中添加一个注册页面,由于注册页面是用户高度自定义的页面,所以这里默认的注册页面仅仅提示用户配置相关属性即可,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
</head>
<body>
<h2>标准注册页</h2>
<h3>这里是标准的注册页面,需要用户自己配置属性com.lemon.security.browser.signUpUrl属性类配置自己的注册页面</h3>
</body>
</html>

这里就提示了用户去配置com.lemon.security.browser.signUpUrl属性,然后调用自己的页面。那么我们在BrowserProperties配置类加一个属性signUpUrl,这个属性的默认值是指向我们在lemon-security-browser下的signUp.html。还有一点,为了项目的可用性,我们在lemon-security-demo项目中也加入自定义的登录页面,和系统默认的一致,然后配置application.yml如下所示:

com:
  lemon:
    security:
      browser:
        loginPage: /lemon-login.html
        signUpUrl: /lemon-signUp.html
      code:
        image:
          length: 6
          url: /user,/user/*

这样就是是要用户自定义的登录页面(虽然本案例中和默认的页面是一样的)和注册页面。接下来,我们来完成用户自主注册的逻辑。
因为注册逻辑是用户自定义的,所以只能在demo项目中写注册逻辑,并将注册好的用户存储到数据库中,我们现在来实现这个功能。

用户自定义注册绑定页面

这里在demo项目中加入一个简单的用户自定义的注册绑定页面,代码如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>欢迎注册</title>
</head>
<body>
<h2>用户注册页</h2>
<form action="/user/register" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><label>
                <input name="username" type="text">
            </label></td>
        </tr>
        <tr>
            <td>密 码:</td>
            <td><label>
                <input name="password" type="password">
            </label></td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit" name="type" value="register">注册</button>
                <button type="submit" name="type" value="binding">绑定</button>
            </td>
        </tr>

    </table>
</form>
</body>
</html>

页面写完了,我们还需要在demo项目中提供一个注册的Controller,具体代码稍后提供,我们还需要配置一下,我们要告诉Spring Social,我们的注册页面不需要授权就可以访问,那么需要在BrowserSecurityConfig类中将securityProperties.getBrowser().getSignUpUrl()设置到HttpSecurity对象中去,具体方式如下所示:

.antMatchers(securityProperties.getBrowser().getSignUpUrl()).permitAll()

还需要配置一下SocialConfig类,将代码:

@Bean
public SpringSocialConfigurer lemonSocialSecurityConfig() {
    String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
    return new LemonSpringSocialConfigurer(filterProcessesUrl);;
}

改成

@Bean
public SpringSocialConfigurer lemonSocialSecurityConfig() {
    String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
    LemonSpringSocialConfigurer configurer = new LemonSpringSocialConfigurer(filterProcessesUrl);
    configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
    return configurer;
}

这里就是将Spring Social默认的/signup改成了我们自己配置的/lemon-signUp.html,这样,当数据库没有当前授权登录的用户的时候,就会跳转到本页,提示用户注册或者绑定本站账号。我们启动项目,访问http://www.itlemon.cn/lemon-login.html页面,点击QQ登录,授权后就直接跳到了我们设定的注册绑定界面,如下所示:

这样,我们就将用户引导了注册绑定页面,那么用户在没有本站账户的情况下,可以选择注册,在有账户的情况下,可以选择绑定,这里对于密码的处理没有进行二次确认,这仅仅是为了方便,实际开发中对于密码的处理要复杂一些,比如加密,二次校验等。
我们在大多数网站上,当用户到达注册或者绑定的时候,页面旁边都会显示QQ的相关信息,比如用户的QQ昵称,第三方的服务提供商ID,头像等信息,我们这里也这样做,请看接下来的内容。
其实这个需求,Spring Social已经为我们考虑好了,它提供了一个工具类ProviderSignInUtils,这个工具类提供了两个解决方案,一个是在业务系统中拿到Spring Social的用户数据,另一个是将业务系统中注册的用户ID再传递给Spring Social。这两个方案就可以帮助我们在注册绑定页面显示用户第三方信息,且注册后将业务系统中的用户和第三方用户信息绑定起来。
我们在SocialConfig类中加一个Bean配置,代码如下:

@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
    return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator));
}

其中ConnectionFactoryLocatorSpring Boot中已经被实例化了,我们直接通过参数形式注入进来即可,实例化ProviderSignInUtils还需要UsersConnectionRepository对象,那么直接调用本类中的getUsersConnectionRepository方法即可。那么这里就配置好了ProviderSignInUtils的实例对象,那么在需要的地方就可以直接使用注解@Autowired注入即可。
我们需要使用到用户在第三方的信息用于展示,那么这个需求我们可以帮他做好,我们在lemon-security-browser项目中的BrowserSecurityController类中引入ProviderSignInUtilsSpring Bean,且加一个获取用户信息的接口,代码如下:

@GetMapping("/social/user")
public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
    Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
    return SocialUserInfo.builder().providerId(connection.getKey().getProviderId())
            .providerUserId(connection.getKey().getProviderUserId())
            .nickname(connection.getDisplayName())
            .headImg(connection.getImageUrl())
            .build();
}

代码块中,我们的providerSignInUtils工具类是从Session中拿到的用户信息,那么这个信息是什么时候存储到session中的呢?在本文章的开头部分,我们讲到了信息的存储,你可以到前面看看。如果用户自定义的注册绑定页面需要显示这些信息,那么直接访问这个接口就可以实现了,在本案例中,我只提供接口,就不在去实现具体的页面逻辑了,感兴趣的朋友可以自行实现。
我们接着来提供一下注册绑定的Controller,代码如下:

package com.lemon.security.web.controller;

import com.lemon.security.web.dto.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;

/**
 * 用户注册的Controller
 *
 * @author jiangpingping
 * @date 2019-02-18 19:43
 */
@RestController
@RequestMapping("/demo")
public class RegisterController {

    private final ProviderSignInUtils providerSignInUtils;

    @Autowired
    public RegisterController(ProviderSignInUtils providerSignInUtils) {
        this.providerSignInUtils = providerSignInUtils;
    }

    @PostMapping("/register")
    public String register(User user, HttpServletRequest request) {
        // 不管是注册还是绑定,都会拿到用户在业务系统中的唯一标识,注册是新生成标识,绑定是从数据库中获取唯一标识
        // 那么我们就以用户传递过来名称作为唯一标识,将这个标识和session中的用户信息一同传输给Spring Social
        // Spring Social拿到数据以后,就会将这个唯一标识和用户在QQ上的信息一同存储到UserConnection表中
        String userId = user.getUsername();
        providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
        return "注册并绑定成功";
    }
}

在上面类中,我并没有提供用户注册的具体逻辑,无非就是一些增删改查,这里提供的就是一种思路,不管是注册还是绑定,都会拿到用户在业务系统中的唯一标识,注册是新生成标识,绑定是从数据库中获取唯一标识,那么我们就以用户传递过来名称作为唯一标识,将这个标识和session中的用户信息一同传输给Spring SocialSpring Social拿到数据以后,就会将这个唯一标识和用户在QQ上的信息一同存储到UserConnection表中,那么下次授权登录的时候,再次走到认证代码中的时候,如下图所示:

它就会调用findUserIdsWithConnection方法从数据库表UserConnection中查找用户信息,具体的查找代码如下源码所示:

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());		
	if (localUserIds.size() == 0 && connectionSignUp != null) {
		String newUserId = connectionSignUp.execute(connection);
		if (newUserId != null)
		{
			createConnectionRepository(newUserId).addConnection(connection);
			return Arrays.asList(newUserId);
		}
	}
	return localUserIds;
}

在源码中我们可以看出,查找依据就是providerIdproviderUserId(实际就是openIdQQ用户对于每个授权应用都会生成的一个唯一的ID),那么注册后,或者绑定后,就会查询到数据,这时候就不会返回null了,也就不会再抛出重定向的异常了,那么就可以正确地进入到系统中了。在此之前,我们还需要配置一下,那就是配置注册URL可以未授权就可以登录,我们在BrowserSecurityConfig中配置一下,具体请参考前面的配置或查看码云上chapter015的源码,需要注意的一点是,这个注册URLdemo项目中的,在我们这个安全模块中是不知道有这么个URL的,我们只是暂时配置到BrowserSecurityConfig中,后面的重构中会将其配置到demo项目中。
我们再次启动demo项目,访问http://www.itlemon.cn/lemon-login.html页面,点击QQ登录,授权后就直接跳到了我们设定的注册绑定界面,如下所示:

然后我们输入任意的用户名和密码:

点击注册,然后显示如下所示:

此时,我们观察数据库的UserConnection表,发现多了一条数据:

这样,我们就将业务系统中的用户和QQ用户绑定起来了,下次再次登录的时候,就不会跳转到注册页面了,直接进入到主页。

三、默认帮助用户注册

上面的内容讲述了用户自己注册账号或者绑定账号,本小节将介绍默认帮助用户注册的行为,这也是一般网站常用的方法之一。其实,Spring Social也提供了相关功能,这个需要我们一起去源码中进行探索。我们都知道,当用户使用QQ登录的时候,会从QQ资源服务器上获取用户的信息来封装成SocialAuthenticationToken然后交给对应的SocialAuthenticationProvider来进行认证操作,如果用户第一次登录,那么Spring SocialUserConnection表中就查不到用户的数据,那么用户就会跳转到主页页面要求用户注册或者绑定,那么我们一起来看看具体的认证代码:

这段代码是SocialAuthenticationProvider的认证方法代码,我们进入到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());		
	if (localUserIds.size() == 0 && connectionSignUp != null) {
		String newUserId = connectionSignUp.execute(connection);
		if (newUserId != null)
		{
			createConnectionRepository(newUserId).addConnection(connection);
			return Arrays.asList(newUserId);
		}
	}
	return localUserIds;
}

这段代码较长,所以没有截图,这段代码我们在上面已经介绍过了,这里再补充一点,请看条件localUserIds.size() == 0 && connectionSignUp != null,条件也就是说,当Spring SocialUserConnection表中没有查到用户的信息,且connectionSignUp对象(它是接口ConnectionSignUp的实现类对象)存在的时候,会进入到if方法体中,就会调用ConnectionSignUp接口的实习类的execute方法来注册一个用户,然后返回用户的userId,这时候Spring Social就会将这个userIdconnection数据一同存入到表UserConnection中,这也就完成了默认的注册行为。而我们进入到接口ConnectionSignUp的时候,发现它没有任何实现,所以我们需要自己写一个类去实现默认的注册行为。由于默认的注册行为要和系统的业务关联起来,所以这里默认的注册类要写在demo项目中,代码如下:

package com.lemon.security.web.authentication;

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;

/**
 * 默认为用户注册账户的实现类
 *
 * @author jiangpingping
 * @date 2019-02-20 20:20
 */
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {

    @Override
    public String execute(Connection<?> connection) {
        // 这里应该写与业务相关的默认注册行为,这里为了简便,生成的系统用户的userId就是要QQ的相关信息
        // 这里使用的是QQ用户对本网站的唯一的openId作为userId来注册的
        return connection.getKey().getProviderUserId();
    }

}

这里为了简便,没有涉及太多的业务逻辑,这里使用用户的openId作为userId来注册了一个用户,在实际的业务系统中,应该还有一张以上的表来记录用户的信息,而UserConnection表只是用来记录业务系统中的用户和QQ用户之间的关系的表。我们再将这个Spring Bean注入到SocialConfig类中,代码如下所示:

@Autowired(required = false)
private ConnectionSignUp connectionSignUp;

这里设置required值为false,这是因为ConnectionSignUp并不是一定会有开发者提供,这得针对项目要求来决定,所以这里的required的值就被设置为false。还要将代码:

@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
    // 创建一个JDBC连接仓库,需要dataSource、connectionFactory加载器,对存到数据库中的加密策略,这里选择不做加密,信息原样存入数据库
    // 这里创建的JdbcUsersConnectionRepository可以设置UserConnection表的前缀
    return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
}

修改为:

@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
    // 创建一个JDBC连接仓库,需要dataSource、connectionFactory加载器,对存到数据库中的加密策略,这里选择不做加密,信息原样存入数据库
    // 这里创建的JdbcUsersConnectionRepository可以设置UserConnection表的前缀
    JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
    repository.setConnectionSignUp(connectionSignUp);
    return repository;
}

我将UserConnection表中的数据删除掉,重新登录,发现数据库表又新增了一条数据,这就完成了默认的注册行为。

那么文章写道这里,我们就一起完成了Spring Social集成QQ登录的开发内容,这里提供的案例很简单,朋友们可以根据自己实际的业务需求,来开发适合自己系统的代码。接下来,我会继续更新Spring Social集成微信登录的开发案例,请继续关注后面的内容。

Spring Security技术栈开发企业级认证与授权系列文章列表:

Spring Security技术栈开发企业级认证与授权(一)环境搭建
Spring Security技术栈开发企业级认证与授权(二)使用Spring MVC开发RESTful API
Spring Security技术栈开发企业级认证与授权(三)表单校验以及自定义校验注解开发
Spring Security技术栈开发企业级认证与授权(四)RESTful API服务异常处理
Spring Security技术栈开发企业级认证与授权(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈开发企业级认证与授权(六)使用REST方式处理文件服务
Spring Security技术栈开发企业级认证与授权(七)使用Swagger自动生成API文档
Spring Security技术栈开发企业级认证与授权(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈开发企业级认证与授权(九)开发图形验证码接口
Spring Security技术栈开发企业级认证与授权(十)开发记住我功能
Spring Security技术栈开发企业级认证与授权(十一)开发短信验证码登录
Spring Security技术栈开发企业级认证与授权(十二)将短信验证码验证方式集成到Spring Security
Spring Security技术栈开发企业级认证与授权(十三)Spring Social集成第三方登录验证开发流程介绍
Spring Security技术栈开发企业级认证与授权(十四)使用Spring Social集成QQ登录验证方式
Spring Security技术栈开发企业级认证与授权(十五)解决Spring Social集成QQ登录后的注册问题
Spring Security技术栈开发企业级认证与授权(十六)使用Spring Social集成微信登录验证方式

示例代码下载地址:

项目已经上传到码云,欢迎下载,内容所在文件夹为chapter015

更多干货分享,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)
在这里插入图片描述

发布了73 篇原创文章 · 获赞 84 · 访问量 47万+

猜你喜欢

转载自blog.csdn.net/Lammonpeter/article/details/86717003