第三方登陆

OAuth协议简介

  • OAuth协议解决什么问题
  • OAuth协议中的主要角色
  • OAuth协议运行流程

微信自拍数据分享的例子

微信自拍得到的照片,第三方应用想要对照片进行美化。怎么做才能让第三方应用获取到微信的用户权限呢?

  • 难点

1、腾讯不会允许第三方应用直接访问用户私有的数据信息

2、如果用户同意第三方应用读取用户数据,那么怎么个"同意"法儿?

​ 总不能直接把账号密码提供给第三方吧。

3、如果第三方应用获取到了用户在微信的用户名密码,就会存在以下几个问题

​ 1、第三方应用可以访问用户在微信的所有数据

​ 2、用户只有修改密码,才能收回授权

​ 3、密码泄露的可能性大大提高

第三方登陆

在本例中OAuth协议是这么解决授权问题的:

不把微信的账号密码给第三方应用,而是给一个Token,这个Token中有权限信息、过期时间,并且无法通过这个Token**出用户名密码。

这样就同时解决了安全性、便捷性等问题。

以上,就是OAuth协议的基本介绍

OAuth协议的角色与运行流程

角色

第三方登陆

  • 服务提供商:Provider

提供令牌的,如:本例中的微信(腾讯)

  • 资源所有者:Resource Owner

需要被访问的数据资源的所有者,如:本例中的微信用户

  • 第三方应用:Client
  • 认证服务器:Authorization Server

认证用户的身份并产生令牌

  • 资源服务器:Resource Server

1、保存用户数据资源

2、验证令牌

一般实际开发中,认证服务器和资源服务器,可以是一台机器

流程

第三方登陆

0、微信用户访问第三方应用

1、第三方应用请求微信用户授权

2、微信用户同意第三方应用的授权请求

3、第三方应用去认证服务器申请令牌

4、任务服务器验证本次令牌申请是否合法,即是不是真的用户同意了授权,然后发放令牌

5、第三方应用拿着令牌去资源服务器中申请资源数据

6、资源服务器验证令牌是否合法,合法就开放资源给第三方应用

授权模式

第三方登陆

授权码模式

最完整,最全面,最常见的授权模式。

第三方登陆

特点:

1、用户同意授权的动作,是在认证服务器完成的

2、因为第三方应用需要接收认证服务器的授权码,然后去获取令牌,所以要求第三方应用必须有自己的服务器。如果第三方应用就是个普通的静态网站,就不能用授权码模式;可以使用简化模式

Spring Social基本原理

OAuth本质上是一个授权协议,其解决了用户在不暴露自己用户名密码信息的情况下,对第三方应用进行授权的问题

Spring Social用于实现第三方登录

现在有很多网站或者APP,我们可以使用微信或者QQ进行登录,只要进行授权即可,不需要重新注册APP或网站

第三方登陆

第三方应用使用令牌获取到用户基本信息,把这个信息按照步骤7处理,就相当于进行了一次登录

第三方登陆

Spring Social把6、7的过程,封装到了SocialAuthenticationFilter

Spring Social基本接口

  • AbstractOAuth2ServiceProvider:服务提供商
  • OAuth2OperationsOAuth2Template
  • AbstractOAuth2ApiBinding
  • ConnectionOAuth2Connection
  • ConnectionFactoryOAuth2ConnectionFactory
  • ……

第三方登陆

第三方登录:QQ

QQ的第三方登录,是一个 标准的OAuth协议,微信则不然,略有不同,具体的后续会讲到。

第三方登陆在网页和APP中都可以被用到,所以我们的相关代码会写在imooc-security-core项目中。

API接口

API接口是用于从腾讯获取用户信息的。

相关类:
第三方登陆

那么如何从腾讯中获取用户信息呢?这个就得看腾讯的文档了

腾讯文档

获取用户基本信息的接口为
腾讯文档

QQUserInfo

封装获取到的QQ用户的信息

package com.imooc.security.core.social.qq.api;

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * 封装从腾讯获取到的QQ用户的认证信息
 *
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-27
 * @Modified By:
 */
@Data
@Accessors(chain = true)
public class QQUserInfo {
    private int ret;
    private String openId;
	  private String msg;
    private String nickname;
    private String figureurl;
    private String figureurl_1;
    private String figureurl_2;
    private String figureurl_qq_1;
    private String figureurl_qq_2;
    private String gender;
    private String is_yellow_vip;
    private String vip;
    private String yellow_vip_level;
    private String level;
    private String is_yellow_year_vip;

}

这里的内容由腾讯文档决定

参数说明 描述
ret 返回码
msg 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
nickname 用户在QQ空间的昵称。
figureurl 大小为30×30像素的QQ空间头像URL。
figureurl_1 大小为50×50像素的QQ空间头像URL。
figureurl_2 大小为100×100像素的QQ空间头像URL。
figureurl_qq_1 大小为40×40像素的QQ头像URL。
figureurl_qq_2 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100x100的头像,但40x40像素则是一定会有。
gender 性别。 如果获取不到则默认返回"男"

QQ

接口,获取QQ用户信息

package com.imooc.security.core.social.qq.api;

import java.io.IOException;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-27
 * @Modified By:
 */

public interface QQ {
    QQUserInfo getUserInfo();
}

QQImpl

package com.imooc.security.core.social.qq.api;

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

import java.io.IOException;


@Slf4j
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {


    private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
    private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";


    private String appId;
    private String openId;

    private ObjectMapper objectMapper = new ObjectMapper();

    public QQImpl(String accessToken, String appId) {
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);//将access_token放在param参数上
        this.appId = appId;

        String url = String.format(URL_GET_OPENID, accessToken);
        String result = getRestTemplate().getForObject(url, String.class);

        log.info(result);

        this.openId = StringUtils.substringBetween(result, "\"openId\":", "}");
    }

    @Override
    public QQUserInfo getUserInfo()  {
        String url = String.format(URL_GET_USERINFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);
        log.info(result);
        return objectMapper.readValue(result, QQUserInfo.class);
    }
}

/*
AbstractOAuth2ApiBinding对象中有一个全局的accessToken属性,这个属性就是用来存储走完OAuth协议的大部分流程后,
从认证服务器中获取到的token的,可见,我们的QQImpl不是一个单例对象
 */

ServiceProvider接口

第三方登陆

QQServiceProvider

package com.imooc.security.core.social.qq.connect;

import com.imooc.security.core.social.qq.api.QQ;
import com.imooc.security.core.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;

public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {

    private String appId;
    private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize";
    private static final String URL_ACCESS_TOKEN="https://graph.qq.com/oauth2.0/token";

    /**
     * Create a new {@link OAuth2ServiceProvider}.
     *
     * @param oauth2Operations the OAuth2Operations template for conducting the OAuth 2 flow with the provider.
     */
    public QQServiceProvider(String appId,String appSecret) {
        // 这里改用我们自己实现的QQOAuth2Template,里面有对令牌、过期时间和刷新令牌正确解析
        super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
        this.appId = appId;
    }

    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken,appId);
    }
}



QQOAuth2Template

package com.imooc.security.core.social.qq.connect;

import org.springframework.social.oauth2.*;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-28
 * @Modified By:
 */

public class QQOAuth2Template extends OAuth2Template {
    public QQOAuth2Template(String appId, String appSecret, String urlAuthorize, String urlAccessToken) {
        super(appId,appSecret,urlAuthorize,urlAccessToken);
    }

}

ApiAdapter接口

用于将腾讯提供的用户信息格式转化成需要的格式

QQAdapter

package com.imooc.security.core.social.qq.connect;

import com.imooc.security.core.social.qq.api.QQ;
import com.imooc.security.core.social.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;

public class QQAdapter implements ApiAdapter<QQ> {

    /**
     * 测试当前Api是否可用
     *
     * @param api
     * @return
     */
    @Override
    public boolean test(QQ api) {
        return true;
    }

    /**
     * Connection数据和Api数据做适配
     *
     * @param api
     * @param values
     */
    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo qqUserInfo = api.getUserInfo();

        // 将Api获取的QQ用户信息设置到Connection中
        values.setDisplayName(qqUserInfo.getNickname());
        values.setImageUrl(qqUserInfo.getFigureurl_qq_1());
        // QQ没有个人主页,所以ProfileUrl为空就行
        values.setProfileUrl(null);
        //QQ用户在腾讯的唯一标识
        values.setProviderUserId(qqUserInfo.getOpenId());
    }

    @Override
    public UserProfile fetchUserProfile(QQ api) {
        // 绑定解绑的时候使用
        return null;
    }

    /**
     * 更新个人状态(QQ没有这个)
     *
     * @param api
     * @param message
     */
    @Override
    public void updateStatus(QQ api, String message) {
        // do nothing
    }
}

ConnectionFactory

QQConnectionFactory

package com.imooc.security.core.social.qq.connect;

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

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {

    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }
}

providerId:提供商唯一标识,通过配置文件配进来

UsersConnectionRepository

操作UserConnection表,表结构为

表结构

create table UserConnection (userId varchar(255) not null,
	providerId varchar(255) not null,
	providerUserId varchar(255),
	rank int not null,
	displayName varchar(255),
	profileUrl varchar(512),
	imageUrl varchar(512),
	accessToken varchar(512) not null,
	secret varchar(512),
	refreshToken varchar(512),
	expireTime bigint,
	primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);

配置

package com.imooc.security.core.social;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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;

import javax.sql.DataSource;

@Configuration
@EnableSocial
@Order(1)
public class SocialConfigurer extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        // 第一个参数是数据源,第二个参数是产生Connection的工厂(随着不同的服务提供商,这个会不同),第三个参数是加密的方式(这里不加密)
        JdbcUsersConnectionRepository jdbcUsersConnectionRepository =
                new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());

        return jdbcUsersConnectionRepository;
    }
}