使用SpringSocial 开发第三方QQ登录

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
OAuth 协议
    OAuth协议要解决的问题
        解决传统模式的授权(授权协议),认证资源访问问题
    OAuth协议中的各种角色
        Privider 服务提供商(提供令牌) (如微信)
            Authorization Server 认证服务器
            ResourceServer 资源服务器
        Resource Owner 资源所有者 (用户)
        Client 第三方应用
    OAuth协议运行流程
        0访问Client
        1将用户导向认证服务器
        2用户同意授权
        3返回Client并携带授权码
        4申请令牌
        5发放令牌 (1-5是OAuth标准流程)
        6获取用户信息 (每个服务商提供返回的字段不一样)
        7根据用户信息构建Authentictioin并放入SecurityContext
    OAuth协议授权模式
        授权码模式 (authorization code)
            认证授权在服务器提供商认证服务器完成,更安全
        密码模式(Resource owner password credentials)
        客户端模式(client credentials)
        简化模式(implicit)
SpringSocial 基本原理 (就是封装了OAuth2的基本流程 SocialAuthenticationFilter加入到过滤器链中)
    ServiceProvider (AbstractOAuth2ServiceProvider服务提供商的抽象实现)
        OAuth2Operations (OAuth2Template 封装1-5 会帮你去完成OAuth的认证流程)
        Api(AbstractOAuth2ApiBinding获取用户信息抽象实现 6)
    7步是在第三方应用上操作
    Connection(OAuth2Connection服务提供商的信息)  是由 ConnectionFactory(OAuth2ConnectionFactory) 创建的
    ConnectionFactory 中是包含(ServiceProvider 的)
    ApiAdapter  (在Aip 和 Connection之间做一个适配,因为Connection字段是固定的,服务商是个性化的)
    用户和第三方用户有个关联表,UserConnection
    UserConnection 表是由 JdbcUsersConnectionRepository(UsersConnectionRepository) 这个类进行操作CRUD

 第一步: 根据图2 先 构建ServiceProvider

 (6获取用户信息): 根据OAuth2Operations (OAuth2Template),Api(AbstractOAuth2ApiBinding) 构建 ServiceProvider

创建API 获取用户信息接口

1
2
3
4
5
6
7
8
9
10
package com.imooc.security.core.social.qq.api;
 
/**
 * @Title: QQ
 * @ProjectName spring-security-main
 * @date 2020/12/710:21
 */
public interface QQ {
    QQUserInfo getUserInfo();
}

 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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;
 
/**
 * @Title: QQImpl
 * @ProjectName spring-security-main
 * @date 2020/12/710:22
 */
@Slf4j
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
    // 获取openId
    private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
    // 获取用户信息 accessToken 父类会处理,这里不用拼接参数了
    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); // 默认父类是放在header中,这里要放在参数里
        this.appId = appId;
        // 获取openId
        String url = String.format(URL_GET_OPENID, accessToken);
        String result = getRestTemplate().getForObject(url, String.class);
        log.info(result);
        // 截取一下openId
        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);
        QQUserInfo userInfo = null;
        try {
            userInfo = objectMapper.readValue(result, QQUserInfo.class);
            userInfo.setOpenId(openId);
            return userInfo;
        } catch (Exception e) {
            throw new RuntimeException("获取用户信息失败", e);
        }
    }
}

 QQUserInfo  实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.imooc.security.core.social.qq.api;
 
import lombok.Data;
 
@Data
public class QQUserInfo {
     
    /**
     *  返回码
     */
    private String ret;
    /**
     * 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
     */
    private String msg;
    /**
     *
     */
    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;
 
 
    private String constellation;
 
 
    /**
     *  标识用户是否为黄钻用户(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 figureurl_qq;
 
    private String figureurl_type;
 
}

 

创建QQOAuth2Template  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.imooc.security.core.social.qq.connet;
 
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.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;
@Slf4j
public class QQOAuth2Template extends OAuth2Template {
     
 
    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true); // 设置true client_secret client_id才会带上这两个参数
    }
     
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
        // 因为qq返回的是 用&拼接的字符串 需要自己处理一下
        log.info("获取accessToke的响应:"+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();
        // qq返回的是text/html 默认的template没有添加处理这样的类型,自己添加一个
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
 
}

  

根据  QQOAuth2Template ,QQImpl  创建 QQServiceProvider 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.imooc.security.core.social.qq.connet;
 
import com.imooc.security.core.social.qq.api.QQ;
import com.imooc.security.core.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;
 
/**
 * @Title: QQServiceProvider
 * @ProjectName spring-security-main
 * @date 2020/12/711:01
 */
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
 
    private String appId;
    // 1.导向认证服务器rul
    private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
    // 4.用户同意授权,返回授权码去申请令牌的url
    private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
    public QQServiceProvider(String appId, String appSecret) {
//        super(oauth2Operations);
        // OAuth2Template 用系统默认的
//        super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
        // 默认Template不能满足使用
        super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
        this.appId = appId;
    }
 
    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken, appId);
    }
 
}

  图2右边的代码已经完成

 图2 左边开发

创建QQAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.imooc.security.core.social.qq.connet;
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;
/**
 * 服务商和第三方应用之间做适配的,适配的类型就是QQ
 */
public class QQAdapter implements ApiAdapter<QQ> {
    /**
     * 测试方法
     * @param api
     * @return
     */
    @Override
    public boolean test(QQ api) {
        return true;
    }
    /**
     * 主要方法, qq api的信息设置到connectionValues
     * @param api
     * @param values
     */
    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();
        values.setDisplayName(userInfo.getNickname());
        // 头像
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        // 个人主页
        values.setProfileUrl(null);
        values.setProviderUserId(userInfo.getOpenId());
    }
 
    @Override
    public UserProfile fetchUserProfile(QQ api) {
        return null;
    }
    @Override
    public void updateStatus(QQ api, String message) {
 
    }
}

 根据 QQAdapter 和 QQServiceProvider 可以构建QQConnectionFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.imooc.security.core.social.qq.connet;
import com.imooc.security.core.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
    /**
     * 根据 QQAdapter 和 QQServiceProvider 可以构建QQConnectionFactory
     * @param providerId 服务商Id
     * @param appId
     * @param appSecret
     */
    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }
}

  建表 查找 JdbcUsersConnectionRepository.sql 

创建  SocialConfig 配置类,启用Social

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.imooc.security.core.social;
 
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.ConnectionSignUp;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.social.security.SpringSocialConfigurer;
 
import javax.sql.DataSource;
 
/**
 * @Title: SocialConfig
 * @ProjectName spring-security-main
 * @date 2020/12/713:57
 */
@Configuration
@EnableSocial
@Slf4j
public class SocialConfig extends SocialConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;
    // 数据源
    @Autowired
    private DataSource dataSource;
 
    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;
 
    /**
     * 默认使用InMemoryUsersConnectionRepository类 , Primary 候选bean中优先使用,Bean 添加这两个注解
     * @param connectionFactoryLocator 作用就是查找ConnectionFactory
     * @return
     */
    @Primary
    @Bean
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
                connectionFactoryLocator, Encryptors.noOpText());
        // 配置数据库前缀
//        repository.setTablePrefix("imooc_");
        if(connectionSignUp != null) {
            repository.setConnectionSignUp(connectionSignUp);
        }
        return repository;
    }
 
    /**
     * 配置自定义路径  默认是auth
     *需要 把SpringSocialConfigure 加入到过滤器链上
     * @return
     */
    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
        log.info(filterProcessesUrl);
        ImoocSpringSocialConfigurer imoocSpringSocialConfigurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
        // 登录成功 回调的url
        imoocSpringSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
        return imoocSpringSocialConfigurer;
//        return new SpringSocialConfigurer();
    }
 
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator)) {
        };
    }
}

  创建 ImoocSpringSocialConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.imooc.security.core.social;
 
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
public class ImoocSpringSocialConfigurer extends SpringSocialConfigurer {
 
    /**
     * 修改默认的路由auth 可配置,默认是auth,自定义回调地址, 要和备案的一致
     */
    private String filterProcessesUrl;
     
    public ImoocSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }
     
    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filterProcessesUrl);
        return (T) filter;
    }
 
}

 

创建 QQAutoConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.imooc.security.core.social.qq.config;
 
import com.imooc.security.core.properties.QQProperties;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.social.qq.connet.QQConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.connect.ConnectionFactory;
 
@Configuration
@ConditionalOnProperty(prefix = "imooc.security.social.qq", name = "app-id") // 当配置文件中,有配置属性才会生效
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;
    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        QQProperties qqConfig = securityProperties.getSocial().getQq();
        return new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret());
    }
}

  

把SpringSocialConfigurer 加入到过滤器链上
1
2
3
4
@Autowired
private SpringSocialConfigurer imoocSocialSecurityConfig;
 
.apply(imoocSocialSecurityConfig)

  

imooc-signIn.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
    <h2>标准登录页面</h2>
    <h3>表单登录</h3>
    <form action="/authentication/form" 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>图形验证码:</td>
                <td>
                    <input type="text" name="imageCode">
                    <img src="/code/image?width=200">
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">登录</button></td>
            </tr>
        </table>
    </form>
     
    <h3>短信登录</h3>
    <form action="/authentication/mobile" method="post">
        <table>
            <tr>
                <td>手机号:</td>
                <td><input type="text" name="mobile" value="13012345678"></td>
            </tr>
            <tr>
                <td>短信验证码:</td>
                <td>
                    <input type="text" name="smsCode">
                    <a href="/code/sms?mobile=13012345678">发送验证码</a>
                </td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">登录</button></td>
            </tr>
        </table>
    </form>
    <br>
    <h3>社交登录</h3>
    <a href="/qqLogin/callback.do">QQ登录2</a>
    <a href="/auth/qq">QQ登录1</a>
         
    <a href="/qqLogin/weixin">微信登录</a>
</body>
</html>

  

 

注册问题 SocialConfig 

 

首次登录自动注册 

DemoConnectionSignUp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.imooc.security;
 
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;
 
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
    @Override
    public String execute(Connection<?> connection) {
        //根据社交用户信息默认创建用户并返回用户唯一标识
        return connection.getDisplayName();
    }
 
}

  

点击按钮进行手动注册

1
2
3
4
5
6
7
8
9
10
@Autowired
private ProviderSignInUtils providerSignInUtils;
 
@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {
 
    //不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。
    String userId = user.getUsername();
    providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}

  获取当前用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 前端可以显示 当前登录的用户信息
 * @param request
 * @return
 */
@GetMapping("/social/user")
public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
    SocialUserInfo userInfo = new SocialUserInfo();
    // 在session中取出用户信息
    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;
}

  

posted @   qukaige  阅读(199)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示