SpringSecurity安全控件使用指南(1),2024年最新啃下这些Framework技术笔记

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注网络安全)
img

正文

SpringSecurity简介

1、简介

2、Security适配器

SpringSecurity核心类

1、Authentication类

2、SecurityContextHolder类

3、UserDetails类

4、UserDetailsService类

5、GrantedAuthority类

6、DaoAuthenticationProvider类

7、PasswordEncoder类

SpringSecurity的验证机制

1、SpringSecurity的验证机制

搭建项目

1、新建Springboot项目

2、项目结构。

编写代码

1、登录页面

2、登录成功页面

3、Security配置类

4、认证成功处理类LoginSuccessHandle

5、测试控制器

6、测试应用

用户和权限保存在数据库中

1、修改后项目的目录结构。

2、添加依赖

3、配置基本属性。

4、创建用户类和角色权限实体类

5、创建数据访问接口

6、创建自定义服务类Service

7、修改配置类

8、测试应用

9、案例下载


SpringSecurity简介


1、简介

SpringSecurity是一个能够基于Spring的应用程序提供声明式安全保护的安全性框架,它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了SpringIOC(控制反转)和AOP(面向切面编程)功能,为应用系统提供安全访问控制功能,减少了为系统安全控制编写大量重复代码的工作。

官网地址:https://spring.io/projects/spring-security

安全框架主要包含两个操作。

认证:确认用户可以访问当前系统。

授权:确定用户在当前系统中是否能够执行某个操作,即用户所拥有的功能权限。

2、Security适配器

创建一个自定义类继承WebSecurityConfigurerAdapter,并在改类中使用@EnableWebSecurity注解就可以通过重写config方法类配置所需要的安全配置。

WebSecurityConfigurerAdapter是SpringSecurity为Web应用提供的一个适配器,实现了WebSecurityConfigurerAdapter接口,提供了两个方法用于重写开发者需要的安全配置。

protected void configure(HttpSecurity http) throws Exception {

}

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

}

configure(HttpSecurity http)方法中可以通过HTTPSecurity的authorizeRequests()方法定义那些URL需要被保护、那些不需要被保护;通过formLogin()方法定义当前需要用户登录的时候,跳转到的登录页面。

configure(AuthenticationManagerBuilder auth)方法用于创建用户和用户的角色。

用户认证

SpringSecurity是通过configure(AuthenticationManagerBuilder auth)完成用户认证的。使用AuthenticationManagerBuilder的inMemoryAuthentication()方法可以添加用户,并给用户指定权限。

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication().withUser(“aaa”).password(“{noop}1234”).roles(“USER”);

auth.inMemoryAuthentication().withUser(“admin”).password(“{noop}admin”).roles(“ADMIN”, “DBA”);

}

上面的代码中添加了两个用户,其中一个用户名是“aaa”,密码是“1234”,用户权限是“USER”;另一个用户名是“admin”,密码是“admin”,用户权限有两个,分别是“ADMIN”和“DBA”。需要注意的是SpringSecurity保存用户权限的时候,会默认使用“ROLE_”,也就是说“USER”实际上是“ROLE_USER”,“ADMIN”实际是“ROLE_ADMIN”,“DBA”实际上是“ROLE_DBA”。

当然,也可以查询数据库获取用户和权限。下面有写到。

用户授权

SpringSecurity是通过configure(HttpSecurity http)完成用户授权的。

HTTPSecurity的authorizeRequests()方法有多个子节点,每个macher按照它们的声明顺序执行,指定用户可以访问的多个URL模式。

  • antMatchers使用Ant风格匹配路径。

  • regexMatchers使用正则表达式匹配路径。

在匹配了请求路径后,可以针对当前用户的信息对请求路径进行安全处理。下表是SpringSecurity提供的安全处理方法。

方法用途
anyRequest匹配所有请求路径
access(String)Spring EL 表达式结果为true时可以访问
anonymous()匿名可以访问
denyAll()用户不能访问
fullyAuthenticated()用户完全认证可以访问(非remember-me下自动登录)
hasAnyAuthority(String...)如果有参数,参数表示权限,则其中任何一个权限可以访问
hasAnyRole(String...)如果有参数,参数表示角色,则其中任何一个角色可以访问
hasAuthority(String...)如果有参数,参数表示权限,则其权限可以访问
hasIpAddress(String)如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
hasRole(String)如果有参数,参数表示角色,则其角色可以访问
permitAll()用户可以任意访问
rememberMe()允许通过remember-me登录的用户访问
authenticated()用户登录后可访问

示例代码如下:

@Override

protected void configure(HttpSecurity http) throws Exception {

http.csrf().disable(); //安全器令牌

http.authorizeRequests()

.antMatchers(“/login”).permitAll()

.antMatchers(“/”, “/home”).hasRole(“USER”)

.antMatchers(“/admin/**”).hasAnyRole(“ADMIN”, “DBA”)

.anyRequest().authenticated()

.and()

.formLogin()

.loginPage(“/login”)

.usernameParameter(“loginName”).passwordParameter(“password”)

.defaultSuccessUrl(“/main”)

.failureUrl(“/login?error”)

.and()

.logout()

.permitAll()

.and()

.exceptionHandling().accessDeniedPage(“/accessDenied”);

}

  1. http.authorizeRequests() 开始请求权限配置。

  2. antMatchers(“/login”).permitAll() 请求匹配“/login”,所有用户都可以访问。

  3. antMatchers(“/”, “/home”).hasRole(“USER”)  请求匹配“/”和“/home”,拥有“ROLE_USER”角色的用户可以访问。

  4. antMatchers(“/admin/**”).hasAnyRole(“ADMIN”, “DBA”)  请求匹配“/admin/**”,拥有“ROLE_ADMIN”或“ROLE_DBA”角色的用户可以访问。

  5. anyRequest().authenticated()  其余所有的请求都需要认证(用户登录)之后才可以访问。

  6. formLogin()  开始设置登录操作

  7. loginPage(“/login”)  设置登录页面的访问地址

  8. usernameParameter(“loginName”).passwordParameter(“password”)  登录时接收传递的参数“loginName”的值作为用户名,接收传递参数的“password”的值作为密码。如果不设置,默认“username”为用户名,“password”为密码

  9. defaultSuccessUrl(“/main”)  指定登录成功后转向的页面。

  10. failureUrl(“/login?error”)  指定登录失败后转向的页面和传递的参数。

  11. logout()   设置注销操作

  12. permitAll()  所有用户都可以访问。

  13. exceptionHandling().accessDeniedPage(“/accessDenied”)  指定异常处理页面

SpringSecurity核心类


SpringSecurity核心类包括Authentication、SecurityContextHolder、UserDetails、UserDetailsService、GrantedAuthority、DaoAuthenticationProvider和PasswordEncoder。只要掌握了这些SpringSecurity核心类,SpringSecurity就会变得非常简单。

1、Authentication类

Authentication用来表示用户认证信息,用户登录认证之前,SpringSecurity会将相关信息封装为一个Authentication具体实现类的对象,在登录认证成功之后又会生成一个信息更全面、包含用户权限等信息的Authentication对象,然后把它保存在SecurityContextHolder所持有的SecurityContext中,供后续的程序进行调用,如访问权限的鉴定等。

SecurityContextHolder中的getContext()方法

public static SecurityContext getContext() {

return strategy.getContext();

}

2、SecurityContextHolder类

SecurityContextHolder是用来保存SecurityContext的。SecurityContext中含有当前所访问系统的用户的详细信息。默认情况下,SecurityContextHolder将使用ThreadLocal来保存SecurityContext,这也就意味着在处于同一线程的方法中,可以从ThreadLocal获取到当前的SecurityContext。

SpringSecurity使用一个Authentication对象来描述当前用户的相关信息。SecurityContextHolder中持有的是当前用户的SecurityContext,而SecurityContext持有的是代表当前用户相关信息的Authentication的引用。这个Authentication对象不需要我们自己创建,在与系统交互的过程中,SpringSecurity会自动创建相应的Authentication对象,然后赋值给当前的SecurityContext。开发过程中常常需要在程序中获取当前用户的相关信息,比如最常见的获取当前登录用户的用户名。

String username = SecurityContextHolder.getContext().getAuthentication().getName();

获取UserDetails类,该类中包含用户认证相关等信息。

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

3、UserDetails类

UserDetails是SpringSecurity的一个核心接口。其中定义了一些可以获取用户名,密码、权限等与认证相关的信息的方法。SpringSecurity内部使用UserDetails实现类大都是内置的User类,要使用UserDetails,也可以直接使用该类。在SpringSecurity内部,很多需要使用用户信息的时候,基本上都是使用UserDetails,比如在登录认证的时候。

通常需要在应用中获取当前用户的其他信息,如E-mail、电话等。这时存放在Authentication中的principal只包含认证相关信息的UserDetails对象可能就不能满足我们的要求了。这时可以实现自己的UserDetails,在该实现类中可以定义一些获取用户其他信息的方法,这样将来就可以直接从当前SecurityContext的Authentication的principal中获取这些信息。

UserDetails是通过UserDetailsService的loadUserByUsername()方法进行加载的,UserDetailsService也是一个接口,我们也需要实现自己的UserDetailsService来加载自定义的UserDetails信息。

新建用户表的Service类实现UserDetailsService接口,来重写UserDetailsService的loadUserByUsername()方法,根据用户名查询当前用户信息并返回,返回的类必须继承Security内置的User类。

@Service

public class SysUserService implements UserDetailsService{

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{

}

}

4、UserDetailsService类

Authentication.getPrincipal() 的返回类型是Object。

Object getPrincipal();

但很多情况下返回的其实是一个UserDetails的实例。登录认证的时候SpringSecurity会通过UserDetailsService的loadUserByUsername()方法获取对应的UserDetails进行认证,认证通过后会将该UserDetails赋给认证通过的Authentication的Principal,然后在把该Authentication存入SecurityContext。之后如果需要使用用户信息,可以通过SecurityContextHolder获取存放在SecurityContext中的Authentication的principal,转为UserDetails类。

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

5、GrantedAuthority类

Authentication的getAuthorities()方法可以返回当前Authentication对象用户的所有的权限。

Collection<? extends GrantedAuthority> getAuthorities();

即当前用户拥有的权限。其返回值是一个GrantedAuthority类型的数组,每一个GrantedAuthority对象代表赋予给当前用户的一种权限。GrantedAuthority是一个接口,其通常是通过UserDetailsService进行加载,然后赋予UserDetails的。

GrantedAuthority中只定义了一个getAuthority()方法,该方法返回一个字符串,表示对应的权限,如果对应权限不能用字符串表示,则返回null

获取当前用户的所有权限,存放到List集合中。

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

List roleCodes=new ArrayList<>();

for (GrantedAuthority authority : userDetails.getAuthorities()) {

roleCodes.add(authority.getAuthority());

}

6、DaoAuthenticationProvider类

SpringSecurity默认了会使用DaoAuthenticationProvider实现AuthenticationProvider接口,专门进行用户认证的处理。DaoAuthenticationProvider在进行认证的时候需要一个UserDetailsService来获取用户的信息UserDetails,其中包括用户名,密码和所拥有的权限等。如果需要改变认证的方式,开发者可以实现自己的AuthenticationProvider。

7、PasswordEncoder类

在SpringSecurity中,对密码的加密都是由PasswordEncoder来完成的。在SpringSecurity中,已经对PasswordEncoder有了很多实现,包括md5加密,SHA-256加密等,开发者只需要直接拿来用就可以。在DaoAuthenticationProvider中,有一个就是PasswordEncoder熟悉,密码加密功能主义靠它来完成。

SpringSecurity的验证机制


1、SpringSecurity的验证机制

SpringSecurity大体上是由一堆Filter实现的,Filter会在SpringMVC前拦截请求。Filter包括登出Filter(LogoutFilter)、用户名密码验证Filter(UsernamePasswordAuthenticationFilter)之类。Filter在交由其他组件完成细分的功能,最常用的UsernamePasswordAuthenticationFilter会持有一个AuthenticationManager引用,AuthenticationManager是一个验证管理器,专门负责验证。但AuthenticationManager本身并不做具体的验证工作,AuthenticationManager持有一个AuthenticationProvider集合,AuthenticationProvider才是做验证工作的组件,验证成功或失败之后调用对应的Handler。

搭建项目


1、新建Springboot项目

创建一个SpringBoot项目,添加SpringSecurity和Thymeleaf依赖。

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”>

4.0.0

org.springframework.boot

spring-boot-starter-parent

2.2.2.RELEASE

com.mcy

security-demo

0.0.1-SNAPSHOT

security-demo

Demo project for Spring Boot

<java.version>1.8</java.version>

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-thymeleaf

org.thymeleaf.extras

thymeleaf-extras-springsecurity5

org.springframework.boot

spring-boot-starter-test

test

org.junit.vintage

junit-vintage-engine

org.springframework.security

spring-security-test

test

org.springframework.boot

spring-boot-maven-plugin

2、项目结构。

编写代码


1、登录页面

用于测试的HTML页面,用了bootstrap的样式文件。

login.html登录页面,主要有一个form表单和账号密码输入框,用于向login请求提交username和password,从而进行登录。Security默认登录地址login,退出地址为logout。代码如下。

Security登录
登录

用户名或密码错误!

注销成功!

//sec:authorize可以判断用户是否登录,用户权限,设置该菜单是否显示

未登录显示

登录显示

用于用户名或密码错误提示。

用户名或密码错误!

退出,注销提示。

注销成功!

注意这些提示都是SpringSecurity里边自带的。

sec:authorize用于判断用户是否登录,用户是否拥有哪些角色权限,一般在前台页面控制菜单是否显示。

2、登录成功页面

home.html是ROLE_USER用户登录之后显示的页面,同时提供了一个超链接到admin页面,代码如下。

Home页面

Home页面

欢迎[用户名]访问Home页面!

您的权限是:权限

访问admin页面

安全退出

admin.html是ROLE_ADMIN用户登录之后显示的页面,同时提供了一个到dba页面的超链接,代码如下。

Admin页面

Admin页面

欢迎[用户名]访问Admin页面!

您的权限是:权限

访问dba页面

安全退出

dba.html页面只是显示简单的欢迎语句,代码如下。

dba页面

dba页面

欢迎[用户名]访问dba页面!

您的权限是:权限

安全退出

未登录显示

登录显示

权限包含ROLE_ADMIN显示

权限不包含ROLE_ADMIN登录显示

accessDenied.html是访问拒绝页面,如果登录的用户没有权限访问该页面,会进行提示,代码如下。

访问拒绝页面

AccessDenied页面

欢迎[用户名],您没有权限访问页面!

您的权限是:权限

安全退出

3、Security配置类

在项目中新建一个security包,在该包下新建一个WebSecurityConfig类,继承WebSecurityConfigurerAdapter类,用于处理SpringSecurity的用户认证和授权操作,设置页面访问权限。

configure(AuthenticationManagerBuilder auth)和configure(HttpSecurity http)两个方法中分别打印了一句话,用于启动项目时的跟踪调试。

successHandler(new LoginSuccessHandle())用于处理登出成功之后的操作,LoginSuccessHandle类用于处理不同用户跳转到不同页面。

具体代码如下。

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/**

  • 用户认证操作

  • @param auth

  • @throws Exception

*/

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

System.out.println(“WebSecurityConfig configure(AuthenticationManagerBuilder auth) 方法被调用。。。。。。”);

//添加用户,并给予权限

auth.inMemoryAuthentication().withUser(“aaa”).password(“{noop}1234”).roles(“USER”);

auth.inMemoryAuthentication().withUser(“admin”).password(“{noop}admin”).roles(“ADMIN”, “DBA”);

}

/**

  • 用户授权操作

  • @param http

  • @throws Exception

*/

@Override

protected void configure(HttpSecurity http) throws Exception {

System.out.println(“WebSecurityConfig configure(HttpSecurity http) 方法被调用。。。。。。”);

http.csrf().disable(); //安全器令牌

http.authorizeRequests() //开始请求权限配置。

.antMatchers(“/login”, “/static/**”).permitAll()

.antMatchers(“/”, “/home”).hasRole(“USER”)

.antMatchers(“/admin/**”).hasAnyRole(“ADMIN”, “DBA”)

.anyRequest().authenticated() //其余所有的请求都需要认证(用户登录)之后才可以访问。

.and()

.formLogin() //开始设置登录操作

.loginPage(“/login”) //设置登录页面的访问地址

//.defaultSuccessUrl(“/main”)  //指定登录成功后转向的页面。

.successHandler(new LoginSuccessHandle()) //登录成功跳转,LoginSuccessHandle处理不同权限跳转不同页面

.failureUrl(“/login?error”) //指定登录失败后转向的页面和传递的参数。

.and()

.logout().permitAll() //退出

.and()

.exceptionHandling().accessDeniedPage(“/accessDenied”); //指定异常处理页面

}

}

4、认证成功处理类LoginSuccessHandle

认证成功处理类LoginSuccessHandle,实现了AuthenticationSuccessHandler接口,是Spring用来处理用户认证授权并跳转到指定URL的。

重新onAuthenticationSuccess方法,获取当前用户的权限,根据权限跳转到指定的URL路径,代码如下。

import javax.servlet.Servlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.authority.AuthorityUtils;

import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import java.io.IOException;

import java.util.Set;

public class LoginSuccessHandle implements AuthenticationSuccessHandler {

@Override

public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException {

//authentication.getAuthorities() 获取当前用户的权限

Set roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());

//获取到登陆者的权限,然后做跳转

if (roles.contains(“ROLE_ADMIN”)){

response.sendRedirect(“/admin”);

return;

}else if (roles.contains(“ROLE_USER”)){

response.sendRedirect(“/home”);

return;

}else {

response.sendRedirect(“/accessDenied”);

}

}

}

5、测试控制器

新建一个IndexController控制器,提供响应login,home,admin,dba,AccessDenied请求的方法。每个方法通过getUsername()方法获得当前认证用户的用户名,通过getAuthorith()方法获取当前认证用户的权限,并设置到ModelMap当中。

import org.springframework.security.core.Authentication;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

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

import org.springframework.web.bind.annotation.RequestMethod;

import java.util.ArrayList;

import java.util.List;

@Controller

public class IndexController {

@RequestMapping(value = {“/”, “/login”}, method = RequestMethod.GET)

public String index(){

return “login”;

}

@RequestMapping(“/home”)

public String homePage(ModelMap map){

map.put(“user”, getUsername());

map.put(“role”, getAuthority());

return “home”;

}

@RequestMapping(“/admin”)

public String admin(ModelMap map){

map.put(“user”, getUsername());

map.put(“role”, getAuthority());

return “admin”;

}

@RequestMapping(“/dba”)

public String dba(ModelMap map){

map.put(“user”, getUsername());

map.put(“role”, getAuthority());

return “dba”;

}

@RequestMapping(“/accessDenied”)

public String accessDenied(ModelMap map){

学习路线:

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

import org.springframework.web.bind.annotation.RequestMethod;

import java.util.ArrayList;

import java.util.List;

@Controller

public class IndexController {

@RequestMapping(value = {“/”, “/login”}, method = RequestMethod.GET)

public String index(){

return “login”;

}

@RequestMapping(“/home”)

public String homePage(ModelMap map){

map.put(“user”, getUsername());

map.put(“role”, getAuthority());

return “home”;

}

@RequestMapping(“/admin”)

public String admin(ModelMap map){

map.put(“user”, getUsername());

map.put(“role”, getAuthority());

return “admin”;

}

@RequestMapping(“/dba”)

public String dba(ModelMap map){

map.put(“user”, getUsername());

map.put(“role”, getAuthority());

return “dba”;

}

@RequestMapping(“/accessDenied”)

public String accessDenied(ModelMap map){

学习路线:

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)
[外链图片转存中…(img-A5F201Fo-1713365137196)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值