Security+jwt+验证码实现验证和授权

微服务Security+jwt+验证码实现认证和授权

简要介绍

本次博客采用Spring Security、jwt、验证码的形式实现登录验证,项目本身是一个前后端分离项目。如果你的项目在登陆时不需要验证码,你只需要在后续的代码中,将有关验证码的过滤器删除。
gitee仓库连接

基本流程

1、前端请求后端"/captcha"验证码接口,后端生成验证码文本及编码并将其存入redis缓存,然后返回验证码文本(五个字符)和验证码base64编码给前端。
2、前端显示验证码图片,用户输入用户名、密码、验证码点击登录。
3、后端开启验证
(1)开启验证码验证,走验证码过滤器,如果正确则放行走下一个过滤器,如果错误则抛出异常给登录失败过滤器,返回失败信息给前端。
(2)开启jwt验证。

a:如果请求没有携带token,则认为是首次登录,jwt过滤器不做任何事情,放行走UsernamePasswordAuthenticationFilter过滤器,该过滤器会通过查数据库验证用户的身份信息决定用户是否能登录。如果验证成功会生成一个Authentication,并保存在SecurityContext(security上下文)中。Authentication包含用户的信息及权限
b:如果请求中携带了token,走jwt过滤器,过滤器判断jwt是否为空、携带信息(用户名)是否为空,jwt是否过期,如果上述条件都正常,创建一个Authentication的实现类对象,并通过自定义的获取用户权限方法获取权限,然后通过userDetailService的loadUserByUsername方法得到UserDetails对象,里面包含用户信息和权限,调用Authentication的setUserDetails方法,最后将该Authentication对象存入到Security上下文中,后续的过滤器查询到该Authentication,就会直接放行,比如UsernamePasswordAuthenticationFilter过滤器。

上述认证都是由过滤器完成,因为认证是有顺序的,所以在security配置文件中我们要设置这三个过滤器的顺序为:验证码过滤器=》jwt过滤器=》UsernamePasswordAuthenticationFilter

.addFilterBefore(captchaFilter,UsernamePasswordAuthenticationFilter.class)
.addFilterAt(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)

.addFilterAt(a,b)默认会将a设置在b之前。

至此登录认证就结束了,需要注意的是,我们在认证的时候已经将用户的权限列表加入到了Authentication并放在了Security上下文中,所以后续对于资源做权限判断时时,只需要再目标接口上加入一下注解实现。@PreAuthorize(“hasAuthority(‘sys:role:list’)”)
@PreAuthorize(“hasRole(‘ROLE_admin’)”)

当请求该接口时,security就会去Authentication中查询有无该权限或者该角色。

核心代码

1、验证码过滤器
该过滤器用于验证验证码是否正确。该过滤器继承的是OncePerRequestFilter,因为每次登录只需要验证一次。

package com.komorebi.security;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.komorebi.common.CaptchaException;
import com.komorebi.common.Const;
import com.komorebi.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*CaptchaFilter用于验证码验证
* 因为验证码值需要一次校验,所以继承OncePerRequestFilter
* 自己写的过滤器要在security配置类中配置
* */
@Slf4j
@Component
public class CaptchaFilter extends OncePerRequestFilter {

    @Autowired
    RedisUtil redisUtil;
    @Autowired
    LoginFailureHandler loginFailureHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("开始验证码验证");
        String url = request.getRequestURI();
        //只有登陆时才会验证验证码
        if("/login".equals(url) && request.getMethod().equals("POST")){
            try{
                //校验验证码
                validate(request);
                // 校验成功才放行
                filterChain.doFilter(request,response);
            }catch (CaptchaException e){
                //发现异常,则交给登录失败处理器
                loginFailureHandler.onAuthenticationFailure(request,response,e);
            }
        }
        //将请求转发给下一个过滤器
        //filterChain.doFilter(request,response);
    }
    //校验验证码逻辑
    private void validate(HttpServletRequest request) {
        String key = request.getParameter("tokens");
        String code = request.getParameter("code");
        if(StringUtils.isBlank(key) || StringUtils.isBlank(code)){
            throw new CaptchaException("验证码信息为空");
        }
        if(!code.equals(redisUtil.hget(Const.CAPTCHA_KEY,key))){
            throw new CaptchaException("验证码错误");
        }
        //保证每个验证码只使用一次:安全
        redisUtil.del(Const.CAPTCHA_KEY,key);
    }
}

2、jwt过滤器

package com.komorebi.security;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.komorebi.entity.SysUser;
import com.komorebi.mapper.SysUserMapper;
import com.komorebi.service.SysUserService;
import com.komorebi.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

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

//jwt认证过滤器
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    UserDetailServiceImpl userDetailService;
    @Autowired
    SysUserService sysUserService;
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("开启jwt认证");

        String jwt = request.getHeader(jwtUtils.getHeader());
        if(jwt != null){
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if(claim != null){
                String username = claim.getSubject();
                log.info("jwt认证:检查用户名");
                if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
                    SysUser sysUser = sysUserService.getByUserName(username);
                    Long userId = sysUser.getId();
                    UserDetails userDetails = userDetailService.loadUserByUsername(username);
                    if(!jwtUtils.isTokenExpired(claim)){
                        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(userId));
                        auth.setDetails(userDetails);
                        log.info("通过jwt认证,设置Authentication,后续过滤器放行");
                        SecurityContextHolder.getContext().setAuthentication(auth);
                    }
                }
            }
        }else {
            log.info("首次登陆 jwt为空");
        }
        chain.doFilter(request,response);
    }
}

由于该过滤器我们继承于BasicAuthenticationFilter,也可以继承BasicAuthenticationFilter 类,并且重写了构造函数,所以在SecurityConfig中要采用Bean注入。对应SecurityConfig文件

@Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JwtAuthenticationFilter(authenticationManager());
    }

3、UserDetailServiceImpl
UsernamePasswordAuthenticationFilter主要功能为:用户登录信息验证,获取用户权限,并将上述信息封装为UserDetails,然后生成Authentication,将UserDetails加入到Authentication中,最终将Authentication加入到security上下文中。
封装为UserDetails的功能是通过UserDetailService实现的,因为UserDetailService是接口,所以定义UserDetailServiceImpl实现该接口,即UserDetailServiceImpl用于验证用户信息和获取用用户权限。

package com.komorebi.security;

import com.komorebi.entity.SysUser;
import com.komorebi.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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.Component;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    SysUserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("开始登陆验证,用户名为: {}",username);
        SysUser user = userService.getByUserName(username);
        if(user == null){
            log.info("用户名或密码不正确");
            throw new UsernameNotFoundException("用户名或密码不正确");
        }
        //UserDetails是接口,User是它的一个实现类
        //将用户密码告诉springSecurity
        //剩下的认证 就由框架Security帮我们完成

        return new User(user.getUsername(),user.getPassword(),getUserAuthority(user.getId()));
    }
    /*获取用户权限信息(角色,菜单权限)
    * */
    public List<GrantedAuthority> getUserAuthority(Long userId){
        //返回拥有角色和权限,逗号分隔
        String authority = userService.getUserAuthority(userId);
        //AuthorityUtils.commaSeparatedStringToAuthorityList将逗号分隔的字符串转为权限列表
        return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
    }
}

4、登陆成功过滤器

package com.komorebi.security;

import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import com.komorebi.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component

public class LoginSuccessHandler implements AuthenticationSuccessHandler{

    @Autowired
    JwtUtils jwtUtils;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();
        String username = authentication.getName();
        // 生成jwt,并放置到请求头中
        String jwt = jwtUtils.generateToken(username);
        //将jwt放入response header中:Authorization
        response.setHeader(jwtUtils.getHeader(), jwt);

        Result result = Result.success("登陆成功过滤器执行");

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

这里,我们过滤器需要向前端传递json数据,但是security是不支持return json数据的,所以我们只能通过流的方式返回数据。
基本步骤:
(1)获取reponse的字节输出流
(2)创建返回对象
(3)将对象转为字节数组输出给response
(4)刷新缓冲区,关闭流
后续的过滤器只要涉及到返回数据给前端,都会使用该方法。
5、登陆失败过滤器

package com.komorebi.security;

import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        response.setContentType("application/json;charset=UTF-8");
        //security的返回值是重定向,对于前后端分离的项目需要返回json数据,所以使用流的形式
        //因为返回类型是void,并且返回值是json类型数据,所以要使用到流
        ServletOutputStream outputStream = response.getOutputStream();

        Result result = Result.fail(exception.getMessage());

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

6、登出成功过滤器
该处理器会返还给前端一个空的jwt,即前端下次请求时jwt为空,代表未登录。如果是将jwt存在redis中,还要清除缓存。

package com.komorebi.security;

import cn.hutool.json.JSONUtil;
import com.komorebi.common.Const;
import com.komorebi.common.Result;
import com.komorebi.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
    @Autowired
    JwtUtils jwtUtils;
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("退出成功处理器");
        //如果认证的身份凭证不为空,需要手动退出
        if(authentication != null){
            new SecurityContextLogoutHandler().logout(request,response,authentication);
        }
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();
        //等处成功将jwt设置为空,因为jwt是无状态的只能在过期后消失
        response.setHeader(jwtUtils.getHeader(),"");
        Result result = Result.success("登出成功");

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

7、权限不足过滤器
该过滤器是用于处理权限不足时的情况。

package com.komorebi.security;

import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
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, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();
        //权限不足
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        Result result = Result.fail(accessDeniedException.getMessage());

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

8、未认证过滤器
该过滤器是用于处理用户未登录的情况。

package com.komorebi.security;

import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
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, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();

        //未认证
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        Result result = Result.fail("请先登录");

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

9、JWT工具类

package com.komorebi.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix = "komorebi.jwt")
//通过配置文件赋值expire、secret
public class JwtUtils {
    //两个是参数
    private long expire;//保留天数
    private String secret;//密钥
    private String header;//{header:jwt}传给前端

    //生成jwt
    public String generateToken(String username){
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() * expire);
        return Jwts.builder()
                .setHeaderParam("typ","JWT")
                .setSubject(username)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS256,secret)
                .compact();
    }

    //解析jwt
    public Claims getClaimByToken(String jwt){
        try{
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(jwt)
                    .getBody();
        }catch (Exception e){
            return null;
        }
    }

    //jwt是否过期
    public boolean isTokenExpired(Claims claims){
        //如果过期时间在当前时间之前,就代表过期
        return claims.getExpiration().before(new Date());
    }
}

expire、secret、header三个变量存放在application.yml文件中
在这里插入图片描述
通过该注解@ConfigurationProperties(prefix = “komorebi.jwt”)实现通过配置文件赋值expire、secret、header。
10、SecurityConfig
该配置类会对前面定义的所有过滤器进行配置

package com.komorebi.config;

import com.komorebi.security.*;
import org.springframework.beans.factory.annotation.Autowired;
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.builders.AuthenticationManagerBuilder;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginSuccessHandler loginSuccessHandler;
    @Autowired
    LoginFailureHandler loginFailureHandler;
    @Autowired
    CaptchaFilter captchaFilter;
    @Autowired
    JwtAccessDeniedHandler jwtAccessDeniedHandler;
    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Autowired
    UserDetailsService userDetailService;
    @Autowired
    JwtLogoutSuccessHandler  jwtLogoutSuccessHandler;
    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JwtAuthenticationFilter(authenticationManager());
    }

    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    //Java数组初始化用的是{花括号}
    //URL白名单,访问时不需要拦截
    private static final String [] URL_WHITELIST = {
            "/login",
            "/logout",
            "/captcha",
            "/favicon.ico",
    };

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //设置userDetail和加密方法

        auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()

                //登陆配置
                .formLogin()
                .successHandler(loginSuccessHandler)//登陆成功处理器
                .failureHandler(loginFailureHandler)//登录失败处理器

                //退出
                .and()
                .logout()
                .logoutSuccessHandler(jwtLogoutSuccessHandler)

                //设置不生成session策略
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                //配置拦截规则
                .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll()//所有人都可以访问
                .anyRequest().authenticated()//需要登陆,即需要认证

                //异常处理器
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)//没有认证
                .accessDeniedHandler(jwtAccessDeniedHandler)//没有权限

                //配置自定义的过滤器=》前置过滤器
                .and()
                .addFilterBefore(captchaFilter,UsernamePasswordAuthenticationFilter.class)
                .addFilterAt(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        ;
    }
}

测试

1、当验证码错误时
在这里插入图片描述

2、验证码、密码正确时(没有携带jwt登录)
在这里插入图片描述
此时会返回token
在这里插入图片描述

3、携带jwt请求后端接口

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    SysUserService sysUserService;

    @GetMapping
    @PreAuthorize("hasRole('admin')")
    public Object test(){
        return Result.success(sysUserService.list());
    }
}

/test接口需要用户具备admin角色,此处登录的用户具有该角色,则访问成功。
在这里插入图片描述
4、对于需要某种权限或者角色才能访问的后端接口只需要在节后上面进行设置

(1)需要权限才能访问

//新增
    @PostMapping("/save")
    @PreAuthorize("hasAuthority('sys:role:save')")
    public Result save(@Validated @RequestBody SysRole sysRole){
        //添加时设置create时间
        sysRole.setCreated(LocalDateTime.now());
        sysRole.setStatu(Const.STATUS_ON);
        sysRoleService.save(sysRole);
        return Result.success(sysRole);
    }

(2)需要用于某种角色才可以访问

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    SysUserService sysUserService;

    @GetMapping
    @PreAuthorize("hasRole('admin')")
    public Object test(){
        return Result.success(sysUserService.list());
    }
}

此处登录的用户不具备该角色,所以会走权限不足过滤器
在这里插入图片描述

  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值