SpringSecurity之QQ登录

做~自己de王妃 提交于 2020-01-19 22:22:59

在这里插入图片描述
编写顺序为:

  • Api
    获取用户信息
  • Oauth2Operations
    代表用户与服务提供者进行Oauth认证
  • ServiceProvider
    服务提供商
  • ApiAdapter
    连接统一的{@link Connection}模型和特定的提供者API模型的适配器
  • ConnectionFactory
    构造服务提供者{@link Connection}实例的工厂的基本抽象。
    创建Connection保存
创建用户信息类QQUserInfo:

按照QQ互联官方返回值获取到的用户信息进行构建:

package com.cong.security.core.social.qq.api;
import lombok.Data;
@Data
public class QQUserInfo {
    /* 返回码*/
    private String ret;
    /* 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。 */
    private String msg;
    /* QQ用户在系统中的唯一标识 */
    private String openId;
    /* 不知道什么东西,文档上没写,但是实际api返回里有。 */
    private String is_lost;
    /* 省(直辖市) */
    private String province;
    /* 市(直辖市区) */
    private String city;
    /* 出生年月 */
    private String year;
    /* 用户在QQ空间的昵称。*/
    private String nickname;
    /* 大小为30×30像素的QQ空间头像URL。*/
    private String figureurl;
    /*大小为50×50像素的QQ空间头像URL。*/
    private String figureurl_1;
    /* 大小为100×100像素的QQ空间头像URL。 */
    private String figureurl_2;
    /* 大小为40×40像素的QQ头像URL。 */
    private String figureurl_qq_1;
    /* 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。 */
    private String figureurl_qq_2;
    /* 性别。 如果获取不到则默认返回”男” */
    private String gender;
    /* 新增参数???? */
    private String gender_type;
    /* 标识用户是否为黄钻用户(0:不是;1:是)。 */
    private String is_yellow_vip;
    /* 标识用户是否为黄钻用户(0:不是;1:是) */
    private String vip;
    /* 黄钻等级*/
    private String yellow_vip_level;
    /* 黄钻等级 */
    private String level;
    /* 标识是否为年费黄钻用户(0:不是; 1:是)*/
    private String is_yellow_year_vip;
    // 未知参数
    private String constellation;
    private String figureurl_type;
    private String figureurl_qq;
}

注意其中有一个gender_type参数,我以前也是没有的,但是在使用过程中突然有一次报错,查看日志发现就是多了这个参数,虽然我也不知道是干啥的。。

QQ获取信息接口:
package com.cong.security.core.social.qq.api;

public interface QQ {
	/**
	 * 获取用户QQ信息
	 * 
	 * @return QQUserInfo
	 */
	QQUserInfo getUserInfo();
}
QQ获取信息接口实现类:
package com.cong.security.core.social.qq.api;

import org.apache.commons.lang3.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;

/**
 * 
 * @author single-聪
 * @date 2019年8月12日
 * @version 0.0.1
 */
@Slf4j
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {

	// 根据用户access_token获取用户openId
	private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
	// 根据系统appId以及用户openId获取用户信息(access_token值的绑定已经在父类中实现)
	private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";

	// accessToken由父类处理,当前类不处理

	// QQ互联注册到的APPID,在系统中属于唯一值
	private String appId;

	// 每一个用户QQ互联给出的唯一标识
	private String openId;

	private ObjectMapper objectMapper = new ObjectMapper();

	/**
	 * 构造函数(openId根据accessToken去QQ互联获取,不需要传入)
	 * 
	 * @param accessToken
	 *            走完oAuth流程获取到的令牌
	 * @param appId
	 *            系统appId
	 */
	public QQImpl(String accessToken, String appId) {
		// 父类默认是放在请求头,QQ要求是将accessToken作为参数传入,此时系统会将accessToken作为参数组装到URL_GET_USERINFO里面
		super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
		// appId
		this.appId = appId;
		// 使用accessToken替换URL_GET_OPENID的%s
		String url = String.format(URL_GET_OPENID, accessToken);
		String result = getRestTemplate().getForObject(url, String.class);
		log.info("调用腾讯QQ登录,返回值为[{}]", result);
		// 发送请求获取openId
		this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
	}

	@Override
	public QQUserInfo getUserInfo() {
		// accessToken在类初始化时已经实现
		// 将appId和openId更换到URL_GET_USERINFO字符串中
		String url = String.format(URL_GET_USERINFO, appId, openId);
		String result = getRestTemplate().getForObject(url, String.class);
		log.info("调用腾讯QQ登录,接口为:[{}]获取用户信息为[{}]", url, result);
		// 将返回的用户信息转换成QQUserInfo类型,如果转换失败则代表获取用户信息失败
		QQUserInfo userInfo = null;
		try {
			// 将JSON格式字符串转换成QQUserInfo对象
			userInfo = objectMapper.readValue(result, QQUserInfo.class);
			userInfo.setOpenId(openId);
			return userInfo;
		} catch (Exception e) {
			throw new RuntimeException("获取用户信息失败", e);
		}
	}
}
编写QQServiceProvider
package com.cong.security.core.social.qq.connect;

import com.cong.security.core.social.qq.api.QQ;
import com.cong.security.core.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;
import lombok.extern.slf4j.Slf4j;
/**
 * 用来整合QQOAuth2Template和AbstractOAuth2ApiBinding
 */
@Slf4j
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {

	private String appId;
	// 在QQ互联上可查到,此配置不变,将用户导向认证服务器的URL
	private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
	// 在QQ互联上可查到,此配置不变,申请令牌的URL
	private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
	// 默认的构造函数
	public QQServiceProvider(String appId, String appSecret) {
		/**
		 * 在QQ互联注册时QQ互联分配的appId以及appSecret<br>
		 * clientId---appId<br>
		 * clientSecret---appSecret<br>
		 * authorizeUrl---对应第一步导向的认证服务器<br>
		 * accessTokenUrl---对应第四步申请令牌的地址<br>
		 */
		super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
		this.appId = appId;
	}

	@Override
	public QQ getApi(String accessToken) {
		log.info("获取到用户accessToken值为:[{}]", accessToken);
		return new QQImpl(accessToken, appId);
	}
}
编写QQAdapter:
package com.cong.security.core.social.qq.connect;

import com.cong.security.core.social.qq.api.QQ;
import com.cong.security.core.social.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
import lombok.extern.slf4j.Slf4j;

/**
 * 适配的API的类型为QQ
 */
@Slf4j
public class QQAdapter implements ApiAdapter<QQ> {
	@Override
	public boolean test(QQ api) {
		// 默认QQ永远是通的,实际上需要发送请求判断
		return true;
	}
	@Override
	public void setConnectionValues(QQ api, ConnectionValues values) {
		// Connection数据和API数据之间实现适配
		QQUserInfo userInfo = api.getUserInfo();
		log.info("从服务商获取用户信息:[{}]", userInfo);
		// 显示的用户名
		values.setDisplayName(userInfo.getNickname());
		// 用户头像
		values.setImageUrl(userInfo.getFigureurl_qq_1());
		// 个人主页(QQ不存在这个信息)
		values.setProfileUrl(null);
		// 服务商的用户ID
		values.setProviderUserId(userInfo.getOpenId());
	}

	@Override
	public UserProfile fetchUserProfile(QQ api) {
		// 绑定解绑
		return null;
	}

	@Override
	public void updateStatus(QQ api, String message) {
		// QQ不存在此类方法调用,本系统中只使用QQ登录,不进行用户分享之类的
	}
}
构建QQConnectionFactory:
package com.cong.security.core.social.qq.connect;

import com.cong.security.core.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {

	/**
	 * 创建连接工厂构造函数
	 * 
	 * @param providerId
	 *            QQ的openId
	 * @param appId
	 *            QQ互联AppId
	 * @param appSecret
	 *            QQ互联appSecret
	 */
	public QQConnectionFactory(String providerId, String appId, String appSecret) {
		/**
		 * providerId---服务提供商唯一标识、配置文件配进来<br>
		 * appId---QQ互联appId<br>
		 * appSecret---QQ互联appSecret<br>
		 */
		super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
	}
}
配置SocialConfig:
package com.cong.security.core.social;

import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;

/**
 * 社交登陆的适配器
 * 
 * @author single-聪
 * @date 2019年8月12日
 * @version 0.0.1
 */
@EnableSocial
@Configuration
public class SocialConfig extends SocialConfigurerAdapter {

	@Autowired
	private DataSource dataSource;

	@Override
	public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
		/**
		 * dataSource---数据源<br>
		 * connectionFactoryLocator---连接工厂,辨别<br>
		 * Encryptors.noOpText()---暂时采用不加密的方式,方便数据库查看<br>
		 */
		JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
				connectionFactoryLocator, Encryptors.noOpText());
		// 设置表名前缀,数据库可以对表加前缀,需要注意windows和Linux系统的数据库区表名分大小写,如果报错找不到当前表,可以查看是否为此种情况
		// repository.setTablePrefix("my_");
		return repository;
	}
	
	// 过滤器链
	@Bean
	public SpringSocialConfigurer mySocialSecurityConfig() {
		return new SpringSocialConfigurer();
	}
}
重写用户名密码登录模块的MyUserDetailsServiceImpl实现类:
package com.cong.security.service.impl;

import com.cong.security.core.social.MySocialUser;
import com.cong.security.entity.Account;
import com.cong.security.mapper.AccountMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import sun.security.util.Password;

/**
 * @Description 自定义用户名密码登录
 * @Author single-聪
 * @Date 2020/1/7 18:42
 * @Version 1.0.1
 **/
@Slf4j
@Component
public class MyUserDetailsServiceImpl implements UserDetailsService, SocialUserDetailsService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return buildUser(username);
    }

    @Override
    public SocialUserDetails loadUserByUserId(String id) throws UsernameNotFoundException {
        return buildUser(id);
    }

    private SocialUserDetails buildUser(String id) {
        Account account = null;
        // 如果id长度为11,即代表用户采用手机号方式登陆系统,手机号即为用户用户名,否则即为第三方账户登录
        if (id.length() == 11) {
            account = accountMapper.loginByPhone(id);
        } else {
            account = accountMapper.loginById(id);
        }
        if (account != null) {
            log.info("用户信息封装:[{}]", account);
            String password = account.getPassword();
            return new MySocialUser(account.getId(), id, password, account.getEnabled(), account.getExpired(), true,
                    account.getLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList(account.getRole()));
        } else {
            return new MySocialUser(id, id, null, false, false, false, false,
                    AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
        }
    }
}

其中MySocialUser为自定义类继承SocialUser:
SocialUser中没有UserId属性,提供的getUserId方法实际返回值为this.username,在本系统中对应用户手机号,但是手机号可以更换,所以此方式不符合本系统逻辑。

package com.cong.security.core.social;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.social.security.SocialUser;

public class MySocialUser extends SocialUser {

	public MySocialUser(String id, String username, String password, boolean enabled, boolean accountNonExpired,
			boolean credentialsNonExpired, boolean accountNonLocked,
			Collection<? extends GrantedAuthority> authorities) {
		super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
		this.id = id;
	}
	private static final long serialVersionUID = 1L;
	private String id;
	public String getId() { return id; }
	public void setId(String id) { this.id = id; }
}
定义三方登录常量:

在这里插入图片描述
配置使系统使用当前配置参数:

package com.cong.security.core.social;

import javax.sql.DataSource;
import com.cong.security.core.properties.QQProperties;
import com.cong.security.core.properties.SecurityProperties;
import com.cong.security.core.social.qq.connect.QQConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactory;

/**
 * 如果系统中没有配置QQ的appId以及appSecret,此配置不起作用
 *
 * @author single-聪
 */
@Configuration
@ConditionalOnProperty(prefix = "my.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private SecurityProperties securityProperties;

    protected ConnectionFactory<?> createConnectionFactory() {
        QQProperties qqConfig = securityProperties.getSocial().getQq();
        // 创建连接工厂,初始化参数
        return new QQConnectionFactory(securityProperties.getSocial().getQq().getProviderId(), securityProperties.getSocial().getQq().getAppId(), securityProperties.getSocial().getQq().getAppSecret());
    }
}

yml配置文件添加配置(自行去QQ互联申请账户)
将上述配置加入浏览器过滤器链:
在这里插入图片描述

编写登录页面:
<h2>社交登录</h2>
<a href="/auth/qq">QQ登录</a>

其中/authSocialAuthenticationFilter类中定义的常量DEFAULT_FILTER_PROCESSES_URL的值,/qq是QQProperties中定义的providerId值。
进入登录页面点击QQ登录链接应该会跳转到以下页面:
在这里插入图片描述
注意,如果报错/auth/qq接口权限不足,跳转到自定义授权接口时,大概率是SpringBoot升级到2.X以上版本的问题,具体解决办法查看文章:SpringBoot2.X版本SocialAutoConfigurerAdapter缺失造成第三方登录接口授权失败

还有两个问题需要解决:

  • 域名配置
    参考文章域名解析

  • href参数可配置
    自定义MySpringSocialConfigurer:

    package com.cong.security.core.social;
    
    import org.springframework.social.security.SocialAuthenticationFilter;
    import org.springframework.social.security.SpringSocialConfigurer;
    
    public class MySpringSocialConfigurer extends SpringSocialConfigurer {
    	// 拦截请求
    	private String filterProcessesUrl;
    
    	//object---需要放到过滤器链上的Filter
    	@Override
    	protected <T> T postProcess(T object) {
    		SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
    		filter.setFilterProcessesUrl(filterProcessesUrl);
    		return (T) filter;
    	}
    
    	// 构造函数设置拦截请求
    	public MySpringSocialConfigurer(String filterProcessesUrl) {
    		this.filterProcessesUrl = filterProcessesUrl;
    	}
    }
    

    修改SocialConfig配置:

    package com.cong.security.core.social;
    
    import javax.sql.DataSource;
    import com.cong.security.core.properties.SecurityProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.crypto.encrypt.Encryptors;
    import org.springframework.social.UserIdSource;
    import org.springframework.social.config.annotation.EnableSocial;
    import org.springframework.social.config.annotation.SocialConfigurerAdapter;
    import org.springframework.social.connect.ConnectionFactoryLocator;
    import org.springframework.social.connect.UsersConnectionRepository;
    import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
    import org.springframework.social.security.AuthenticationNameUserIdSource;
    import org.springframework.social.security.SpringSocialConfigurer;
    
    /**
     * 社交登陆的适配器
     */
    @EnableSocial
    @Configuration
    public class SocialConfig extends SocialConfigurerAdapter {
    
    	@Autowired
    	private DataSource dataSource;
    
    	@Override
    	public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
    		/**
    		 * dataSource---数据源<br>
    		 * connectionFactoryLocator---连接工厂,辨别<br>
    		 * Encryptors.noOpText()---暂时采用不加密的方式,方便数据库查看<br>
    		 */
    		// TODO 数据存储加密方式
    		JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
    				connectionFactoryLocator, Encryptors.noOpText());
    		// 设置表名前缀,数据库可以对表加前缀,需要注意windows和Linux系统的数据库区表名分大小写,如果报错找不到当前表,可以查看是否为此种情况
    		// repository.setTablePrefix("my_");
    		return repository;
    	}
    
    	@Autowired
    	private SecurityProperties securityProperties;
    
    	// 过滤器链
    	@Bean
    	public SpringSocialConfigurer mySocialSecurityConfig() {
    		return new MySpringSocialConfigurer(securityProperties.getSocial().getFilterProcessesUrl());
    	}
    
    	@Override
    	public UserIdSource getUserIdSource() {
    		return new AuthenticationNameUserIdSource();
    	}
    }
    

    最后配置参数使QQ登录的链接值为QQ互联设置的参数后缀。(尽量为二级,三方登录还有微信支付宝微博等)
    启动项目,点击QQ登录即可跳转到正确的页面,使用QQ扫描即可显示你在QQ互联申请的应用信息。点击确认后端接收到的请求时signin,在SocialAuthenticationFilter类中DEFAULT_FAILURE_URL值,未授权,最终跳转到授权接口。
    造成上述原因是:
    OAuth2AuthenticationService在去换取令牌时获取到的返回值是text/html模式,但是在构建与提供者进行API通信的RestTemplate时未添加text/html类型的转换器
    在这里插入图片描述
    流程图
    在这里插入图片描述
    解决上述问题:
    自定义QQOAuth2Template:

    package com.cong.security.core.social.qq.connect;
    
    import java.nio.charset.Charset;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.http.converter.StringHttpMessageConverter;
    import org.springframework.social.oauth2.AccessGrant;
    import org.springframework.social.oauth2.OAuth2Template;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.client.RestTemplate;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    public class QQOAuth2Template extends OAuth2Template {
    
        public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
            // clientId和clientSecret对应APP的appId以及appSecret
            super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
            // 默认是false,必须设置为true才会设置clientId以及clientSecret
            setUseParametersForClientAuthentication(true);
        }
    
        /**
         * AccessGrant---访问令牌信息的封装
         */
        @Override
        protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
            // 返回的不是json数据,返回的是字符串,需要对字符串切割,截取数据
            String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
            log.info("获取accessToken的响应:[{}]", responseStr);
            String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
            // 令牌
            String accessToken = StringUtils.substringAfterLast(items[0], "=");
            // 过期时间
            Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
            // 刷新令牌
            String refreshToken = StringUtils.substringAfterLast(items[2], "=");
            return new AccessGrant(accessToken, null, refreshToken, expiresIn);
        }
    
        @Override
        protected RestTemplate createRestTemplate() {
            // 拿到父类的创建结果,此时父类的三种转换器已经添加完成已经完成
            RestTemplate restTemplate = super.createRestTemplate();
            // 添加新的转换器,处理text/html类型
            restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
            return restTemplate;
        }
    }
    

    QQServiceProvider的构造方法new QQOAuth2Template替换OAuth2Template。
    此时再次启动项目扫码授权可以获取到当前QQ信息,路径跳转到signup上。

处理QQ注册

  • 跳转到signup的原因是:
    在这里插入图片描述
    查询不到userId,抛出BadCredentialsException异常,SocialAuthenticationFilter捕获到BadCredentialsException异常之后跳转到注册页:
    在这里插入图片描述
    signupUrl的默认值为/signup

    解决上述问题
    在BrowserProperties设置默认注册页属性private String signUpUrl = "/regist.html";
    yml文件进行配置。

    配置BrowserSecurityConfig对自定义的注册页面授权。
    配置使SocialAuthenticationFilter使用自定义的注册页面跳转(SocialConfig.java):
    在这里插入图片描述

    默认的注册页代码:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>默认注册页面</title>
    </head>
    <body>
    <h3>注册页面</h3>
    <form action="/user/bind" method="post">
        <table>
            <tr>
                <td>手机号</td>
                <td><input type="text" name="phone"></td>
            </tr>
            <tr>
                <td>验证码</td>
                <td><input type="password" name="code"></td>
            </tr>
            <tr>
                <td>
                    <button type="submit">登录</button>
                </td>
            </tr>
        </table>
    </form>
    </body>
    </html>
    
  • 用户绑定逻辑是:

    • 输入手机号,发送短信验证码(目前写死为手机号后六位,实际中发送在验证码校验接口校验即可)
    • 点击登录,如果用户不存在,那么为用户根据手机号创建默认账号,并和Session会话中的QQ信息绑定,加入UserConnection表,如果用户存在,那么直接绑定。(目前大多数应用都是短信验证码登录默认注册,没几个用户会记得住那么多用户名密码,所以基本可以省略设置密码步骤,但是如果设置密码需要注意密码加密方式与用户名密码登录接口加密方式一致。)

    编写表单提交的user/bind接口(安全配置中加入本接口权限)

    package com.cong.security.controller;
    
    import com.cong.security.service.AccountService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import javax.servlet.http.HttpServletRequest;
    
    @Slf4j
    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @Autowired
        private AccountService accountService;
    
        @RequestMapping("bind")
        public String bind(HttpServletRequest request) {
            log.info("执行用户绑定操作,获取用户手机号即短信校验码进行校验");
            String phone = request.getParameter("phone");// 手机号
            String code = request.getParameter("code");// 验证码
            // 验短信验证码返回该手机号对应的用户标识
            String userId = accountService.testSms(phone, code);
    		// TODO 将系统userId和第三方账号进行绑定
            return userId;
        }
    	
    	@RequestMapping("me")
        public Object me(@AuthenticationPrincipal  UserDetails user) {
            return user;
        }
    }
    
  • 使用Spring工具类ProviderSignInUtils:
    在SocialConfig中添加如下配置:

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

    1、在注册过程中拿到Spring Social信息
    SocialUser为自定义类,存储第三方信息

    @RequestMapping("social/user")
    public SocialUser getSocialUserInfo(HttpServletRequest request) {
        SocialUser socialUser = new SocialUser();
        Connection connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
        socialUser.setProviderId(connection.getKey().getProviderId());
        socialUser.setProviderUserId(connection.getKey().getProviderUserId());
        socialUser.setNickName(connection.getDisplayName());
        socialUser.setHead(connection.getImageUrl());
        return socialUser;
    }
    

    2、将业务系统userId传给Spring Social进行第三方账号绑定:

    @Autowired
    private AccountService accountService;
    
    @Autowired
    private ProviderSignInUtils providerSignInUtils;
    
    @RequestMapping("bind")
    public String bind(HttpServletRequest request) {
        log.info("执行用户绑定操作,获取用户手机号即短信校验码进行校验");
        String phone = request.getParameter("phone");// 手机号
        String code = request.getParameter("code");// 验证码
        // 验验证码(隐式注册,只要验证码正确就会返回唯一用户标识)
        String userId = accountService.testSms(phone, code);
        // TODO 将系统userId和第三方账号进行绑定
        providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
        return userId;
    }
    

    此时启动项目,QQ登录会跳转到注册页,输入手机号及短信即可实现账户绑定,此时回到登录页再次执行QQ登录,理论上而言会跳转到默认展示页,但是如果依然跳回注册页,按照前面的分析问题出现在SocialAuthenticationProvider中,断点调试:
    在这里插入图片描述
    我们使用的三方登录数据存储在数据库中,所以使用的ConnectionRepository应该是JdbcUsersConnectionRepository,解决:
    在QQAutoConfig配置中重写getUsersConnectionRepository方法:

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        // 将微信登陆的实现类设置为jdbc实现
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
                connectionFactoryLocator, Encryptors.noOpText());
        return repository;
    }
    

    重新注册,第一次输入手机号绑定,第二次使用QQ登录即跳转到系统默认展示页。

隐式注册

默认注册账户,和手机号验证码默认注册逻辑一致(但是目前90%应用需要实名认证,个人认为隐式注册注定要被抛弃)
实现ConnectionSignUp接口:

package com.cong.security.service.impl;

import com.cong.security.entity.Account;
import com.cong.security.mapper.AccountMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;

/**
 * @Description TODO
 * @Author single-聪
 * @Date 2020/1/19 19:53
 * @Version 1.0.1
 **/
@Slf4j
@Component
public class MyConnectionSignUp implements ConnectionSignUp {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public String execute(Connection<?> connection) {
        // TODO 根据社交账户信息默认创建用户
        log.info("第三方数据来源为:[{}]", connection.getKey().getProviderId());
        log.info("第三方数据openId为:[{}]", connection.getKey().getProviderUserId());
        log.info("第三方数据昵称为:[{}]", connection.getDisplayName());// 昵称,可以当做本系统用户昵称,但是不能作为登录用户名
        log.info("第三方数据头像路径为:[{}]", connection.getImageUrl());// 需要将该图片传至自身服务器
        // 1、构建系统用户信息(使用三方账户数据)
        Account account = new Account("18812345678");
        // 2、加入本系统表中
        accountMapper.insert(account);
        // 3、返回创建的用户唯一标识(UUID)
        return account.getId();
    }
}

QQAutoConfig中添加配置:

package com.cong.security.core.social;

import javax.sql.DataSource;
import com.cong.security.core.properties.SecurityProperties;
import com.cong.security.core.social.qq.connect.QQConnectionFactory;
import com.cong.security.core.social.support.SocialAutoConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;

/**
 * 如果系统中没有配置QQ的appId以及appSecret,此配置不起作用
 *
 * @author single-聪
 */
@Configuration
@ConditionalOnProperty(prefix = "my.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialAutoConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;

    protected ConnectionFactory<?> createConnectionFactory() {
        // 创建连接工厂,初始化参数
        return new QQConnectionFactory(securityProperties.getSocial().getQq().getProviderId(), securityProperties.getSocial().getQq().getAppId(), securityProperties.getSocial().getQq().getAppSecret());
    }

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
                connectionFactoryLocator, Encryptors.noOpText());
        if (connectionSignUp != null) {
            repository.setConnectionSignUp(connectionSignUp);
        }
        return repository;
    }
}

至此,关于QQ三方登录笔记完成。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!