1. 概述

这是 Spring 构建 Reddit 应用系列 的第二篇文章。本文将实现一个核心功能:通过 Reddit 官方 API,从我们的 Spring 应用中发布一条外部链接。

目标很明确:✅ 实现用户授权 → ✅ 判断是否需要验证码 → ✅ 提交表单 → ✅ 调用 Reddit 接口发布链接。整个流程要简洁、可控,避免踩坑。


2. 必要的 OAuth2 安全配置

要调用 Reddit 的写操作接口(比如发帖),必须先完成 OAuth2 授权,并申请正确的权限范围(scope)。

我们需要注册一个受保护的 OAuth2 资源,关键点是设置 submit 权限,同时加上 identity 以便获取用户身份信息。

@Bean
public OAuth2ProtectedResourceDetails reddit() {
    AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
    details.setId("reddit");
    details.setClientId("your-client-id-12345"); // 替换为实际 Client ID
    details.setClientSecret("your-client-secret-abcde"); // 替换为实际 Secret
    details.setAccessTokenUri("https://www.reddit.com/api/v1/access_token");
    details.setUserAuthorizationUri("https://www.reddit.com/api/v1/authorize");
    details.setTokenName("oauth_token");
    details.setScope(Arrays.asList("identity", "submit"));
    details.setGrantType("authorization_code");
    return details;
}

⚠️ 注意:

  • submit scope 是发帖的硬性要求,缺了会 403
  • identity 用于调用 /api/v1/me 获取当前用户,调试时很有用
  • 回调地址(redirect_uri)需在 Reddit 应用配置中提前注册,例如 http://localhost:8080/login

3. 判断是否需要 Captcha

Reddit 对新用户或低 Karma 用户设置了反爬机制:发帖前必须填写验证码(Captcha)。

所以我们不能假设所有用户都能直接发帖,必须先探测:

private String needsCaptcha() {
    String result = redditRestTemplate.getForObject(
      "https://oauth.reddit.com/api/needs_captcha.json", String.class);
    return result;
}

private String getNewCaptcha() {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity req = new HttpEntity(headers);

    Map<String, String> param = new HashMap<>();
    param.put("api_type", "json");

    ResponseEntity<String> result = redditRestTemplate.postForEntity(
      "https://oauth.reddit.com/api/new_captcha", req, String.class, param);
    String[] split = result.getBody().split("\""); 
    return split[split.length - 2];
}

✅ 解读:

  • needs_captcha.json 返回 "true""false" 字符串
  • 若需要,则调用 new_captcha 获取一个 iden(验证码 ID),用于后续展示图片和提交验证

⚠️ 踩坑提醒:
返回的 JSON 是原始字符串格式,没有结构化,所以用了 split("\"") 取倒数第二个字段拿 iden —— 虽然简单粗暴,但 Reddit 的这个接口确实就这么设计的。


4. 提交表单页面

前端页面需要收集必要字段,并根据后端判断动态显示 Captcha。

后端控制器

@RequestMapping("/post")
public String showSubmissionForm(Model model) throws JsonProcessingException, IOException {
    String needsCaptchaResult = needsCaptcha();
    if (needsCaptchaResult.equalsIgnoreCase("true")) {
        String iden = getNewCaptcha();
        model.addAttribute("iden", iden);
    }
    return "submissionForm";
}

逻辑很清晰:如果需要验证码,就把 iden 传给前端用于渲染图片。

前端页面(submissionForm.html)

<form>
    <input name="title" placeholder="标题" required/>
    <input name="url" type="url" placeholder="链接" required/>
    <input name="sr" placeholder="子版块(如 programming)" required/>
    <input type="checkbox" name="sendReplies" value="true"/> 接收回复通知

    <div th:if="${iden != null}">
        <input type="hidden" name="iden" value="${iden}"/>
        <input name="captcha" placeholder="请输入验证码" required/>
        <img src="http://www.reddit.com/captcha/${iden}" alt="captcha" width="200"/>
    </div>
    <button type="submit" onclick="submitPost()">发布</button>
</form>

<script>
function submitPost(){
    var data = {};
    $('form').serializeArray().map(function(x){data[x.name] = x.value;});
    $.ajax({
        url: "/api/posts",
        data: JSON.stringify(data),
        type: 'POST',
        contentType:'application/json'
    }).done(function(data) {
        if(data.length < 2){ 
            alert(data[0]); // 显示错误信息
        } else {
            window.location.href = "/submissionResponse?msg=" + 
              encodeURIComponent(data[0]) + "&url=" + encodeURIComponent(data[1]);
        }
    }).fail(function(error) { 
        alert("请求失败: " + error.responseText); 
    }); 
}
</script>

✅ 关键点:

  • 使用 th:if 动态渲染 Captcha 区域
  • AJAX 提交避免页面刷新,用户体验更平滑
  • 错误统一通过 fail 回调处理,防止静默失败

5. 调用 Reddit API 发布链接

真正的发帖逻辑在后端完成,通过 redditRestTemplate 调用 Reddit 的 /api/submit 接口。

REST 接口定义

@Controller
@RequestMapping(value = "/api/posts")
public class RedditPostRestController {

    @Autowired
    private RedditService service;

    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody
    public List<String> submit(@Valid @RequestBody PostDto postDto) {
        return service.submitPost(postDto);
    }
}

使用 @Valid 自动校验必填字段,失败直接返回 400。

服务层实现

public List<String> submitPost(PostDto postDto) {
    MultiValueMap<String, String> params = constructParams(postDto);
    JsonNode node = redditTemplate.submitPost(params);
    return parseResponse(node);
}

private MultiValueMap<String, String> constructParams(PostDto postDto) {
    MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
    param.add("title", postDto.getTitle());
    param.add("sr", postDto.getSubreddit());
    param.add("url", postDto.getUrl());
    param.add("iden", postDto.getIden());
    param.add("captcha", postDto.getCaptcha());
    if (postDto.isSendReplies()) {
        param.add("sendReplies", "true");
    }

    param.add("api_type", "json");
    param.add("kind", "link");        // 发布的是链接,不是文本帖
    param.add("resubmit", "true");    // 允许重复提交同一链接
    param.add("then", "comments");    // 成功后跳转到评论页
    return param;
}

✅ 参数说明: | 参数 | 说明 | |------|------| | kind | link 表示链接帖,self 是自述帖 | | resubmit | 防止重复链接被拦截 | | then | 控制成功后跳转目标 |

响应解析

Reddit 的返回是嵌套 JSON,需小心处理:

private List<String> parseResponse(JsonNode node) {
    String result = "";
    JsonNode errorNode = node.get("json").get("errors").get(0);
    if (errorNode != null && errorNode.size() > 0) {
        for (JsonNode child : errorNode) {
            result += child.toString().replaceAll("\"|null", "") + "<br>";
        }
        return Arrays.asList(result);
    } else {
        JsonNode urlNode = node.get("json").get("data").get("url");
        if (urlNode != null) {
            return Arrays.asList("Post submitted successfully", urlNode.asText());
        } else {
            return Arrays.asList("Error Occurred while parsing Response");
        }
    }
}

⚠️ 注意:errors 是数组,即使只有一个错误也要遍历。常见错误包括:

  • BAD_CAPTCHA:验证码错误
  • RATELIMIT:发帖太频繁
  • NO_TEXT:标题或链接为空

6. 数据传输对象(DTO)与响应页面

PostDto

public class PostDto {
    @NotNull
    private String title;

    @NotNull
    private String url;

    @NotNull
    private String subreddit;

    private boolean sendReplies;

    private String iden;
    private String captcha;

    // standard getters and setters
}

字段与前端表单一一对应,配合 @Valid 实现基础校验。

提交结果页(submissionResponse.html)

<html>
<body>
    <h1 th:text="${msg}">操作结果</h1>
    <h2 th:if="${param.containsKey('url')}">
        <a th:href="${param.url[0]}" target="_blank">点击查看发布的帖子</a>
    </h2>
</body>
</html>

使用 Thymeleaf 解析 URL 并生成跳转链接,简洁明了。


7. 总结

本文完整实现了从 Spring 应用发布链接到 Reddit 的流程:

  • ✅ OAuth2 授权配置
  • ✅ 动态处理 Captcha
  • ✅ 前后端协作表单
  • ✅ 调用 Reddit submit 接口
  • ✅ 错误统一处理

虽然功能简单,但涵盖了实际集成中常见的坑:权限、验证码、接口返回格式不规范等。

下一步计划:实现“定时发布”功能,结合 Quartz 或 Spring Scheduler,把自动化做到极致。

🔧 完整代码示例 已发布至 GitHub:https://github.com/baeldung/reddit-app
项目基于 Eclipse 构建,导入即可运行,适合快速验证和二次开发。


原始标题:Post a Link to the Reddit API

« 上一篇: Baeldung周报10期
» 下一篇: Baeldung周报11