前言
前一阵子,为我的开源项目添加了第三方登录的功能,实现过程还算顺利,本文就跟大家分享下我的实现思路与过程,欢迎各位感兴趣的开发者阅读本文。
环境搭建
我的项目后端基于SpringBoot搭建,所以此处直接采用justauth库来做第三方登录。
引入依赖
在pom.xml
中添加下属代码。
<!--第三方登录库-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>1.15.9</version>
</dependency>
<!--http请求库-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.0</version>
</dependency>
上述代码中多引入了http请求库,是因为新版的JustAuth默认移除了http请求库,需要自己引入,否则会报错。
封装工具类
根据文档所述,我们封装几个我们需要平台的工具类,代码如下所示:
package com.lk.utils;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.request.*;
import java.util.ArrayList;
import java.util.List;
// 第三方授权登录工具类
public class AuthUtil {
public static AuthRequest getGithubRequest() {
return new AuthGithubRequest(AuthConfig.builder()
.clientId("平台id")
.clientSecret("平台密钥")
.redirectUri("回调地址")
.build());
}
public static AuthRequest getBaiduRequest() {
return new AuthBaiduRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri("")
.build());
}
public static AuthRequest getGiteeRequest() {
return new AuthGiteeRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri("")
.build());
}
// 开源中国授权
public static AuthRequest getOschinaRequest() {
return new AuthOschinaRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri("")
.build());
}
// 腾讯云授权
public static AuthRequest getCodingRequest() {
List<String> scopeList = new ArrayList<>();
scopeList.add("user");
return new AuthCodingRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri("")
.scopes(scopeList)
.codingGroupName("")
.build());
}
}
上述代码中的clientId、clientSecret换成自己账号平台的即可,详细步骤请移步官方文档:oauth/github.html)
⚠️注意:coding授权登录时,需要配置scopes,且只能配置一个user,它只能获取到用户的基本信息,一开始我加多了个email,授权时报错我没权限调用。
实现过程
此处我们来看下具体的实现过程。
后端实现
此处我们需要写2个请求接口,供客户端调用。
- 生成授权链接
- 获取用户信息
生成授权链接
我们需要客户端传一个平台名称参数,随后我们根据平台名称来调用我们刚才封装好的工具类中的方法,实现代码如下所示:
@ApiImplicitParams({
@ApiImplicitParam(name = "platform", value = "平台名称", dataType = "String", paramType = "query", example = "GitHub", required = true)
})
@ApiOperation(value = "获取第三方登录授权地址", notes = "授权url地址")
// 允许跨域访问
@CrossOrigin()
@RequestMapping(value = "/getAuthorize", method = RequestMethod.GET)
public ResultVO<?> getAuthorize(@RequestParam(value = "platform") String platform) {
AuthRequest authRequest = null;
switch (platform) {
case "github":
authRequest = AuthUtil.getGithubRequest();
break;
case "gitee":
authRequest = AuthUtil.getGiteeRequest();
break;
case "baidu":
authRequest = AuthUtil.getBaiduRequest();
break;
case "oschina":
authRequest = AuthUtil.getOschinaRequest();
break;
case "coding":
authRequest = AuthUtil.getCodingRequest();
break;
default:
log.error("未识别的平台" + platform);
return ResultVOUtil.error(-1, "平台未识别,未找到处理方法。");
}
// 生成状态码
String state = AuthStateUtils.createState();
// 生成授权链接
String authorizeUrl = authRequest.authorize(state);
HashMap<String, String> result = new HashMap<>();
result.put("authorizeUrl", authorizeUrl);
// 将状态码给客户端,授权成功后获取用户信息时将state传回服务端,保证请求完整性,防止CSRF风险
result.put("state", state);
return ResultVOUtil.success(result);
}
获取用户信息
客户端拿到授权链接后,用户同意授权,第三方网站返回code码,客户端携带授权链接接口返回的state码与code码,用来获取用户信息。
实现代码如下:
@ApiOperation(value = "第三方登录", notes = "用户授权后,通过第三方网站返回的字段来获取用户信息,随后执行登录操作")
// 允许跨域访问
@CrossOrigin()
@RequestMapping(value = "/authorizeLogin", method = RequestMethod.POST)
public ResultVO<?> authorizeLogin(@ApiParam(name = "传入授权成功后返回的信息", required = true) @Valid @RequestBody GitHubLoginDto loginDto) throws Exception {
String state = loginDto.getState();
String code = loginDto.getCode();
String platform = loginDto.getPlatform();
AuthRequest authRequest;
// 用户信息
AuthResponse<?> result;
AuthCallback callback = new AuthCallback();
callback.setState(state);
callback.setCode(code);
switch (platform) {
case "github":
authRequest = AuthUtil.getGithubRequest();
result = authRequest.login(callback);
break;
case "gitee":
authRequest = AuthUtil.getGiteeRequest();
result = authRequest.login(callback);
break;
case "baidu":
authRequest = AuthUtil.getBaiduRequest();
result = authRequest.login(callback);
break;
case "oschina":
authRequest = AuthUtil.getOschinaRequest();
result = authRequest.login(callback);
break;
case "coding":
authRequest = AuthUtil.getCodingRequest();
result = authRequest.login(callback);
break;
default:
log.error("未识别的平台" + platform);
return ResultVOUtil.error(-1, "平台未识别,用户信息获取失败");
}
if (result.getData() == null) {
// 授权失败, 返回错误信息
return ResultVOUtil.error(-1, "授权失败:" + result.getMsg());
}
JSONObject data = new JSONObject(result.getData());
log.info(platform + "的用户信息: " + data);
return LoginUtil.getLoginResult(data);
}
前端实现
接下来我们来看下前端部分的具体实现。
创建授权页面
在后端实现章节中,获取授权链接时,配置了一个回调地址,这个地址里会自动拼接上第三方平台返回的code码,因此我们需要创建一个html页面,用于获取地址栏中的code码,将获取到的code放进本地存储中,方便我们在登录页面获取。
实现代码如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>授权回调页面</title>
<script type="text/javascript">
/**
* 获取url参数
* @param url
* @param variable
*/
function getQueryVariable(
url,
variable
) {
// 对url进行截取
url = url.substring(url.indexOf("?"), url.length);
const query = url.substring(1);
const vars = query.split("&");
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split("=");
if (pair[0] === variable) {
return pair[1];
}
}
return -1;
}
window.onload = () => {
// 获取url中的授权码
const code = getQueryVariable(window.location.href, "code");
// 将授权码放进本地存储中
if (code !== -1) {
localStorage.setItem("authCode", code);
}else {
localStorage.setItem("authCode", "");
}
// 关闭页面
window.close();
}
</script>
</head>
<body>
</body>
</html>
打开授权页面
接下来,我们需要在登录页面打开后端接口所返回的授权页,用户授权成功后,将code与state回传给服务端,从而实现登录。
那么,我们接下来要考虑的是在当前页面以弹窗形式打开授权页面后,用户授权成功后,如何立即获取到第三方平台返回的code调用授权登录接口。
经过一番思考后,我有了下述思路:
- 使用window.open()打开授权窗口
- 在登录页面监听localstorage改变
- 授权成功,授权页面向localstorage写入code
- 登录页面监听到改变,根据code码判断是用户是否授权成功
- 授权成功则执行登录操作
实现代码如下:
<!--登录页面-->
<template>
<!--第三方登录-->
<div
class="auth-panel"
v-if="isLoginStatus === loginStatusEnum.NOT_LOGGED_IN"
>
<div class="item-panel" @click.once="getAuthorize('github')">
<img src="@/assets/img/auth/github.png" alt="github登录" />
</div>
<div class="item-panel" @click.once="getAuthorize('gitee')">
<img src="@/assets/img/auth/gitee.png" alt="gitee登录" />
</div>
<div class="item-panel" @click.once="getAuthorize('baidu')">
<img src="@/assets/img/auth/baidu.png" alt="百度登录" />
</div>
<div class="item-panel" @click.once="getAuthorize('oschina')">
<img src="@/assets/img/auth/oschina.png" alt="开源中国登录" />
</div>
<div class="item-panel" @click.once="getAuthorize('coding')">
<img src="@/assets/img/auth/coding.png" alt="腾讯云登录" />
</div>
</div>
</template>
<script lang="ts">
name: "login",
data(){
state: "",
platform: ""
},
methods: {
getAuthorize: function(name: string) {
// 获取授权链接
this.$api.authLoginAPI
.getAuthorize({ platform: name })
.then((res: responseDataType<getAuthorizeDataType>) => {
if (!res.data.state || !res.data.authorizeUrl)
throw "服务器错误: 授权链接获取失败";
const authorizeUrlres = res.data.authorizeUrl;
// 更新状态码与登录平台名称
this.state = res.data.state;
this.platform = name;
// 打开授权窗口
window.open(
authorizeUrlres,
"_blank",
"toolbar=no,width=800, height=600"
);
// 开始监听localStorage,获取授权码
window.addEventListener("storage", this.getAuthCode);
});
},
getAuthCode: function() {
// 获取授权码
const code = localStorage.getItem("authCode");
localStorage.removeItem("authCode");
// 移除localStorage监听
this.removeStorageListener();
if (code) {
// 调用登录函数
this.authLogin(this.state, code, this.platform);
return;
}
throw this.platform + "授权码获取失败";
},
authLogin: function(state: string, code: string, platform: string) {
this.$api.authLoginAPI
.authorizeLogin({
state: state,
code: code,
platform: platform
})
.then((res: responseDataType) => {
if (res.code == 0) {
// 存储当前用户信息
return;
}
// 切回登录界面
});
}
}
</script>
实现效果
接下来我们来看下,最终实现的效果。
项目地址
- 在线体验地址:chat-system
- GitHub地址:chat-system-github
写在最后
- 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
- 本文首发于掘金,未经许可禁止转载💌
评论区