Shiro框架基础内容

今天内容

  • Shiro概述
  • Shiro环境搭建
  • Shiro完成用户认证
  • Shiro完成用户授权
  • 自定义Shiro过滤器

第一章 Shiro概述

1. 当前项目的问题

  1. 用户不用登录,就能直接通过url访问saas系统内部的页面 直接输入http://localhost:8080/system/module/list.do就能看到所有模块
  2. 使用低权限的管理员身份登录,也可以访问高权限的用户菜单 比如使用普通员工登录, 直接url就能访问到它本应访问不到的页面
  3. 页面上标签未做到权限控制 给一个员工分配了部门管理\查看权限, 但没分配新增权限, 但页面上依旧显示新增按钮
问题:
	1. 用户不登录就可以访问系统资源
	2. 用户登录之后可以访问到不属于自己的资源
	3. 页面标签没有做到权限限制

​ 上述功能的描述,可以使用过滤器拦截器完成,当然在实际开发者已经有组织把这些功能封装为框架,比如Shiro或者SpringSecurity,来简化权限的控制,我们今天使用shiro框架

2. Shiro简介

Apache Shiro是Java的一个安全框架。不仅功能强大,而且使用简单,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。

 

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

3. Shiro的功能

* Authentication(认证)
		用户登录,身份识别    ---      结果:不是系统用户不能访问系统
		
* Authorization(授权)
		限定用户可进行的操作  ----     结果:没权限不能访问资源

* Cryptography(加密)
		安全数据加密
		
* Session Manager(会话)
		用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中(类似于web的session)
		
* Web Integration
		web容器集成
		
* Integrations
		集成其他应用,比如spring、缓存

4. Shiro的架构(画)

 

* Application Code(应用)
		我们自己编写的项目
		
* Subject(主体)
		它是一个工具类,负责用户和shiro框架交互
		
* SecurityManager(安全管理器)
		它是Shiro架构中最核心的组件,通过它可以协调其他组件完成用户认证和授权
		
* Realm(域)
		它定义了访问数据的方式,用来连接不同的数据源,如:数据库,配置文件等

明确:我们使用shiro主要要做两件事

  • 认证:认证指的是匹配用户名(邮箱)和密码,让平台认识你。

  • 授权:授权指的是当前认证的用户进入平台,能操作哪些页面。

     

5. Shiro过滤器

过滤器简称对应的java类描述
anonorg.apache.shiro.web.filter.authc.AnonymousFilter未认证访问
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter认证后访问
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilterhttpBasic认证后访问
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter指定权限访问
portorg.apache.shiro.web.filter.authz.PortFilter指定端口访问
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilter指定rest访问
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter指定角色访问
sslorg.apache.shiro.web.filter.authz.SslFilterssl认证后访问
userorg.apache.shiro.web.filter.authc.UserFilter指定用户访问
logoutorg.apache.shiro.web.filter.authc.LogoutFilter用户退出

6. Shiro标签

标签名称标签条件(均是显示标签内容)
<shiro:authenticated >登录之后
<shiro:notAuthenticated >不在登录状态时
<shiro:guest >用户在没有RememberMe时
<shiro:user >用户在RememberMe时
<shiro:hasAnyRoles name=”abc,123” >在有abc或者123角色时
<shiro:hasRole name=”abc”>拥有角色abc
<shiro:lacksRole name=”abc”>没有角色abc
<shiro:hasPermission name=”abc”>拥有权限资源abc
<shiro:lacksPermission name=”abc”>没有abc权限资源
<shiro:principal >默认显示用户名称

第二章 Shiro环境搭建

1. 导入坐标(已完成)

<!--shiro和spring整合-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
<!--shiro核心包-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.3.2</version>
</dependency>

2. web.xml配置

在web.xml中添加如下过滤器,注意filter-name的值是shiroFilter

<!-- Shiro Security filter filter-name这个名字的值将来还会在spring中用到-->
<filter>
   <filter-name>shiroFilter</filter-name>
   <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
   <init-param>
      <param-name>targetFilterLifecycle</param-name>
      <param-value>true</param-value>
   </init-param>
</filter>
<filter-mapping>
   <filter-name>shiroFilter</filter-name>
   <!--过滤所有请求-->
   <url-pattern>/*</url-pattern>
</filter-mapping>

3. Spring整合shiro

添加spring整合shiro的配置文件

applicationContext-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
      http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <!-- filter-name这个名字的值来自于web.xml中filter的名字 -->
  <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <!--认证不通过, 跳转的页面 -->
    <property name="loginUrl" value="/login.jsp"></property>
    <!--授权不通过, 跳转的页面 -->
    <property name="unauthorizedUrl" value="/unauthorized.jsp"></property>
    <!--拦截规则,注意拦截的顺序.次规则从上向下执行-->
    <property name="filterChainDefinitions">
      <value>
        /login.jsp = anon
        /css/** = anon
        /img/** = anon
        /plugins/** = anon
        /make/** = anon
        /login.do = anon
        /** = authc
      </value>
    </property>
  </bean>

  <!-- 引用自定义的realm -->
  <bean id="saasRealm" class="com.itheima.web.realm.SaasRealm"/>

  <!-- 安全管理器 -->
  <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="saasRealm"/>
  </bean>

  <!--下面所有内容为 shiro注解使用-->
  <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
  </bean>
  <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
  <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
        depends-on="lifecycleBeanPostProcessor">
    <property name="proxyTargetClass" value="true"/>
  </bean>
  <aop:aspectj-autoproxy proxy-target-class="true"/>

</beans>

4. 自定义Realm

在export_manager_web的com.itheima.web.realm包创建类SaasRealm

package com.itheima.web.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class SaasRealm extends AuthorizingRealm {
	/**
	 * @description 用户授权
	 * @author mryhl
	 * @date 2020/10/11 16:15 No such property: code for class: Script1
	 * @return
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
		System.out.println("=====================授权=====================");
		return null;
	}

	/** 
	 * @description 用户认证
	 * @author mryhl
	 * @date 2020/10/11 16:14 No such property: code for class: Script1
	 * @return 
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		System.out.println("=====================认证=====================");
		return null;
	}
}

5. shiro执行流程的说明

 

第三章 Shiro用户认证(重点)

1. 传统方式和shiro方式完成登录的流程区别

 

2. 代码实现

2.1 修改LoginController

/**
	 * @description 用户登陆认证
	 * @author mryhl
	 * @date 2020/10/11 16:25 No such property: code for class: Script1
	 * @return
	 */
	@RequestMapping("/login")
	public String login(String email,String password) {
		// 1. 封装email和password为Token
		AuthenticationToken authenticationToken = new UsernamePasswordToken(email, new Md5Hash(password, email, 2).toString());
		// 2. 获取subject,并且调用login方法
		// 通过SecurityUtils获取subject
		Subject subject = SecurityUtils.getSubject();
		/**
		 * @author mryhl
		 * 调用login方法, 传入token.
		 * 并对此进行捕获异常
		 */
		try {
			subject.login(authenticationToken);
			// 登陆成功
			User user = (User) subject.getPrincipal();
			// 通过则保存用户数据
			session.setAttribute("loginUser",user);
			//根据用户查询对应的权限
			List<Module> moduleList=userService.findModuleByUser(user);
			session.setAttribute("modules", moduleList);
			return "redirect:/home/main.do";

		} catch (Exception e) {			
			request.setAttribute("error", "用户名或者密码错误");
			return "forward:/login.jsp";
		}

	}

2.2 修改SaasReam

/**
	 * @description 用户认证
	 * @author mryhl
	 * @date 2020/10/11 16:56
	 * @return AuthenticationInfo 的实现类 SimpleAuthenticationInfo 简单认证信息
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		// 进入方法标识
		System.out.println("=====================认证=====================");

		// 强制类型转换
		UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
		// 获取传入的email
		String email = usernamePasswordToken.getUsername();
		// 根据用户进行查询
		User user = userService.findByEmail(email);
		// 判断返回信息
		if (user==null) {
			// 返回空的简单身份认证信息,代表没有找到内容
			return new SimpleAuthenticationInfo();
		}else {
			/**
			 * @author mryhl
			 * SimpleAuthenticationInfo三个参数
			 * Object principal 主角---->user
			 * Object credentials  密码--->user.getPassword()
			 * String realmName 当前realm的名称--->this.getName()
			 */
			return new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
		}

	}

3. 再谈shiro认证代码的执行流程

 

4. 用户退出

//退出
@RequestMapping(value = "/logout", name = "用户登出")
public String logout() {
    SecurityUtils.getSubject().logout();   //登出
    return "redirect:/login.jsp";
}

第四章 Shiro用户授权(重点)

1. 授权流程说明

shiro授权的意思就是: 当一个用户试图访问一个资源的时候, shiro会判断此用户是否有访问此资源的权限

shiro授权的步骤:

  1. 定义每个资源访问所需要的权限
  2. 当用户访问资源的时候, 要去数据库查询到此用户拥有的权限
  3. 使用用户查询到的权限列表 跟 访问资源需要的权限比对, 决定授权是否通过

 

2. 授权代码实现(xml版)

2.1 xml中定义权限

applicationContext-shiro.xml

<!--
URL=perms["权限标识"]
当前访问的用户只有  有权限标识的权限的时候,才能访问URL对应的资源
注意位置必须在/**的上面
-->
/company/list.do = perms["企业管理"]

2.2 SaasRealm中实现授权逻辑

/**
	 * @description 用户授权
	 * @author mryhl
	 * @date 2020/10/11 16:15
	 * @return simpleAuthorizationInfo 简单授权信息
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
		System.out.println("=====================授权=====================");
		// 查询当前用户的权限信息
		User user = (User) principalCollection.getPrimaryPrincipal();
		List<Module> moduleList = userService.findModuleByUser(user);
		// 将信息传递到Shiro
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		for (Module module : moduleList) {
			simpleAuthorizationInfo.addStringPermission(module.getName());
		}
		System.out.println(simpleAuthorizationInfo);
		return simpleAuthorizationInfo;
	}

3. 授权代码实现(注解版)

3.1 去掉xml中的注解配置

spring/applicationContext-shiro.xml

<!-- filter-name这个名字的值来自于web.xml中filter的名字 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  <property name="securityManager" ref="securityManager"/>
  <!--认证不通过, 跳转的页面 -->
  <property name="loginUrl" value="/login.jsp"></property>
  <!--授权不通过, 跳转的页面 -->
  <property name="unauthorizedUrl" value="/unauthorized.jsp"></property>
  <!--拦截规则,注意拦截的顺序.次规则从上向下执行-->
  <property name="filterChainDefinitions">
    <value>
      /login.jsp = anon
      /css/** = anon
      /img/** = anon
      /plugins/** = anon
      /make/** = anon
      /login.do = anon
      <!--
      URL=perms["权限标识"]
      当前访问的用户只有  有权限标识的权限的时候,才能访问URL对应的资源
      注意位置必须在/**的上面
      -->
      <!--/company/list.do = perms["企业管理"]-->
      /** = authc
    </value>
  </property>
</bean>

3.2 使用注解声明资源访问权限

CompanyController.java

/**
	 * 查看列表
	 * name字段主要用于打印日志
	 */
	//@RequiresPermissions("企业管理")  代表只有用户有企业管理的权限,才能访问当前方法
	//相当于XML中的   /company/list.do = perms["企业管理"]
	@RequiresPermissions("企业管理")
	@RequestMapping(value = "/list",name = "企业列表查询")
	public String list(
			@RequestParam(defaultValue = "1", name = "page") Integer pageNum,
			@RequestParam(defaultValue = "1") Integer pageSize
	){
		PageInfo pageInfo = companyService.findByPage(pageNum, pageSize);
		request.setAttribute("page", pageInfo);
		return "company/company-list";
	}

3.3 全局异常处理修改

基于注解的授权控制, 当授权失败的时候, 不会跳转授权失败页面, 而是抛出一个异常, 需要我们在全局异常处理器中自己处理

package com.itheima.web.handlers;

import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class CommonExceptionHandler {

    //处理所有的Exception及其子类
    @ExceptionHandler(Exception.class)
    public String exceptionHandler(Exception e, HttpServletRequest request) {

        //1. 打印异常,给程序员
        e.printStackTrace();
        request.setAttribute("errorMsg", e.getMessage());

        //2. 返回页面, 给用户
        return "error";
    }


    //处理未授权的异常
    @ExceptionHandler(UnauthorizedException.class)
    public String UnauthorizedException(Exception e, HttpServletRequest request) {
        //2. 返回未授权页面, 给用户
        return "redirect:/unauthorized.jsp";
    }
}

4. 页面元素权限控制

4.1 说明

当用户没有相关资源的具体操作权限的时候,我们应该是不让其看到响应按钮的

这就要使用到了shiro标签, 这里主要介绍一个<shiro:hasPermission>, 用法如下:

<!--这代表用户有   删除部门   的权限, 才能看到  删除   按钮-->
<shiro:hasPermission name="删除部门">
    <button>删除</button>
</shiro:hasPermission>

 

4.2 代码

以部门管理为例子, 演示效果:

1 引入shiro标签库

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags"%>

2 使用shiro标签控制权限

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="../../base.jsp" %>
<!DOCTYPE html>
<html>

    <head>
        <!-- 页面meta -->
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>数据 - AdminLTE2定制版</title>
        <meta name="description" content="AdminLTE2定制版">
        <meta name="keywords" content="AdminLTE2定制版">
        <!-- Tell the browser to be responsive to screen width -->
        <meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
    </head>
    <script>
        function deleteById() {
            var id = getCheckId()
            if (id) {
                if (confirm("你确认要删除此条记录吗?")) {
                    location.href = "/system/dept/delete.do?id=" + id;
                }
            } else {
                alert("请勾选待处理的记录,且每次只能勾选一个")
            }
        }
    </script>
    <body>
        <div id="frameContent" class="content-wrapper" style="margin-left:0px;">
            <section class="content-header">
                <h1>
                    系统管理
                    <small>部门管理</small>
                </h1>
                <ol class="breadcrumb">
                    <li><a href="all-admin-index.html"><i class="fa fa-dashboard"></i> 首页</a></li>
                </ol>
            </section>
            <!-- 内容头部 /-->

            <!-- 正文区域 -->
            <section class="content">

                <!-- .box-body -->
                <div class="box box-primary">
                    <div class="box-header with-border">
                        <h3 class="box-title">部门列表</h3>
                    </div>

                    <div class="box-body">

                        <!-- 数据表格 -->
                        <div class="table-box">

                            <!--工具栏-->
                            <div class="pull-left">
                                <div class="form-group form-inline">
                                    <div class="btn-group">
                                        <shiro:hasPermission name="新增部门">
                                            <button type="button" class="btn btn-default" title="新建"
                                                    onclick='location.href="/system/dept/toAdd.do"'><i
                                                    class="fa fa-file-o"></i> 新建
                                            </button>
                                        </shiro:hasPermission>
                                        <shiro:hasPermission name="删除部门">
                                            <button type="button" class="btn btn-default" title="删除"
                                                    onclick='deleteById()'><i class="fa fa-trash-o"></i> 删除
                                            </button>
                                        </shiro:hasPermission>
                                        <button type="button" class="btn btn-default" title="刷新"
                                                onclick="window.location.reload();"><i class="fa fa-refresh"></i> 刷新
                                        </button>
                                    </div>
                                </div>
                            </div>
                            <div class="box-tools pull-right">
                                <div class="has-feedback">
                                    <input type="text" class="form-control input-sm" placeholder="搜索">
                                    <span class="glyphicon glyphicon-search form-control-feedback"></span>
                                </div>
                            </div>
                            <!--工具栏/-->

                            <!--数据列表-->
                            <table id="dataList" class="table table-bordered table-striped table-hover dataTable">
                                <thead>
                                <tr>
                                    <th class="" style="padding-right:0px;">
                                        <input type="checkbox" name="selid" onclick="checkAll('id',this)">
                                    </th>
                                    <th class="sorting">序号</th>
                                    <th class="sorting">编号</th>
                                    <th class="sorting">上级</th>
                                    <th class="sorting">名称</th>
                                    <th class="text-center">操作</th>
                                </tr>
                                </thead>
                                <tbody>
                                <c:forEach items="${page.list}" var="dept" varStatus="st">
                                    <tr>
                                        <td><input type="checkbox" name="id" value="${dept.id }"/></td>
                                        <td>${st.count }</td>
                                        <td>${dept.id }</td>
                                        <td>${dept.parent.deptName }</td>
                                        <td><a href="/system/dept/toUpdate.do?id=${dept.id }">${dept.deptName }</a></td>
                                        <th class="text-center">
                                            <shiro:hasPermission name="编辑部门">
                                                <button type="button" class="btn bg-olive btn-xs"
                                                        onclick='location.href="/system/dept/toUpdate.do?id=${dept.id}"'>
                                                    编辑
                                                </button>
                                            </shiro:hasPermission>
                                        </th>
                                    </tr>
                                </c:forEach>
                                </tbody>
                            </table>
                        </div>
                    </div>
                    <div class="box-footer">
                        <jsp:include page="../../common/page.jsp">
                            <jsp:param value="${ctx}/system/dept/list.do" name="pageUrl"/>
                        </jsp:include>
                    </div>
                </div>
            </section>
        </div>
    </body>
</html>

5. 授权数据缓存

​ 我们现在访问被拦截的页面时,每次shiro的安全过滤器都需要从realm中获取认证方法,也意味这每次查询数据库,浪费服务性能,造成访问压力,这时候我们必须要进行优化:经常访问,但又不经常修改的这部分数据可以使用缓存

spring/applicationContext-shiro.xml

<!--缓存管理器-->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean>

<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
  <property name="realm" ref="saasRealm"/>
  <property name="cacheManager" ref="cacheManager"></property>
</bean>

第五章 自定义Shiro过滤器(高级)

1. 需求明确

shiro 支持如下的权限写法, 代表同时拥有多个权限, 才能访问指定资源

/system/dept/edit.do = perms["新增部门","删除部门"]

但是我们现在有这样一个需求: 只需要满足其中一个个权限, 就要能对资源访问, 这时怎么办呢?

2. 自定义过滤器

操作步骤:

  1. 自定义过滤器, 继承AuthorizationFilter, 并实现里面的方法
  2. 配置文件中添加配置

自定义过滤器

package com.itheima.web.filters;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class MyPermissionsAuthorizationFilter extends AuthorizationFilter {

    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

        Subject subject = getSubject(request, response); //subject里面就有当前用户的权限    ["查看部门","新增部门","修改部门"]
        String[] perms = (String[]) mappedValue; //访问资源需要的权限 ["新增部门","删除部门"]

        //如果没有配置,放行
        if (perms == null || perms.length == 0) {
            return true;
        }

        for (String perm : perms) {
            if (subject.isPermitted(perm)) {
                return true;
            }
        }

        return false;
    }
}

修改配置文件

<!--拦截规则,注意拦截的顺序.次规则从上向下执行-->
  <property name="filterChainDefinitions">
    <value>
      /login.jsp = anon
      /css/** = anon
      /img/** = anon
      /plugins/** = anon
      /make/** = anon
      /login.do = anon
      <!--
      URL=perms["权限标识"]
      当前访问的用户只有  有权限标识的权限的时候,才能访问URL对应的资源
      注意位置必须在/**的上面
      -->
      <!--/company/list.do = perms["企业管理"]-->
      /company/list.do = myPerms["新增部门","删除部门"]
      /** = authc
    </value>
  </property>
  <!-- 将自定义的过滤器加入到Shiro的过滤器链中-->
  <property name="filters">
    <map>
      <!--key就是当前过滤器的一个唯一标识,类似于anon authc perms-->
      <entry key="myPerms" value-ref="myPermissionsAuthorizationFilter" />
    </map>
  </property>
</bean>
<!--将自定义的过滤器声明到Spring中-->
<bean id="myPermissionsAuthorizationFilter" class="com.itheima.web.filters.MyPermissionsAuthorizationFilter"></bean>

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.YHL

谢谢您的肯定

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值