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):授权服务器,在获取用户的同意授权后,颁发访问令牌给应用程序,以便其获取用户资源。
如上图所示,授权流程场景可以描述为如下几个步骤:
- 用户在应用程序中,应用程序尝试获取用户保存在资源服务器上的信息,比如用户的身份信息和头像,应用程序提供自己的 clientId/clientSecret/redirectUri 给到授权服务器。
- 到授权服务器,用户输入用户名和密码,服务器对其认证成功后,提示用户即将要颁发一个读权限给应用程序,在用户确认后,授权服务器颁发一个授权码(authorization code)重定向 redirectUri。
- 应用程序获取到授权码之后,使用这个授权码和自己的 clientId/clientSecret/redirectUri 向认证服务器申请访问令牌/刷新令牌(access token/refresh token)。授权服务器对这些信息进行校验,如果一切OK,则颁发给应用程序访问令牌/刷新令牌。
- 应用程序在拿到访问令牌之后,向资源服务器申请用户的资源信息
- 资源服务器在获取到访问令牌后,对令牌进行解析(如果令牌已加密,则需要进行使用相应算法进行解密)并校验,并向授权服务器校验其合法性,如果一起OK,则返回应用程序所需要的资源信息。
具体实现
gitee 第三方登陆流程
Gitee 因为申请轻松,我们使用 Gitee 来完成第三方登陆。gitee 需要注册第三方平台的开发者账号,按照如下步骤即可完成:
-
找到gitee的设置,进入第三方应用,如下:
-
创建应用
- 填写信息
应用回调不能乱填,当我们gitee登录成功之后,gitee会自动跳转到应用回调地址,并且gitee会带上code,利用code可以得到所登录gitee用户信息。
创建成功后,会生成 Cliend ID 和 Client Secret
代码实现
整体的流程图如下:
按照流程图可以分为两个主要步骤:获取第三方登陆页面和第三方授权
获取第三方登陆页面
前端代码:
<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>
点击图标,获得第三方登陆页面
后端代码:
/**
* 第三方登录:获取第三方授权页面
*
* @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;
会进入了如下界面:
点击登陆后得到的回调地址如下:
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);
}
最后附上其他第三方平台的API文档: