【项目实践】梳理如何实现第三方登录

440 阅读5分钟

oAuth 2.0

第三方登录大都是基于OAuth2.0协议实现的,oAuth 2.0是一个业界标准的授权协议。允许第三方应用程序访问用户的数据而不共享其密码。它的工作原理是让用户与服务进行身份验证,然后授予第三方应用程序访问其数据的权限。OAuth2.0使用访问令牌来授予对资源的访问权限,并具有多种不同的授权类型:

  • 授权码授权(Authorization Code Grant)
  • 隐式授权(Implicit Grant)
  • 密码授权(Resource Owner Password Credentials Grant)
  • 客户端凭据授权(Client Credentials Grant)

授权码授权流程

  • 资源所有者(Resource Owner):顾名思义,资源的所有者,很多时候其就是我们普通的自然人(但不限于自然人,如某些应用程序也会创建资源),拥有资源的所有权。
  • 资源服务器(Resource Server):保存着受保护的用户资源。
  • 应用程序(Client):准备访问用户资源的应用程序,可能是web应用,或是一个后端web服务应用,或是一个移动端应用,也或是一个桌面可执行程序。
  • 授权服务器(Authorization Server):授权服务器,在获取用户的同意授权后,颁发访问令牌给应用程序,以便其获取用户资源。

image.png

如上图所示,授权流程场景可以描述为如下几个步骤:

  1. 用户在应用程序中,应用程序尝试获取用户保存在资源服务器上的信息,比如用户的身份信息和头像,应用程序提供自己的 clientId/clientSecret/redirectUri 给到授权服务器。
  2. 到授权服务器,用户输入用户名和密码,服务器对其认证成功后,提示用户即将要颁发一个读权限给应用程序,在用户确认后,授权服务器颁发一个授权码(authorization code)重定向 redirectUri。
  3. 应用程序获取到授权码之后,使用这个授权码和自己的 clientId/clientSecret/redirectUri 向认证服务器申请访问令牌/刷新令牌(access token/refresh token)。授权服务器对这些信息进行校验,如果一切OK,则颁发给应用程序访问令牌/刷新令牌。
  4. 应用程序在拿到访问令牌之后,向资源服务器申请用户的资源信息
  5. 资源服务器在获取到访问令牌后,对令牌进行解析(如果令牌已加密,则需要进行使用相应算法进行解密)并校验,并向授权服务器校验其合法性,如果一起OK,则返回应用程序所需要的资源信息。

具体实现

gitee 第三方登陆流程

Gitee 因为申请轻松,我们使用 Gitee 来完成第三方登陆。gitee 需要注册第三方平台的开发者账号,按照如下步骤即可完成:

  1. 找到gitee的设置,进入第三方应用,如下: image.png

  2. 创建应用

image.png

  1. 填写信息

image.png

应用回调不能乱填,当我们gitee登录成功之后,gitee会自动跳转到应用回调地址,并且gitee会带上code,利用code可以得到所登录gitee用户信息。

创建成功后,会生成 Cliend ID 和 Client Secret

image.png

代码实现

整体的流程图如下:

image.png

按照流程图可以分为两个主要步骤:获取第三方登陆页面和第三方授权

获取第三方登陆页面

前端代码:

<div class="img">
    <icon icon="svg-icon:gitee" :size="35" @click="giteeLogin" class="pointer" />
</div>
<script setup lang="ts">
async function preLoginByThirdPartyApi(source: string) {
    return await service.get<UserInfo>("/login-third-party", {
        params: {
            source
        },
        headers: {
            isNotSetToken: true
        }
    });
}
const giteeLogin = async () => {
    const result = await preLoginByThirdPartyApi('gitee');
    if (result.code === 200) {
        window.location = result.data.authorizeUrl;
        localStorage.setItem("giteeUuid", result.data.uuid);
        localStorage.setItem("thirdPartySource","gitee")
    }
}
</script>

点击图标,获得第三方登陆页面

image.png

后端代码:

/**
 * 第三方登录:获取第三方授权页面
 *
 * @param source 第三方
 * @return {@link ResultVO}<{@link LoginVo}>
 * @throws BadRequestException 错误请求异常
 */
@GetMapping("/login-third-party")
@ApiOperation(value = "第三方登录授权页面URL查询", notes = "第三方登录,获取到授权页面url")
public ResultVO<LoginVo> preLoginByThirdParty(@Validated @EnumValue(enumClass = TripartiteSourceEnum.class, ignoreCase = true, message = "传入的参数不正确")
                                              @RequestParam("source") String source) throws BadRequestException {
    if (StringUtils.isEmpty(source)) {
        throw new BadRequestException("source 未传");
    }
    LoginVo loginVo = new LoginVo();
    AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder()
            .clientId(tripartiteConfiguration.getClientId(source))
            .clientSecret(tripartiteConfiguration.getClientSecret(source))
            .redirectUri(tripartiteConfiguration.getRedirectUri(source))
            .build());
    String uuid = UUID.fastUUID().toString();
    String authorizeUrl = authRequest.authorize(uuid);
    loginVo.setAuthorizeUrl(authorizeUrl);
    loginVo.setUuid(uuid);
    return ResultVO.success(loginVo);
}

以上的代码是生成跳转路径。生成一个 gitee 的登入路径返回给到,在该页面 gitee 只要登录完成,gitee 会自动跳转到我们之前设置好的回调地址。

前端拿到地址后设置 window.location = result.data.authorizeUrl;会进入了如下界面:

image.png

点击登陆后得到的回调地址如下:

http://127.0.0.1:5173/3th-auth?
code=11686997826a39c48ae97447693c9656546ca69817b71f122ac4cd2d2bd60d4b&
state=f770c06d-6b18-4b03-9c7a-234903b76bf4

其中 code 为授权码(authorization code),认证时需要带上 state 为 uuid

第三方授权

前端代码:设置好的回调地址对应的组件

<template>
    <div class="center">
        <div v-loading="isLoading"></div>
    </div>
</template>
<script setup lang="ts">
const isLoading = ref(false);
const route = useRoute();
const router = useRouter();
onMounted(async () => {
    const result = await loginByThirdPartyApi({
        source: localStorage.getItem("thirdPartySource") || "",
        code: route.query.code as string,
        uuid: localStorage.getItem("giteeUuid") || "",
    });
    if (result.code == 200) {
        setToken(result.data.token);
        router.push("/home");
        localStorage.removeItem("giteeUuid");
        localStorage.removeItem("thirdPartySource");
    } else {
        ElMessage({
            message: result.msg,
            type: 'warning',
        })
    }
    isLoading.value = false;
})
</script>

此页面会自动提交请求到后端,带上 code 和 uuid

后端代码:

/**
 * 第三方登录
 *
 * @param loginVo loginVo
 * @return {@link ResultVO}<{@link LoginVo}>
 */
@PostMapping("/login-third-party")
@ApiOperation(value = "第三方登录", notes = "第三方登录,将第三方给过来的授权信息保存到本地数据库")
public ResultVO<LoginVo> loginByThirdParty(@RequestBody LoginVo loginVo) {
    LoginValidation.loginByThirdPartyParamsValid(loginVo);
    log.info("loginByThirdParty ---> UUID:{}", loginVo.getUuid());
    AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder()
            .clientId(tripartiteConfiguration.getClientId(loginVo.getSource()))
            .clientSecret(tripartiteConfiguration.getClientSecret(loginVo.getSource()))
            .redirectUri(tripartiteConfiguration.getRedirectUri(loginVo.getSource()))
            .build());
    AuthResponse<AuthUser> login = authRequest.login(AuthCallback.builder().state(loginVo.getUuid()).code(loginVo.getCode()).build());
    AuthUser authUser = login.getData();
    if(authUser == null ){
        throw new BadRequestException("认证超时");
    }
    SysUser sysUser = userService.giteeUser2User(authUser);
    SysUser oriUser = userService.getUserInfoByName(sysUser.getUserName());
    if (oriUser == null) {
        userService.insertUser(sysUser);
    }else{
        sysUser.setUserId(oriUser.getUserId());
    }
    List<SysRole> roleList = roleService.getUserRoleInfoById(sysUser.getUserId());
    sysUser.setRoleList(roleList);
    UserDetail userDetail = new UserDetail(sysUser, permissionService.getUserPermissionByUser(sysUser));
    loginVo.setToken(jwtManager.generate(userDetail));
    return ResultVO.success(loginVo);
}

源码地址:java-auth vue3-auth

最后附上其他第三方平台的API文档: