新浪微博開放平台採用 OAuth 2.0 授權體系。本文詳細講解 Java 後端集成微博登錄授權、Access Token 管理、API 調用(發布微博、上傳圖片)的完整實現流程。
+--------+ +---------------+
| |--(A) Authorization Request -->| Resource |
| | | Owner |
| |<-(B) Authorization Grant -----| |
| | +---------------+
| |
| | +---------------+
| |--(C) Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D) Access Token -----------| |
| | +---------------+
| |
| | +---------------+
| |--(E) Access Token ---------->| Resource |
| | | Server |
| |<-(F) Protected Resource -----| |
+--------+ +---------------+
@ConfigurationProperties(prefix = "weibo")
@Data
public class WeiboProperties {
private String clientId; // App Key
private String clientSecret; // App Secret
private String redirectUri; // 回調地址,需在開放平台配置
private String authUrl = "https://api.weibo.com/oauth2/authorize";
private String tokenUrl = "https://api.weibo.com/oauth2/access_token";
private String apiBase = "https://api.weibo.com/2/";
}
@Service
public class WeiboOAuthService {
@Autowired
private WeiboProperties props;
public String buildAuthorizeUrl(String state) {
return String.format("%s?client_id=%s&redirect_uri=%s&response_type=code&state=%s",
props.getAuthUrl(),
props.getClientId(),
URLEncoder.encode(props.getRedirectUri(), StandardCharsets.UTF_8),
state); // 防 CSRF
}
// 回調處理
public WeiboTokenResponse handleCallback(String code, String state) {
// 1. 驗證 state(防 CSRF)
if (!stateCache.verifyAndRemove(state)) {
throw new WeiboOAuthException("無效的 state 參數");
}
// 2. 換取 Access Token
return exchangeCodeForToken(code);
}
private WeiboTokenResponse exchangeCodeForToken(String code) {
String url = String.format("%s?client_id=%s&client_secret=%s&grant_type=authorization_code&code=%s&redirect_uri=%s",
props.getTokenUrl(),
props.getClientId(),
props.getClientSecret(),
code,
URLEncoder.encode(props.getRedirectUri(), StandardCharsets.UTF_8));
String response = httpClient.post(url, null); // 表單提交
return gson.fromJson(response, WeiboTokenResponse.class);
}
}
@Data
public class WeiboTokenResponse {
private String access_token; // 接口調用憑證
private Long expires_in; // 有效期(秒)
private String uid; // 用戶 ID
private String reminder; // 提醒信息
private Integer error; // 錯誤碼
private String error_description; // 錯誤描述
public boolean isSuccess() { return error == null || error == 0; }
}
@Service
public class WeiboTokenManager {
private static final String TOKEN_KEY_PREFIX = "weibo:token:";
private static final String REFRESH_LOCK_PREFIX = "weibo:refresh:lock:";
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private WeiboOAuthService oAuthService;
@Autowired
private WeiboProperties props;
public String getValidAccessToken(Long uid) {
String key = TOKEN_KEY_PREFIX + uid;
// 1. 讀取緩存
String token = redisTemplate.opsForValue().get(key);
if (StringUtils.hasText(token)) {
return token;
}
// 2. 緩存未命中,需用戶重新授權(或使用 Refresh Token)
throw new WeiboTokenExpiredException("Token 已過期,請重新授權");
}
public void cacheToken(Long uid, WeiboTokenResponse tokenResp) {
String key = TOKEN_KEY_PREFIX + uid;
// 提前 10 分鐘過期,留出刷新時間
long ttl = tokenResp.getExpires_in() - 600;
redisTemplate.opsForValue().set(key, tokenResp.getAccess_token(), ttl, TimeUnit.SECONDS);
// 可選:持久化 Refresh Token 到數據庫
// weiboUserRepository.updateTokens(uid, tokenResp.getAccess_token(), refreshToken);
}
// 刷新 Token(微博支持 refresh_token)
public WeiboTokenResponse refreshToken(String refreshToken) {
String url = String.format("%s?client_id=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s",
props.getTokenUrl(), props.getClientId(), props.getClientSecret(), refreshToken);
String response = httpClient.post(url, null);
return gson.fromJson(response, WeiboTokenResponse.class);
}
}
@Service
public class WeiboApiClient {
@Autowired
private WeiboTokenManager tokenManager;
@Autowired
private WeiboProperties props;
private final Gson gson = new GsonBuilder().create();
private final HttpClient httpClient;
// 發布純文本微博
public WeiboStatusResponse postStatus(Long uid, String content) {
String token = tokenManager.getValidAccessToken(uid);
String url = props.getApiBase() + "statuses/update.json";
Map<String, Object> params = new HashMap<>();
params.put("access_token", token);
params.put("status", content); // 必須 URL 編碼
return postForm(url, params, WeiboStatusResponse.class);
}
// 發布帶圖片微博
public WeiboStatusResponse uploadStatus(Long uid, String content, File image) {
String token = tokenManager.getValidAccessToken(uid);
String url = props.getApiBase() + "statuses/upload.json";
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.addTextBody("access_token", token, ContentType.TEXT_PLAIN);
builder.addTextBody("status", content, ContentType.TEXT_PLAIN);
builder.addBinaryBody("pic", image, ContentType.IMAGE_JPEG, image.getName());
HttpPost post = new HttpPost(url);
post.setEntity(builder.build());
return executeRequest(post, WeiboStatusResponse.class);
}
// 獲取用戶信息
public WeiboUserInfo getUserInfo(Long uid) {
String token = tokenManager.getValidAccessToken(uid);
String url = props.getApiBase() + "users/show.json?access_token=" + token + "&uid=" + uid;
return executeGet(url, WeiboUserInfo.class);
}
private <T> T postForm(String url, Map<String, Object> params, Class<T> respType) {
// 構建表單實體...
}
private <T> T executeRequest(HttpRequestBase request, Class<T> respType) {
// 統一執行、異常處理、日誌...
}
}
@Data
public class WeiboStatusResponse {
private String id; // 微博 ID
private String idstr; // 字符串 ID
private String text; // 微博內容
private String source; // 來源
private Boolean favorited;
private Boolean truncated;
private String created_at;
private WeiboUserInfo user; // 發布者信息
private Integer error;
private String error_description;
}
@Data
public class WeiboUserInfo {
private Long id;
private String screen_name;
private String profile_image_url;
private String profile_url;
private Integer followers_count;
private Integer friends_count;
private Integer statuses_count;
private Boolean verified;
}
@Controller
@RequestMapping("/weibo")
public class WeiboController {
@Autowired
private WeiboOAuthService oAuthService;
@Autowired
private WeiboApiClient apiClient;
@Autowired
private WeiboTokenManager tokenManager;
// 步驟1:跳轉授權頁
@GetMapping("/login")
public String login(HttpSession session) {
String state = UUID.randomUUID().toString();
session.setAttribute("weibo_state", state);
return "redirect:" + oAuthService.buildAuthorizeUrl(state);
}
// 步驟2:回調處理
@GetMapping("/callback")
public String callback(@RequestParam("code") String code,
@RequestParam("state") String state,
HttpSession session,
RedirectAttributes attrs) {
try {
// 驗證 state
String sessionState = (String) session.getAttribute("weibo_state");
if (!state.equals(sessionState)) {
throw new IllegalStateException("State 不匹配");
}
WeiboTokenResponse tokenResp = oAuthService.handleCallback(code, state);
if (!tokenResp.isSuccess()) {
throw new WeiboOAuthException(tokenResp.getError_description());
}
// 緩存 Token
tokenManager.cacheToken(tokenResp.getUid(), tokenResp);
// 獲取用戶信息
WeiboUserInfo user = apiClient.getUserInfo(tokenResp.getUid());
session.setAttribute("weibo_user", user);
return "redirect:/weibo/home";
} catch (Exception e) {
attrs.addFlashAttribute("error", "授權失敗:" + e.getMessage());
return "redirect:/login";
}
}
// 發布微博
@PostMapping("/post")
@ResponseBody
public Result<String> postStatus(@RequestParam String content,
@RequestParam(required = false) MultipartFile image,
HttpSession session) {
WeiboUserInfo user = (WeiboUserInfo) session.getAttribute("weibo_user");
if (user == null) return Result.error("未授權");
try {
WeiboStatusResponse resp;
if (image != null && !image.isEmpty()) {
File tempFile = File.createTempFile("weibo_", ".jpg");
image.transferTo(tempFile);
resp = apiClient.uploadStatus(user.getId(), content, tempFile);
tempFile.deleteOnExit();
} else {
resp = apiClient.postStatus(user.getId(), content);
}
if (resp.getError() != null && resp.getError() != 0) {
return Result.error("發布失敗:" + resp.getError_description());
}
return Result.success("發布成功,微博 ID:" + resp.getId());
} catch (Exception e) {
return Result.error("發布異常:" + e.getMessage());
}
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| redirect_uri_mismatch | 回調地址與開放平台配置不一致 | 精確匹配域名、路徑、協議、端口 |
| access_token 過期 | 有效期 30 天,未刷新 | 使用 refresh_token 或引導重新授權 |
| 圖片上傳失敗 | 格式/大小/Content-Type | 支持 jpg/png/gif <5MB;multipart/form-data |
| 發布內容被屏蔽 | 敏感詞/重複/廣告 | 內容預檢;錯誤碼 10007/10008 需提示用戶 |
| 簽名驗證失敗 | App Secret 泄露/參與簽名參數錯誤 | 服務端保密;嚴格按文檔構建簽名基礎串 |
微博 OAuth2 集成核心在於:授權碼流程正確性、Token 生命周期管理、API 參數規範構建。封裝統一客戶端、集成 Spring 生態、做好異常處理與安全防護,即可構建穩健的微博開放平台應用。注意微博 API 頻率限制(默認 10000/天/應用),高併發場景需申請提額或實現本地限流。