一文带你读懂Session

一文带你读懂Session

目录


1. Session简介

1.1 什么是Session

Session(会话) 是Web开发中用于在服务器端存储用户状态信息的机制。它允许服务器在多个HTTP请求之间保持用户的状态数据,解决了HTTP协议无状态的问题。

1.2 Session的特点

  • 服务器端存储: 数据存储在服务器内存或持久化存储中
  • 唯一标识: 每个Session都有唯一的SessionID
  • 生命周期: 有明确的创建和销毁时间
  • 安全性: 比Cookie更安全,敏感数据不会暴露给客户端
  • 跨请求保持: 在多个HTTP请求间保持数据

1.3 Session vs Cookie

特性SessionCookie
存储位置服务器端客户端
安全性
存储容量小(4KB)
生命周期可控制可设置过期时间
网络传输只传输SessionID传输完整数据
服务器资源占用内存不占用

1.4 Session工作原理

1. 用户首次访问
   ↓
2. 服务器创建Session
   ↓
3. 生成唯一SessionID
   ↓
4. 将SessionID返回给客户端(Cookie/URL)
   ↓
5. 客户端后续请求携带SessionID
   ↓
6. 服务器根据SessionID查找Session数据
   ↓
7. 返回用户状态信息

2. Session如何实现用户数据共享

2.1 SessionID传递机制

2.1.1 基于Cookie的传递
// 服务器端设置Session
HttpSession session = request.getSession();
session.setAttribute("userId", 12345);
session.setAttribute("username", "john_doe");

// 客户端自动携带Cookie
// Cookie: JSESSIONID=ABC123DEF456
2.1.2 基于URL重写的传递
// URL重写方式
String url = response.encodeURL("/user/profile");
// 结果: /user/profile;jsessionid=ABC123DEF456

// 表单隐藏字段
<input type="hidden" name="jsessionid" value="ABC123DEF456">

2.2 Session数据存储

2.2.1 内存存储
// 在内存中存储Session数据
public class SessionManager {
    private Map<String, Map<String, Object>> sessions = new ConcurrentHashMap<>();
  
    public void setAttribute(String sessionId, String key, Object value) {
        sessions.computeIfAbsent(sessionId, k -> new ConcurrentHashMap<>())
                .put(key, value);
    }
  
    public Object getAttribute(String sessionId, String key) {
        Map<String, Object> session = sessions.get(sessionId);
        return session != null ? session.get(key) : null;
    }
}
2.2.2 数据库存储
// 数据库存储Session
@Entity
@Table(name = "sessions")
public class SessionData {
    @Id
    private String sessionId;
  
    @Column(columnDefinition = "TEXT")
    private String sessionData; // JSON格式存储
  
    @Column
    private LocalDateTime lastAccessedTime;
  
    @Column
    private LocalDateTime creationTime;
  
    @Column
    private Integer maxInactiveInterval;
}

// Session存储服务
@Service
public class DatabaseSessionStore {
  
    @Autowired
    private SessionDataRepository sessionRepository;
  
    public void storeSession(String sessionId, Map<String, Object> data) {
        SessionData sessionData = new SessionData();
        sessionData.setSessionId(sessionId);
        sessionData.setSessionData(JSON.toJSONString(data));
        sessionData.setLastAccessedTime(LocalDateTime.now());
        sessionRepository.save(sessionData);
    }
}

2.3 跨域Session共享

2.3.1 基于Redis的共享
// Redis Session存储
@Service
public class RedisSessionStore {
  
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
  
    private static final String SESSION_PREFIX = "session:";
  
    public void setAttribute(String sessionId, String key, Object value) {
        String redisKey = SESSION_PREFIX + sessionId;
        redisTemplate.opsForHash().put(redisKey, key, value);
        redisTemplate.expire(redisKey, Duration.ofMinutes(30));
    }
  
    public Object getAttribute(String sessionId, String key) {
        String redisKey = SESSION_PREFIX + sessionId;
        return redisTemplate.opsForHash().get(redisKey, key);
    }
}
2.3.2 基于JWT的共享
// JWT Session实现
@Service
public class JwtSessionService {
  
    private static final String SECRET = "mySecretKey";
    private static final int EXPIRATION = 86400; // 24小时
  
    public String createSession(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }
  
    public Claims parseSession(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

3. 实际应用场景

3.1 用户认证与授权

3.1.1 登录状态管理
@Controller
public class AuthController {
  
    @PostMapping("/login")
    public String login(@RequestParam String username, 
                       @RequestParam String password,
                       HttpSession session) {
        // 验证用户凭据
        User user = userService.authenticate(username, password);
        if (user != null) {
            // 将用户信息存储到Session
            session.setAttribute("userId", user.getId());
            session.setAttribute("username", user.getUsername());
            session.setAttribute("userRole", user.getRole());
            return "redirect:/dashboard";
        }
        return "redirect:/login?error=true";
    }
  
    @GetMapping("/logout")
    public String logout(HttpSession session) {
        // 清除Session数据
        session.invalidate();
        return "redirect:/login";
    }
}
3.1.2 权限控制
@Component
public class SessionPermissionChecker {
  
    public boolean hasPermission(HttpSession session, String permission) {
        String userRole = (String) session.getAttribute("userRole");
        return checkPermission(userRole, permission);
    }
  
    public boolean isLoggedIn(HttpSession session) {
        return session.getAttribute("userId") != null;
    }
}

3.2 购物车功能

3.2.1 购物车数据存储
@Controller
public class CartController {
  
    @PostMapping("/cart/add")
    public String addToCart(@RequestParam Long productId,
                           @RequestParam Integer quantity,
                           HttpSession session) {
        // 获取购物车
        Map<Long, Integer> cart = getCart(session);
      
        // 添加商品
        cart.put(productId, cart.getOrDefault(productId, 0) + quantity);
      
        // 更新Session
        session.setAttribute("cart", cart);
      
        return "redirect:/cart";
    }
  
    @SuppressWarnings("unchecked")
    private Map<Long, Integer> getCart(HttpSession session) {
        Map<Long, Integer> cart = (Map<Long, Integer>) session.getAttribute("cart");
        if (cart == null) {
            cart = new HashMap<>();
            session.setAttribute("cart", cart);
        }
        return cart;
    }
}

3.3 表单数据暂存

3.3.1 多步骤表单
@Controller
public class MultiStepFormController {
  
    @PostMapping("/form/step1")
    public String step1(@ModelAttribute Step1Data data, HttpSession session) {
        // 暂存第一步数据
        session.setAttribute("step1Data", data);
        return "form/step2";
    }
  
    @PostMapping("/form/step2")
    public String step2(@ModelAttribute Step2Data data, HttpSession session) {
        // 暂存第二步数据
        session.setAttribute("step2Data", data);
        return "form/step3";
    }
  
    @PostMapping("/form/complete")
    public String complete(@ModelAttribute Step3Data data, HttpSession session) {
        // 获取所有步骤的数据
        Step1Data step1 = (Step1Data) session.getAttribute("step1Data");
        Step2Data step2 = (Step2Data) session.getAttribute("step2Data");
      
        // 处理完整表单
        processCompleteForm(step1, step2, data);
      
        // 清除临时数据
        session.removeAttribute("step1Data");
        session.removeAttribute("step2Data");
      
        return "form/success";
    }
}

3.4 用户偏好设置

3.4.1 主题和语言设置
@Controller
public class PreferenceController {
  
    @PostMapping("/preferences/theme")
    public String setTheme(@RequestParam String theme, HttpSession session) {
        session.setAttribute("theme", theme);
        return "redirect:/settings";
    }
  
    @PostMapping("/preferences/language")
    public String setLanguage(@RequestParam String language, HttpSession session) {
        session.setAttribute("language", language);
        return "redirect:/settings";
    }
  
    @GetMapping("/dashboard")
    public String dashboard(Model model, HttpSession session) {
        // 应用用户偏好
        String theme = (String) session.getAttribute("theme");
        String language = (String) session.getAttribute("language");
      
        model.addAttribute("theme", theme != null ? theme : "default");
        model.addAttribute("language", language != null ? language : "en");
      
        return "dashboard";
    }
}

4. 重难点分析

4.1 技术难点

4.1.1 Session超时管理

难点: 如何合理设置Session超时时间
解决方案:

// 动态Session超时设置
@Configuration
public class SessionConfig {
  
    @Bean
    public ServletListenerRegistrationBean<HttpSessionListener> sessionListener() {
        return new ServletListenerRegistrationBean<>(new HttpSessionListener() {
            @Override
            public void sessionCreated(HttpSessionEvent se) {
                HttpSession session = se.getSession();
                // 根据用户类型设置不同的超时时间
                String userType = (String) session.getAttribute("userType");
                if ("admin".equals(userType)) {
                    session.setMaxInactiveInterval(3600); // 1小时
                } else {
                    session.setMaxInactiveInterval(1800); // 30分钟
                }
            }
        });
    }
}

// Session超时处理
@Component
public class SessionTimeoutHandler {
  
    @EventListener
    public void handleSessionTimeout(HttpSessionEvent event) {
        HttpSession session = event.getSession();
        String userId = (String) session.getAttribute("userId");
      
        // 记录超时日志
        log.info("Session timeout for user: {}", userId);
      
        // 清理相关资源
        cleanupUserResources(userId);
    }
}
4.1.2 并发访问控制

难点: 多线程环境下Session数据的安全访问
解决方案:

// 线程安全的Session管理
public class ThreadSafeSessionManager {
  
    private final Map<String, ReadWriteLock> sessionLocks = new ConcurrentHashMap<>();
  
    public void setAttribute(String sessionId, String key, Object value) {
        ReadWriteLock lock = getSessionLock(sessionId);
        lock.writeLock().lock();
        try {
            HttpSession session = getSession(sessionId);
            if (session != null) {
                session.setAttribute(key, value);
            }
        } finally {
            lock.writeLock().unlock();
        }
    }
  
    public Object getAttribute(String sessionId, String key) {
        ReadWriteLock lock = getSessionLock(sessionId);
        lock.readLock().lock();
        try {
            HttpSession session = getSession(sessionId);
            return session != null ? session.getAttribute(key) : null;
        } finally {
            lock.readLock().unlock();
        }
    }
  
    private ReadWriteLock getSessionLock(String sessionId) {
        return sessionLocks.computeIfAbsent(sessionId, k -> new ReentrantReadWriteLock());
    }
}
4.1.3 内存泄漏防护

难点: 防止Session数据导致内存泄漏
解决方案:

// Session清理机制
@Component
public class SessionCleanupService {
  
    @Autowired
    private SessionRegistry sessionRegistry;
  
    @Scheduled(fixedRate = 300000) // 每5分钟执行一次
    public void cleanupExpiredSessions() {
        List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
      
        for (Object principal : allPrincipals) {
            List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false);
          
            for (SessionInformation session : sessions) {
                if (session.isExpired()) {
                    // 清理过期Session
                    sessionRegistry.removeSessionInformation(session.getSessionId());
                  
                    // 清理相关资源
                    cleanupSessionResources(session.getSessionId());
                }
            }
        }
    }
  
    private void cleanupSessionResources(String sessionId) {
        // 清理Session相关的临时文件、缓存等
        log.info("Cleaning up resources for session: {}", sessionId);
    }
}

4.2 性能难点

4.2.1 大量Session的内存占用

难点: 高并发下Session占用大量内存
解决方案:

// Session数据压缩
public class CompressedSessionStore {
  
    public void setAttribute(String sessionId, String key, Object value) {
        try {
            // 序列化并压缩数据
            byte[] serialized = serialize(value);
            byte[] compressed = compress(serialized);
          
            // 存储压缩后的数据
            storeCompressedData(sessionId, key, compressed);
        } catch (Exception e) {
            log.error("Failed to compress session data", e);
        }
    }
  
    public Object getAttribute(String sessionId, String key) {
        try {
            // 获取压缩数据
            byte[] compressed = getCompressedData(sessionId, key);
            if (compressed == null) {
                return null;
            }
          
            // 解压并反序列化
            byte[] decompressed = decompress(compressed);
            return deserialize(decompressed);
        } catch (Exception e) {
            log.error("Failed to decompress session data", e);
            return null;
        }
    }
}
4.2.2 Session持久化性能

难点: Session持久化影响性能
解决方案:

// 异步Session持久化
@Service
public class AsyncSessionPersistence {
  
    @Async
    public void persistSessionAsync(String sessionId, Map<String, Object> data) {
        try {
            // 异步持久化Session数据
            sessionRepository.saveSessionData(sessionId, data);
        } catch (Exception e) {
            log.error("Failed to persist session data", e);
        }
    }
  
    @EventListener
    public void handleSessionChange(SessionChangeEvent event) {
        // 延迟批量持久化
        CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS)
                .execute(() -> persistSessionAsync(event.getSessionId(), event.getData()));
    }
}

4.3 安全难点

4.3.1 Session劫持防护

难点: 防止SessionID被劫持
解决方案:

// Session安全增强
@Component
public class SessionSecurityEnhancer {
  
    public void enhanceSessionSecurity(HttpSession session, HttpServletRequest request) {
        // 绑定IP地址
        String clientIP = getClientIP(request);
        session.setAttribute("clientIP", clientIP);
      
        // 绑定User-Agent
        String userAgent = request.getHeader("User-Agent");
        session.setAttribute("userAgent", userAgent);
      
        // 生成CSRF Token
        String csrfToken = generateCSRFToken();
        session.setAttribute("csrfToken", csrfToken);
    }
  
    public boolean validateSessionSecurity(HttpSession session, HttpServletRequest request) {
        String sessionIP = (String) session.getAttribute("clientIP");
        String sessionUserAgent = (String) session.getAttribute("userAgent");
      
        String currentIP = getClientIP(request);
        String currentUserAgent = request.getHeader("User-Agent");
      
        // 验证IP和User-Agent是否匹配
        return Objects.equals(sessionIP, currentIP) && 
               Objects.equals(sessionUserAgent, currentUserAgent);
    }
}
4.3.2 Session固定攻击防护

难点: 防止Session固定攻击
解决方案:

// Session固定攻击防护
@Component
public class SessionFixationProtection {
  
    public void protectAgainstFixation(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            // 在认证成功后重新生成SessionID
            String oldSessionId = session.getId();
            session.invalidate();
          
            HttpSession newSession = request.getSession(true);
            newSession.setAttribute("regenerated", true);
          
            log.info("Session regenerated: {} -> {}", oldSessionId, newSession.getId());
        }
    }
  
    @EventListener
    public void handleSuccessfulLogin(AuthenticationSuccessEvent event) {
        HttpServletRequest request = ((WebAuthenticationDetails) event.getAuthentication().getDetails()).getRequest();
        HttpServletResponse response = ((WebAuthenticationDetails) event.getAuthentication().getDetails()).getResponse();
      
        protectAgainstFixation(request, response);
    }
}

5. 结合Spring的应用

5.1 Spring Session配置

5.1.1 基础配置
@Configuration
@EnableSpringHttpSession
public class SessionConfig {
  
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
  
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
  
    @Bean
    public RedisIndexedSessionRepository sessionRepository() {
        return new RedisIndexedSessionRepository(redisTemplate());
    }
}
5.1.2 配置文件
# application.yml
spring:
  session:
    store-type: redis
    redis:
      namespace: "spring:session"
      flush-mode: on_save
    timeout: 1800 # 30分钟
  redis:
    host: localhost
    port: 6379
    password: 
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

5.2 Session管理服务

5.2.1 Session服务实现
@Service
public class SessionService {
  
    @Autowired
    private SessionRepository sessionRepository;
  
    public void setAttribute(String sessionId, String key, Object value) {
        Session session = sessionRepository.findById(sessionId);
        if (session != null) {
            session.setAttribute(key, value);
            sessionRepository.save(session);
        }
    }
  
    public Object getAttribute(String sessionId, String key) {
        Session session = sessionRepository.findById(sessionId);
        return session != null ? session.getAttribute(key) : null;
    }
  
    public void removeAttribute(String sessionId, String key) {
        Session session = sessionRepository.findById(sessionId);
        if (session != null) {
            session.removeAttribute(key);
            sessionRepository.save(session);
        }
    }
  
    public void invalidateSession(String sessionId) {
        sessionRepository.deleteById(sessionId);
    }
}
5.2.2 Session事件监听
@Component
public class SessionEventListener {
  
    @EventListener
    public void handleSessionCreated(SessionCreatedEvent event) {
        Session session = event.getSession();
        log.info("Session created: {}", session.getId());
      
        // 初始化Session数据
        initializeSessionData(session);
    }
  
    @EventListener
    public void handleSessionDestroyed(SessionDestroyedEvent event) {
        Session session = event.getSession();
        log.info("Session destroyed: {}", session.getId());
      
        // 清理Session相关资源
        cleanupSessionResources(session);
    }
  
    @EventListener
    public void handleSessionExpired(SessionExpiredEvent event) {
        Session session = event.getSession();
        log.info("Session expired: {}", session.getId());
      
        // 处理Session过期逻辑
        handleSessionExpiration(session);
    }
}

5.3 控制器层应用

5.3.1 用户认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
  
    @Autowired
    private UserService userService;
  
    @Autowired
    private SessionService sessionService;
  
    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request,
                                             HttpServletRequest httpRequest) {
        try {
            // 验证用户凭据
            User user = userService.authenticate(request.getUsername(), request.getPassword());
            if (user != null) {
                // 创建Session
                HttpSession session = httpRequest.getSession(true);
                String sessionId = session.getId();
              
                // 存储用户信息到Session
                sessionService.setAttribute(sessionId, "userId", user.getId());
                sessionService.setAttribute(sessionId, "username", user.getUsername());
                sessionService.setAttribute(sessionId, "userRole", user.getRole());
              
                // 设置Session超时
                session.setMaxInactiveInterval(1800); // 30分钟
              
                return ResponseEntity.ok(new LoginResponse(true, "Login successful", sessionId));
            } else {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                        .body(new LoginResponse(false, "Invalid credentials", null));
            }
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new LoginResponse(false, "Login failed", null));
        }
    }
  
    @PostMapping("/logout")
    public ResponseEntity<LogoutResponse> logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            String sessionId = session.getId();
            sessionService.invalidateSession(sessionId);
            session.invalidate();
        }
      
        return ResponseEntity.ok(new LogoutResponse(true, "Logout successful"));
    }
}
5.3.2 用户信息控制器
@RestController
@RequestMapping("/api/user")
public class UserController {
  
    @Autowired
    private SessionService sessionService;
  
    @GetMapping("/profile")
    public ResponseEntity<UserProfile> getProfile(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
      
        String sessionId = session.getId();
        Long userId = (Long) sessionService.getAttribute(sessionId, "userId");
        String username = (String) sessionService.getAttribute(sessionId, "username");
        String userRole = (String) sessionService.getAttribute(sessionId, "userRole");
      
        UserProfile profile = new UserProfile();
        profile.setUserId(userId);
        profile.setUsername(username);
        profile.setRole(userRole);
      
        return ResponseEntity.ok(profile);
    }
  
    @PutMapping("/preferences")
    public ResponseEntity<String> updatePreferences(@RequestBody UserPreferences preferences,
                                                   HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
      
        String sessionId = session.getId();
        sessionService.setAttribute(sessionId, "theme", preferences.getTheme());
        sessionService.setAttribute(sessionId, "language", preferences.getLanguage());
        sessionService.setAttribute(sessionId, "timezone", preferences.getTimezone());
      
        return ResponseEntity.ok("Preferences updated successfully");
    }
}

5.4 拦截器应用

5.4.1 Session验证拦截器
@Component
public class SessionValidationInterceptor implements HandlerInterceptor {
  
    @Autowired
    private SessionService sessionService;
  
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
      
        String sessionId = session.getId();
        Long userId = (Long) sessionService.getAttribute(sessionId, "userId");
      
        if (userId == null) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
      
        // 更新最后访问时间
        sessionService.setAttribute(sessionId, "lastAccessTime", System.currentTimeMillis());
      
        return true;
    }
}
5.4.2 拦截器配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
  
    @Autowired
    private SessionValidationInterceptor sessionValidationInterceptor;
  
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(sessionValidationInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/auth/login", "/api/auth/register");
    }
}

5.5 集群环境配置

5.5.1 Redis集群配置
@Configuration
@EnableSpringHttpSession
public class ClusterSessionConfig {
  
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
        clusterConfig.setClusterNodes(Arrays.asList(
                new RedisNode("192.168.1.10", 7000),
                new RedisNode("192.168.1.11", 7000),
                new RedisNode("192.168.1.12", 7000)
        ));
      
        return new LettuceConnectionFactory(clusterConfig);
    }
  
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

6. 总结

6.1 Session的优势

  1. 状态保持: 解决HTTP无状态问题
  2. 安全性: 敏感数据存储在服务器端
  3. 灵活性: 支持复杂的数据结构
  4. 可控制性: 可以精确控制生命周期

6.2 使用建议

  1. 合理设置超时时间: 根据业务需求设置合适的Session超时时间
  2. 避免存储大量数据: 不要在Session中存储过多数据
  3. 及时清理: 及时清理不需要的Session数据
  4. 安全防护: 实施适当的安全防护措施

6.3 注意事项

  1. 内存管理: 注意Session对服务器内存的影响
  2. 集群部署: 在集群环境中使用共享存储
  3. 安全考虑: 防止Session劫持和固定攻击
  4. 性能优化: 合理使用缓存和异步处理

Session是Web开发中重要的状态管理机制,合理使用可以提升用户体验和系统安全性。在实际应用中,需要根据具体业务场景选择合适的Session管理策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值