使用shiro实现用户登录认证和简单权限的实现(法院项目)

实现登录认证

步骤一:导入依赖包

  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.csource</groupId>
            <artifactId>fastdfs-client-java</artifactId>
            <version>1.27-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-all</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
        </dependency>
    </dependencies>

步骤二:构建配置文件类ShiroConfig

package com.qf.fayuan.config;

import com.qf.fayuan.bean.MyShiroFactoryBean;
import com.qf.fayuan.mapper.PermissonMapper;
import com.qf.fayuan.mapper.UserMapper;
import com.qf.fayuan.shiro.realm.MyRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;


@Configuration
public class ShiroConfig {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PermissonMapper permissonMapper;
    @Bean
    public DefaultWebSecurityManager securityManager(MyRealm myRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);
        return securityManager;
    }
    @Bean
    public MyRealm myRealm(CredentialsMatcher credentialsMatcher, UserMapper userMapper,PermissonMapper permissonMapper){
    //需要另外编写MyRealm类
        MyRealm myRealm = new MyRealm();
        myRealm.setUserMapper(userMapper);
        myRealm.setCredentialsMatcher(credentialsMatcher);
        myRealm.setPermissonMapper(permissonMapper);
        return myRealm;
    }
    @Bean
    public CredentialsMatcher credentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashIterations(1024);
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        return hashedCredentialsMatcher;
    }
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public FilterRegistrationBean delegatingFilterProxy() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        DelegatingFilterProxy delegatingFilterProxy = new DelegatingFilterProxy("shiroFilter");
        filterRegistrationBean.setFilter(delegatingFilterProxy);
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.addInitParameter("targetFilterLifecycle", "true");
        return filterRegistrationBean;
    }

    @Bean
    public MyShiroFactoryBean shiroFilter(SecurityManager securityManager, PermissonMapper permissonMapper) {
    //这个类也要另建
        MyShiroFactoryBean myShiroFactoryBean = new MyShiroFactoryBean();
        myShiroFactoryBean.setPermissonMapper(permissonMapper);
        myShiroFactoryBean.setSecurityManager(securityManager);
        return myShiroFactoryBean;
    }

}

第三步:创建MyRealm类

package com.qf.fayuan.shiro.realm;

import com.qf.fayuan.mapper.PermissonMapper;
import com.qf.fayuan.mapper.UserMapper;
import com.qf.fayuan.shiro.pojo.CourtAdmin;
import com.qf.fayuan.shiro.usernamepasswordtoken.UserNamePassWordToken;
import com.qf.fayuan.user.pojo.User;
import com.qf.fayuan.vo.UserVo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.util.List;

public class MyRealm extends AuthorizingRealm {

    private UserMapper userMapper;

    private PermissonMapper permissonMapper;

    public void setPermissonMapper(PermissonMapper permissonMapper) {
        this.permissonMapper = permissonMapper;
    }

    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
			return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //获取用户的用户名
        String username = ((UserNamePassWordToken) authenticationToken).getUsername();
        //获取用户的角色
        int role = ((UserNamePassWordToken) authenticationToken).getUser_role();
        //根据用户名和用户角色,从数据库中查询该用户的信息
        CourtAdmin courtAdmin = userMapper.getCourtAdmin(username,role);
        //将用户存在数据库中的盐取出并且变成字节类型
        ByteSource bytes = ByteSource.Util.bytes(courtAdmin.getSalt());
        //传入用户名,用户在数据库中真实的的密码,盐,getName();
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,courtAdmin.getPassword(),bytes,getName());
        //返回带有用户信息的对象
        return simpleAuthenticationInfo;

    }
}

步骤四:编写contorller层,实现用户的登录验证

//    @LogAnno(operationtype = "/login",operationname = "用户登陆,服务器返回用户的部分信息")
    @RequestMapping("/login")// @RequestParam(value = "user_role",required = false,defaultValue = "1")
    public ResultBean login(String userinfo, Integer user_role, String password){
    //获取当前登录用户的链接
        Subject subject = SecurityUtils.getSubject();
        if(!subject.isAuthenticated()){
        //将用户的用户名和密码存入到UserNamePassWordToken中
            UserNamePassWordToken usernamePasswordToken = new UserNamePassWordToken(userinfo,password,user_role);
            try{
            //验证登录用户的信息是否正确,如果失败的话就会报错处理,所以需要捕获异常
                subject.login(usernamePasswordToken);
            }catch (Exception e){
                e.printStackTrace();
                return ResultBean.setError(ErrorCodeInterface.MIMACUOWU,"信息错误",null);
            }
        }
        UserVo userVo =  userService.login(userinfo,user_role);
        SecurityUtils.getSubject().getSession().setAttribute("user",userVo);
        return ResultBean.setOk(userVo);

    }

在上面中发现使用到了UserNamePassWordToken 这个对象,这个对象中可以携带几个字段,如username和password等,这个时候我想让这个对象也要携带我自定义的一个字段role,那么需要自定义一个类,让这个类去继承UserNamePassWordToken,然后添加一个新的字段role

类的内容如下:

package com.qf.fayuan.shiro.usernamepasswordtoken;

import org.apache.shiro.authc.UsernamePasswordToken;

public class UserNamePassWordToken extends UsernamePasswordToken {

    private int user_role;

    public UserNamePassWordToken(String username,String password,int user_role){
        super(username,password);
        this.user_role = user_role;
    }

    public int getUser_role() {
        return user_role;
    }

    public void setUser_role(int user_role) {
        this.user_role = user_role;
    }
}

根据上面的内容就可以使用shiro实现简单的登录认证功能了。

实现权限部分

步骤一:首先定义一个自定义注解,注解中包含,访问需要的权限(设置该路径名就是需要访问的权限) 、权限表述的相关信息

package com.qf.fayuan.anno;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissonAnno {

    String value();//权限的值,将方法或者类的路径设置为所要权限的值
    String comment();//权限的描述 ,该权限的描述不应为空或者重复
    boolean isMenu() default false;//判断是否是一个表单
    String parent();  //其父类的权限描述

}

步骤二:将注解添加到类和方法的上面

    @PermissonAnno(value = "/getnoticeinfo",comment="查询公告",isMenu = false,parent = "公告列表")

步骤三:遍历每一个类和类中的每一个方法,将权限添加到表中
首先定义一个工具类来封装注解中的信息

package com.qf.fayuan.shiro.pojo;

public class PermissonBean {

    private int id;
    private String value;
    private String comment;
    private boolean isMenu;
    private String parent;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public boolean isMenu() {
        return isMenu;
    }

    public void setMenu(boolean menu) {
        isMenu = menu;
    }

    public String getParent() {
        return parent;
    }

    public void setParent(String parent) {
        this.parent = parent;
    }

    @Override
    public String toString() {
        return "PermissonBean{" +
                "id=" + id +
                ", value='" + value + '\'' +
                ", comment='" + comment + '\'' +
                ", isMenu=" + isMenu +
                ", parent='" + parent + '\'' +
                '}';
    }
}

然后通过给定一个项目的路径,就可以扫描这个项目中的所以类和方法

package com.qf.fayuan.utils;

import com.qf.fayuan.anno.PermissonAnno;
import com.qf.fayuan.shiro.pojo.PermissonBean;
import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

public class PermissonAnnoUtils {
//  定义一个set集合用来存储判断是含有该注解的类
    private static Set<Class> classSet = new HashSet<>();
    //将直接定义在类上面的注解的信息封装到对象,然后存入到该集合中
    private static  Set<PermissonBean> classBean = new LinkedHashSet<>();
    //将定义在方法上的注解的信息封装到对象中然后存入到该集合中
    private static  Set<PermissonBean> methodBean = new LinkedHashSet<>();

    public static void getClassSet(String packagename) throws Exception {
        /**
         *  xxx.class.getResource()用来从当前类(xxx)所在的目录下(也就是以当前类所在路径为根路径)获得资源;
         * xxx.class.getClassLoader().getResource()用来从classpath路径下(也就是以classpath所在路径为根路径)获得资源。
         */

        //根据项目中一段路径(com.qf.fayuan),获得  file:/D:/idea/idea-workspace/fayuanxiangmu/target/classes/com/qf/fayuan  其中file为协议
        URL resource = PermissonAnnoUtils.class.getClassLoader().getResource(packagename.replace(".", "/"));
        if(resource!=null||"file".equalsIgnoreCase(resource.getProtocol())){
            //getProtocol获取协议:也就是   file:   部分
            String packagepath = URLDecoder.decode(resource.getFile(),"utf-8");
            ///D:/idea/idea-workspace/fayuanxiangmu/target/classes/com/qf/fayuan
            addClass(packagename,packagepath);
        }
    }

    private static void addClass(String packagename, String packagepath) throws Exception {

        //获得该路径下的所有文件,并且添加一个过滤器,设置条件
        File[] files = new File(packagepath).listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                //如果是一个文件或者是一个类,或者是一个目录,就遍历进入数组中
                return (pathname.isFile() || pathname.getName().endsWith(".class") || pathname.isDirectory());
            }
        });
        for (File file : files) {
            //file的格式为:  D:\idea\idea-workspace\fayuanxiangmu\target\classes\com\qf\fayuan\anno
            //获得文件的名字
            String name = file.getName();
            //name  的值是  anno
            if(file.isFile()){
                if(!name.contains("Controller")){
                    continue;
                }
                String classname = name.substring(0,name.lastIndexOf("."));
                classname = packagename + "." +classname;
                doAddClass(classname);
            }else {
                String subpackagepath = name;
                if(!StringUtils.EmptyString(packagepath)){
                    subpackagepath = packagepath+"/"+name;
                }
                String subpackagename = name;
                if(!StringUtils.EmptyString(packagename)){
                    subpackagename = packagename+"."+name;
                }
                addClass(subpackagename,subpackagepath);
            }
        }
    }
    private static void doAddClass(String classname) throws ClassNotFoundException {
        Class<?> aClass = Class.forName(classname);
        classSet.add(aClass);
    }

    public static void inject(String packagename) throws Exception {

        classSet.clear();
        classBean.clear();
        methodBean.clear();
        getClassSet(packagename);

        for (Class clazz : classSet) {
            //扫描类上的注解
            PermissonAnno annotation = (PermissonAnno) clazz.getAnnotation(PermissonAnno.class);
            String value = null;
            String comment = null;
            boolean isMenu = false;
            String parent = null;
            String parentValue = null;
            if(annotation!=null){
                comment = annotation.comment();
                parentValue = annotation.value();
                isMenu = annotation.isMenu();
                parent = annotation.parent();
                PermissonBean permissonBean = new PermissonBean();
                permissonBean.setComment(comment);
                permissonBean.setValue(parentValue);
                permissonBean.setMenu(isMenu);
                permissonBean.setParent(parent);
                classBean.add(permissonBean);
            }
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                //扫描方法上的注解
                PermissonAnno annotation1 = method.getAnnotation(PermissonAnno.class);
                if(annotation1!=null){
                    value = annotation1.value();
                    comment = annotation1.comment();
                    isMenu = annotation1.isMenu();
                    parent = annotation1.parent();
                    value = parentValue+value;
                    PermissonBean permissonBean = new PermissonBean();
                    permissonBean.setValue(value);
                    permissonBean.setComment(comment);
                    permissonBean.setMenu(isMenu);
                    permissonBean.setParent(parent);
                    methodBean.add(permissonBean);
                }
            }
        }

    }

    public static Set<Class> getClassSet() {
        return classSet;
    }

    public static Set<PermissonBean> getClassBean() {
        return classBean;
    }

    public static Set<PermissonBean> getMethodBean() {
        return methodBean;
    }

    public static void main(String[] args) {
        File[] files = new File("D:/idea/idea-workspace/fayuanxiangmu/target/classes/com/qf/fayuan").listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                System.out.println(pathname);//D:\idea\idea-workspace\fayuanxiangmu\target\classes\com\qf\fayuan\anno
                System.out.println(pathname.getName());//anno
                return (pathname.isFile() || pathname.getName().endsWith(".class") || pathname.isDirectory());
            }
        });
        for (File file : files) {
            System.out.println(file.getName());
        }
//
//        URL resource = PermissonAnnoUtils.class.getClassLoader().getResource("com.qf.fayuan".replace(".", "/"));
//        System.out.println(resource);//file:/D:/idea/idea-workspace/fayuanxiangmu/target/classes/com/qf/fayuan
//
//        URL resource1 = PermissonAnnoUtils.class.getResource("com.qf.fayuan");
//        System.out.println(resource1);//null
//
//        String file = PermissonAnnoUtils.class.getClassLoader().getResource("com.qf.fayuan".replace(".", "/")).getFile();
//        System.out.println(file);///D:/idea/idea-workspace/fayuanxiangmu/target/classes/com/qf/fayuan

    }

}

别写controller层,访问该层,将所有的注解中的信息存入到数据库中

package com.qf.fayuan.shiro.controller;

import com.qf.fayuan.anno.PermissonAnno;
import com.qf.fayuan.bean.ResultBean;
import com.qf.fayuan.mapper.PermissonMapper;
import com.qf.fayuan.shiro.pojo.PermissonBean;
import com.qf.fayuan.utils.ErrorCodeInterface;
import com.qf.fayuan.utils.PermissonAnnoUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Set;

@RequestMapping("/permisson")
@RestController()
public class PermissonController {
    @Autowired
    private PermissonMapper permissonMapper;

    @RequestMapping("/addallpermisson")
    public ResultBean addAllPermisson(){

        try {
            PermissonAnnoUtils.inject("com.qf.fayuan");
            Set<PermissonBean> classBean = PermissonAnnoUtils.getClassBean();
            permissonMapper.addAllPermisson(classBean);

            Set<PermissonBean> methodBean = PermissonAnnoUtils.getMethodBean();
            permissonMapper.addAllPermisson(methodBean);
        } catch (Exception e) {
            e.printStackTrace();
            return ResultBean.setError(ErrorCodeInterface.XIUGAIMIMASHIBAI,"失败了",null);
        }


        return ResultBean.setOk(null);
    }


}

步骤四:上面将所有的权限添加到了数据库中,下面就是将访问各个类或者方法的权限添加到shiro中

在MyShiroFactoryBean中添加代码即可

package com.qf.fayuan.bean;

import com.qf.fayuan.mapper.PermissonMapper;
import com.qf.fayuan.shiro.pojo.PermissonValue;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;

import javax.annotation.PostConstruct;
import java.util.LinkedHashMap;
import java.util.List;

public class MyShiroFactoryBean extends ShiroFilterFactoryBean {
    //这里不应该使用自动注入的方式
    private PermissonMapper permissonMapper;

    public void setPermissonMapper(PermissonMapper permissonMapper) {
        this.permissonMapper = permissonMapper;
    }
//添加这个注解以为着
    @PostConstruct
    public void init(){
        //设置登录的界面的位置
        setLoginUrl("/login.html");
        //创建一个map集合,将要访问的路径和所需要的权限添加进去
        LinkedHashMap<String, String> perms = new LinkedHashMap<>();
        //访问首页,需要的权限是任何权限都可以访问
        perms.put("/index.html","anon");
        perms.put("/login.html","anon");
        //如果想访问success.html页面,必须进行登录认证,而且会自动跳转到登录界面
        perms.put("/success.html","authc");


        //将本项目的所有权限添加到添加到shiro中
       List<PermissonValue> list  = permissonMapper.getAllPermisson();
       if(list!=null){
           for (PermissonValue value : list) {
               //注意添加权限的时候,权限的书写格式
               perms.put(value.getValue(),"perms["+value.getValue()+"]");
           }
       }
        setFilterChainDefinitionMap(perms);
    }

}

这样,就设置好了访问每个页面时所需要的权限,但是只有这些还不够,因为,只是知道了访问这些界面所需要的权限,但是没有给相应用户添加权限,否则,这些界面用户是无法访问的

步骤五:将用户的所有权限遍历出来放到shiro中,让shiro自行比对
在doGetAuthorizationInfo方法中

package com.qf.fayuan.shiro.realm;

import com.qf.fayuan.mapper.PermissonMapper;
import com.qf.fayuan.mapper.UserMapper;
import com.qf.fayuan.shiro.pojo.CourtAdmin;
import com.qf.fayuan.shiro.usernamepasswordtoken.UserNamePassWordToken;
import com.qf.fayuan.user.pojo.User;
import com.qf.fayuan.vo.UserVo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.util.List;

public class MyRealm extends AuthorizingRealm {

    private UserMapper userMapper;

    private PermissonMapper permissonMapper;

    public void setPermissonMapper(PermissonMapper permissonMapper) {
        this.permissonMapper = permissonMapper;
    }

    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        UserVo user = (UserVo) SecurityUtils.getSubject().getSession().getAttribute("user");
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //获取用户的所有权限
        List<String> list = userMapper.getSelfPermisson(user.getUser_id());
        if(list!=null){
            for (String s : list) {
            //将所有的权限添加到shiro中
                simpleAuthorizationInfo.addStringPermission(s);
            }
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = ((UserNamePassWordToken) authenticationToken).getUsername();
        int role = ((UserNamePassWordToken) authenticationToken).getUser_role();
        CourtAdmin courtAdmin = userMapper.getCourtAdmin(username,role);
        ByteSource bytes = ByteSource.Util.bytes(courtAdmin.getSalt());
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,courtAdmin.getPassword(),bytes,getName());
        return simpleAuthenticationInfo;

    }
}

至此权限部分完成

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值