SpringBoot + SpringSecurity+jwt 实现验证 & 过程中直接添加Authentication对象

1、

如何骗过Spring Security——直接添加Authentication对象

1.需求

在最近的项目中,出现了这样的需求。我需要在后台使用spring security,但是android端显然不能使用像web端登录那样的处理方式,所以如何"骗过"spring security直接在它的认证流程中插入我自己的对象,这成为了我急切的问题。

2.Spring Security的核心组件

SecurityContextHolder

SecurityContextHolder用于获取当前用户的信息,SecurityContextHolder默认使用ThreadLocal来存储认证信息,这是一种与线程绑定的策略。只要在同一个线程中进行,即使不在各个方法之间以参数的形式传递,各个方法也能通过SecurityContextHolder工具获取到安全上下文。

获取当前的用户信息

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
  • Authentication

源码

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();//密码信息

    Object getDetails();//细节信息,记录了ip地址和sessionId的值

    Object getPrincipal();//大部分情况下返回UserDetais的实现类

    boolean isAuthenticated();//是否认证

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

AuthenticationManager

正如他的名字一样,认证的管理者。他的作用不是直接亲自上场进行认证,而是委派其他有能力的AuthenticationProvider(实际上是实现此接口的实现类进行认证)进行认证,而AuthenticationProvider又由ProviderManager提供。在实际的需求中,我们登陆的方式不同,认证的方式就会不同,所以设计成这样后,如果我们需要自己定义登录方式,则只需要提供相应的AuthenticationProvider就可以了。

DaoAuthenticationProvider

提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource。

UserDetails与UserDetailsService

UserDetais源码

public interface UserDetails extends Serializable {
   Collection<? extends GrantedAuthority> getAuthorities();
   String getPassword();
   String getUsername();
   boolean isAccountNonExpired();
   boolean isAccountNonLocked();
   boolean isCredentialsNonExpired();
   boolean isEnabled();
}

UserDetails是与UsernamePasswordAuthenticationToken比对的对象,由UserDetailsService获得。UserDetailsService是我们组装UserDetails的地方,我们也可以实现这个接口来完成自定义的组装。

3.Spring Security身份认证流程

  1. 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. AuthenticationManager 身份管理器负责验证这个Authentication
  3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
  4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

4.将自己需要的Authentication对象放入Spring Security中

@RequestMapping("/doLogin")
    public int doLogin(@RequestParam("userName") String userName,@RequestParam("password") String password,HttpServletRequest request){
        User user = simpleUserService.getUserByName(userName);
		
        //如果用户不存在则抛出异常
        if(user==null){
            throw new UsernameNotFoundException("没有当前用户");
        }

        else {
            //如果用户存在且用户的密码相同,则在SecurityContextHolder.getContext().setAuthentication()放入authentication
            if(user.getPassword().equals(password)){
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password, AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole()));
                authRequest.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authRequest);
                return 1;
            }
        }
        

        return 0;
    }

2、 

SpringBoot + SpringSecurity+jwt 实现验证

记录一下使用springSecurity实现jwt的授权方法,这方法可以实现权限的基本认证。当然这个案例还有许多的问题,不过还是先记录一下。其他功能以后在补充。

  • 建议工程创建流程
    • 创建 JwtTokenUtils
    • 创建 jwtAccessDeniedHandler 和 JwtAuthenticationEntryPoint
    • 创建 UserDetailsServiceImpl
    • 创建 JwtAuthenticationFilter
    • 配置 Security信息
    • 启动类的信息

环境

  • springBoot 2.3.3
  • springSecurity 5.0
  • jjwt 0.91

pox.xml 文件主要信息

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 目录结构信息

请忽略文件命名

iShot2020-09-17下午09.41.02

jwtAccessDeniedHandler 和 JwtAuthenticationEntryPoint

这两个类的作用是用户访问没有授权资源和携带错误token的错误返回处理信息类,要使用这两个类只需要在security的配置文件中配置一下就可以只用了

/**
 * @author Bxsheng
 * @blogAddress www.kdream.cn
 * @createTIme 2020/9/17
 * since JDK 1.8
 * 当用户在没有授权的时候,返回的指定信息
 */
@Component
public class jwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        System.out.println("用户访问没有授权资源");
        System.out.println(e.getMessage());
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e==null?"用户访问没有授权资源":e.getMessage());

    }
}

/**
 * @author Bxsheng
 * @blogAddress www.kdream.cn
 * @createTIme 2020/9/17
 * since JDK 1.8
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        System.out.println("用户访问资源没有携带正确的token");
        System.out.println(e.getMessage());
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e==null?"用户访问资源没有携带正确的token":e.getMessage());

    }
}

UserDetailsServiceImpl 登录信息验证

该类直接继承UserDetailsService 进行登录信息验证,在输入账户密码进行登录的时候,会进入这个类进行验证信息。

当然我这里是直接使用了写死的密码,正常应该从数据库中获取用户的信息和权限信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        //直接写死数据信息,可以在这里获取数据库的信息并进行验证

        UserDetails userDetails  = User.withUsername(s).password(new BCryptPasswordEncoder().encode("123456"))
                .authorities("bxsheng").build();
        return userDetails;
    }
}

JwtTokenUtils jwt包装类

该类直接使用 slyh 的 [SpringBoot+JWT实现登录权限控制(代码))](( https://blog.csdn.net/sfh2018/article/details/104772986))的文章里面的类。

package cn.kdream.securityjwt.utlis;

import io.jsonwebtoken.*;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Bxsheng
 * @blogAddress www.kdream.cn
 * @createTIme 2020/9/16
 * since JDK 1.8
 */
public class JwtTokenUtils {
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String SECRET = "jwtsecret";
    public static final String ISS = "echisan";

    private static final Long EXPIRATION = 60 * 60 * 3L; //过期时间3小时

    private static final String ROLE = "role";

    //创建token
    public static String createToken(String username, String role, boolean isRememberMe){
        Map map = new HashMap();
        map.put(ROLE, role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setClaims(map)
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .compact();
    }

    //从token中获取用户名(此处的token是指去掉前缀之后的)
    public static String getUserName(String token){
        String username;
        try {
            username = getTokenBody(token).getSubject();
        } catch (    Exception e){
            username = null;
        }
        return username;
    }

    public static String getUserRole(String token){
        return (String) getTokenBody(token).get(ROLE);
    }

    private static Claims getTokenBody(String token){
        Claims claims = null;
        try{
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch(ExpiredJwtException e){
            e.printStackTrace();
        } catch(UnsupportedJwtException e){
            e.printStackTrace();
        } catch(MalformedJwtException e){
            e.printStackTrace();
        } catch(SignatureException e){
            e.printStackTrace();
        } catch(IllegalArgumentException e){
            e.printStackTrace();
        }
        return claims;
    }

    //是否已过期
    public static boolean isExpiration(String token){
        try{
            return getTokenBody(token).getExpiration().before(new Date());
        } catch(Exception e){
            System.out.println(e.getMessage());
        }
        return true;
    }
}

JwtAuthenticationFilter 自定义验证jwt

该类直接使用 slyh 的 [SpringBoot+JWT实现登录权限控制(代码))](( https://blog.csdn.net/sfh2018/article/details/104772986))的文章里面的类。

这个类主要的作用是验证jwt信息 ,主要携带了token请求过来,解析jwt并设置在security的上下文中。这样做的其中一个目的是你获得了token中携带的权限信息,并保存在上下文中。你就可以对用户进行权限认证了

/**
 * @author Bxsheng
 * @blogAddress www.kdream.cn
 * @createTIme 2020/9/16
 * since JDK 1.8
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        //如果请求头中没有Authorization信息则直接放行了
        if(tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)){
            chain.doFilter(request, response);
            return;
        }
        //如果请求头中有token,则进行解析,并且设置认证信息
        if(!JwtTokenUtils.isExpiration(tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX,""))){
            //设置上下文
            UsernamePasswordAuthenticationToken authentication = getAuthentication(tokenHeader);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        super.doFilterInternal(request, response, chain);
    }

    //获取用户信息
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader){
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        String username = JwtTokenUtils.getUserName(token);
        // 获得权限 添加到权限上去
        String role = JwtTokenUtils.getUserRole(token);
        List<GrantedAuthority> roles = new ArrayList<GrantedAuthority>();
        roles.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return role;
            }
        });
        if(username != null){
            return new UsernamePasswordAuthenticationToken(username, null,roles);
        }
        return null;
    }

}

security的配置信息

@EnableGlobalMethodSecurity(prePostEnabled = true) 开启prePostEnabled注解方式授权

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


    @Autowired
    private jwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       http.cors().and().csrf().disable().authorizeRequests()
               .antMatchers(HttpMethod.OPTIONS,"/**")
               .permitAll()
               .antMatchers("/").permitAll()
               //login 不拦截
               .antMatchers("/login").permitAll()
               .anyRequest().authenticated()
               //授权
               .and()
               // 禁用session
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
       // 使用自己定义的拦截机制,拦截jwt
        http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
        //授权错误信息处理
                .exceptionHandling()
                //用户访问资源没有携带正确的token
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                //用户访问没有授权资源
                .accessDeniedHandler(jwtAccessDeniedHandler);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        //使用的密码比较方式
        return  new BCryptPasswordEncoder();
    }

}

启动类

我在启动类中配置了三个方法,一个是用来进行登录信息的,另外两个设置了需要权限访问

@SpringBootApplication
@RestController
public class SecurityJwtApplication {

    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    public SecurityJwtApplication(AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }

    public static void main(String[] args) {
        SpringApplication.run(SecurityJwtApplication.class, args);
    }



    @GetMapping("/")
    public String index(){
        return "security jwt";
    }

    @PostMapping("/login")
    public String login(@RequestParam String u,@RequestParam String p){
        // 登陆验证
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(u, p);
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //创建jwt信息
        String token1 = JwtTokenUtils.createToken(u,"bxsheng", true);
        return token1;
    }

    @GetMapping("/role")
    @PreAuthorize("hasAnyAuthority('bxsheng')")
    public String roleInfo(){
        return "需要获得bxsheng权限,才可以访问";
    }

    @GetMapping("/roles")
    @PreAuthorize("hasAnyAuthority('kdream')")
    public String rolekdream(){
        return "需要获得kdream权限,才可以访问";
    }
}

效果

  • 直接访问需要授权的用户信息

    直接没有使用token直接访问只要授权的资源信息,会进入JwtAuthenticationEntryPoint 类

image-20200917223212552

  • 获取token

    访问在启动类中的login方法,获取token信息

    因为我使用了固定的密码,所以在使用错误的密码访问的时候,可以在springboot的全局异常处理中捕获到异常信息

    /**
     * @author Bxsheng
     * @blogAddress www.kdream.cn
     * @createTIme 2020/9/17
     * since JDK 1.8
     */
    @RestControllerAdvice
    public class Error {
        @ExceptionHandler(BadCredentialsException.class)
        public void badCredentialsException(BadCredentialsException e){
            System.out.println(e.getMessage());//用户名或密码错误
    
           // throw new  BadCredentialsException(e.getMessage());
        }
    }
    
    

image-20200917223909142

  • 正确的获取token,并进行受保护的资源访问

    里面有写死的bxsheng权限信息,所以正常是可以获取bxsheng标识的资源信息的。

image-20200917224108616

  • 成功获取信息

image-20200917224234898

  • 尝试获取无权限资源信息

    使用token直接访问无权限资源信息,会进入jwtAccessDeniedHandler 类

image-20200917224417452

至此应该可以说算完成简单的权限管理了。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个基于Spring框架的快速开发Web应用程序的框架,Spring SecuritySpring框架的安全模块,JWT是一种用于身份验证的开放标准。Vue是一种流行的JavaScript框架,用于构建用户界面。 结合这些技术,可以实现前后端分离的登录、权限管理和Token管理。具体步骤如下: 1. 在Spring Boot项目导入Spring SecurityJWT的Maven依赖。 2. 配置Spring Security,包括创建用户、角色和权限等。 3. 创建一个JWT工具类,用于生成和解析Token。 4. 创建一个登录接口,接收用户名和密码,验证用户信息,生成Token并返回给前端。 5. 创建一个Token验证过滤器,用于验证请求的Token是否有效。 6. 在Vue项目使用Axios发送登录请求,获取Token并保存到本地存储。 7. 在Vue项目使用Vue Router和VueX进行路由和状态管理。 8. 创建一个路由守卫,用于验证用户是否登录和是否有权限访问某些页面。 9. 在需要进行身份验证的请求添加Token。 下面是一个简单的示例代码,仅供参考: 后端代码: ```java // 配置Spring Security @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } } // 创建一个JWT工具类 public class JwtUtils { private static final String SECRET_KEY = "mySecretKey"; private static final long EXPIRATION_TIME = 86400000; // 24 hours public static String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject(); } public static boolean validateToken(String token, UserDetails userDetails) { String username = getUsernameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } private static boolean isTokenExpired(String token) { Date expiration = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getExpiration(); return expiration.before(new Date()); } } // 创建一个登录接口 @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername()); String token = JwtUtils.generateToken(userDetails); return ResponseEntity.ok(new JwtAuthenticationResponse(token)); } } // 创建一个Token验证过滤器 public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); String username = jwtUtils.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtils.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } } // 创建一个自定义的AuthenticationEntryPoint @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } } // 创建一个自定义的UserDetailsService @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found with username: " + username); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); } } // 创建一个实体类User和一个接口UserRepository @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username") private String username; @Column(name = "password") private String password; // getters and setters } @Repository public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); } ``` 前端代码: ```javascript // 在Vue项目使用Axios发送登录请求 axios.post('/api/auth/login', { username: 'admin', password: 'password' }).then(response => { localStorage.setItem('token', response.data.token); }); // 在需要进行身份验证的请求添加Token axios.get('/api/users', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }); // 创建一个路由守卫 router.beforeEach((to, from, next) => { const publicPages = ['/login', '/register']; const authRequired = !publicPages.includes(to.path); const loggedIn = localStorage.getItem('token'); if (authRequired && !loggedIn) { return next('/login'); } next(); }); // 使用VueX进行状态管理 const store = new Vuex.Store({ state: { isLoggedIn: !!localStorage.getItem('token') }, mutations: { login(state) { state.isLoggedIn = true; }, logout(state) { state.isLoggedIn = false; } }, actions: { login({ commit }) { return new Promise(resolve => { axios.post('/api/auth/login', { username: 'admin', password: 'password' }).then(response => { localStorage.setItem('token', response.data.token); commit('login'); resolve(); }); }); }, logout({ commit }) { return new Promise(resolve => { localStorage.removeItem('token'); commit('logout'); resolve(); }); } } }); ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值