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 保密)。

​扩展资源​​:

相关推荐
知其然亦知其所以然几秒前
面试被问 G1 GC 懵了?记住这几点就能完美回答!
java·后端·面试
小莫分享25 分钟前
Chrome更新后,扩展不能用问题
前端·chrome
zhangxxxq27 分钟前
前端vue3获取excel二进制流在页面展示
前端·excel
Kiri霧1 小时前
Kotlin集合分组
android·java·前端·kotlin
是2的10次方啊1 小时前
🕵️ 生产环境惊魂记:一个被遗忘的super()引发的"血案"
后端
拾光拾趣录1 小时前
举一反三:合并 K 个有序链表的最小堆实现
前端·算法
杭州杭州杭州1 小时前
JavaWeb
后端·javaweb
拾光拾趣录1 小时前
合并K个有序链表
前端·算法
江城开朗的豌豆1 小时前
Event Bus:Vue组件间的'广播电台',轻松实现跨组件通信!
前端·javascript·vue.js
袁煦丞1 小时前
7.18实验室 碎片灵感秒同步!memos让笔记追着你跑:cpolar内网穿透第613个成功挑战
前端·程序员·远程工作