Spring Security

hello world

1.导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

访问http://localhost:8080/login.html,或其他页面,都会跳转到login.html
username=user(默认)
password=控制台会打印(默认)
自定义:
spring.security.user.name=bjsxt
spring.security.user.password=bjsxt

2.自定义业务层实现类实现UserDetailsService接口,重写loadUserByUsername方法

//如果通过用户名没有查询到对应的数据,应该抛出UsernameNotFoundException,系统就知道用户名没有查询到。
//记得加@Service注解!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@Service
public class serviceImpl implements UserDetailsService {
    @Autowired
    private AdminMapper adminMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("查询用户中-----------------------");
        Admin admin = adminMapper.selectByName(username);
        if(admin == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        //org.springframework.security.core.userdetails.User别和自己定义的User弄混
        //此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security会根据User中的password和客户端传递过来的password进行比较。如果相同则表示认证通过,如果不相同表示认证失败。
        UserDetails user = new User(admin.getUsername(),admin.getPassword(), AuthorityUtils.NO_AUTHORITIES);
        return user;
    }
}

3.配置密码解析器
Spring Security要求容器中必须有PasswordEncoder实例。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象
推荐使用的是BCrypt算法的BCryptPasswordEncoder。

记得在类上加@Configuration
    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

4.编写持久层和控制器层

要么加@Mapper要么在启动类上加@MapperScan(“包名”)

public interface AdminMapper {
    @Select("select * from user where username = #{name}")
    public Admin selectByName(@Param("name")String username);
}

controller

@RestController
public class UserController {
    @RequestMapping("/index")
    public String func(){
        return "hello world";
    }
}

访问localhost:8080/index就会跳转到localhost:8080/login输入默认密码即可访问index浏览器打印hello world

Spring Security配置类

@SpringBootConfiguration
public class Myconfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        //登录及表单提交配置
        httpSecurity.formLogin()
                .usernameParameter("user")//默认username,就是表单中input中的name属性值
                .passwordParameter("pwd")//默认password,就是表单中input中password的name值
                .loginPage("/MyLogin")//表单页面
                .loginProcessingUrl("/abc")//(默认/login)表单提价对应控制器执行,即form表单active的地址
                .defaultSuccessUrl("/successForwardUrl")//重定向指定登录成功后的跳转页面get
        //      .successForwardUrl("/success")//转发指定登录成功后的跳转页面//只能用post
             ;
        //拦截url
        httpSecurity.authorizeRequests()
                .antMatchers("/MyLogin").permitAll()指定放行的路径(例如登录和  登出跳转  路径)
                .anyRequest().authenticated();//拦截所有路径
        //关闭csrf防护
        httpSecurity.csrf().disable();
        return httpSecurity.build();
    }
    //密码解析器
    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

自定义登录成功处理器:
编写继承AuthenticationSuccessHandler的实现类

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //Principal 主体,存放了登录用户的信息
        User user = (User)authentication.getPrincipal();
        System.out.println(user.getUsername());
        System.out.println(user.getPassword());//密码输出为null
        System.out.println(user.getAuthorities());
        //重定向到百度。这只是一个示例,具体需要看项目业务需求
        httpServletResponse.sendRedirect("http://www.baidu.com");
    }
}

1.此时可以修改配置项,及修改成功跳转的页面:

// 表单认证
http.formLogin()
        .loginProcessingUrl("/login")   //当发现/login时认为是登录,需要执行UserDetailsServiceImpl
        .successHandler(new MyAuthenticationSuccessHandler())
        //.successForwardUrl("/toMain")   //此处是post请求
        .failureForwardUrl("/fail")     //登录失败跳转地址
        .loginPage("/login.html");

2.登录失败跳转

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
 操作失败,请重新登录
</body>
</html>

  // 表单认证
  http.formLogin()
    .loginProcessingUrl("/abc")   //当发现/login时认为是登录,需要执行           UserDetailsServiceImpl
    .successForwardUrl("/toMain")   //此处是post请求
    .failureForwardUrl("/fail")     //登录失败跳转地址
    .loginPage("/login");

添加控制器方法
​ 在控制器类中添加控制器方法,方法映射路径/fail。此处要注意:由于是POST请求访问/fail。所以如果返回值直接转发到fail.html中,及时有效果,控制台也会报警告,提示fail.html不支持POST访问方式。

@PostMapping("/fail")
public String fail(){
    return "redirect:/fail.html";
}

3.自定义登录失败处理器

public class MyForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, 
                                        AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.sendRedirect("/fail.html");
    }
}
//在配置类中
// 表单认证
http.formLogin()
        .loginProcessingUrl("/login")   //当发现/login时认为是登录,需要执行UserDetailsServiceImpl
        .successHandler(new MyAuthenticationSuccessHandler())
       //.successForwardUrl("/toMain")   //此处是post请求
        .failureHandler(new MyForwardAuthenticationFailureHandler())
      //.failureForwardUrl("/fail")     //登录失败跳转地址
        .loginPage("/login.html");

Remember Me

1.导入

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

配置数据源
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

2.在客户端登录页面中添加remember-me的复选框,只要用户勾选了复选框下次就不需要进行登录了。

<form action = "/login" method="post">
    用户名:<input type="text" name="username"/><br/>
    密码:<input type="text" name="password"/><br/>
    <input type="checkbox" name="remember-me" value="true"/> <br/>
    <input type="submit" value="登录"/>
</form>

3.配置PersistentTokenRepository
//第一次启动会创建一张persistent_logins的表存储登录信息,和Cookie对应

    @Bean
    public PersistentTokenRepository getPersistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
        jdbcTokenRepositoryImpl.setDataSource(dataSource);
        // 自动建表,第一次启动时需要,第二次启动时注释掉
        // jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
        return jdbcTokenRepositoryImpl;
    }

4.修改SecurityConfig

// remember Me
http.rememberMe()
        .userDetailsService(userDetailsService) // 登录逻辑交给哪个对象
        .tokenRepository(repository);   // 持久层对象

此时基础已经配置完了访问页面,关闭浏览器,在次访问可以不用登录,Cookie的默认有效时间为两周

注意:
Cookie的功能是保证下次不用登录,而不是保证本次
Session是保证本次登录的.

5.整体配置类及其他配置:

@SpringBootConfiguration
public class Myconfig {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AdminServiceImpl adminService;
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity,PersistentTokenRepository pt) throws Exception {
        //登录及表单提交配置
        httpSecurity.formLogin()
                .usernameParameter("user")//默认username,就是表单中input中的name属性值
                .passwordParameter("pwd")//默认password,就是表单中input中password的name值
                .loginPage("/login")//表单页面
                .loginProcessingUrl("/login")//(默认/login)表单提价对应控制器执行,即form表单active的地址
                .defaultSuccessUrl("/success")//重定向指定登录成功后的跳转页面get
        //      .successForwardUrl("/success")//转发指定登录成功后的跳转页面//只能用post
             ;

        httpSecurity.rememberMe()
                .rememberMeCookieName("rm")//Cookie名称
                .tokenValiditySeconds(100)//Cookie存活时间
                .rememberMeParameter("rm")//和form表单中记住我input中的name的值对应,两者要一致
                //.rememberMeCookieDomain("/")//设置Cookie访问的域值,没有就不要写
                        .tokenRepository(pt)//持久层对象
                        .userDetailsService(adminService);//登录逻辑交给哪个对象

        //拦截url
        httpSecurity.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated();
        //关闭csrf防护
        httpSecurity.csrf().disable();
        return httpSecurity.build();
    }
    @Bean
    public PersistentTokenRepository getTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
    //配置密码解析器
    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

退出登录

用户只需要向Spring Security项目中发送/logout退出请求即可。

登录成功后在success页面添加

<a href="/lgo">退出</a>
默认是/logout

修改SecurityConfig

//配置登出页面
        httpSecurity.logout()
                        .logoutUrl("/lgo")//(默认是/logout)和登出标签的href中的值相等
                        .logoutSuccessUrl("/main")//退出成功后跳往的页面
                        //.addLogoutHandler()//自定义登出逻辑,参数为实现LogoutHandler接口并重写方法的实例
                        .invalidateHttpSession(true);//设置注销session 默认是true
//拦截url
        httpSecurity.authorizeRequests()
                .antMatchers("/login","/main").permitAll()//记得在这里添加非拦截url:/main
                .anyRequest().authenticated();

注意:
退出时: 默认会销毁session和Cookie(remember-me)

授权

授权组成两部分:路径匹配 + 执行授权操作
第一部分:路径匹配

//拦截url
        httpSecurity.authorizeRequests()
                .antMatchers("/login","/main").permitAll()//不认证直接放心的url
                .anyRequest().authenticated();//所有类容进行认证

antMatchers(“”):支持ant表达式 ?:匹配一个字符 :匹配0个或多个字符 :0个或多个目录 HttpMethod.GET:可以定义请求方式
/a? 例如:/aa /ab /ac(都可以) /abc(不可以)
/a
例如:/a /ab /acd(都可以) /a/b(不可以)
/a/
例如:/a /a/c /a/c/d(都可以)
anyRequest() :所有的地址
regexMatchers(“.+[.]do”):支持正则表达式 里面直接定义正则表达式即可 HttpMethod.GET:可以定义请求方式

第二部分:执行授权操作:内置权限,自定义权限
内置的权限
permitAll():表示所匹配的URL任何人都允许访问
authenticated():表示所匹配的URL都需要被认证才能访问 :表单提交认证+rememberMe()
anonymous():表示可以匿名访问匹配的URL 必须不用认证 如果已经认证 无法访问 :登录页面需要指定当前权限
denyAll():表示所匹配的URL都不允许被访问。
rememberMe():被“remember me”的用户允许访问
fullyAuthenticated():如果用户不是被remember me的,才可以访问。
自定义权限: 方法 + 注解
方法:
需求:有两个角色 分别为: 管理员和普通用户
有多个权限 分别为: flower:save flower:edit flower:findAll flower:remove
管理员具有4个权限 普通用户 只有 添加 和查询权限
使用方法自定义权限:
1.配置实现UserDetailsService的实现类重写的loadUserByUsername方法
将该用户所有对应的角色/权限告诉到SpringSecurity

@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
    // 如果觉得编译错误闹心,在Mapper上添加@Component即可。
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("此方法被执行");
        User user = userMapper.selectByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 查询用户对应的权限
        List<String> listPermission = userMapper.selectPermissionByUsername(username);
        //将数据表中查出来的List类型数据转换为SimpleGrantedAuthority类型即可
        List<SimpleGrantedAuthority> listAuthority = new ArrayList<SimpleGrantedAuthority>();
        for(String permisssion : listPermission){
            listAuthority.add(new SimpleGrantedAuthority(permisssion));
        }
        return new org.springframework.security.core.userdetails.User(username,user.getPassword(),listAuthority);
    }
}

2.判定
根据访问用户权限判定:
hasAuthority():判断当前用户是否具有指定权限
hasAnyAuthority():判断当前用户是否具有指定权限之一

根据访问用户角色判定:
hasRole():判断当前用户是否具有指定角色
hasAnyRole():判断当前用户是否具有指定角色之一

其他:
hasIpAddress():判断当前访问地址是否是指定的IP
access(): 自定义授权整个流程

例:

         //对URL地址进行拦截
        http.authorizeRequests()
                .antMatchers("/MyLogin").permitAll() //指定的路径全部放行
                /*.antMatchers("/save").hasAuthority("flower:save")
                .antMatchers("/findAll").hasAuthority("flower:findAll")
                .antMatchers("/edit").hasAuthority("flower:edit")
                .antMatchers("/remove").hasAuthority("flower:remove")*/
                /*.antMatchers("/save","/findAll").hasAnyRole("普通用户","管理员")
                .antMatchers("/edit","/remove").hasRole("管理员")*/
                //.antMatchers("/save").hasIpAddress("127.0.0.1")
                //request  authentication  这两个对象已经创建 直接传递即可
                //.antMatchers("/save").access("@myServiceImpl.isMenuPermission(request,authentication)")//
                .anyRequest().authenticated();//所有的请求都需要认证

==================================================================================================================================================================================================================
以上的登录用户权限判断实际上底层实现都是调用access(表达式)
3.可以自定义权限校验: 如某个访问路径要多个权限才能访问,但是security给的都是过关系,所以需要自定义
接口名称任意、方法返回值固定、方法名称任意、参数固定

public interface MyService {
    boolean isMenuPermission(HttpServletRequest request, Authentication authentication);
}

业务层实现

@Service
public class MyServiceImpl  implements MyService {
    @Override
    public boolean isMenuPermission(HttpServletRequest request, Authentication authentication) {
        Object obj = authentication.getPrincipal();
        if(obj instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) obj;
            //获取当前认证成功用户的权限,就是之前数据表中查出来的权限
            Collection<? extends GrantedAuthority> list = userDetails.getAuthorities();
           //自己进行判断即可  
            boolean b1 = list.contains(new SimpleGrantedAuthority("savc"));
            boolean b2 = list.contains(new SimpleGrantedAuthority("数据库权限"));
            return b1&&b2;
        }
            return false;
    }
}

配置: SecurityConfig

// request,authentication  这两个参数不需要改自动进行传递  @bean对象名称+方法名称
.antMatchers("/bjsxt").access("@myServiceImpl.isMenuPermission(request,authentication)")

注解:
判断角色或权限
@PreAuthorize/@PostAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true)
区别:@PreAuthorize如果没有权限 就不会进入控制单元方法执行
@PostAuthorize 如果没有权限 也会进入控制单元方法执行

判断角色
@EnableGlobalMethodSecurity(securedEnabled = true)
@Secured:是专门用于判断是否具有角色的。能写在方法或类上。@Secured参数要以ROLE_开头。

在控制器方法上添加@PreAuthorize,参数可以是任何access()支持的表达式。
如果用户没有管理员角色,不会打印preAuthorize

@RequestMapping("/preAuthorize")
@ResponseBody
@PreAuthorize("hasRole('ROLE_管理员')")
public String preAuthorize(){
    System.out.println("preAuthorize");
    return "preAuthorize";
}

如果用户没有管理员角色也会打印PostAuthorize

@RequestMapping("/postAuthorize")
@ResponseBody
@PostAuthorize("hasRole('ROLE_管理员')")
public String postAuthorize(){
    System.out.println("PostAuthorize");
    return "preAuthorize";
}

无权限访问跳转页面

1.创建控制层实现AccessDeniedHandler接口

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
        out.flush();
     }
}

2.修改配置类

@Autowired
private AccessDeniedHandler accessDeniedHandler;


//异常处理
http.exceptionHandling()
        .accessDeniedHandler(accessDeniedHandler);

Thymeleaf中Spring Security的使用

导入依赖

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在html页面中引入thymeleaf命名空间和security命名空间

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

根据源码得出下面属性:

  1. name:登录账号名称
  2. principal:登录主体,在自定义登录逻辑中是UserDetails
  3. credentials:凭证
  4. authorities:权限和角色
  5. details:实际上是WebAuthenticationDetails的实例。可以获取remoteAddress(客户端ip)和sessionId(当前sessionId)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
success
<a href="/save" sec:authorize="hasAuthority('save')">新增</a>
<a href="/edit" sec:authorize="hasAuthority('edit')">修改</a>
<a href="/find" sec:authorize="hasAuthority('find')">查看</a>
</body>
</html>

获取信息

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登录账号:<span sec:authentication="name">123</span><br/>
    登录账号:<span sec:authentication="principal.username">456</span><br/>
    凭证:<span sec:authentication="credentials">456</span><br/>
    权限和角色:<span sec:authentication="authorities">456</span><br/>
    客户端地址:<span sec:authentication="details.remoteAddress">456</span><br/>
    sessionId:<span sec:authentication="details.sessionId">456</span><br/>

通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增</button>
<button sec:authorize="hasAuthority('/delete')">删除</button>
<button sec:authorize="hasAuthority('/update')">修改</button>
<button sec:authorize="hasAuthority('/select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增</button>
<button sec:authorize="hasRole('abc')">删除</button>
<button sec:authorize="hasRole('abc')">修改</button>
<button sec:authorize="hasRole('abc')">查看</button>
</body>
</html>

编写控制器进行转发

@RestController
public class EController {
    @PreAuthorize("hasAuthority('save')")
    @RequestMapping("/save")
    public String save(){
        return "save";
    }
    @PreAuthorize("hasAuthority('edit')")
    @RequestMapping("/edit")
    public String edit(){
        return "edit";
    }
    @PreAuthorize("hasAuthority('find')")
    @RequestMapping("/find")
    public String find(){
        return "find";
    }
}

Spring Security中CSRF

默认开启
​ CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack” 或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip地址,端口中任何一个不相同就是跨域请求。
​ 客户端与服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
CSRF攻击攻击原理及过程如下:

  1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
  2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
  3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
  4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
  5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

Spring Security中CSRF
​ 从Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
6. 当服务器加载登录页面。(loginPage中的值,默认/login),先生成csrf对象,并放入作用域中,key为_csrf。之后会对${_csrf.token}进行替换,替换成服务器生成的token字符串。
7. 用户在提交登录表单时,会携带csrf的token。如果客户端的token和服务器的token匹配说明是自己的客户端,否则无法继续执行。
8. 用户退出的时候,必须发起POST请求,且和登录时一样,携带csrf的令牌。
在表单中添加下面一行即可:

<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>

在配置类中注释掉CSRF防护失效

// 关闭csrf防护
// http.csrf().disable();
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值