springsocial/oauth2---第三方登陆之QQ登陆7【注册逻辑之mysql里rank为关键字问题解决+springsocial源码解读③】


项目源码地址 https://github.com/nieandsun/security

1 本篇内容概览

上篇文章末尾说过,由于这里的注册其实是从第三方登陆过程中重定向而来的注册,因此这种情况下的注册肯定与普通的用户注册有一定的区别。都要有哪些区别呢?其实主要在两块:

  • 一块是上篇文章说所的当用户注册完,肯定要与该第三方(即QQ、微信等)建立起关系,也就是说下次用户直接用QQ或微信等进行登陆,那肯定直接就可以登陆了。—》落实到代码上,就是用户注册完,需要同时往userconnection表里插入一条数据。
  • 第二块在于注册页面的展示与逻辑。下面是我直接在当当网进行QQ登陆时导向的注册页面,我们可以看到人家在这种情况下的注册页面里就不光有注册功能,还同时把从QQ获取到的你的头像和昵称信息给显示了出来,让你有种亲切感☺。当然下面的注册页面还包括一块逻辑是绑定,即当你已经有用户了,只是没与QQ进行过关联,所以需要绑定,说白了就是需要在userconnection表里插入一条你的用户与QQ号的关联数据,这块逻辑我会在下文进行讲解。

在这里插入图片描述
其实大家除了见过像当当这种直接点击QQ登陆进行登陆,由于没有进行注册或绑定过,而被导向一个用户注册页面的网站,肯定也见过那种从来就没上过的网站直接点击QQ登陆或者微信登陆,但是却可以直接登陆成功而不需要注册的网站,而且很多。那这种又是怎么弄得呢?这一块也是本文需要讲解的内容。
因此,本文将主要讲解如下三块内容:

  • 第三方登陆时用户注册页面如何拿到QQ头像和昵称等信息
  • 用户注册时如何将QQ与用户信息建立关联–》插入userconnection表
  • 第三方登陆时,如何跳过注册逻辑

2 第三方登陆时用户注册页面如何拿到QQ头像和昵称等信息

2.1 配置获取QQ用户信息的工具类 —ProviderSignInUtils

上篇文章解读doAuthentication 方法时,有下面这样一段代码:

catch (BadCredentialsException e) {
			// connection unknown, register new user?
			if (signupUrl != null) {
				// 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;
		}

即SpringSocial/SpringSecurity在抛出跳转向/signup的重定向异常SocialAuthenticationRedirectException之前其实做了一件事,就是将从QQ获取到的Connection对象存到了session里,因此如果想在注册页面上显示QQ的头像和昵称信息,是可以从session里获取到的。springsocial其实提供了一个从session获取Connection对象的工具类,但我们需要配置一下:

  • ProviderSignInUtils
    /**
     * ProviderSignInUtils有两个作用:
     * (1)从session里获取封装了第三方用户信息的Connection对象
     * (2)将注册的用户信息与第三方用户信息(QQ信息)建立关系并将该关系插入到userconnection表里
     * 
     * 需要的两个参数:
     * (1)ConnectionFactoryLocator 可以直接注册进来,用来定位ConnectionFactory
     * (2)UsersConnectionRepository----》getUsersConnectionRepository(connectionFactoryLocator)
     *     即我们配置的用来处理userconnection表的bean
     * @param connectionFactoryLocator
     * @return
     */
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator)) {
        };
    }

2.2 创建获取QQ用户信息的接口

注册页面展示QQ用户信息其实只需要头像和昵称就可以,我们可以简单封装一个实体类如下:

package com.nrsc.security.browser.beans;

import lombok.Data;

/**
 * @author : Sun Chuan
 * @date : 2019/9/11 23:03
 * Description:展示给前端的第三方用户信息
 */
@Data
public class SocialUserInfo {
    /**提供商唯一标识*/
    private String providerId;

    /***用户在提供商的唯一标识(其实就是openId)*/
    private String providerUserId;

    /**用户在提供商的昵称*/
    private String nickName;

    /**用户在提供商的头像*/
    private String headImg;
}
  • 则获取QQ用户信息的接口如下:
    /**
     * 获取第三方用户信息
     *
     * @param request
     * @return
     */
    @GetMapping("/social/user")
    public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
        SocialUserInfo userInfo = new SocialUserInfo();
        //从session里取出封装了第三方信息QQ用户信息)的Connection对象
        Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
        userInfo.setProviderId(connection.getKey().getProviderId());
        userInfo.setProviderUserId(connection.getKey().getProviderUserId());
        userInfo.setNickName(connection.getDisplayName());
        userInfo.setHeadImg(connection.getImageUrl());
        return userInfo;
    }

2.3 注册页面获取QQ头像+昵称

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>注册</title>
    <script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
</head>
<body>
<h2>Demo注册页</h2>
<img id="headImage">hi, <span id="nickName"></span> ,欢迎登陆XXX网
<hr>
<form action="/user/register" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></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>

<script type="text/javascript">
    function getSocialUserInfo() {
        $.get({
            url: "/social/user",
            type: "get",
            success: function (res) {
                $("#headImage").attr("src", res.headImg);
                $("#nickName").text(res.nickName);
            }
        })
    }

    window.onload = getSocialUserInfo;
</script>
</body>
</html>

2.4 对注册页面获取第三方信息接口进行授权

在这里插入图片描述

2.5 测试

再次进行QQ登陆并扫码授权,进入的注册页面如下,即页面上已经获得到了QQ头像和QQ昵称信息。
在这里插入图片描述


3 用户注册时如何将QQ与用户信息建立关联–》插入userconnection表

3.1 对注册接口进行授权–见2.4图

3.2 处理注册逻辑+将QQ与用户信息进行关联

    @PostMapping("/register")
    public void register(NrscUser user, HttpServletRequest request) {
        //注册用户相关逻辑-----》即向用户表里插入一条用户数据-----》这里不写了

        //不管是注册用户还是绑定用户,都会拿到一个用户唯一标识,如用户名。
        String userId = user.getUsername();
        //将用户userId和第三方用户信息建立关系并将其插入到userconnection表
        providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
    }

3.3 测试1 —》mysql里rank为关键字问题解决

3.3.1 问题分析1— mysql

填上用户名+密码点击注册发现会出现如下bug:
在这里插入图片描述


我把图中语句放在navicat里运行,发现仍然报语法错误,此时突然想到之前写的一篇文章:《【MySQL报错处理】[Err] 1064 、[Err] 1055、JdbcUsersConnectionRepository.sql建表报错》,于是豁然开朗,其实rank在我的mysql版本里是一个保留关键字,因此需要再将rank弄成下面的样子才可以。
在这里插入图片描述


3.3.2 问题分析2 — springsocial源码

为什么会报出这个bug呢?跟踪springsocial源码发现在执行 providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));语句进行存储数据时,会调用JdbcConnectionRepository类里的如下方法:

	@Transactional
	public void addConnection(Connection<?> connection) {
		try {
			ConnectionData data = connection.createData();
			int rank = jdbcTemplate.queryForObject("select coalesce(max(rank) + 1, 1) as rank from " + tablePrefix + "UserConnection where userId = ? and providerId = ?", new Object[]{ userId, data.getProviderId() }, Integer.class);
			jdbcTemplate.update("insert into " + tablePrefix + "UserConnection (userId, providerId, providerUserId, rank, displayName, profileUrl, imageUrl, accessToken, secret, refreshToken, expireTime) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
					userId, data.getProviderId(), data.getProviderUserId(), rank, data.getDisplayName(), data.getProfileUrl(), data.getImageUrl(), encrypt(data.getAccessToken()), encrypt(data.getSecret()), encrypt(data.getRefreshToken()), data.getExpireTime());
		} catch (DuplicateKeyException e) {
			throw new DuplicateConnectionException(connection.getKey());
		}
	}

追踪到该方法时,真有种不看不知道一看吓一跳的感觉,因为JdbcConnectionRepository这个类里很多方法都用到了rank字段,那操作数据库时肯定就会有很多的问题了。但是仔细分析一下可以看到该类是一个没有被public,private等字段修饰的类,即该类只允许同一个包中的类进行访问。那它在哪个包呢?其实就是JdbcUsersConnectionRepository所在的包,看下图:
在这里插入图片描述
再仔细一看,可以在JdbcUsersConnectionRepository类里看到如下方法:

	public ConnectionRepository createConnectionRepository(String userId) {
		if (userId == null) {
			throw new IllegalArgumentException("userId cannot be null");
		}
		return new JdbcConnectionRepository(userId, jdbcTemplate, connectionFactoryLocator, textEncryptor, tablePrefix);
	}

即JdbcConnectionRepository类是在JdbcUsersConnectionRepository类里new出来的。那这个问题就好解决了。

3.3.3 我的解决方式

  • 将数据库里的rank字段改成了sequence —》感觉尽量不要用mysql的保留关键字
  • 将org.springframework.social.connect.jdbc包下的JdbcUsersConnectionRepository类和JdbcConnectionRepository类拷贝了一份放到了我的项目目录中—》放在了com.nrsc.security.core.social.jdbc包路径下
  • 将JdbcConnectionRepository中的所有rank改成了sequence
  • 将UsersConnectionRepository换成了我自己的刚复制的NrscJdbcUsersConnectionRepository即:
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {

        /**
         * 第二个参数的作用:根据条件查找该用哪个ConnectionFactory来构建Connection对象
         * 第三个参数的作用: 对插入到userconnection表中的数据进行加密和解密
         */
        NrscJdbcUsersConnectionRepository repository = new NrscJdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        //设置userconnection表的前缀
        repository.setTablePrefix("nrsc_");
        return repository;
    }

具体操作有兴趣的看github的commit记录吧。


3.4 测试2

(1)填上用户名yoyo,密码XXX,点击注册发现跳向了/user/register路径
在这里插入图片描述
(2)数据库userconnection表里发现多了一条记录
在这里插入图片描述
(3)再次登陆可以直接进入到index页面,并且已经可以访问我们的API了,如下,获取的用户名正是我们注册的用户名yoyo
在这里插入图片描述
至此我们的QQ登陆已经开发完成。

4 未完待续。。。

额。。。本没想到会遇到mysql里rank关键字的问题,一搞,一下子又快4点了,且文章篇幅已经很长了,因此第三方登陆时,如何跳过注册逻辑的问题,将在下篇博客中进行分享,绑定和解绑相关功能将一并顺延!!!

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值