侧边栏壁纸
博主头像
神奇的程序员

今天的努力只为未来

  • 累计撰写 167 篇文章
  • 累计创建 25 个标签
  • 累计收到 215 条评论

目 录CONTENT

文章目录

实现Web端第三方登录

神奇的程序员
2021-04-18 / 1 评论 / 7 点赞 / 674 阅读 / 8,514 字 正在检测是否收录...

前言

前一阵子,为我的开源项目添加了第三方登录的功能,实现过程还算顺利,本文就跟大家分享下我的实现思路与过程,欢迎各位感兴趣的开发者阅读本文。

环境搭建

我的项目后端基于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>

实现效果

接下来我们来看下,最终实现的效果。

项目地址

写在最后

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载💌
7

评论区