admin管理员组

文章数量:1614997

 QQ登陆下

登陆本地调试

在上篇文章结尾中我们登陆报回调地址错误 这是怎么回事呢?原来第三方应用引导用户到认证服务器和认证服务器带着授权码的返回到第三方应用的地址是一样的都为/auth/qq。但是由于回调域名只支持80端口,所以将我们的项目端口修改成80端口或者做端口映射,同时修改hosts文件来进行本地调试。

比如在qq互联中心申请的回调地址为 http://www.ictgu/login/qq,那么我们在hosts文件(C:\Windows\System32\drivers\etc)下添加一行

127.0.0.1  www.ictgu

默认的认证地址是  /auth/供应商id  即/auth/qq 

这里有个问题我们注册的回调地址不是/auth/qq(),该怎么办呢?

简单源码分析:

我们自定义实现覆盖,代码如下

package com.rui.tiger.auth.core.social;

import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;

/**
 * 自定义SpringSocialConfigurer 用于覆盖默认的社交登陆拦截请求
 * @author CaiRui
 * @Date 2019/1/5 16:15
 */
public class TigerSpringSocialConfigurer extends SpringSocialConfigurer {

    private String filterProcessesUrl;//覆盖默认的/auth 拦截路径

    public TigerSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter socialAuthenticationFilter=(SocialAuthenticationFilter)super.postProcess(object);
        socialAuthenticationFilter.setFilterProcessesUrl(filterProcessesUrl);
        return(T)socialAuthenticationFilter;
    }
}

修改我们的SocialConfig,将filterProcessesUrl做成可配置

/**
     * 默认配置类  包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
     *
     * @return
     */
    @Bean
    public SpringSocialConfigurer tigerSpringSocialConfigurer() {
        TigerSpringSocialConfigurer tigerSpringSocialConfigurer = new TigerSpringSocialConfigurer(securityProperties.getSocial().getFilterProcessesUrl());
        return tigerSpringSocialConfigurer;
    }

 社交配置文件修改

配置文件中添加

#自定义权限配置
tiger:
  auth:
     browser:
       #loginPage: /demo-login.html # 这里可以配置成自己的非标准登录界面
        loginType: JSON
     imageCaptcha:
        interceptImageUrl: /user/*,/pay/confirm # 这些路径验证码也要拦截校验
     social:
        filterProcessesUrl: /auth
        qq:
          app-id: ***
          app-secret: ***
         # providerId: qq 默认就是
         # http://www.ictgu/login/qq qq互联申请的回调地址 101386962 2a0f820407df400b84a854d054be8b6a

ok 我们再次进行扫码登陆测试

扫码确认 却出现这样的界面 这个是怎么回事呢?

我们看后台日志 

 为什么扫码成功还会跳到signin这个地址呢? 这个就要结合源码分析来看了 

社交登陆流程源码分析

社交登陆原理流程图:

spring security的核心流程不变,social在这基础上增加了组件,和我们之前编写的密码短信登陆流程类似; 
上图蓝色部分是social除了security的东西外都是social实现好的。我们要做的只是 橘色部分;

源码分析,核心方法入口

org.springframework.social.security.SocialAuthenticationFilter#attemptAuthentication

org.springframework.social.security.provider.OAuth2AuthenticationService#getAuthToken

一步步跟踪下去,可以看到以下的报错信息:

 

详细报错信息如下: 

org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]

源码这里找到原因

同时也有另外一个问题,qq返回的是下面的字符串,而不是json结构

我们要对postForAccessGrant进行改造。

TigerSpringSocialConfigurer  自定义回调地址实现

package com.rui.tiger.auth.core.social;

import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;

/**
 * 自定义SpringSocialConfigurer 用于覆盖默认的社交登陆拦截请求
 * @author CaiRui
 * @Date 2019/1/5 16:15
 */
public class TigerSpringSocialConfigurer extends SpringSocialConfigurer {

    private String filterProcessesUrl;//覆盖默认的/auth 拦截路径

    public TigerSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter socialAuthenticationFilter=(SocialAuthenticationFilter)super.postProcess(object);
        socialAuthenticationFilter.setFilterProcessesUrl(filterProcessesUrl);
        return(T)socialAuthenticationFilter;
    }
}

调整 SocialConfig 将SpringSocialConfigurer 修改成我们自定义的如下所示

package com.rui.tiger.auth.core.social;

import com.rui.tiger.auth.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;

import javax.sql.DataSource;

/**
 * 社交配置类
 *
 * @author CaiRui
 * @Date 2019/1/5 11:46
 */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;//数据源
    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 默认配置类  包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
     *
     * @return
     */
    @Bean
    public SpringSocialConfigurer tigerSpringSocialConfigurer() {
        TigerSpringSocialConfigurer tigerSpringSocialConfigurer = new TigerSpringSocialConfigurer(
                securityProperties.getSocial().getFilterProcessesUrl());
        return tigerSpringSocialConfigurer;
    }

    /**
     * 业务系统用户和服务提供商用户对应关系,保存在表UserConnection
     * JdbcUsersConnectionRepository.sql 中有建表语句
     * userId 业务系统Id
     * providerId 服务提供商的Id
     * providerUserId  同openId
     * Encryptors  加密策略 这里不加密
     *
     * @param connectionFactoryLocator
     * @return
     */
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        //设定表UserConnection的前缀 表名不可以改变
        //jdbcUsersConnectionRepository.setTablePrefix("tiger_");
        return jdbcUsersConnectionRepository;
    }

    /**
     * 从认证中获取用户信息
     *
     * @return
     */
    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }

    //https://docs.spring.io/spring-social/docs/1.1.x-SNAPSHOT/reference/htmlsingle/#creating-connections-with-connectcontroller
    /*@Bean
    public ConnectController connectController(
            ConnectionFactoryLocator connectionFactoryLocator,
            ConnectionRepository connectionRepository) {
        return new ConnectController(connectionFactoryLocator, connectionRepository);
    }*/

}

相关配置进行调整

配置文件

现在我们创建QQOAuth2Template 继承 OAuth2Template,来调整qq返回text/html 和非标准json的问题

package com.rui.tiger.auth.core.social.qq.connect;

import lombok.extern.slf4j.Slf4j;
import org.apachemons.lang.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 java.nio.charset.Charset;

/**
 * 
 * @author CaiRui
 * @Date 2019-01-06 14:55
 */
@Slf4j
public class QQOAuth2Template extends OAuth2Template{

    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        // 会发现返回的信息是 callback( {"error":100002,"error_description":"param client_secret is wrong or lost "} )
        // 通过debug可以发现,传递过来的参数少了2个,对比文档中的;
        // 调用本方法之前传递过来的参数,也就是 exchangeForAccess() 方法
        // 其中有一个 useParametersForClientAuthentication 属性需要为true才会携带另外另个参数
        setUseParametersForClientAuthentication(true);//默认是false
    }


    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        // 添加一个处理 [text/plan] 格式的转换器  qq返回的不是json
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("utf-8")));
        return restTemplate;
    }

    // http://wiki.connect.qq/%E4%BD%BF%E7%94%A8authorization_code%E8%8E%B7%E5%8F%96access_token
    // 文档中说明:响应的是 access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr=getRestTemplate().postForObject(accessTokenUrl,parameters,String.class);
        log.info("获取accessToken响应:{}", responseStr);
        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
        String accessToken = StringUtils.substringAfterLast(items[0], "=");
        String expiresIn = StringUtils.substringAfterLast(items[1], "=");
        String refreshToken = StringUtils.substringAfterLast(items[2], "=");
        AccessGrant accessGrant = new AccessGrant(accessToken, null, refreshToken, new Long(expiresIn));
        return accessGrant;
    }
}
QQServiceProvider中  OAuth2Template换成我们自定义的
package com.rui.tiger.auth.core.social.qq.connect;

import com.rui.tiger.auth.core.social.qq.api.QQApi;
import com.rui.tiger.auth.core.social.qq.api.QQApiImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;

/**
 * QQ服务提供商
 * http://wiki.connect.qq/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_server-side
 * @author CaiRui
 * @date 2019-1-3 9:10
 */
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQApi> {

	//流程第1步 将用户导向认证服务器
	public static final String URL_AUTHORIZE  = "https://graph.qq/oauth2.0/authorize";
	//流程第4步 随带授权码获取用户令牌accessToken
	public static final String URL_ACCESS_TOKEN  = "https://graph.qq/oauth2.0/token";

	private String appId;//注册qq互联分配的id

	/**
	 * @param appId 注册qq互联分配的id
	 * @param secret 注册qq互联的分配密码
	 */
	public QQServiceProvider(String appId,String secret) {
		// OAuth2Operations 有一个默认实现类,可以使用这个默认实现类
		super(new QQOAuth2Template(appId, secret, URL_AUTHORIZE , URL_ACCESS_TOKEN ));
		this.appId=appId;
	}

	@Override
	public QQApi getApi(String accessToken) {
		return new QQApiImpl(accessToken, appId);
	}

}
QQApiImpl 
package com.rui.tiger.auth.core.social.qq.api;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apachemons.lang.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
import org.springframework.stereotype.Component;

/**
 * 不同用户信息不一样 不能直接@Component(spring默认单例 线程不安全)
 * @author CaiRui
 * @date 2019-1-3 8:56
 */
@Slf4j
public class QQApiImpl extends AbstractOAuth2ApiBinding implements QQApi {

	//获取openID   http://wiki.connect.qq/%E8%8E%B7%E5%8F%96%E7%94%A8%E6%88%B7openid_oauth2-0
	public final static String URL_GET_OPENID = "https://graph.qq/oauth2.0/me?access_token=%s";
	//获取用户信息 父类会自动携带accessToken http://wiki.connect.qq/get_user_info
	public final static String URL_GET_USER_INFO = "https://graph.qq/user/get_user_info?oauth_consumer_key=%s&openid=%s";

	/**
	 * 申请QQ登录成功后,分配给应用的appid
 	 */
	private String appId;//
	/**
	 * 通过输入在上一步获取的Access Token,得到对应用户身份的OpenID,与qq号码已一对应
	 * OpenID是此网站上或应用中唯一对应用户身份的标识,网站或应用可将此ID进行存储,便于用户下次登录时辨识其身份,或将其与用户在网站上或应用中的原有账号进行绑定
	 */
	private String openId;


	public QQApiImpl(String accessToken,String appId ){

		//accessToken 抽象父类参数
		super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);//token请求策略 放在header或参数中 QQ要求放在get请求参数中

		this.appId=appId;

		String openIdUrl=String.format(URL_GET_OPENID,accessToken);
		String result=getRestTemplate().getForObject(openIdUrl,String.class);
		//callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
		log.info("qq openID callback json:{}",result);
		//先直接截取 这个我们后面重构
	//	this.openId = StringUtils.substringBetween("\"openid\":", "}");
		this.openId = StringUtils.substringBetween(result,"\"openid\":\"", "\"}");
	}


	@Override
	public QQUserInfo getUserInfo()  {
		String url=String.format(URL_GET_USER_INFO,appId,openId);
		String userInfoResult=getRestTemplate().getForObject(url,String.class);
		//  QQUserInfo userInfo= JSON.parseObject(userInfoResult,QQUserInfo.class);
		log.info("qq userInfoResult json response:{}",userInfoResult);
		QQUserInfo userInfo= JSON.parseObject(userInfoResult, new TypeReference<QQUserInfo >(){});
		//返回的json中没有openId
		userInfo.setOpenId(openId);
		return userInfo;
	}
}

ok 到此我们都调整完毕,再来测试下qq社交登录

 看后台日志

可以看到 引发跳转的请求是:http://mrcode/signup 这是应为这个路径我们在权限中没有放行,所以调到认证界面了。下一章我们继续分析为什么会调到这个请求。

再次回顾下上面流程:

1.访问/auth/qq,未携带code参数

2.会重定向认证服务器,用户授权完成后,再调回原地址/auth/qq

3.social检测到携带了code参数,会去调用qqimpl交换accessToken

4.调用api获取accessToken信息

5.拿到令牌,包装成AccessGrant

6.获取用户信息
 

本文标签: SecuritySpringqq