优惠券app安全策略:基于OAuth2.0的第三方平台授权与数据保护

优惠券app安全策略:基于OAuth2.0的第三方平台授权与数据保护

大家好,我是省赚客APP研发者阿可!省赚客APP(juwatech.cn)需对接淘宝联盟、京东联盟等第三方开放平台,以获取用户授权后的商品数据、订单信息及佣金明细。为保障用户隐私与平台安全,我们严格遵循 OAuth 2.0 协议实现授权流程,并在后端构建完整的 Token 管理、加密存储与访问控制机制。本文将结合 Java 实现代码,详解从授权跳转到 API 调用的全链路安全设计。

OAuth2.0 授权码模式集成

以淘宝联盟为例,采用标准 Authorization Code Flow:

  1. 用户点击"绑定淘宝账号";
  2. 跳转至 https://oauth.taobao.com/authorize?client_id=xxx&redirect_uri=https://api.juwatech.cn/oauth/callback/taobao&response_type=code
  3. 用户授权后,回调携带 code
  4. 后端用 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开发者团队,转载请注明出处!

相关推荐
资生算法程序员_畅想家_剑魔1 天前
Java常见技术分享-29-Jackson JSON处理类详解
java·开发语言·json
论迹1 天前
【多线程】-- JUC的常见类
java·开发语言·性能优化·多线程·juc
豆沙沙包?1 天前
2026年--Lc332-649. Dota2 参议院(队列)--java版
java·开发语言
白驹过隙不负青春1 天前
Docker-compose部署java服务及前端服务
java·运维·前端·docker·容器·centos
深念Y1 天前
删除IDEA中的JDK列表项
java·ide·jdk·intellij-idea·idea·sdk
jgyzl1 天前
2026.1.7 tlias三层架构
java·mysql·架构
yaoxin5211231 天前
285. Java Stream API - 通过 Supplier 创建 Stream
java·开发语言
安全渗透Hacker1 天前
IDEA 中主流 API 插件对比及实操建议
java·ide·intellij-idea
moxiaoran57531 天前
使用模板模式+策略模式实现产品推荐
java·模板模式