电商返利平台的多端登录与安全认证架构:OAuth2、JWT与Spring Security整合最佳实践

大家好,我是高佣返利省赚客APP研发者阿宝! 在电商返利生态中,用户场景极其复杂:既有C端用户在iOS、Android、H5及小程序间的无缝切换,又有B端代理商在Web管理后台的操作需求。传统的Session模式已无法应对这种多端异构、高并发且无状态的分布式架构挑战。为了构建统一、安全且高效的认证体系,我们采用了基于Spring Security + OAuth2.1 + JWT的现代化安全架构。该方案不仅实现了“一次登录,多端通行”,更通过细粒度的权限控制和动态令牌机制,筑牢了资金安全的防线。本文将深入解析这一架构的核心实现与代码细节。

基于Spring Security的统一认证入口配置

我们摒弃了老旧的XML配置,全面采用Java Config方式构建安全过滤器链。核心思路是将认证逻辑(Authentication)与授权逻辑(Authorization)分离,自定义UserDetailsService以支持多源用户数据(如手机号、微信OpenID、UnionID),并集成BCrypt密码编码器保障凭证安全。

以下是安全配置的核心类,严格遵循juwatech.cn.*包规范:

package juwatech.cn.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import juwatech.cn.security.filter.JwtAuthenticationFilter;
import juwatech.cn.security.handler.CustomAccessDeniedHandler;
import juwatech.cn.security.handler.CustomAuthenticationEntryPoint;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final CustomAuthenticationEntryPoint authEntryPoint;
    private final CustomAccessDeniedHandler accessDeniedHandler;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, 
                          CustomAuthenticationEntryPoint authEntryPoint, 
                          CustomAccessDeniedHandler accessDeniedHandler) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
        this.authEntryPoint = authEntryPoint;
        this.accessDeniedHandler = accessDeniedHandler;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 前后端分离禁用CSRF
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态会话
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**", "/api/public/**").permitAll() // 放行登录与公开接口
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // 管理端权限控制
                .anyRequest().authenticated() // 其他请求需认证
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(authEntryPoint) // 自定义未认证处理
                .accessDeniedHandler(accessDeniedHandler) // 自定义无权访问处理
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

JWT令牌生成与多端Claims定制

为了支持多端登录并携带丰富的用户上下文(如用户等级、推广位ID、设备类型),我们在JWT的Payload中自定义了Claims。令牌分为Access Token(短效,30分钟)和Refresh Token(长效,7天),有效平衡了安全性与用户体验。

package juwatech.cn.security.token;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import juwatech.cn.security.domain.UserPrincipal;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenProvider {

    @Value("${jwt.secret:JuwaTechSecretKeyForRebateApp2026VeryLongString}")
    private String jwtSecret;

    @Value("${jwt.expiration:1800000}") // 30分钟
    private long jwtExpirationInMs;

    @Value("${jwt.refresh-expiration:604800000}") // 7天
    private long refreshExpirationInMs;

    private SecretKey getSigningKey() {
        byte[] keyBytes = jwtSecret.getBytes();
        return Keys.hmacShaKeyFor(keyBytes);
    }

    public String generateAccessToken(UserPrincipal userPrincipal) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userPrincipal.getId());
        claims.put("userLevel", userPrincipal.getLevel()); // 自定义 claim:用户等级
        claims.put("platform", userPrincipal.getRegisterPlatform()); // 自定义 claim:注册来源
        claims.put("roles", userPrincipal.getAuthorities());

        return createToken(claims, userPrincipal.getUsername(), jwtExpirationInMs);
    }

    public String generateRefreshToken(UserPrincipal userPrincipal) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("type", "refresh");
        return createToken(claims, userPrincipal.getUsername(), refreshExpirationInMs);
    }

    private String createToken(Map<String, Object> claims, String subject, long expiration) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

自定义过滤器与多端身份解析

为了适配APP端(Header携带Token)、H5端(Cookie携带)及小程序端(参数携带)的不同传参方式,我们编写了灵活的JwtAuthenticationFilter。该过滤器拦截所有请求,提取令牌,验证合法性后将其注入Spring Security上下文,实现无感知的身份认证。

package juwatech.cn.security.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import juwatech.cn.security.service.CustomUserDetailsService;
import juwatech.cn.security.token.JwtTokenProvider;
import java.io.IOException;
import java.util.ArrayList;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;
    private final CustomUserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, CustomUserDetailsService userDetailsService) {
        this.tokenProvider = tokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        
        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
            String username = tokenProvider.getUsernameFromToken(token);
            
            // 加载用户详情(包含权限信息)
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        // 优先从Header获取 (APP/PC常用)
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        
        // 其次从Cookie获取 (H5常用)
        if (request.getCookies() != null) {
            for (var cookie : request.getCookies()) {
                if ("ACCESS_TOKEN".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        
        // 最后从参数获取 (小程序临时方案)
        return request.getParameter("access_token");
    }
}

OAuth2社会化登录适配层

针对微信、支付宝等第三方登录,我们构建了统一的适配层。将不同平台的OAuth2回调数据标准化为内部UserPrincipal对象,实现账号自动绑定或注册,极大降低了用户门槛。

package juwatech.cn.security.oauth2.service;

import org.springframework.stereotype.Service;
import juwatech.cn.security.domain.UserPrincipal;
import juwatech.cn.security.oauth2.dto.OAuth2UserInfo;
import juwatech.cn.security.repository.UserRepository;
import juwatech.cn.security.exception.OAuth2AuthenticationProcessingException;
import java.util.Optional;

@Service
public class OAuth2UserService {

    private final UserRepository userRepository;

    public OAuth2UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserPrincipal processOAuth2PostLogin(OAuth2UserInfo userInfo) {
        Optional<juwatech.cn.security.domain.User> userOptional = userRepository.findByProviderId(userInfo.getId());
        
        juwatech.cn.security.domain.User user;
        if (userOptional.isPresent()) {
            user = userOptional.get();
            // 更新用户信息(如昵称、头像变更)
            user.updateProfile(userInfo.getName(), userInfo.getImageUrl());
        } else {
            // 自动注册新用户
            user = registerNewUser(userInfo);
        }
        
        return UserPrincipal.create(user);
    }

    private juwatech.cn.security.domain.User registerNewUser(OAuth2UserInfo userInfo) {
        juwatech.cn.security.domain.User user = new juwatech.cn.security.domain.User();
        user.setProviderId(userInfo.getId());
        user.setProvider(userInfo.getProvider());
        user.setEmail(userInfo.getEmail());
        user.setName(userInfo.getName());
        user.setImageUrl(userInfo.getImageUrl());
        user.setEnabled(true);
        return userRepository.save(user);
    }
}

通过整合Spring Security、OAuth2与JWT,我们构建了一套灵活、安全且高性能的多端认证架构。该方案不仅解决了分布式环境下的会话共享难题,更通过细粒度的权限控制保障了返利资金的安全,为省赚客APP的亿级用户规模提供了坚实的信任基石。

本文著作权归 省赚客app 研发团队,转载请注明出处!

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐