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
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
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
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
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
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
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)
实现思路
- 用户点击登录按钮时:前端将当前页面的回调地址传递给后端,并跳转到第三方授权页面。
- 第三方授权成功后:用户会被重定向到你配置好的前端回调页面,这个页面会接收 URL 中的
code和state参数。 - 前端回调页面:页面加载时,通过 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
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
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
关键点说明
登录按钮逻辑:
- 当用户点击登录按钮时,前端将当前页面的回调地址(例如
/auth/callback)传递给后端。 - 后端生成授权链接后,前端会重定向到第三方授权页面。
- 当用户点击登录按钮时,前端将当前页面的回调地址(例如
授权回调页面逻辑:
- 授权成功后,用户会被第三方平台重定向到前端的回调页面(例如
/auth/callback),并附带code和state参数。 - 回调页面在加载时,自动获取 URL 中的
code和state参数,并将这些参数发送给后端。 - 后端处理这些参数,完成登录逻辑,并返回
token和用户信息。 - 前端将
token和用户信息保存到cookies或 Vuex 中,并根据返回的callbackUrl跳转到用户授权前的页面。
- 授权成功后,用户会被第三方平台重定向到前端的回调页面(例如
# 关于页面重定向
# 1. 前端重定向到授权页面
当前代码中,前端发送请求获取授权链接并重定向到授权页面:
window.location.href = result.data;
1
优点:
- 用户体验更流畅:前端直接控制页面跳转,用户体验更直接。
- 更灵活的处理:前端可以在获取授权链接后,决定是否进行跳转,还可以根据业务需求进行额外的处理。
缺点:
- 前端暴露授权链接:授权链接直接在前端处理,存在被篡改的风险,虽然这种风险较低,但依然需要考虑安全问题。
适用场景:
- 用户体验要求高、逻辑简单的情况下,前端重定向更为合适。
# 2. 后端重定向到授权页面
后端直接处理并返回重定向:
response.sendRedirect(authorizeUrl);
1
优点:
- 安全性更高:所有逻辑在后端处理,前端不直接暴露授权链接,避免可能的篡改风险。
- 更简单的前端代码:前端只需发送请求,后端负责处理跳转逻辑,减少前端代码复杂度。
缺点:
- 用户体验稍差:前端请求后,等待后端重定向的过程中,可能会有短暂的延迟,用户体验不如前端直接跳转流畅。
- 灵活性稍差:前端没有直接控制跳转逻辑的机会。
适用场景:
- 对安全性要求较高、业务流程较复杂的场景下,后端重定向更为适合。
# 完整的流程
- 用户点击登录按钮,前端传递当前页面的回调地址。
- 前端或后端重定向到第三方授权页面。
- 用户授权成功后,第三方平台重定向回前端的回调页面,附带
code和state参数。 - 前端回调页面获取
code和state参数,并发送请求给后端。 - 后端处理授权回调,返回
token和用户信息。 - 前端保存
token和用户信息,并根据返回的callbackUrl跳转到授权前的页面。
这样实现的好处是授权流程清晰,且用户可以回到之前的页面继续操作,提升用户体验。
编辑此页 (opens new window)