Spring Boot + Vue 对接 QQ 登录详细指南

Spring Boot + Vue 对接 QQ 登录详细指南

QQ 登录是基于 OAuth 2.0 协议的第三方认证方式,用户通过 QQ 账号授权后,应用可获取用户基本信息(如昵称、头像)。本文将分 ​​前端(Vue)​ ​ 和 ​​后端(Spring Boot)​​ 两部分,详细讲解如何实现 QQ 登录功能。

一、前置准备

1.1 注册 QQ 互联开发者账号

  1. 访问 QQ 互联开放平台,注册开发者账号并完成实名认证。
  2. 在「应用管理」→「创建应用」中,填写应用信息(如网站名称、域名),获取 ​AppID​​AppSecret​(后续接口调用需要)。
  3. 配置「授权回调域」:在应用管理中设置「回调地址」(如 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 返回的 codestate

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 SecurityJackson):

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_tokenopenid):

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;
    }
}

四、关键流程说明

  1. ​用户点击 QQ 登录​:前端跳转到 QQ 授权页面,用户选择授权。
  2. ​QQ 重定向到回调地址​ :携带 codestate(防 CSRF)。
  3. ​前端发送 code 到后端​ :后端通过 code 换取 access_tokenopenid
  4. ​验证用户身份​ :后端根据 openid 查询本地用户(或引导绑定)。
  5. ​生成 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 保密)。

​扩展资源​​:

相关推荐
想用offer打牌3 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX5 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法6 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端