程序员scholar 程序员scholar
首页
  • Web 三剑客

    • HTML
    • CSS
    • JavaScript
  • 现代 JavaScript

    • ES6
    • TypeScript
  • 前端工具库

    • jQuery
    • Ajax
    • Axios
  • Vue 生态

    • Vue2
    • Vue3
    • Vue3 + TS
    • Vuex
  • 小程序开发

    • 微信小程序
    • uni-app
  • 构建工具

    • Webpack
  • 服务端技术

    • Node.js
  • 实时通信

    • WebSocket
    • 第三方登录
  • Element-UI
  • Apache ECharts
后端 (opens new window)
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Web 三剑客

    • HTML
    • CSS
    • JavaScript
  • 现代 JavaScript

    • ES6
    • TypeScript
  • 前端工具库

    • jQuery
    • Ajax
    • Axios
  • Vue 生态

    • Vue2
    • Vue3
    • Vue3 + TS
    • Vuex
  • 小程序开发

    • 微信小程序
    • uni-app
  • 构建工具

    • Webpack
  • 服务端技术

    • Node.js
  • 实时通信

    • WebSocket
    • 第三方登录
  • Element-UI
  • Apache ECharts
后端 (opens new window)
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • 第三方登录

    • JustAuth 基本使用
    • JustAuth 对接第三方登录
      • 后端代码(SpringBoot)
        • 1. 项目依赖配置
        • 2. 配置多平台登录参数
        • 3. 配置类 - 读取平台配置
        • 4. AuthRequest 工厂类
        • 5. 实现授权流程控制器
        • 6. 重要参数与 API 说明
      • 前端代码(Vue.js)
        • 1. 登录按钮点击处理
        • 2. 授权回调页面
      • 关于页面重定向
        • 1. 前端重定向到授权页面
        • 2. 后端重定向到授权页面
      • 完整的流程
    • 原生QQ登录流程
    • Vue 2 中引入 SVG 图标
    • Vue3 vite引入 SVG 图标
    • Vue3 Vue CLI 中引入 SVG图标
  • 第三方登录
  • 第三方登录
scholar
2025-01-24
目录

JustAuth 对接第三方登录

# JustAuth 对接第三方登录

# 后端代码(SpringBoot)

项目结构

src/
└── main/
    ├── java/
    │   └── com/
    │       └── example/
    │           ├── config/               # 配置类
    │           ├── controller/           # 控制器类
    │           ├── service/              # 服务类
    │           └── factory/              # 第三方平台工厂类
    └── resources/
        └── application.yml               # 配置文件
1
2
3
4
5
6
7
8
9
10
11

# 1. 项目依赖配置

在 pom.xml 中引入 JustAuth 依赖:最新版本地址 (opens new window)

<dependency>
    <groupId>me.zhyd.oauth</groupId>
    <artifactId>JustAuth</artifactId>
    <version>1.16.6</version> <!-- 版本号可以根据需要更新 -->
</dependency>
<!-- 导入配置文件处理器-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>
1
2
3
4
5
6
7
8
9
10
11

# 2. 配置多平台登录参数

在 application.yml 中配置多个第三方平台的 clientId、clientSecret 和 redirectUri。

application.yml 示例:

justauth:
  # 配置 GitHub 平台
  github:
    client-id: your-github-client-id     # 必填,GitHub 平台申请的 Client ID
    client-secret: your-github-client-secret # 必填,GitHub 平台申请的 Client Secret
    redirect-uri: http://localhost:8080/oauth/callback/github # 必填,回调地址,GitHub 授权成功后会重定向到此地址

  # 配置 Google 平台
  google:
    client-id: your-google-client-id     # 必填,Google 平台申请的 Client ID
    client-secret: your-google-client-secret # 必填,Google 平台申请的 Client Secret
    redirect-uri: http://localhost:8080/oauth/callback/google # 必填,Google 授权成功后的回调地址

  # 配置微信平台
  wechat:
    client-id: your-wechat-client-id     # 必填,微信平台申请的 Client ID
    client-secret: your-wechat-client-secret # 必填,微信平台申请的 Client Secret
    redirect-uri: http://localhost:8080/oauth/callback/wechat # 必填,微信授权成功后的回调地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • justauth: 作为配置的根节点,所有 JustAuth 相关的配置都在此节点下。
  • 每个平台(如 github, google, wechat)都有各自的配置,包含 client-id, client-secret, 和 redirect-uri 三个必填项。
  • client-id 和 client-secret 是在第三方平台申请应用时获取的,必须正确配置。
  • redirect-uri 是授权成功后的回调地址,第三方平台授权成功后会自动重定向到此地址,并附带授权码(code)和状态(state)等信息。

# 3. 配置类 - 读取平台配置

将多个属性的配置注入到 Spring Boot 中,使用配置类进行统一管理:

package com.easypan.configuration;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "justauth.github")
public class GitHubConfig {
    private String clientId;
    private String clientSecret;
    private String redirectUri;

    // Getter 和 Setter 方法
    public String getClientId() {
        return clientId;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public String getClientSecret() {
        return clientSecret;
    }

    public void setClientSecret(String clientSecret) {
        this.clientSecret = clientSecret;
    }

    public String getRedirectUri() {
        return redirectUri;
    }

    public void setRedirectUri(String redirectUri) {
        this.redirectUri = redirectUri;
    }
}
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
  • platforms: 使用一个 Map<String, Map<String, String>> 来存储每个平台的配置,外层 Map 的 key 是平台名称(如 github、google),内层 Map 的 key 是具体的配置项(如 client-id、client-secret 等)。
  • getPlatforms() 和 setPlatforms() 方法用于获取和设置这些配置,Spring Boot 会自动将配置文件中的内容注入到该类中。

# 4. AuthRequest 工厂类

通过工厂模式,根据平台名称动态生成对应的 AuthRequest 对象。

package com.example.factory;

import me.zhyd.oauth.config.GitHubConfig;
import me.zhyd.oauth.request.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.example.config.GithubConfig;
import com.example.config.GoogleConfig;
import com.example.config.WechatConfig;

@Component
public class AuthRequestFactory {

    @Autowired
    private GithubConfig githubConfig;

    @Autowired
    private GoogleConfig googleConfig;

    @Autowired
    private WechatConfig wechatConfig;

    public AuthRequest getAuthRequest(String platform) {
        switch (platform.toLowerCase()) {
            case "github":
                return new AuthGithubRequest(
                    AuthConfig.builder()
                        .clientId(githubConfig.getClientId())
                        .clientSecret(githubConfig.getClientSecret())
                        .redirectUri(githubConfig.getRedirectUri())
                        .build()
                );
            case "google":
                return new AuthGoogleRequest(
                    AuthConfig.builder()
                        .clientId(googleConfig.getClientId())
                        .clientSecret(googleConfig.getClientSecret())
                        .redirectUri(googleConfig.getRedirectUri())
                        .build()
                );
            case "wechat":
                return new AuthWeChatRequest(
                    AuthConfig.builder()
                        .clientId(wechatConfig.getClientId())
                        .clientSecret(wechatConfig.getClientSecret())
                        .redirectUri(wechatConfig.getRedirectUri())
                        .build()
                );
            default:
                throw new IllegalArgumentException("不支持的平台: " + platform);
        }
    }
}
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

# 5. 实现授权流程控制器

统一管理多平台的授权流程,处理授权和回调。

import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/oauth")
public class JustAuthController {

    @Autowired
    private AuthRequestFactory authRequestFactory; // 注入工厂类

    /**
     * 根据平台生成授权链接,并直接跳转至对应平台的授权页面
     * @param platform 平台名称(如 "github", "google", "wechat")
     * @param callbackUrl 前端传来的回调地址
     * @param response HTTP 响应对象
     * @param session HttpSession 用于保存回调地址
     * @throws IOException 异常处理
     */
    @GetMapping("/login/{platform}")
    public void renderAuth(@PathVariable String platform,
                           @RequestParam("callbackUrl") String callbackUrl,
                           HttpServletResponse response,
                           HttpSession session) throws IOException {
        AuthRequest authRequest = authRequestFactory.getAuthRequest(platform);
        // 生成一个唯一的状态码 (state),用于防止 CSRF 攻击
        String state = AuthStateUtils.createState();

        // 将回调地址保存到 session 中
        session.setAttribute("callbackUrl", callbackUrl);

        // 生成授权 URL
        String authorizeUrl = authRequest.authorize(state);

        // 直接重定向到授权页面
        response.sendRedirect(authorizeUrl);
    }

    /**
     * 处理授权回调(授权成功后的回调接口),返回 JSON 而不是重定向
     * @param platform 平台名称(如 "github", "google", "wechat")
     * @param authCallback 包含回调参数的对象(如 code, state)
     * @param session HttpSession 用于获取保存的回调地址
     * @return JSON 格式的响应结果
     */
    @GetMapping("/callback/{platform}")
    public ResponseEntity<Map<String, Object>> login(@PathVariable String platform,
                                                     AuthCallback authCallback,
                                                     HttpSession session) {
        AuthRequest authRequest = authRequestFactory.getAuthRequest(platform);

        // 调用 login 方法处理回调并获取用户信息
        AuthResponse<AuthUser> authResponse = authRequest.login(authCallback);

        // 创建返回数据的 Map
        Map<String, Object> responseMap = new HashMap<>();

        if (authResponse.ok()) {
            AuthUser user = authResponse.getData();
            // 此处可以将用户信息保存至数据库或会话中

            // 生成 token(假设有 JWT 生成逻辑)
            String token = jwtService.generateToken(user);

            // 从 session 中获取回调地址
            String callbackUrl = (String) session.getAttribute("callbackUrl");

            // 构造成功响应
            responseMap.put("success", true);
            responseMap.put("token", token);
            responseMap.put("callbackUrl", callbackUrl); // 返回回调地址
            responseMap.put("userInfo", user); // 返回用户信息
        } else {
            // 构造失败响应
            responseMap.put("success", false);
            responseMap.put("message", authResponse.getMsg());
        }

        // 返回 JSON 响应
        return ResponseEntity.ok(responseMap);
    }
}
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
96

注意事项

应用回调地址 重点,该地址为用户授权后需要跳转到的自己网站的地址,默认携带一个code参数

# 6. 重要参数与 API 说明

  • AuthConfig:

    • clientId: 必填,平台提供的应用标识。
    • clientSecret: 必填,平台提供的应用密钥。
    • redirectUri: 必填,授权成功后的回调地址,必须与平台配置一致。
  • AuthRequest:

    • authorize(): 获取授权 URL,用户需跳转至该地址进行授权。
    • login(AuthCallback authCallback): 处理授权回调并获取用户信息,返回 AuthResponse 对象。
  • AuthResponse<AuthUser>:

    • ok(): 判断授权是否成功。
    • getData(): 获取授权成功后的用户信息,包含平台用户 ID、昵称、头像等。
    • getMsg(): 获取授权失败时的错误信息。
  • AuthCallback:

    • code: 授权码,回调时平台会附带。
    • state: 防止 CSRF 攻击的状态码,建议在授权前生成并校验。

# 前端代码(Vue.js)

实现思路

  1. 用户点击登录按钮时:前端将当前页面的回调地址传递给后端,并跳转到第三方授权页面。
  2. 第三方授权成功后:用户会被重定向到你配置好的前端回调页面,这个页面会接收 URL 中的 code 和 state 参数。
  3. 前端回调页面:页面加载时,通过 URL 中的 code 和 state 发送请求给后端,完成登录逻辑,获取 token 和用户信息。

# 1. 登录按钮点击处理

这是用户点击登录按钮时执行的逻辑:

<template>
  <div class="auth-callback">
    <!-- 页面背景 -->
    <div class="background"></div>

    <!-- 主内容区 -->
    <div class="content">
      <i v-if="isLoading" class="el-icon-loading loading-icon"></i>
    </div>
  </div>
</template>

<script>
import request from "@/request";
export default {
  data() {
    return {
      isLoading: true, // 控制加载动画的显示
    };
  },
  created() {
    this.handleAuthCallback();
  },
  methods: {
    async handleAuthCallback() {
      const { code, state } = this.$route.query;

      if (!code || !state) {
        this.handleError("缺少必要的授权参数");
        return;
      }

      try {
        const result = await request.get('/callback/github', {
          params: { code, state }
        });

        if (result && result.code === 200) {
          this.handleSuccess(result.data);
        } else {
          this.handleError(result.message || "登录失败,请重试");
        }
      } catch (error) {
        this.handleError("处理授权回调失败,请稍后再试");
        console.error("处理授权回调失败:", error);
      }
    },
    handleSuccess(data) {
      const user = JSON.stringify(data);
      localStorage.setItem('xm-user', user);

      let redirectUrl = data.callbackUrl || '/main';
      if (redirectUrl === '/login') {
        redirectUrl = '/main';
      }
      this.$router.push(redirectUrl);
    },
    handleError(message) {
      this.$message.error(message); // 显示错误信息
      this.$router.push('/login'); // 立即跳转到登录页面
    }
  }
};
</script>

<style scoped>
.auth-callback {
  position: relative;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #f0f2f5;
}

.background {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
  z-index: -1;
}

.content {
  text-align: center;
  padding: 20px;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}

.loading-icon {
  font-size: 48px;
  color: #409eff;
}
</style>
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
96
97
98

# 2. 授权回调页面

用户完成授权后,会被重定向到这个页面,该页面通过 URL 参数中的 state 和 code 向后端请求完成登录。

<template>
  <div class="auth-callback">
    <!-- 页面背景 -->
    <div class="background"></div>

    <!-- 主内容区 -->
    <div class="content">
      <i v-if="isLoading" class="el-icon-loading loading-icon"></i>
      <div v-else>
        <el-alert
            type="error"
            title="登录失败"
            :description="resultMessage"
            show-icon
            center
            closable={false}
            class="alert"
        />
      </div>
    </div>
  </div>
</template>

<script>
import request from "@/request";
export default {
  data() {
    return {
      isLoading: true, // 控制加载动画的显示
      resultMessage: "", // 结果消息
    };
  },
  created() {
    this.handleAuthCallback();
  },
  methods: {
    async handleAuthCallback() {
      const { code, state } = this.$route.query;

      if (!code || !state) {
        this.handleError("缺少必要的授权参数");
        return;
      }

      try {
        const result = await request.get('/web/callback/gitee', {
          params: { code, state }
        });

        if (result && result.code === 200) {
          this.handleSuccess(result.data);
        } else {
          this.handleError(result.message || "登录失败,请重试");
        }
      } catch (error) {
        this.handleError("处理授权回调失败,请稍后再试");
        console.error("处理授权回调失败:", error);
      }
    },
    handleSuccess(data) {
      const user = JSON.stringify(data);
      localStorage.setItem('xm-user', user);

      let redirectUrl = data.callbackUrl || '/main';
      if (redirectUrl === '/login') {
        redirectUrl = '/main';
      }
      this.$router.push(redirectUrl);
    },
    handleError(message) {
      this.isLoading = false;
      this.resultMessage = message;
      // 显示错误信息后,2秒钟后自动跳转到登录页面
      setTimeout(() => {
        this.$router.push('/login');
      }, 2000);
    }
  }
};
</script>

<style scoped>
.auth-callback {
  position: relative;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #f0f2f5;
}

.background {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
  z-index: -1;
}

.content {
  text-align: center;
  padding: 20px;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}

.loading-icon {
  font-size: 48px;
  color: #409eff;
}

.alert {
  margin-top: 20px;
}
</style>
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

关键点说明

  1. 登录按钮逻辑:

    • 当用户点击登录按钮时,前端将当前页面的回调地址(例如 /auth/callback)传递给后端。
    • 后端生成授权链接后,前端会重定向到第三方授权页面。
  2. 授权回调页面逻辑:

    • 授权成功后,用户会被第三方平台重定向到前端的回调页面(例如 /auth/callback),并附带 code 和 state 参数。
    • 回调页面在加载时,自动获取 URL 中的 code 和 state 参数,并将这些参数发送给后端。
    • 后端处理这些参数,完成登录逻辑,并返回 token 和用户信息。
    • 前端将 token 和用户信息保存到 cookies 或 Vuex 中,并根据返回的 callbackUrl 跳转到用户授权前的页面。

# 关于页面重定向

# 1. 前端重定向到授权页面

当前代码中,前端发送请求获取授权链接并重定向到授权页面:

window.location.href = result.data;
1

优点:

  • 用户体验更流畅:前端直接控制页面跳转,用户体验更直接。
  • 更灵活的处理:前端可以在获取授权链接后,决定是否进行跳转,还可以根据业务需求进行额外的处理。

缺点:

  • 前端暴露授权链接:授权链接直接在前端处理,存在被篡改的风险,虽然这种风险较低,但依然需要考虑安全问题。

适用场景:

  • 用户体验要求高、逻辑简单的情况下,前端重定向更为合适。

# 2. 后端重定向到授权页面

后端直接处理并返回重定向:

response.sendRedirect(authorizeUrl);
1

优点:

  • 安全性更高:所有逻辑在后端处理,前端不直接暴露授权链接,避免可能的篡改风险。
  • 更简单的前端代码:前端只需发送请求,后端负责处理跳转逻辑,减少前端代码复杂度。

缺点:

  • 用户体验稍差:前端请求后,等待后端重定向的过程中,可能会有短暂的延迟,用户体验不如前端直接跳转流畅。
  • 灵活性稍差:前端没有直接控制跳转逻辑的机会。

适用场景:

  • 对安全性要求较高、业务流程较复杂的场景下,后端重定向更为适合。

# 完整的流程

  1. 用户点击登录按钮,前端传递当前页面的回调地址。
  2. 前端或后端重定向到第三方授权页面。
  3. 用户授权成功后,第三方平台重定向回前端的回调页面,附带 code 和 state 参数。
  4. 前端回调页面获取 code 和 state 参数,并发送请求给后端。
  5. 后端处理授权回调,返回 token 和用户信息。
  6. 前端保存 token 和用户信息,并根据返回的 callbackUrl 跳转到授权前的页面。

这样实现的好处是授权流程清晰,且用户可以回到之前的页面继续操作,提升用户体验。

编辑此页 (opens new window)
JustAuth 基本使用
原生QQ登录流程

← JustAuth 基本使用 原生QQ登录流程→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式