基于 OAuth2 的用户授权体系在返利类应用中的安全集成实践
大家好,我是 微赚淘客系统3.0 的研发者省赚客!
返利类应用需频繁调用淘宝联盟、京东联盟等开放平台 API 获取订单与佣金数据,而这些平台普遍采用 OAuth2.0 授权机制。为保障用户授权安全、避免 Token 泄露与滥用,微赚淘客系统3.0 构建了统一的 OAuth2 安全集成框架,实现授权流程标准化、Token 安全存储与自动刷新。
一、授权流程设计
我们采用 Authorization Code 模式(适用于 Web 应用),流程如下:
- 用户点击"绑定淘宝账号";
- 跳转至淘宝 OAuth2 授权页;
- 用户同意后,淘宝重定向回
redirect_uri并携带code; - 后端用
code换取access_token与refresh_token; - 安全存储 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 研发团队,转载请注明出处!