从永远到永远-SpringSecurity

关于权限框架,面试过程中也被问到过很多次,以前学校做项目时候,基本都是基于RBAC自己写逻辑实现功能。现在想抽时间来看看,关于shiro和security都不是很了解,非要选一个肯定security,毕竟牌子硬。

1.搭建SSM+Security实现权限控制,标准的ssm项目,没有用到Springboot,各种配置可能显得有限复杂。该项目暂时省略了service层。
1)搭建SSM项目环境,创建mavenweb项目
2)基于RBAC模型的权限控制,要求我们至少有五个表,相对应也就需要至少三个实体类。
数据库表看图自行手写,捎带手会议一下RBAC:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

用户类,是最重要的。要使用security,需要实现接口。实体类中的一些属性其实是接口中定义好的,看似与以前一样的属性及getset方法其实是实现的接口的。

package com.scbg.pojo;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @program: 01_SpringSecurity
 * @description: 用户信息类,必须实现接口
 * @author: 三层饼干儿
 * @create: 2019-08-24 20:46
 **/
public class User implements UserDetails{
	private Integer id;
	private Date createDate;
	private Date lastLoginTime;

	/**
	 * 以下属性其实是security接口需要的属性,详情可查api
	 */
	private String username;
	private String password;
	private boolean enabled;
	private boolean accountNonExpired;
	private boolean accountNonLocked;
	private boolean credentialsNonExpired;
	//security规定的获取用户权限的集合
	private List<GrantedAuthority> authorities= new ArrayList<>();

	public Integer getId() {
		return id;
	}

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

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Date getCreateDate() {
		return createDate;
	}

	public void setCreateDate(Date createDate) {
		this.createDate = createDate;
	}

	public Date getLastLoginTime() {
		return lastLoginTime;
	}

	public void setLastLoginTime(Date lastLoginTime) {
		this.lastLoginTime = lastLoginTime;
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public boolean isAccountNonExpired() {
		return accountNonExpired;
	}

	public void setAccountNonExpired(boolean accountNonExpired) {
		this.accountNonExpired = accountNonExpired;
	}

	public boolean isAccountNonLocked() {
		return accountNonLocked;
	}

	public void setAccountNonLocked(boolean accountNonLocked) {
		this.accountNonLocked = accountNonLocked;
	}

	public boolean isCredentialsNonExpired() {
		return credentialsNonExpired;
	}

	public void setCredentialsNonExpired(boolean credentialsNonExpired) {
		this.credentialsNonExpired = credentialsNonExpired;
	}

	public List<GrantedAuthority> getAuthorities() {
		return authorities;
	}

	public void setAuthorities(List<GrantedAuthority> authorities) {
		this.authorities = authorities;
	}

	@Override
	public String toString() {
		return "User{" +
				"id=" + id +
				", createDate=" + createDate +
				", lastLoginTime=" + lastLoginTime +
				", username='" + username + '\'' +
				", password='" + password + '\'' +
				", enabled=" + enabled +
				", accountNonExpired=" + accountNonExpired +
				", accountNonLocked=" + accountNonLocked +
				", credentialsNonExpired=" + credentialsNonExpired +
				", authorities=" + authorities +
				'}';
	}
}

角色类:

package com.scbg.pojo;

/**
 * @program: 01_SpringSecurity
 * @description: 角色类
 * @author: 三层饼干儿
 * @create: 2019-08-24 21:03
 **/
public class Role {
	private Integer id;
	private String roleName;
	private String roleDesc;

	public Integer getId() {
		return id;
	}

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

	public String getRoleName() {
		return roleName;
	}

	public void setRoleName(String roleName) {
		this.roleName = roleName;
	}

	public String getRoleDesc() {
		return roleDesc;
	}

	public void setRoleDesc(String roleDesc) {
		this.roleDesc = roleDesc;
	}
}

权限:

package com.scbg.pojo;

/**
 * @program: 01_SpringSecurity
 * @description: 权限
 * @author: 三层饼干儿
 * @create: 2019-08-24 21:09
 **/
public class Permission {
	private Integer id;
	private String permName;
	//跟security有关
	private String permTag;

	public Integer getId() {
		return id;
	}

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

	public String getPermName() {
		return permName;
	}

	public void setPermName(String permName) {
		this.permName = permName;
	}

	public String getPermTag() {
		return permTag;
	}

	public void setPermTag(String permTag) {
		this.permTag = permTag;
	}
}

同时完善一下项目结构,创建好service,mapper等包,项目结构如下:
在这里插入图片描述
2)业务场景需要的,我们肯定要从数据库查询用户的权限。完善mapper层代码,与之前的mybatis一样。
mapper接口

package com.scbg.mapper;

import com.scbg.pojo.Permission;
import com.scbg.pojo.User;

import java.util.List;

/**
 * @program: 01_SpringSecurity
 * @description:
 * @author: 三层饼干儿
 * @create: 2019-08-24 21:13
 **/
public interface UserMapper {
	/**
	 * 查询当前用户对象
	 * @param username
	 * @return
	 */
	public User findByUsername(String username);

	/**
	 * 查询当前用户拥有的权限
	 * @param username
	 * @return
	 */
	public List<Permission> findPermissionByUsername(String username);
}

mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.scbg.mapper.UserMapper">
    <!--查询用户-->
    <select id="findByUsername" parameterType="String" resultType="User">
        SELECT * FROM sys_user WHERE username=#{VALUE}
    </select>
    <!--查询用户权限-->
    <select id="findPermissionByUsername" parameterType="String" resultType="Permission">
        SELECT   *
     FROM sys_user u JOIN sys_user_role ur ON u.`id`=ur.`user_id`
     JOIN sys_role_permission rp ON ur.`role_id`=rp.`role_id`
     JOIN sys_permission p ON rp.`permission_id`=p.`id`
     WHERE u.`username`=#{VALUE}
    </select>

</mapper>

3)引入开发需要的依赖,已经有注解了。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.scbg</groupId>
  <artifactId>01_SpringSecurity</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <jdk.version>1.8</jdk.version>
      <spring.version>4.3.10.RELEASE</spring.version>
      <spring.security.version>4.2.3.RELEASE</spring.security.version>
      <jstl.version>1.2</jstl.version>
      <servlet.version>2.5</servlet.version>
  </properties>

  <dependencies>
      <!--spring-->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-core</artifactId>
          <version>${spring.version}</version>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
          <version>${spring.version}</version>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>${spring.version}</version>
      </dependency>
      <!--springsecurity-->
      <dependency>
          <groupId>org.springframework.security</groupId>
          <artifactId>spring-security-web</artifactId>
          <version>${spring.security.version}</version>
      </dependency>
      <dependency>
          <groupId>org.springframework.security</groupId>
          <artifactId>spring-security-config</artifactId>
          <version>${spring.security.version}</version>
      </dependency>
    <!--jstl,后边会有简单页面用到-->
      <dependency>
          <groupId>jstl</groupId>
          <artifactId>jstl</artifactId>
          <version>${jstl.version}</version>
      </dependency>
      <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>servlet-api</artifactId>
          <version>${servlet.version}</version>
          <scope>provided</scope>
      </dependency>
      <!--mybatis-->
      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.4.4</version>
      </dependency>
      <!--mybatis与spring整合-->
      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>1.3.0</version>
      </dependency>
      <!--数据库连接池-->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.7</version>
      </dependency>
      <!--数据库连接驱动-->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.41</version>
      </dependency>
      <!--jdbc,需要使用到事务-->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>${spring.version}</version>
      </dependency>
      <!--用于junit测试-->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>${spring.version}</version>
          <scope>test</scope>
      </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
      <plugins>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>3.2</version>
              <configuration>
                  <source>1.8</source>
                  <target>1.8</target>
                  <encoding>UTF-8</encoding>
                  <showWarnings>true</showWarnings>
              </configuration>
          </plugin>
          <!--tomcat7插件-->
          <plugin>
              <groupId>org.apache.tomcat.maven</groupId>
              <artifactId>tomcat7-maven-plugin</artifactId>
              <version>2.1</version>
              <configuration>
                  <!--<prot>8080</prot>-->
                  <path>/ss1</path>
                  <server>tomcat7</server>
              </configuration>
          </plugin>
      </plugins>
  </build>
</project>

4)配置web.xml,与以前差不多。增加了springsecurity过滤器,并且要多配置一个spring-security.xml文件。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <!--springsecurity过滤器-->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name><!--必须是这个名字-->
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern><!--所有资源全部过滤-->
    </filter-mapping>
    <!--spring启动-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:applicationContext.xml
            classpath:spring-security.xml
        </param-value>
    </context-param>
    
    <!--springmvc-->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

5)jdbc.properties

在这里插入图片描述
6)配置spring的xml文件:applicationContext.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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--读取jdbc.properties-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!--数据库连接吃-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxActive" value="10"/><!--最大连接数-->
        <property name="maxWait" value="3000"/><!--等待时间-->
    </bean>
    <!--mybatis和spring整合-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!--别名扫描-->
        <property name="typeAliasesPackage" value="com.scbg.pojo"/>
    </bean>
    <!--mapper接口扫描-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.scbg.mapper"/>
    </bean>
    <!--s事务配置-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--开启事务注解,-->
    <tx:annotation-driven/>

    <context:component-scan base-package="com.scbg.service"/>
</beans>

7)配置spring-mvc.xml,spring-security.xml暂时不用配置,但是为防止不错,先创建文件。
spring-security.xm,默认写如下内容,不然启动报错:

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

      <security:form-login/>

    </security:http>
    <security:authentication-manager>

    </security:authentication-manager>
</beans>

目前项目结构如下,可以没有test,下一步才做的:
在这里插入图片描述

8)创建测试类,测试mapper方法能否正确读取数据。先看下项目结构,缺什么自己加一下:

package com.scbg.mapper;

import com.scbg.mapper.UserMapper;
import com.scbg.pojo.Permission;
import com.scbg.pojo.User;
import org.junit.Test;
import org.junit.internal.runners.JUnit4ClassRunner;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

/**
 * @program: 01_SpringSecurity
 * @description: 用户测试类  测试类报错,我草泥马的不知道为啥
 * @author: 三层饼干儿
 * @create: 2019-08-24 21:49
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class UserMapperTest {
	@Autowired
	private UserMapper userMapper;
	@Test
	public void testFindByUsername(){
		User user = userMapper.findByUsername("三层饼干儿");
		System.out.println(user);
	}
	@Test
	public void testFindPermissionByUsername(){
		List<Permission> list = userMapper.findPermissionByUsername("三层饼干儿");
		for (Permission p:list
			 ) {
			System.out.println(p.getId()+p.getPermName()+p.getPermTag());

		}
	}
}

在此报了一个错,因为我们通常会将mapper.xml放在resources中,且与mapper接口同一层级目录下,如果创建层级目录时是一次创建多层级(如一次性创建com.scbg.mapper目录),会报如下错误:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.scbg.mapper.UserMapper.findByUsername

	at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:225)
	at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:48)
	at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:65)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58)
	at com.sun.proxy.$Proxy16.findByUsername(Unknown Source)
	at com.scbg.mapper.UserMapperTest.testFindByUsername(UserMapperTest.java:25)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

解决:一层层创建,如下是两种差异,长得都不一样,肯定有问题。

在这里插入图片描述
另外,测试类不要以Java开头做包结构,会报错:
在这里插入图片描述

最终测试通过:
在这里插入图片描述
9)完善下controller

package com.scbg.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @program: 01_SpringSecurity
 * @description: 假装项目有产品
 * @author: 三层饼干儿
 * @create: 2019-08-24 23:15
 **/
@Controller
@RequestMapping("/product")
public class ProductController {

	@RequestMapping("/index")
	public String index(){
		return "index";
	}
	@RequestMapping("/add")
	public String add(){
		return "product/productAdd";
	}

	@RequestMapping("/delete")
	public String delete(){
		return "product/productDelete";
	}
	@RequestMapping("/update")
	public String update(){
		return "product/productUpdate";
	}
	@RequestMapping("/list")
	public String list(){
		return "product/productList";
	}
}

package com.scbg.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @program: 01_SpringSecurity
 * @description: 登陆页面等的跳转controller
 * @author: 三层饼干儿
 * @create: 2019-08-24 23:17
 **/
@Controller
public class MainController {
	/**
	 * 跳转登陆页面
	 * @return
	 */
	@RequestMapping("/userLogin")
	public String login(){
		return "login";
	}
	@RequestMapping("/error")
	public String error(){
		
		return "error";
	}

}

10)跟据需求完善下jsp页面,自己写吧

测试一下效果,因为目录放在了WEB-INF下所以直接访问不行,都是通过controller访问的:
在这里插入图片描述
在这里插入图片描述其他页面页确保可以正常访问。

11)重点:
需求:

1>显然对于一个实际项目来说,除了index首页及登陆页面,其他功能页面是不可以在没有登陆的情况下进行访问的。
在security.xml中配置如下:
对所有资源进行拦截,但是单独允许index可以访问

<security:http>
        <!--拦截资源-->
        <security:intercept-url pattern="/product/index" access="permitAll()"/><!--单独放行index页面,不然进行登陆。。。-->
        <security:intercept-url pattern="/**" access="isFullyAuthenticated()"/> <!--其他所有资源全部拦截-->
        <security:form-login />
    </security:http>

重启项目测试:
在这里插入图片描述
但是点击其他任何一个功能,这里我们以商品添加为例,都是会被强制定向到security默认的登陆页面(注意url):
在这里插入图片描述
2>对登录页不满意,可以替换为我们自己的登陆页面
xml文件做如下配置

 <security:http>
        <security:intercept-url pattern="/userLogin" access="permitAll()"/><!--单独放行login页面,不然进行登陆。。。-->
        <security:intercept-url pattern="/product/index" access="permitAll()"/><!--单独放行index页面,不然进行登陆。。。-->
        <security:intercept-url pattern="/**" access="isFullyAuthenticated()"/> <!--其他所有资源全部拦截-->
        <!--表单方式登陆-->
        <security:form-login login-page="/userLogin"/>
    </security:http>

注意:页面都是无法直接访问的,配置的登陆页地址,其实是controller方法的地址。
测试,在index页面点击商品添加进入了定制的登陆页面如下:
在这里插入图片描述

3>接下来当然需要对用户进行验证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值