基于 OAuth2 的用户授权体系在返利类应用中的安全集成实践

基于 OAuth2 的用户授权体系在返利类应用中的安全集成实践

大家好,我是 微赚淘客系统3.0 的研发者省赚客!

返利类应用需频繁调用淘宝联盟、京东联盟等开放平台 API 获取订单与佣金数据,而这些平台普遍采用 OAuth2.0 授权机制。为保障用户授权安全、避免 Token 泄露与滥用,微赚淘客系统3.0 构建了统一的 OAuth2 安全集成框架,实现授权流程标准化、Token 安全存储与自动刷新。

一、授权流程设计

我们采用 Authorization Code 模式(适用于 Web 应用),流程如下:

  1. 用户点击"绑定淘宝账号";
  2. 跳转至淘宝 OAuth2 授权页;
  3. 用户同意后,淘宝重定向回 redirect_uri 并携带 code
  4. 后端用 code 换取 access_tokenrefresh_token
  5. 安全存储 Token,并关联到本地用户 ID。

二、OAuth2 客户端配置

使用 Spring Security OAuth2 Client 实现:

java 复制代码
package juwatech.cn.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/oauth2/callback/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .redirectionEndpoint(redir -> redir
                    .baseUri("/oauth2/callback/*")
                )
            );
        return http.build();
    }
}

application.yml 中配置淘宝客户端:

yaml 复制代码
spring:
  security:
    oauth2:
      client:
        registration:
          taobao:
            client-id: ${TAOBAO_CLIENT_ID}
            client-secret: ${TAOBAO_CLIENT_SECRET}
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/oauth2/callback/taobao"
            scope: taobao_user_info,taobao_order_data
        provider:
          taobao:
            authorization-uri: https://oauth.taobao.com/authorize
            token-uri: https://oauth.taobao.com/token
            user-info-uri: https://eco.taobao.com/router/rest?method=taobao.user.info.get
            user-name-attribute: user_id

三、自定义授权成功处理器

授权成功后,需将 Token 与本地用户绑定并持久化:

java 复制代码
package juwatech.cn.oauth.handler;

import juwatech.cn.oauth.service.OAuth2TokenStoreService;
import juwatech.cn.user.service.UserService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class TaobaoOAuth2SuccessHandler implements AuthenticationSuccessHandler {

    private final OAuth2TokenStoreService tokenStoreService;
    private final UserService userService;

    public TaobaoOAuth2SuccessHandler(OAuth2TokenStoreService tokenStoreService,
                                      UserService userService) {
        this.tokenStoreService = tokenStoreService;
        this.userService = userService;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        org.springframework.security.core.Authentication authentication) throws IOException {
        OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
        String principalName = oauthToken.getName(); // 淘宝 user_id

        // 获取本地用户(若未注册则自动创建)
        Long localUserId = userService.getOrCreateByOpenId("taobao", principalName);

        // 保存 Token
        OAuth2AccessToken accessToken = oauthToken.getAuthorizedClientRegistrationId()
            .equals("taobao") ? extractAccessToken(oauthToken) : null;
        OAuth2RefreshToken refreshToken = extractRefreshToken(oauthToken);

        tokenStoreService.saveToken(
            "taobao",
            localUserId,
            principalName,
            accessToken.getTokenValue(),
            refreshToken != null ? refreshToken.getTokenValue() : null,
            System.currentTimeMillis() + accessToken.getExpiresAt().toEpochMilli()
        );

        // 重定向至用户中心
        response.sendRedirect("/user/bind-success");
    }

    private OAuth2AccessToken extractAccessToken(OAuth2AuthenticationToken token) {
        // 从上下文获取,此处简化
        return null;
    }

    private OAuth2RefreshToken extractRefreshToken(OAuth2AuthenticationToken token) {
        return null;
    }
}

四、Token 安全存储

Token 不得明文存数据库。我们采用 AES 加密存储:

java 复制代码
package juwatech.cn.oauth.crypto;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AesEncryptor {
    private final SecretKeySpec keySpec;

    public AesEncryptor(String secret) {
        this.keySpec = new SecretKeySpec(secret.getBytes(), "AES");
    }

    public String encrypt(String data) {
        try {
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes()));
        } catch (Exception e) {
            throw new RuntimeException("Encrypt failed", e);
        }
    }

    public String decrypt(String encryptedData) {
        try {
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decoded = Base64.getDecoder().decode(encryptedData);
            return new String(cipher.doFinal(decoded));
        } catch (Exception e) {
            throw new RuntimeException("Decrypt failed", e);
        }
    }
}

DAO 层自动加解密:

java 复制代码
package juwatech.cn.oauth.dao;

import juwatech.cn.oauth.crypto.AesEncryptor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class OAuth2TokenDao {

    private final JdbcTemplate jdbcTemplate;
    private final AesEncryptor encryptor;

    public void saveToken(String platform, Long userId, String openId,
                          String accessToken, String refreshToken, long expireAt) {
        String encryptedAt = encryptor.encrypt(accessToken);
        String encryptedRt = refreshToken != null ? encryptor.encrypt(refreshToken) : null;
        jdbcTemplate.update(
            "INSERT INTO oauth2_tokens (platform, user_id, open_id, access_token, refresh_token, expire_at) " +
            "VALUES (?, ?, ?, ?, ?, ?)",
            platform, userId, openId, encryptedAt, encryptedRt, expireAt
        );
    }

    public OAuth2TokenRecord getTokenByUserId(String platform, Long userId) {
        // 查询后 decrypt
        return null;
    }
}

五、Token 自动刷新机制

在调用淘宝 API 前检查 Token 是否过期,若过期则用 refresh_token 刷新:

java 复制代码
package juwatech.cn.taobao.client;

import juwatech.cn.oauth.dao.OAuth2TokenDao;
import juwatech.cn.oauth.crypto.AesEncryptor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class TaobaoApiClient {

    private final RestTemplate restTemplate;
    private final OAuth2TokenDao tokenDao;
    private final AesEncryptor encryptor;

    public String callApi(Long userId, String apiMethod) {
        OAuth2TokenRecord tokenRecord = tokenDao.getTokenByUserId("taobao", userId);
        if (tokenRecord == null) throw new IllegalStateException("Not bound");

        String accessToken = encryptor.decrypt(tokenRecord.getAccessToken());
        if (System.currentTimeMillis() >= tokenRecord.getExpireAt()) {
            accessToken = refreshToken(userId, tokenRecord);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        HttpEntity<?> entity = new HttpEntity<>(headers);
        ResponseEntity<String> resp = restTemplate.exchange(
            "https://eco.taobao.com/router/rest?method=" + apiMethod,
            HttpMethod.GET, entity, String.class
        );
        return resp.getBody();
    }

    private String refreshToken(Long userId, OAuth2TokenRecord oldRecord) {
        String refreshToken = encryptor.decrypt(oldRecord.getRefreshToken());
        // 调用淘宝 refresh_token 接口
        String newAccessToken = "..."; // 省略 HTTP 调用
        long newExpire = System.currentTimeMillis() + 86400_000L; // 24h

        tokenDao.updateAccessToken("taobao", userId, 
            encryptor.encrypt(newAccessToken), newExpire);
        return newAccessToken;
    }
}

六、安全加固措施

  • 所有回调 URL 强制 HTTPS;
  • client_secret 通过环境变量注入,不硬编码;
  • Token 表字段权限最小化,DB 账号无 DROP 权限;
  • 敏感操作(如解绑)需二次验证。

该方案已在生产环境稳定运行,支撑百万级用户授权,未发生 Token 泄露事件。

本文著作权归 微赚淘客系统3.0 研发团队,转载请注明出处!

相关推荐
C澒5 小时前
面单打印服务的监控检查事项
前端·后端·安全·运维开发·交通物流
运维有小邓@5 小时前
生物制药企业 AD 域管理破局:合规 · 效率 · 安全三维解决方案
人工智能·安全
大力财经5 小时前
喜茶2025年批量重装130多家门店
安全
青岛前景互联信息技术有限公司5 小时前
政策支撑:应急部推动化工园区安全风险智能化管控平台有效应用!
大数据·人工智能·安全
珑哥说自养号采购5 小时前
TEMU采购下单,卖家如何搭建安全的环境?
安全
浩浩测试一下5 小时前
DDOS 应急响应Linux防火墙 Iptable 使用方式方法
linux·网络·安全·web安全·网络安全·系统安全·ddos
浩浩测试一下5 小时前
洪水猛兽攻击 Ddos Dos cc Drdos floods区别
安全·web安全·网络安全·系统安全·wpf·可信计算技术·安全架构
vortex56 小时前
GRUB原理与加固教程
安全
Elieal6 小时前
JWT 登录校验机制:5 大核心类打造 Spring Boot 接口安全屏障
spring boot·后端·安全