优惠券app安全策略:基于OAuth2.0的第三方平台授权与数据保护
大家好,我是省赚客APP研发者阿可!省赚客APP(juwatech.cn)需对接淘宝联盟、京东联盟等第三方开放平台,以获取用户授权后的商品数据、订单信息及佣金明细。为保障用户隐私与平台安全,我们严格遵循 OAuth 2.0 协议实现授权流程,并在后端构建完整的 Token 管理、加密存储与访问控制机制。本文将结合 Java 实现代码,详解从授权跳转到 API 调用的全链路安全设计。
OAuth2.0 授权码模式集成
以淘宝联盟为例,采用标准 Authorization Code Flow:
- 用户点击"绑定淘宝账号";
- 跳转至
https://oauth.taobao.com/authorize?client_id=xxx&redirect_uri=https://api.juwatech.cn/oauth/callback/taobao&response_type=code; - 用户授权后,回调携带
code; - 后端用
code换取access_token。
前端跳转逻辑(由 APP 内 WebView 触发):
java
@GetMapping("/oauth/authorize/taobao")
public String authorizeTaobao() {
String url = "https://oauth.taobao.com/authorize?" +
"client_id=" + TAOTAO_APP_KEY +
"&redirect_uri=" + URLEncoder.encode("https://api.juwatech.cn/oauth/callback/taobao", StandardCharsets.UTF_8) +
"&response_type=code" +
"&state=" + generateSecureState();
return "redirect:" + url;
}

Token 获取与安全存储
回调接口接收 code 并请求 Token:
java
@PostMapping("/oauth/callback/taobao")
public ResponseEntity<String> handleTaobaoCallback(@RequestParam String code,
@RequestParam String state,
@RequestHeader("X-User-ID") Long userId) {
if (!isValidState(state)) {
return ResponseEntity.status(400).body("Invalid state");
}
// 请求 access_token
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("code", code);
params.add("client_id", TAOTAO_APP_KEY);
params.add("client_secret", TAOTAO_APP_SECRET);
params.add("redirect_uri", "https://api.juwatech.cn/oauth/callback/taobao");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
TaobaoTokenResponse response = restTemplate.postForObject(
"https://oauth.taobao.com/token", request, TaobaoTokenResponse.class);
// 加密存储(使用 AES-GCM)
String encryptedRefreshToken = AesGcmUtil.encrypt(response.getRefreshToken(), ENCRYPTION_KEY);
String encryptedAccessToken = AesGcmUtil.encrypt(response.getAccessToken(), ENCRYPTION_KEY);
// 存入数据库,关联用户ID
oauthTokenRepository.save(OauthToken.builder()
.userId(userId)
.platform("TAOBAO")
.accessToken(encryptedAccessToken)
.refreshToken(encryptedRefreshToken)
.expiresIn(response.getExpiresIn())
.createTime(System.currentTimeMillis())
.build());
return ResponseEntity.ok("绑定成功");
}
加密工具类:
java
package juwatech.cn.security;
public class AesGcmUtil {
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
public static String encrypt(String plaintext, SecretKey key) {
try {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(ByteBuffer.allocate(iv.length + ciphertext.length)
.put(iv).put(ciphertext).array());
} catch (Exception e) {
throw new RuntimeException("Encryption failed", e);
}
}
public static String decrypt(String encrypted, SecretKey key) {
try {
byte[] data = Base64.getDecoder().decode(encrypted);
ByteBuffer buffer = ByteBuffer.wrap(data);
byte[] iv = new byte[GCM_IV_LENGTH];
buffer.get(iv);
byte[] ciphertext = new byte[buffer.remaining()];
buffer.get(ciphertext);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Decryption failed", e);
}
}
}
API 调用时自动刷新 Token
封装平台客户端,在调用前检查 Token 有效性:
java
@Component
public class TaobaoApiClient {
public TaobaoItem getItemInfo(String itemId, Long userId) {
OauthToken tokenRecord = oauthTokenRepository.findByUserIdAndPlatform(userId, "TAOBAO");
String accessToken = AesGcmUtil.decrypt(tokenRecord.getAccessToken(), ENCRYPTION_KEY);
// 尝试调用
try {
return doCall(itemId, accessToken);
} catch (InvalidTokenException e) {
// 刷新 Token
String newAccessToken = refreshToken(tokenRecord);
return doCall(itemId, newAccessToken);
}
}
private String refreshToken(OauthToken record) {
String encryptedRefresh = record.getRefreshToken();
String refreshToken = AesGcmUtil.decrypt(encryptedRefresh, ENCRYPTION_KEY);
// 调用淘宝 refresh_token 接口
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "refresh_token");
params.add("refresh_token", refreshToken);
params.add("client_id", TAOTAO_APP_KEY);
params.add("client_secret", TAOTAO_APP_SECRET);
TaobaoTokenResponse resp = restTemplate.postForObject(
"https://oauth.taobao.com/token", new HttpEntity<>(params), TaobaoTokenResponse.class);
// 更新数据库
String newEncryptedAccess = AesGcmUtil.encrypt(resp.getAccessToken(), ENCRYPTION_KEY);
oauthTokenRepository.updateAccessToken(record.getId(), newEncryptedAccess, resp.getExpiresIn());
return resp.getAccessToken();
}
}
权限隔离与审计日志
所有涉及用户数据的接口强制校验 userId 与 Token 所属用户一致:
java
@GetMapping("/user/orders")
public List<Order> getUserOrders(@RequestHeader("X-User-ID") Long currentUserId,
@RequestParam String platform) {
// 防止越权:确保当前用户已绑定该平台
if (!oauthTokenRepository.existsByUserIdAndPlatform(currentUserId, platform)) {
throw new AccessDeniedException("未授权访问");
}
return orderService.queryByUser(currentUserId, platform);
}
关键操作记录审计日志:
java
@EventListener
public void onTokenRefresh(TokenRefreshedEvent event) {
auditLogService.log(AuditLog.builder()
.userId(event.getUserId())
.action("REFRESH_TOKEN")
.target("TAOBAO")
.timestamp(System.currentTimeMillis())
.build());
}
本文著作权归聚娃科技省赚客app开发者团队,转载请注明出处!