2万字带你从0到1搭建一套企业级微服务安全框架_从 0 到 框架 到企业级管理系统

        put("user", userDto);
    }};
    return ResponseEntity.ok(authInfo);
}


package com.ossa.system.filter;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ossa.common.api.bean.Privilege;
import com.ossa.common.api.bean.Role;
import com.ossa.common.api.bean.User;
import com.ossa.system.mapper.PrivilegeMapper;
import com.ossa.system.mapper.RoleMapper;
import com.ossa.system.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service(“userDetailsService”)
public class UserDetailsServiceImpl implements UserDetailsService {

private final UserService userService;
private final RoleMapper roleMapper;
private final PrivilegeMapper privilegeMapper ;


@Override
public UserDetails loadUserByUsername(String username) {
    User user;
    org.springframework.security.core.userdetails.User userDetails;
    try {
        user = userService.getOne(new QueryWrapper<User>().eq("username", username));

    } catch (EntityNotFoundException e) {
        // SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException
        throw new UsernameNotFoundException("", e);
    }
    if (user == null) {
        throw new UsernameNotFoundException("");
    } else {

        List<Role> roles = roleMapper.listByUserId(user.getId());

        ArrayList<Privilege> privileges = new ArrayList<>();

        roles.forEach(role -> privileges.addAll(privilegeMapper.listByRoleId(role.getId())));

        ArrayList<String> tag = new ArrayList<>();

        privileges.forEach(p -> tag.add(p.getTag()));

        List<SimpleGrantedAuthority> collect = tag.stream().map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        userDetails = new org.springframework.security.core.userdetails.User(username, user.getPassword(), collect);

    }
    return userDetails;
}

}



package com.ossa.system.filter;

import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.ossa.common.bean.SecurityProperties;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class TokenProvider implements InitializingBean {

private final SecurityProperties properties;
private final StringRedisTemplate stringRedisTemplate;
public static final String AUTHORITIES_KEY = "user";
private JwtParser jwtParser;
private JwtBuilder jwtBuilder;

public TokenProvider(SecurityProperties properties, StringRedisTemplate stringRedisTemplate) {
    this.properties = properties;
    this.stringRedisTemplate = stringRedisTemplate;
}

@Override
public void afterPropertiesSet() {
    byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret());
    Key key = Keys.hmacShaKeyFor(keyBytes);
    jwtParser = Jwts.parserBuilder()
            .setSigningKey(key)
            .build();
    jwtBuilder = Jwts.builder()
            .signWith(key, SignatureAlgorithm.HS512);
}

/\*\*

* 创建Token 设置永不过期,
* Token 的时间有效性转到Redis 维护
*
* @param authentication /
* @return /
*/
public String createToken(Authentication authentication) {
return jwtBuilder
// 加入ID确保生成的 Token 都不一致
.setId(IdUtil.simpleUUID())
.claim(AUTHORITIES_KEY, authentication.getName())
.setSubject(authentication.getName())
.compact();
}

/\*\*

* 依据Token 获取鉴权信息
*
* @param token /
* @return /
*/
Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
User principal = new User(claims.getSubject(), “******”, new ArrayList<>());
return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>());
}

public Claims getClaims(String token) {
    return jwtParser
            .parseClaimsJws(token)
            .getBody();
}

/\*\*

* @param token 需要检查的token
*/
public void checkRenewal(String token) {
// 判断是否续期token,计算token的过期时间
Long expire = stringRedisTemplate.getExpire(properties.getOnlineKey() + token, TimeUnit.SECONDS);
long time = expire == null ? 0 : expire * 1000;
Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time);
// 判断当前时间与过期时间的时间差
long differ = expireDate.getTime() - System.currentTimeMillis();
// 如果在续期检查的范围内,则续期
if (differ <= properties.getDetect()) {
long renew = time + properties.getRenew();
stringRedisTemplate.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS);
}
}

public String getToken(HttpServletRequest request) {
    final String requestHeader = request.getHeader(properties.getHeader());
    if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) {
        return requestHeader.substring(7);
    }
    return null;
}

}


## 授权设计


1. 设计自己filter,拦截我们生成的token,如果token合法,则将token解析并封装成`UsernamePasswordAuthenticationToken`,存到安全上下文中
2. 为了确保授权成功,我们需要将我们的filter放在`UsernamePasswordAuthenticationFilter`前执行



package com.ossa.system.filter;

import cn.hutool.core.util.StrUtil;
import com.ossa.common.bean.SecurityProperties;
import io.jsonwebtoken.ExpiredJwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class OssaTokenFilter extends GenericFilterBean {
private static final Logger log = LoggerFactory.getLogger(OssaTokenFilter.class);

private final StringRedisTemplate stringRedisTemplate;

private final TokenProvider tokenProvider;
private final SecurityProperties properties;

/\*\*

* @param tokenProvider Token
* @param properties JWT
*/
public OssaTokenFilter(TokenProvider tokenProvider, SecurityProperties properties, StringRedisTemplate stringRedisTemplate) {
this.properties = properties;
this.tokenProvider = tokenProvider;
this.stringRedisTemplate = stringRedisTemplate;
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    String token = resolveToken(httpServletRequest);
    // 对于 Token 为空的不需要去查 Redis
    if (StrUtil.isNotBlank(token)) {
        String s = null;
        try {
            s = stringRedisTemplate.opsForValue().get(properties.getOnlineKey() + token);
        } catch (ExpiredJwtException e) {
            log.error(e.getMessage());
        }
        if (s != null && StringUtils.hasText(token)) {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // Token 续期
            tokenProvider.checkRenewal(token);
        }
    }
    filterChain.doFilter(servletRequest, servletResponse);
}

/\*\*

* 初步检测Token
*
* @param request /
* @return /
*/
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(properties.getHeader());
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(properties.getTokenStartWith())) {
// 去掉令牌前缀
return bearerToken.replace(properties.getTokenStartWith(), “”);
} else {
log.debug(“非法Token:{}”, bearerToken);
}
return null;
}
}


## 核心配置



package com.ossa.common.security.core.config;

import com.ossa.common.api.anno.AnonymousAccess;
import com.ossa.common.api.bean.SecurityProperties;
import com.ossa.common.api.enums.RequestMethodEnum;
import com.ossa.common.security.core.filter.OssaTokenFilter;
import com.ossa.common.security.core.filter.TokenProvider;
import com.ossa.common.security.core.handler.JwtAccessDeniedHandler;
import com.ossa.common.security.core.handler.JwtAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
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.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.*;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class OssaSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final SecurityProperties properties;
private final ApplicationContext applicationContext;
private final JwtAuthenticationEntryPoint authenticationErrorHandler;

private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

private final StringRedisTemplate stringRedisTemplate;

@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
    // 去除 ROLE\_ 前缀
    return new GrantedAuthorityDefaults("");
}

@Bean
public PasswordEncoder passwordEncoder() {
    // 密码加密方式
    return new BCryptPasswordEncoder();

}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {

    OssaTokenFilter customFilter = new OssaTokenFilter(tokenProvider, properties,stringRedisTemplate);

    // 搜寻匿名标记 url: @AnonymousAccess
    RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
    Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
    // 获取匿名标记
    Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);
    httpSecurity
            // 禁用 CSRF
            .csrf().disable()
            .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)

            // 授权异常
            .exceptionHandling()
            .authenticationEntryPoint(authenticationErrorHandler)
            .accessDeniedHandler(jwtAccessDeniedHandler)
            // 防止iframe 造成跨域
            .and()
            .headers()
            .frameOptions()
            .disable()
            // 不创建会话
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            // 静态资源等等
            .antMatchers(
                    HttpMethod.GET,
                    "/\*.html",
                    "/\*\*/\*.html",
                    "/\*\*/\*.css",
                    "/\*\*/\*.js",
                    "/webSocket/\*\*"
            ).permitAll()
            // swagger 文档
            .antMatchers("/swagger-ui.html").permitAll()
            .antMatchers("/swagger-resources/\*\*").permitAll()
            .antMatchers("/webjars/\*\*").permitAll()
            .antMatchers("/\*/api-docs").permitAll()
            // 文件
            .antMatchers("/avatar/\*\*").permitAll()
            .antMatchers("/file/\*\*").permitAll()
            // 阿里巴巴 druid
            .antMatchers("/druid/\*\*").permitAll()
            // 放行OPTIONS请求
            .antMatchers(HttpMethod.OPTIONS, "/\*\*").permitAll()
            // 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
            // GET
            .antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
            // POST
            .antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
            // PUT
            .antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
            // PATCH
            .antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
            // DELETE
            .antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
            // 所有类型的接口都放行
            .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()

            // 所有请求都需要认证
            .anyRequest().authenticated();
}

private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
    Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
    Set<String> get = new HashSet<>();
    Set<String> post = new HashSet<>();
    Set<String> put = new HashSet<>();
    Set<String> patch = new HashSet<>();
    Set<String> delete = new HashSet<>();
    Set<String> all = new HashSet<>();
    for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
        HandlerMethod handlerMethod = infoEntry.getValue();
        AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
        if (null != anonymousAccess) {
            List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
            RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
            switch (Objects.requireNonNull(request)) {
                case GET:
                    get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                    break;
                case POST:
                    post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                    break;
                case PUT:
                    put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                    break;
                case PATCH:
                    patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                    break;
                case DELETE:
                    delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                    break;
                default:
                    all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                    break;
            }
        }
    }
    anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
    anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
    anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
    anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
    anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
    anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
    return anonymousUrls;
}

}


## 自定义权限注解



package com.ossa.common.security.core.config;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Service(value = “pc”)
public class PermissionConfig {

public Boolean check(String... permissions) {
    // 获取当前用户的所有权限
    List<String> permission = SecurityContextHolder.getContext()
            .getAuthentication()
            .getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors
                    .toList());
    // 判断当前用户的所有权限是否包含接口上定义的权限
    return permission.contains("ADMIN") || permission.contains("INNER") || permission.contains("OFFICEIT") || Arrays.stream(permissions).anyMatch(permission::contains);
}

}


## 权限异常处理



package com.ossa.common.security.core.handler;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
    //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
    response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}

}



package com.ossa.common.security.core.handler;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request,
                     HttpServletResponse response,
                     AuthenticationException authException) throws IOException {
    // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException == null ? "Unauthorized" : authException.getMessage());
}

}


## 网关处理


网关只需要转发token到具体服务即可


在写这篇文章之前,此部分我已经升级成UAA认证授权中心,故没有此处相关代码。


## 内部流量处理


在内部流量的设计过程中,我们并不需要网关分发的token,故在此设计时,我只在feign的api接口处统一增加权限标识,并经过简单加密。


并在上述的自定的权限注解处放过该标识,不进行权限校验。



package com.ossa.feign.config;

import com.ossa.feign.util.EncryptUtil;
import feign.Logger;
import feign.Request;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
* @author issavior
*
* =================================
* **
* * 修改契约配置,支持Feign原生的注解
* * @return 返回 new Contract.Default()
* *
* @Bean
* public Contract feignContract(){
* return new Contract.Default();
* }
* ====================================
*/
@Configuration
public class FeignClientConfig implements RequestInterceptor {

如何自学黑客&网络安全

黑客零基础入门学习路线&规划

初级黑客
1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)

2、渗透测试基础(一周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等

3、操作系统基础(一周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)

4、计算机网络基础(一周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现

5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固

6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)
恭喜你,如果学到这里,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web 渗透、安全服务、安全分析等岗位;如果等保模块学的好,还可以从事等保工程师。薪资区间6k-15k

到此为止,大概1个月的时间。你已经成为了一名“脚本小子”。那么你还想往下探索吗?

如果你想要入坑黑客&网络安全,笔者给大家准备了一份:282G全网最全的网络安全资料包评论区留言即可领取!

7、脚本编程(初级/中级/高级)
在网络安全领域。是否具备编程能力是“脚本小子”和真正黑客的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力.

如果你零基础入门,笔者建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习;搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP, IDE强烈推荐Sublime;·Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,不要看完;·用Python编写漏洞的exp,然后写一个简单的网络爬虫;·PHP基本语法学习并书写一个简单的博客系统;熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选);·了解Bootstrap的布局或者CSS。

8、超级黑客
这部分内容对零基础的同学来说还比较遥远,就不展开细说了,附上学习路线。
img

网络安全工程师企业级学习路线

img
如图片过大被平台压缩导致看不清的话,评论区点赞和评论区留言获取吧。我都会回复的

视频配套资料&国内外网安书籍、文档&工具

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。

img
一些笔者自己买的、其他平台白嫖不到的视频教程。
img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值