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技术栈学习笔记(二)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)
在这里插入图片描述

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值