Shiro学习笔记(用于记录个人学习)

注:
本篇为 楠哥教你学Java【硬核干货】2小时学会Spring Boot整合Shiro 的学习笔记。
本人水平有限,仅供参考,有所意见和建议欢迎指出。

Shiro

1、什么是Shiro

官网:http://shiro.apache.org/

是一款主流的Java安全框架,不依赖任何容器,可以在Java SE和Java EE项目中,它的主要作用是对访问系统的用户进行身份认证、授权、会话管理、加密等操作。

Shiro就是用来解决安全管理的系统化框架。

2、Shiro核心组件

 用户、角色、权限关系

 会给角色赋予权限,给用户赋予角色
(1)Shiro核心组件
  1. UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息来创建令牌Token。
  2. SecurityManager,Shiro 的核心部分,负责安全认证和授权。
  3. Suject,Shiro 的一个抽象概念,包含了用户信息。
  4. Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
  5. AuthenticationInfo,用户的角色信息集合,认证时使用。
  6. AuthorzationInfo,角色的权限信息集合,授权时使用。
  7. DefaultWebSecurityManager,安全管理器,开发者自定义的Realm需要注入到 DefaultWebSecurityManager 进行管理才能生效。
  8. ShiroFilterFactoryBean,过滤器工厂,Shiro的基本运行机制时开发者定制规则,Shiro去执行,具体的执行操作就是由ShiroFilterFactoryBean创建的一个个Filter对象来完成。
(2)Shiro的运行机制

Shiro的运行机制

3、SpingBoot整合Shiro

(1)新建SpringBoot项目

在这里插入图片描述
在这里插入图片描述

(2)添加Shiro依赖
1.查找相应依赖版本

可以百度搜索 shiro-spring maven 查找相应依赖版本

2.添加Shiro依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.7.1</version>
</dependency>
3.验证工程

可以在终端输入命令mvn clean install,构建工程验证;

也可以通过Maven中的插件进行验证。

注:遇到mvn不是内部或外部命令,也不是可运行的程序或批处理文件的解决方法

​ 首先配置环境变量:在系统变量中添加了maven的环境变量后,cmd可运行 mvn -v查看版本,但IDEA仍提示mvn不是内部或外部命令,也不是可运行的程序或批处理文件

​ 则设置IDEA管理员运行:在IDEA快捷方式的属性中设置为以管理员身份运行。

4.Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot-shiro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-shiro</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

(3)新建mysql数据库

​ 步骤略

(4)配置application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/shiro
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
(5)新建前置条件
1.结构

在这里插入图片描述

2.entity
package com.example.entity;

import lombok.Data;

@Data
public class Account {
    private Integer id;
    private String username;
    private String password;
    private String perms;
    private String role;
}
3.mapper
package com.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.Account;
import org.springframework.stereotype.Repository;

/*
可加可不加
不加时,
	@Autowired
    private AccountMapper accountMapper;
中的accountMapper会标红,但不影响正常使用

@Repository用于将类识别为Bean,
同时它还能将所标注的类中抛出的数据访问异常封装为 Spring 的数据访问异常类型
*/
@Repository
public interface AccountMapper extends BaseMapper<Account> {

}

测试:

package com.example.mapper;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class AccountMapperTest {
    @Autowired
    private AccountMapper accountMapper;

    @Test
    void test() {
        accountMapper.selectList(null).forEach(System.out::println);
    }
}
4.service
package com.example.service;

import com.example.entity.Account;
import org.springframework.stereotype.Repository;

//可加可不加,这里理论上应该不加,但若要去红,加上也不影响使用
@Repository 
public interface AccountService {
    public Account findByUsername(String username);
}

5.serviceImpl
package com.example.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.entity.Account;
import com.example.mapper.AccountMapper;
import com.example.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public Account findByUsername(String username) {
        QueryWrapper wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        return accountMapper.selectOne(wrapper);
    }
}

测试:

package com.example.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    void findByUsername() {
        System.out.println(accountService.findByUsername("ls"));
    }
}
(6)自定义Shiro过滤器
1.realm
package com.example.realm;

import com.example.entity.Account;
import com.example.service.AccountService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

public class AccountRealm extends AuthorizingRealm {

    @Autowired
    private AccountService accountService;

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 封装客户输入的用户名和密码
        UsernamePasswordToken token =(UsernamePasswordToken)authenticationToken;
        // 根据用户名进行查询
        Account account = accountService.findByUsername(token.getUsername());
        if (account != null) {
            // 验证密码
            return new SimpleAuthenticationInfo(account, account.getPassword(), getName());
        }
        return null;
    }
}
2.config 配置类
package com.example.config;

import com.example.realm.AccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

    /**
     * 注入ShiroFilterFactoryBean
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        return factoryBean;
    }

    /**
     * 注入DefaultWebSecurityManager
     * @param accountRealm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("accountRealm") AccountRealm accountRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(accountRealm);
        return manager;
    }

    /**
     * 注入Realm,把创建的Realm装到ioc里面
     * @return
     */
    @Bean
    public AccountRealm accountRealm() {
        return new AccountRealm();
    }
}

在这里插入图片描述

@Qualifier("") //表明了哪个实现类才是我们所需要的
3.编写认证和授权规则

认证过滤器:

anon:无需认证。

authc:必须认证。

authcBasic:需要通过HTTPBasic认证。

user:不一定通过认证,只要曾经被Shiro记录即可,比如:记住我。

授权过滤器:

perms:必须拥有某个权限才能访问。

role:必须拥有某个角色才能访问。

port:请求的端口必须时指定值才可以。

rest:请求必须基于RESTful,POST、PUT、GET、DELETE。

ssl:必须是安全的URL请求,协议HTTPS。

示例

创建3个页面:

1、main.html:必须登录才能访问

2、manage.html:当前用户必须用于manage授权才能访问

3、administrator.html:当前用户必须用于administrator角色才能访问

实现:

前端实现:

<link rel="shortcut icon" href="#">
用于防止读取icon文件,加上后不报错,但不加上也不影响

main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
    <h1>main</h1>
</body>
</html>

manage.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
  <h1>manage</h1>
</body>
</html>

administrator.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
  <h1>administrator</h1>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
  <h1>index</h1>
  <a href="/main">main</a> |
  <a href="/manage">manage</a> |
  <a href="/administrator">administrator</a>
</body>
</html>

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
  <form action="/login" method="post">
    <table>
      <tr>
        <td>用户名:</td>
        <td>
          <input type="text" name="username" />
        </td>
      </tr>
      <tr>
        <td>密码:</td>
        <td>
          <input type="password" name="password" />
        </td>
      </tr>
      <tr>
        <td>
          <input type="submit" value="登录" />
        </td>
      </tr>
    </table>
  </form>
</body>
</html>

后端实现:

controller类:

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class AccountController {

    @GetMapping("/{url}")
    public String redirect(@PathVariable("url") String url) {
        return url;
    }
}

配置视图解析器:

在这里插入图片描述

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/shiro
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

ShiroConfig.java

在这里插入图片描述

package com.example.config;

import com.example.realm.AccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        //权限设置
        Map<String,String> map = new HashMap<>();
        map.put("/main","authc");
        map.put("/manage","perms[manage]");
        map.put("/administrator","roles[administrator]");
        factoryBean.setFilterChainDefinitionMap(map);
        //设置登录页面
        factoryBean.setLoginUrl("/login");
        return factoryBean;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("accountRealm") AccountRealm accountRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(accountRealm);
        return manager;
    }

    @Bean
    public AccountRealm accountRealm() {
        return new AccountRealm();
    }
}

取消Shiro:

在这里插入图片描述

实现效果:

在这里插入图片描述

不符合访问条件,跳转至login.html

在这里插入图片描述

取消Shiro

在这里插入图片描述

4.登录

前端代码:

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymelear.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
  <form action="/login" method="post">
    <table>
      <span th:text="${msg}" style="color: red"></span>
      <tr>
        <td>用户名:</td>
        <td>
          <input type="text" name="username" />
        </td>
      </tr>
      <tr>
        <td>密码:</td>
        <td>
          <input type="password" name="password" />
        </td>
      </tr>
      <tr>
        <td>
          <input type="submit" value="登录" />
        </td>
      </tr>
    </table>
  </form>
</body>
</html>

代码实现:

AccountController.java

package com.example.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class AccountController {

    @GetMapping("/{url}")
    public String redirect(@PathVariable("url") String url) {
        return url;
    }

    @PostMapping("/login")
    public String login(String username, String password, Model model) {
        //Suject,Shiro 的一个抽象概念,包含了用户信息
        //取得Subject,注意不要引错包
        Subject subject = SecurityUtils.getSubject();
        //封装用户登录信息
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        //调用,进入到之前自定义的realm内的认证方法
        try {
            //登录成功
            subject.login(token);
            return "index";
        } catch (UnknownAccountException e) { //用户名不存在
            e.printStackTrace();
            model.addAttribute("msg","用户名错误");
            return "login";
        } catch (IncorrectCredentialsException e) { //密码不存在
            e.printStackTrace();
            model.addAttribute("msg","密码错误");
            return "login";
        }
    }
}

运行逻辑(可以自行打断点查看):
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实现结果(登录成功后):

无权限,无角色:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.授权

主要代码:

/**
 * 授权
 *
 * @param principalCollection
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //获取当前登录的用户信息
    Subject subject = SecurityUtils.getSubject();
    Account account = (Account) subject.getPrincipal();

    //设置角色
    Set<String> roles = new HashSet<>();
    roles.add(account.getRole());
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);

    //设置权限
    info.addStringPermission(account.getPerms());
    return info;
}

数据库:

在这里插入图片描述

实现结果:

实现结果(登录成功后):

有权限,无角色:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
有权限,有角色:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

补充:

前端界面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymelear.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
  <h1>index</h1>
  <div th:if="${session.account != null}">
      <span th:text="${session.account.username}+'欢迎回来'"></span>
      <a href="/logout">退出</a>
  </div>
  <a href="/main">main</a> |
  <a href="/manage">manage</a> |
  <a href="/administrator">administrator</a>
</body>
</html>

修改无权限界面:

效果:

在这里插入图片描述

ShiroConfig:

在这里插入图片描述

AccountController:

在这里插入图片描述

欢迎回来和退出

效果:

在这里插入图片描述

AccountController:

XXX欢迎回来:

在这里插入图片描述

退出:

在这里插入图片描述

完整代码:

ShiroConfig:

package com.example.config;

import com.example.realm.AccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        //权限设置
        Map<String,String> map = new HashMap<>();
        map.put("/main","authc");
        map.put("/manage","perms[manage]");
        map.put("/administrator","roles[administrator]");
        factoryBean.setFilterChainDefinitionMap(map);
        //设置登录页面
        factoryBean.setLoginUrl("/login");
        //设置未授权页面
        factoryBean.setUnauthorizedUrl("/unauth");
        return factoryBean;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("accountRealm") AccountRealm accountRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(accountRealm);
        return manager;
    }

    @Bean
    public AccountRealm accountRealm() {
        return new AccountRealm();
    }
}

AccountController:

package com.example.controller;

import com.example.entity.Account;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AccountController {

    @GetMapping("/{url}")
    public String redirect(@PathVariable("url") String url) {
        return url;
    }

    @PostMapping("/login")
    public String login(String username, String password, Model model) {
        //Suject,Shiro 的一个抽象概念,包含了用户信息
        //取得Subject,注意不要引错包
        Subject subject = SecurityUtils.getSubject();
        //封装用户登录信息
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        //调用,进入到之前自定义的realm内的认证方法
        try {
            //登录成功
            subject.login(token);
            Account account = (Account) subject.getPrincipal();
            subject.getSession().setAttribute("account",account);
            return "index";
        } catch (UnknownAccountException e) { //用户名不存在
            e.printStackTrace();
            model.addAttribute("msg","用户名错误");
            return "login";
        } catch (IncorrectCredentialsException e) { //密码不存在
            e.printStackTrace();
            model.addAttribute("msg","密码错误");
            return "login";
        }
    }

    @GetMapping("/unauth")
    @ResponseBody
    public String unauth() {
        return "未授权,无法访问!";
    }

    @GetMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "login";
    }
}
(7)Shiro整合Thymeleaf
1、pom.xml引入依赖
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>
2、配置类添加ShiroDialect

ShiroConfig.java:

@Bean
public ShiroDialect shiroDialect() {
    return new ShiroDialect();
}
3、index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymelear.org"
                xmlns:shiro="http://www.thymelear.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#">
</head>
<body>
  <h1>index</h1>
  <div th:if="${session.account != null}">
      <span th:text="${session.account.username}+'欢迎回来'"></span>
      <a href="/logout">退出</a>
  </div>
  <a href="/main">main</a>
  <div shiro:hasPermission="manage">
      <a href="/manage">manage</a>
  </div>
  <div shiro:hasRole="administrator">
      <a href="/administrator">administrator</a>
  </div>
</body>
</html>

实现结果:

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值