SpringBoot集成QQ登录与JWT鉴权视频播放接口实战
1. 项目概述与核心价值
最近在做一个个人媒体站,后端用SpringBoot,前端打算用Vue。用户系统是基础,但让用户注册账号再填一堆信息,门槛太高,流失率吓人。直接集成第三方登录是最高效的解决方案,国内环境,QQ和微信是绕不开的两座大山。微信开放平台的申请流程对企业资质要求不低,相比之下,QQ互联对个人开发者友好得多,审核也快。所以,我决定先搞定QQ第三方登录。
这个需求拆开看,其实是两个相对独立但又需要串联的功能模块。第一个是 QQ OAuth2.0授权登录集成 ,核心是引导用户跳转到QQ登录页,授权后拿回一个代表用户身份的 openid 和 access_token 。第二个是 基于用户身份的视频播放接口 ,重点在于如何安全、高效地根据用户身份(无论是QQ登录用户还是我们自己的系统用户)返回可播放的视频地址,尤其是涉及版权或付费视频时,身份鉴权是关键。
网上教程很多,但要么只讲登录,要么只讲文件服务,把两者结合,并讲清楚在SpringBoot里如何优雅设计、如何避坑的完整分享不多。我把自己从零搭建、调试到上线的全过程,包括那些官方文档没明说、但实际开发中一定会遇到的“坑”,都整理出来。无论你是想快速实现QQ登录,还是需要设计一个带鉴权的资源服务接口,这篇文章都能给你一套可直接复制、修改的代码和清晰的思路。
2. 整体架构设计与核心思路
2.1 技术栈选型与理由
先亮出我的技术栈清单和选择理由,这决定了后续代码的写法。
- SpringBoot 2.7.x : 选这个相对稳定的版本,而非最新的3.x,主要是为了生态兼容性。很多公司项目还在用2.x,相关的教程、问题解决方案也更成熟。对于第三方登录这种网络请求密集的操作,2.7.x完全够用且稳定。
- Java 8/11 : 项目基础。注意Lombok的版本兼容性,后面会提到一个巨坑。
- QQ互联开放平台 : 这是唯一官方渠道。所有第三方登录的逻辑,都必须遵循它的OAuth2.0流程。 提前准备一个已备案的域名 ,这是申请QQ互联应用的前提,回调地址必须使用域名。
- Spring Security OAuth2 Client : 这是核心中的核心 。很多人会用
HttpClient或RestTemplate手动去拼装URL、处理回调,代码冗长且容易出错。Spring Security OAuth2 Client模块为我们封装了标准的OAuth2登录流程,只需简单配置,就能自动处理跳转、回调、换票(用code换token)等一系列复杂操作,我们只需要关注拿到用户信息后的业务逻辑。这是实现“最简方法”的关键。 - JWT (JSON Web Token) : 用于生成我们服务自己的令牌。用户通过QQ登录后,我们服务器会生成一个JWT返回给前端。后续前端访问像“视频播放接口”这类需要认证的接口时,只需在请求头中带上这个JWT即可。这样实现了无状态的认证,减轻服务器会话管理压力。
- 数据库 (MySQL) : 需要一张表来关联QQ的
openid和我们系统内部的user_id。表结构可以很简单:id,openid,user_id,unionid(可选),create_time。
为什么不用Session? Session在单体应用且流量不大时没问题,但在集群部署或前后端完全分离(跨域)时,需要额外的方案(如Session共享)来解决状态同步问题,增加了复杂度。JWT是无状态的,更契合RESTful风格和分布式架构。
2.2 业务流程全景图
整个流程涉及三个角色: 用户浏览器 、 我们的SpringBoot应用 、 QQ互联服务器 。理解数据流向至关重要。
- 前端触发 : 用户点击网站上的“QQ登录”按钮。
- 授权请求 : 前端引导用户跳转到我们后端的一个固定地址,例如
/oauth2/authorization/qq。这个地址由Spring Security OAuth2 Client自动注册。 - 跳转QQ : 我们的后端接收到这个请求后,自动重定向用户到QQ互联的授权页面,并携带我们申请到的
appid、回调地址redirect_uri、申请的权限scope等信息。 - 用户授权 : 用户在QQ的页面上输入账号密码并授权。
- QQ回调 : QQ服务器将用户重定向回我们预先在QQ互联平台登记的回调地址(例如
https://your-domain.com/login/oauth2/code/qq),并在URL中附带一个临时的授权码code。 - 换取令牌 : Spring Security OAuth2 Client 自动捕获这个回调,并用这个
code, 加上我们的appid和appkey, 向QQ服务器发起一个 后端到后端 的请求,换取access_token。 - 获取用户信息 : 再用这个
access_token去调用QQ的接口(如https://graph.qq.com/user/get_user_info)获取用户的昵称、头像等基本信息。同时,另一个接口(https://graph.qq.com/oauth2.0/me)会返回openid(该用户在此appid下的唯一标识)。 - 业务处理 : 我们根据
openid查询数据库。如果是新用户,则在我们的用户表创建一条记录,并关联openid;如果是老用户,则获取对应的系统user_id。 - 颁发JWT : 生成一个包含
user_id等信息的JWT,返回给前端(通常通过重定向到前端页面并附在URL参数中,或由前端在回调页通过接口请求获取)。 - 访问受保护接口 : 前端将JWT存储在本地(如localStorage),在请求需要认证的接口(如视频播放接口)时,在HTTP Header(通常是
Authorization: Bearer <your-jwt-token>)中携带。 - 接口鉴权 : 视频播放接口的过滤器或拦截器会校验JWT的有效性,解析出
user_id,进而判断该用户是否有权限播放请求的视频。
3. 核心细节解析与实操要点
3.1 QQ互联应用申请与配置详解
这一步是源头,配置错了后面全白搭。
- 访问QQ互联官网 : 搜索“QQ互联”或直接访问
connect.qq.com, 使用你的QQ号登录。 - 创建应用 : 在“应用管理”中创建新应用。应用类型根据实际情况选择,个人项目选“网站应用”即可。
- 关键信息填写 :
- 应用名称 : 会显示在用户授权时的提示信息里。
- 应用域名 : 必须是你已备案的域名 ,不能是IP或
localhost。开发阶段可以用localhost测试,但上线前必须改为正式域名。 - 回调地址 : 这是重中之重 。格式为:
https://你的域名/login/oauth2/code/qq。注意,Spring Security OAuth2 Client默认的回调路径就是/login/oauth2/code/{registrationId}, 这里的{registrationId}就是你配置的QQ登录的标识(如qq)。 必须确保这个地址在QQ互联后台准确填写,一字不差 ,包括http还是https。
- 等待审核 : 提交后等待审核,通常个人应用几个小时到一天就能通过。审核通过后,你会获得
appid和appkey。appkey非常重要,相当于密码,要保密。
踩坑记录:回调地址的坑 我最开始用
http://localhost:8080测试,在QQ互联后台也这么填,但始终回调失败。原因是QQ互联对回调地址的校验非常严格,localhost在某些情况下不被允许。 解决方案是修改本地hosts文件,将一个域名(如test.qqlogin.com)指向127.0.0.1, 然后在QQ互联后台填写http://test.qqlogin.com:8080/login/oauth2/code/qq。 同时,SpringBoot的application.yml里也要配置server.address=0.0.0.0以确保能正确绑定。
3.2 Spring Security OAuth2 Client 配置精讲
这是实现自动登录流程的魔法所在。在 application.yml 中的配置如下:
spring:
security:
oauth2:
client:
registration:
qq: # registrationId,可以自定义,但建议就用qq,和回调地址对应
client-id: 你的appid # 从QQ互联获取
client-secret: 你的appkey # 从QQ互联获取
client-name: QQ登录 # 显示在登录页的名称(如果你用了默认登录页)
authorization-grant-type: authorization_code # OAuth2授权码模式
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" # 模板,会自动替换
scope: get_user_info # 申请的权限,获取用户信息
client-authentication-method: post # QQ接口要求用POST传client_secret
provider:
qq:
authorization-uri: https://graph.qq.com/oauth2.0/authorize # QQ授权地址
token-uri: https://graph.qq.com/oauth2.0/token # 换取token的地址
user-info-uri: https://graph.qq.com/user/get_user_info # 获取用户信息的地址
user-name-attribute: openid # 将openid作为用户标识
关键点解析:
redirect-uri使用了SpEL表达式{baseUrl}和{registrationId}, Spring Security会自动将其替换为当前应用的基础URL和注册ID,非常灵活。client-authentication-method: post必须设置,因为QQ互联的token端点要求以POST表单形式提交client_secret, 默认的basic方式会失败。user-info-uri这个地址返回的是昵称、头像等,但 不包含openid。openid需要通过另一个接口(/oauth2.0/me)获取。Spring Security的默认实现可能不包含这一步,所以我们需要自定义一个OAuth2UserService。
3.3 自定义OAuth2UserService:获取完整的QQ用户信息
Spring Security默认的 OAuth2UserService 可能无法直接适配QQ的接口(因为QQ的用户信息分在两个接口)。我们需要自己实现一个。
@Component
public class QQOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1. 获取access_token
String accessToken = userRequest.getAccessToken().getTokenValue();
// 2. 调用QQ的 /oauth2.0/me 接口,获取openid
String openIdUrl = String.format("https://graph.qq.com/oauth2.0/me?access_token=%s", accessToken);
String openIdResponse = restTemplate.getForObject(openIdUrl, String.class);
// QQ返回的数据是 callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
// 需要解析出json部分
String openid = extractOpenId(openIdResponse); // 这是一个解析json字符串的方法
// 3. 调用QQ的 /user/get_user_info 接口,获取用户资料
String userInfoUrl = String.format("https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s",
accessToken, userRequest.getClientRegistration().getClientId(), openid);
Map<String, Object> userAttributes = restTemplate.getForObject(userInfoUrl, Map.class);
// 4. 将openid也放入用户属性中,方便后续使用
userAttributes.put("openid", openid);
// 5. 构建OAuth2User对象
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
userAttributes,
"openid" // 指定哪个属性是用户的“名称”,这里我们用openid
);
}
private String extractOpenId(String response) {
// 简单解析,实际生产环境需要更健壮的解析和异常处理
String jsonStr = response.substring(response.indexOf("{"), response.lastIndexOf("}") + 1);
JsonNode node = objectMapper.readTree(jsonStr);
return node.get("openid").asText();
}
}
然后,我们需要在Security配置类中,将这个自定义的Service配置进去。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private QQOAuth2UserService qqOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/public/**", "/oauth2/**").permitAll() // 公开访问的路径
.anyRequest().authenticated() // 其他所有请求都需要认证
.and()
.oauth2Login() // 启用OAuth2登录
.userInfoEndpoint()
.userService(qqOAuth2UserService) // 使用我们自定义的UserService
.and()
.defaultSuccessUrl("/login-success") // 登录成功后的跳转地址(前端页面)
.and()
.csrf().disable(); // 前后端分离项目通常禁用CSRF,根据情况选择
}
}
3.4 JWT的生成与校验
用户通过QQ登录后,我们需要生成自己的JWT给前端。我使用 jjwt 这个库。
1. 添加依赖 (pom.xml):
<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>
2. 编写JWT工具类:
@Component
public class JwtUtil {
@Value("${jwt.secret}") // 从配置读取一个密钥,务必复杂且保密
private String secret;
@Value("${jwt.expiration}") // 过期时间,如 86400000 (24小时)
private Long expiration;
public String generateToken(Long userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userId.toString()) // 通常用userId作为主题
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret) // 使用HS512算法
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secret)
.build()
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secret).build().parseClaimsJws(token);
return true;
} catch (Exception e) {
// 日志记录token无效的具体原因
return false;
}
}
}
3. 登录成功后的处理: 在QQ登录成功后的回调处理逻辑里(比如上面配置的 /login-success 对应的控制器),我们需要做:
- 从Spring Security的SecurityContext中获取认证信息,拿到
openid。 - 用
openid查询或创建本地用户,得到user_id。 - 调用
jwtUtil.generateToken(userId)生成JWT。 - 将这个JWT返回给前端。对于前后端分离项目,通常不跳转页面,而是让前端在回调页(一个空白页或加载页)通过JS向后端发起一个请求(如
/api/auth/success)来获取JWT,或者更常见的做法是,在OAuth2UserService成功返回后,自定义一个AuthenticationSuccessHandler,直接在该处理器中生成JWT并写入响应。
4. 视频播放接口设计与实现
视频播放接口的核心不是播放器,而是 鉴权 和 地址生成 。我们不会直接返回视频文件流,而是返回一个 临时的、有权限验证的播放地址 。
4.1 接口设计思路
假设我们有一个视频表 video , 包含 id , title , file_path (视频文件在OSS或服务器上的路径), is_free (是否免费)等字段。
- 公开视频 (is_free=true) : 所有用户(包括未登录用户)都可以直接获取播放地址。
- 私有/付费视频 (is_free=false) : 只有登录用户,并且拥有相应权限(如已购买)的用户才能获取播放地址。
接口设计:
GET /api/video/{videoId}/play-url
Headers: Authorization: Bearer <jwt-token> (对于需要认证的视频)
Response:
{
"code": 200,
"msg": "success",
"data": {
"title": "视频标题",
"playUrl": "https://your-cdn.com/encrypted/temp-token/video.mp4",
"format": "mp4",
"duration": 3600
}
}
playUrl 不是一个永久链接,而是一个 签名的临时URL 。这是为了防止地址被爬取或盗链。
4.2 实现带鉴权的播放接口
@RestController
@RequestMapping("/api/video")
public class VideoController {
@Autowired
private VideoService videoService;
@Autowired
private JwtUtil jwtUtil;
@GetMapping("/{videoId}/play-url")
public ResponseEntity<?> getPlayUrl(@PathVariable Long videoId,
@RequestHeader(value = "Authorization", required = false) String authHeader) {
// 1. 查询视频信息
Video video = videoService.getById(videoId);
if (video == null) {
return ResponseEntity.status(404).body("视频不存在");
}
// 2. 权限校验
Long currentUserId = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (jwtUtil.validateToken(token)) {
currentUserId = jwtUtil.getUserIdFromToken(token);
}
}
if (!video.getIsFree()) {
// 付费视频,需要登录且拥有权限
if (currentUserId == null) {
return ResponseEntity.status(401).body("请登录后观看");
}
// 进一步检查用户是否购买了这个视频
boolean hasPermission = videoService.checkUserPermission(currentUserId, videoId);
if (!hasPermission) {
return ResponseEntity.status(403).body("您没有权限观看此视频");
}
}
// 3. 生成临时播放地址(以阿里云OSS为例)
String playUrl = generateSignedUrl(video.getFilePath());
// 4. 返回结果
Map<String, Object> data = new HashMap<>();
data.put("title", video.getTitle());
data.put("playUrl", playUrl);
data.put("format", "mp4"); // 可从文件路径解析
data.put("duration", video.getDuration());
return ResponseEntity.ok(data);
}
private String generateSignedUrl(String filePath) {
// 这里以阿里云OSS SDK为例,生成一个有效期为1小时的签名URL
// 实际开发中,你需要注入OSS Client
// OSS ossClient = ...;
// Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
// URL url = ossClient.generatePresignedUrl(bucketName, filePath, expiration);
// return url.toString();
// 模拟返回一个地址
return "https://your-oss-cdn.com/" + filePath + "?token=temporary_signature";
}
}
4.3 使用云服务(OSS)的签名URL
对于视频等大文件,强烈建议使用对象存储服务(如阿里云OSS、腾讯云COS)。它们都提供了生成签名URL的功能。
阿里云OSS生成签名URL示例:
// 初始化OSSClient
String endpoint = "oss-cn-hangzhou.aliyuncs.com";
String accessKeyId = "your-access-key";
String accessKeySecret = "your-secret-key";
String bucketName = "your-bucket";
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 生成签名URL,有效期1小时
Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, "video/example.mp4");
request.setExpiration(expiration);
// 可以设置响应头,比如强制下载
// request.setResponseHeaders(...);
URL signedUrl = ossClient.generatePresignedUrl(request);
String playUrl = signedUrl.toString();
ossClient.shutdown();
这样做的好处是:
- 安全 : 临时URL过期即失效,防止资源被无限期盗链。
- 减负 : 视频流量直接由CDN和OSS承担,不经过你的应用服务器。
- 高性能 : OSS和CDN为文件下载做了专门优化。
5. 常见问题与排查技巧实录
在实际集成和开发过程中,我遇到了不少问题,这里总结几个最具代表性的。
5.1 QQ登录回调失败,错误码:100010
- 问题现象 : 用户授权后,跳转回你的回调地址时,页面显示错误或控制台日志报错,错误信息包含
100010。 - 排查思路 : 这是QQ互联最常见的错误之一, 根本原因是回调地址不匹配 。
- 解决步骤 :
- 检查QQ互联后台配置 : 登录QQ互联,进入你的应用管理,检查“回调地址”一栏。必须与你的SpringBoot应用实际接收回调的地址 完全一致 ,包括协议(
http/https)、域名、端口、路径。Spring Security OAuth2 Client默认路径是/login/oauth2/code/qq。 - 检查本地Hosts配置(开发环境) : 如果你在本地开发,使用了自定义域名(如
test.qqlogin.com),请确保C:\Windows\System32\drivers\etc\hosts(Windows)或/etc/hosts(Mac/Linux)文件中有对应的映射:127.0.0.1 test.qqlogin.com。 - 检查应用启动配置 : 确保SpringBoot应用的
server.address没有绑定到localhost, 最好是0.0.0.0, 这样它才能响应来自域名(test.qqlogin.com)的请求。 - 网络环境 : 确保你的服务器或开发机网络能正常访问
graph.qq.com。
- 检查QQ互联后台配置 : 登录QQ互联,进入你的应用管理,检查“回调地址”一栏。必须与你的SpringBoot应用实际接收回调的地址 完全一致 ,包括协议(
5.2 获取access_token时返回错误:client_secret错误或无效
- 问题现象 : 在SpringBoot应用日志中,看到类似
invalid client_secret的错误。 - 排查思路 : 这通常是因为Spring Security OAuth2 Client默认使用
Basic认证方式在Header中传递client_secret, 但QQ互联的token接口要求以POST表单形式传递。 - 解决方案 : 确保在
application.yml中为QQ的provider配置了client-authentication-method: post。具体配置见上文3.2节。
5.3 成功登录后,无法获取到openid
- 问题现象 : 登录流程走通了,也能拿到用户昵称和头像,但在数据库里找不到对应的
openid, 或者用户信息里没有openid字段。 - 排查思路 : Spring Security默认的
OAuth2UserService只处理user-info-uri返回的数据。而QQ的openid是通过另一个接口 (/oauth2.0/me) 获取的。 - 解决方案 : 这就是为什么我们必须 自定义
OAuth2UserService(如3.3节所示)。在loadUser方法里,先调用/oauth2.0/me拿到openid, 再调用/user/get_user_info拿到基本信息,最后将两者合并,构造一个完整的OAuth2User对象。
5.4 视频播放地址被泄露或盗链
- 问题现象 : 生成的视频播放地址被用户分享出去,非授权用户也能观看。
- 解决方案 :
- 使用签名URL : 如上文4.3节所述,一定要用OSS/COS的签名URL功能,设置一个较短的过期时间(如30分钟到2小时)。
- 动态生成 : 每次请求播放接口都重新生成签名URL,不要缓存或重复使用。
- Referer防盗链 : 在OSS控制台或CDN控制台配置Referer白名单,只允许你的域名访问。但这只能防君子,不能防高手(Referer可以被伪造)。
- IP限流 : 在应用层或网关层,对频繁请求播放地址的IP进行限制。
5.5 关于Lombok的版本兼容性警告
- 问题现象 : 启动项目时,控制台出现警告:
java: You aren‘t using a compiler supported by lombok, so lombok will not work...。 - 问题根源 : 这是Lombok插件与JDK或IDE编译版本不兼容导致的。特别是在JDK 17+和IntelliJ IDEA的组合中常见。
- 解决方案 :
- 确保IDE安装了Lombok插件 : 在IntelliJ IDEA中,
File -> Settings -> Plugins, 搜索Lombok并确保已安装启用。 - 启用注解处理 :
Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors, 勾选Enable annotation processing。 - 检查项目JDK版本 : 确保
Project Structure(Ctrl+Shift+Alt+S)中Project和Modules的SDK版本一致,且与pom.xml中指定的maven-compiler-plugin版本匹配。 - 升级Lombok版本 : 在
pom.xml中尝试使用更新的Lombok版本,如1.18.30。有时旧版本Lombok不支持新JDK的特性。 - 终极方案 : 如果上述都不行,可以尝试在启动配置中添加JVM参数:
-Djps.track.ap.dependencies=false。
- 确保IDE安装了Lombok插件 : 在IntelliJ IDEA中,
把QQ登录和视频播放这两个功能串起来,核心在于理解OAuth2的授权流程和前后端分离下的无状态认证(JWT)。Spring Security OAuth2 Client极大地简化了登录集成的复杂度,而自定义 OAuth2UserService 则是适配不同平台的关键。视频播放接口的重点则从“播放”转移到了“权限控制”和“资源链接安全管理”。这套方案经过实际项目检验,代码拿来即用,希望能帮你少走弯路。
更多推荐


所有评论(0)