Spring Boot + Vue 对接 QQ 登录详细指南
QQ 登录是基于 OAuth 2.0 协议的第三方认证方式,用户通过 QQ 账号授权后,应用可获取用户基本信息(如昵称、头像)。本文将分 前端(Vue) 和 后端(Spring Boot) 两部分,详细讲解如何实现 QQ 登录功能。
一、前置准备
1.1 注册 QQ 互联开发者账号
- 访问 QQ 互联开放平台,注册开发者账号并完成实名认证。
- 在「应用管理」→「创建应用」中,填写应用信息(如网站名称、域名),获取 AppID 和 AppSecret(后续接口调用需要)。
- 配置「授权回调域」:在应用管理中设置「回调地址」(如
http://localhost:8080/qq/callback
,需与前端/后端实际域名一致)。
二、前端(Vue)实现
2.1 依赖安装
无需额外依赖,Vue 原生 axios
处理 HTTP 请求,vue-router
管理路由。
2.2 登录按钮与跳转逻辑
在 Vue 组件中添加 QQ 登录按钮,点击后跳转到 QQ 授权页面:
xml
<template>
<div>
<button @click="qqLogin">QQ 登录</button>
</div>
</template>
<script>
import axios from 'axios';
export default {
methods: {
qqLogin() {
// QQ 互联提供的授权入口(需替换为你的 AppID 和回调地址)
const qqAuthUrl = `https://graph.qq.com/oauth2.0/authorize?
response_type=code&
client_id=YOUR_APP_ID&
redirect_uri=${encodeURIComponent('http://localhost:8080/qq/callback')}&
state=random_state`; // 防 CSRF 随机数
window.location.href = qqAuthUrl;
}
}
};
</script>
2.3 处理回调(接收 code)
在 Vue 路由中配置回调路径(如 /qq/callback
),通过 $route.query
获取 QQ 返回的 code
和 state
:
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Callback from '../views/Callback.vue';
const routes = [
{
path: '/qq/callback',
name: 'QQCallback',
component: Callback
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
在回调页面组件中,将 code
发送到后端:
xml
<!-- views/Callback.vue -->
<template>
<div>正在登录...</div>
</template>
<script>
import axios from 'axios';
export default {
async mounted() {
const code = this.$route.query.code;
const state = this.$route.query.state;
// 验证 state(防 CSRF 攻击,需与前端生成的一致)
if (state !== localStorage.getItem('saved_state')) {
alert('非法请求!');
return;
}
try {
// 将 code 发送到后端换取用户信息
const res = await axios.post('http://localhost:8081/api/qq/login', { code });
// 登录成功,存储 token 并跳转首页
localStorage.setItem('token', res.data.token);
this.$router.push('/');
} catch (error) {
console.error('QQ 登录失败', error);
alert('登录失败,请重试');
}
}
};
</script>
三、后端(Spring Boot)实现
3.1 依赖配置
在 pom.xml
中添加 OAuth 2.0 相关依赖(如 Spring Security
、Jackson
):
xml
<dependencies>
<!-- Spring Boot 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security(用于 JWT 生成和鉴权) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 依赖(推荐 jjwt) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- OkHttp(用于调用 QQ 接口) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
</dependencies>
3.2 配置 QQ 参数
在 application.yml
中配置 QQ AppID、AppSecret 和回调地址:
yaml
qq:
app-id: YOUR_APP_ID # 替换为你的 AppID
app-secret: YOUR_APP_SECRET # 替换为你的 AppSecret
redirect-uri: http://localhost:8080/qq/callback # 与 QQ 互联后台配置的回调地址一致
3.3 QQ 接口调用工具类
封装调用 QQ 授权服务器的逻辑(获取 access_token
、openid
):
vbscript
@Component
public class QqAuthUtils {
@Value("${qq.app-id}")
private String appId;
@Value("${qq.app-secret}")
private String appSecret;
@Value("${qq.redirect-uri}")
private String redirectUri;
private final OkHttpClient okHttpClient = new OkHttpClient();
/**
* 通过 code 获取 access_token
*/
public String getAccessToken(String code) throws IOException {
String url = "https://graph.qq.com/oauth2.0/token?" +
"grant_type=authorization_code" +
"&client_id=" + appId +
"&client_secret=" + appSecret +
"&code=" + code +
"&redirect_uri=" + URLEncoder.encode(redirectUri, "UTF-8");
Request request = new Request.Builder().url(url).build();
Response response = okHttpClient.newCall(request).execute();
String responseBody = response.body().string();
// 解析返回的 access_token(格式:access_token=XXX&expires_in=3600&refresh_token=XXX)
String[] params = responseBody.split("&");
for (String param : params) {
if (param.startsWith("access_token=")) {
return param.split("=")[1];
}
}
throw new RuntimeException("获取 access_token 失败:" + responseBody);
}
/**
* 通过 access_token 获取 openid
*/
public String getOpenid(String accessToken) throws IOException {
String url = "https://graph.qq.com/oauth2.0/me?" +
"access_token=" + accessToken;
Request request = new Request.Builder().url(url).build();
Response response = okHttpClient.newCall(request).execute();
String responseBody = response.body().string();
// 解析返回的 openid(格式:callback({"client_id":"YOUR_APP_ID","openid":"XXX"});)
JSONObject json = JSON.parseObject(responseBody.substring(10, responseBody.length() - 2));
return json.getString("openid");
}
}
3.4 用户认证与 JWT 生成
编写 Controller 处理 QQ 登录请求,验证 code
并生成 JWT:
less
@RestController
@RequestMapping("/api/qq")
public class QqLoginController {
@Autowired
private QqAuthUtils qqAuthUtils;
@Autowired
private UserService userService; // 自定义用户服务(用于查询/绑定用户)
@PostMapping("/login")
public Result login(@RequestBody Map<String, String> params) throws IOException {
String code = params.get("code");
if (StringUtils.isBlank(code)) {
throw new IllegalArgumentException("code 不能为空");
}
// 1. 通过 code 获取 access_token
String accessToken = qqAuthUtils.getAccessToken(code);
// 2. 通过 access_token 获取 openid(用户在 QQ 中的唯一标识)
String openid = qqAuthUtils.getOpenid(accessToken);
// 3. 查询本地是否已绑定该 openid 的用户
User user = userService.findByQqOpenid(openid);
if (user == null) {
// 未绑定:返回提示信息(前端引导用户绑定本地账号)
return Result.error("未绑定本地账号,请先注册并关联 QQ");
}
// 4. 生成 JWT 并返回
String token = JwtUtils.generateToken(user.getId()); // 自定义 JWT 生成工具
return Result.success(token);
}
}
3.5 JWT 工具类(示例)
typescript
@Component
public class JwtUtils {
@Value("${jwt.secret}") // 从配置文件获取密钥
private String secret;
@Value("${jwt.expiration}") // 过期时间(毫秒)
private Long expiration;
/**
* 生成 JWT
*/
public String generateToken(Long userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 验证 JWT 并解析用户 ID
*/
public Long validateAndGetUserId(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
} catch (Exception e) {
throw new RuntimeException("JWT 无效或已过期");
}
}
}
3.6 安全配置(Spring Security)
允许前端跨域访问,并放行登录接口:
less
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // 配置 CORS
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/qq/**").permitAll() // 放行 QQ 登录接口
.anyRequest().authenticated() // 其他接口需要认证
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:8080"); // 前端域名
config.addAllowedMethod("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
四、关键流程说明
- 用户点击 QQ 登录:前端跳转到 QQ 授权页面,用户选择授权。
- QQ 重定向到回调地址 :携带
code
和state
(防 CSRF)。 - 前端发送 code 到后端 :后端通过
code
换取access_token
和openid
。 - 验证用户身份 :后端根据
openid
查询本地用户(或引导绑定)。 - 生成 JWT 返回前端:前端存储 JWT,后续请求携带 JWT 完成鉴权。
五、常见问题与优化
5.1 常见问题
- 回调地址不匹配:QQ 互联后台配置的回调地址必须与前端/后端实际地址一致(包括端口)。
- state 验证失败 :前端生成
state
后需存储(如localStorage
),回调时校验防止 CSRF 攻击。 - 获取 openid 失败 :检查
access_token
是否有效(可通过 QQ 提供的 调试工具 验证)。 - 跨域问题:确保 Spring Security 配置了正确的 CORS 策略,允许前端域名访问。
5.2 优化建议
- 绑定本地账号 :未绑定用户时,前端跳转到绑定页面(输入本地账号密码),后端关联
openid
和本地用户 ID。 - 缓存 access_token :若需多次调用 QQ 接口(如获取用户详细信息),可缓存
access_token
(有效期 3 个月)。 - 日志与监控 :记录 QQ 登录的关键步骤(如
code
获取、access_token
换取),方便排查问题。
六、总结
通过本文,你已掌握 Spring Boot + Vue 对接 QQ 登录的核心流程。实际项目中需根据业务需求完善用户绑定、信息同步等功能,并注意 OAuth 2.0 的安全规范(如 state
校验、access_token
保密)。
扩展资源: