Oauth2学习笔记

直接上代码了,都是齐雷老师教的,本文为梳理笔记
理解Oauth2.0: http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

创建父工程

1. 创建父工程

用于统一定义SpringBootSpringCloud的版本

<?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.jt</groupId>
    <artifactId>02-sso</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--maven父工程的pom文件中一般要定义子模块,
    子工程中所需依赖版本的管理,公共依赖并且父工程的打包方式一般为pom方式-->

    <!--第一步: 定义子工程中核心依赖的版本管理(注意,只是版本管理)-->
    <dependencyManagement>
        <dependencies>
            <!--spring boot 核心依赖版本定义(spring官方定义)-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--Spring Cloud 微服务规范(由spring官方定义)-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR9</version>
                <type>pom</type><!--假如scope是import,type必须为pom-->
                <scope>import</scope><!--引入三方依赖的版本设计-->
            </dependency>

            <!--Spring Cloud alibaba 依赖版本管理 (参考官方说明)-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.6.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <!--第二步: 添加子工程的所需要的公共依赖-->
    <dependencies>
        <!--lombok 依赖,子工程中假如需要lombok,不需要再引入-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope><!--provided 表示此依赖仅在编译阶段有效-->
        </dependency>
        <!--单元测试依赖,子工程中需要单元测试时,不需要再次引入此依赖了-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope><!--test表示只能在test目录下使用此依赖-->
            <exclusions>
                <exclusion><!--排除一些不需要的依赖-->
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--其它依赖...-->
    </dependencies>
    <!--第三步: 定义当前工程模块及子工程的的统一编译和运行版本-->
    <build><!--项目构建配置,我们基于maven完成项目的编译,测试,打包等操作,
    都是基于pom.xml完成这一列的操作,但是编译和打包的配置都是要写到build元素
    内的,而具体的编译和打包配置,又需要plugin去实现,plugin元素不是必须的,maven
    有默认的plugin配置,常用插件可去本地库进行查看-->
        <plugins>
            <!--通过maven-compiler-plugin插件设置项目
            的统一的jdk编译和运行版本-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <!--假如本地库没有这个版本,这里会出现红色字体错误-->
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2. 创建子module

这里选择maven项目就可以了,以下为pom.xml文件,最简单的只需要引入weboauth2就可以了

<?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">
    <parent>
        <artifactId>sca</artifactId>
        <groupId>com.jt</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sca-auth4</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
    </dependencies>

</project>

3. 创建启动类

如果是用Spring Initializar创建的module可能已经自带了,就不用手工创建了

package com.jt.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

4. 启动测试

观察启动时的console里面输出的信息,找到默认自动生成的密码,就是Using generated security password:后面的内容

2022-01-02 21:42:27.011  INFO 10032 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: 9bf7e6ce-7a49-4dad-b03d-21a953e9b40d

5. 在浏览器中输入网址:http://localhost:8080

6. 在启动类的同包下,创建config.MyUserDetailsService

实现UserDetailsService接口,并实现里面的抽象方法public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException{},可以看到返回值类型是UserDetails,我们可以通过查找其现有的实现类为org.springframework.security.core.userdetails.User。查找方法为:按住Ctrl键,点击重写的public UserDetails loadUserByUsername(String s)方法的返回值,也就是UserDetails,就可以进到UserDetails的源码文件了,此时按Ctrl+H快捷键可以看到其实现结构,在这里插入图片描述
如上图,可以看到其最直接的实现类就是User类了,我们只要创建这个对象并返回就可以了。在其构造函数列表里面可以找到最简单的一个构造如下

/**
* Calls the more complex constructor with all boolean arguments set to {@code true}.
*/
public User(String username, String password,
		Collection<? extends GrantedAuthority> authorities) {
	this(username, password, true, true, true, true, authorities);
}

即传入用户名、密码(加密后的)和权限列表,我们只要把自己的用户相关的信息封装到这个对象里面返回出去,再登陆时就可以使用我们自己的账户信息了

7. 声明PasswordEncoder

由于我们传给系统的是加密后的密码,而且Spring接收到前端传回来的明文密码后也是先加密成密文再进行对比的,所以这里我们先声明一下,我们把下面的代码放到启动类里面

@Bean
  public BCryptPasswordEncoder passwordEncoder(){
      return new BCryptPasswordEncoder();
  }

至于为什么要选这个加密方式,齐老师给的理由是比MD5的方式还要好。

8. 创建UserDetailsService

此时我们就可以在第6步里面声明一个PasswordEncoder的变量,并使用自动注入了,然后我们的密码也可以使用这个来进行加密了,代码如下

package com.jt.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return new User("admin",passwordEncoder.encode("123"), AuthorityUtils.createAuthorityList("sys:user:list"));
    }
}

当前了,如果忘记做第7步的话也是没有关系的,因为SpringBoot启动后,在登陆时后台console里面就会打印出如下错误

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

大概意思就是说没有找到可用的PasswordEncoder

9. 大功告成一半

好了至此,我们的账号就生效了,可以在登陆界面登陆了,登陆成功后,显示404页面,这是因为我们没有定义任何首页的原因,可以在resources文件夹下创建static文件夹,然后在static文件夹下创建一个index.html的文件,如下图所示
在这里插入图片描述

重启项目,并使用我们自己的账号admin/123登陆后就可以看到这个网页了
在这里插入图片描述
以上就是使用我们自己的账号进行登陆的实现了。下面来说如何获取访问令牌和刷新令牌

访问和刷新令牌

1. 放行所有请求

首先为了不影响令牌获取地址的访问,也就是不用再登陆了,我们可以先定义一个WebSecurityConfigurerAdapter的类,来配置放行所有的请求都不用登陆,代码如下

package com.jt.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 重写了一下这个方法,用于获得我们自定义的这个AuthenticationManager的Bean一会儿用得到
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关掉跨域攻击防御的功能
        // 不然一会儿测试的时候还要在请求的头部中添加“referer"信息,比较麻烦
        http.csrf().disable();
        
        //这句的意思是放行所有的请求,都不需要登陆就可以直接访问
        http.authorizeRequests().anyRequest().permitAll();
    }
}

2. JWT令牌。

以下是简书上的一个说明

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
作者:Dearmadman
链接:https://www.jianshu.com/p/576dbf44b2ae
来源:简书

有兴趣的可以去源网页详细了解一下,下面是官网 ,我们先进官网首页就给出了例子。

官网:https://jwt.io/
在这里插入图片描述
可以看到这种令牌是通过小数点分成了三部分,
第一部分称作Header,定义了加密算法和令牌的类型信息。
第二部分是PAYLOAD:DATA也就是我们用来存储相关数据的,比如权限等信息,这样用户过来后我们直接取出权限列表来判断有无权限就可以了,就不用再去查数据库了。
第三部分是VERIFY SIGNATURE,密码给的说明很清晰了,就是把前两部分我们预先定义好的密钥放一起做一个信息摘要,这样如果内容发生了改变,则此摘要也需要跟着改变,但是摘要的计算是需要我们的密钥的,因此客户端在没有密钥的情况下是无法正确修改此令牌里面的内容的。

此处再多说一句,此令牌加密的信息是可以被解读出来的,就是说客户拿到令牌后,就可以到官网看到令牌里面存放的信息的,但是用户没法私自改动,因为第三部分的签名是根据前两部分的内容和我们手里面的密钥共同决定的,我们的服务端在收到此令牌后也是先把令牌里面的信息解析出来后,根据前两部分的信息再加上我们的密钥重新算一遍签名,如果与此令牌里同的签名一致,则说明此令牌没有被更改过,就可以使用了,否则说明令牌被不正确的修改过了,不能使用了的。

3. 定义一个MyTokenService

用来配置信息生成的相关信息

package com.jt.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class MyTokenService {

    //注册成Bean是因为还有别的依赖此对象
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //这个是指生成jwt令牌时,计算第三部分签名时使用的密钥,是需要服务端持有的,不能让客户或第三方知道的
        jwtAccessTokenConverter.setSigningKey("123");
        return jwtAccessTokenConverter;
    }

	
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter){
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

	@Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices(TokenStore tokenStore,JwtAccessTokenConverter jwtAccessTokenConverter){
        DefaultTokenServices ds = new DefaultTokenServices();
        ds.setTokenStore(tokenStore);
        ds.setTokenEnhancer(jwtAccessTokenConverter);
        //开启刷新令牌,否则只能获取到访问令牌,而没有刷新令牌
        ds.setSupportRefreshToken(true);
        //设置访问令牌的有效期
        ds.setAccessTokenValiditySeconds(3600);
        //设置刷新令牌的有效期,一般会设置的比访问令牌有效期长一些
        //以便访问令牌失效后,使用刷新令牌重新获取访问令牌
        ds.setRefreshTokenValiditySeconds(7200);
        return ds;
    }
}
  1. 创建AuthorizationServerConfigurerAdapter的类,用来配置,开放获取令牌的地址、配置第三方用来获取令牌时需要携带的appkeyappsecret、令牌的生成方式
package com.jt.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

@EnableAuthorizationServer
@Configuration
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    //提供一个认证的入口(客户端去哪里认证)?(http://ip:port/.....)
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //super.configure(security);
        //对外发布认证入口(/oauth/token),认证通过服务端会生成一个令牌
        security.tokenKeyAccess("permitAll()")
                //对外发布检查令牌的入口(/oauth/check_token)
                .checkTokenAccess("permitAll()")
                //允许用户通过表单方式提交认证,完成认证
                .allowFormAuthenticationForClients();
    }
    //定义客户端应该携带什么信息去认证?
    //指明哪些对象可以到这里进行认证(哪个客户端对象需要什么特点)。
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //super.configure(clients);
        clients.inMemory()
                //客户端标识
                .withClient("appkey")
                //客户端密钥(随意)
                .secret(passwordEncoder.encode("appsecret"))
                //指定认证类型(码密,刷新令牌,三方令牌,...)
                .authorizedGrantTypes("password","refresh_token")
                //作用域(在这里可以理解为只要包含我们规定信息的客户端都可以进行认证)
                .scopes("all");
    }
    //提供一个负责认证授权的对象?(完成客户端认证后会颁发令牌,默认令牌格式是uuid方式的)
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //super.configure(endpoints);
        //设置认证授权对象
        endpoints.authenticationManager(authenticationManager)
                //设置令牌业务对象(此对象提供令牌创建及有效机制设置)
                .tokenServices(authorizationServerTokenServices)//不写,默认是uuid
                //设置允许对哪些请求方式进行认证(默认支支持post):可选
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
    }
}
  1. 至此,所有的配置都完成了,打开浏览器,输入下面的网址

http://localhost:8080/oauth/token?client_id=appkey&client_secret=appsecret&grant_type=password&username=admin&password=123

应该就可以看到jwt格式的访问令牌以及刷新令牌了

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDEyOTU5MDYsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI1YWRjNTFjOC1iZGEwLTRkYjMtOTE1OS00MWU2ZjEyN2YzN2IiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.3ZUhpBdljrt4d9tgxtqHHjFopq1AATAW7ODOHY2FuzY",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI1YWRjNTFjOC1iZGEwLTRkYjMtOTE1OS00MWU2ZjEyN2YzN2IiLCJleHAiOjE2NDEzMzE5MDYsImF1dGhvcml0aWVzIjpbInN5czpyZXM6Y3JlYXRlIiwic3lzOnJlczpsaXN0Iiwic3lzOnJlczpkZWxldGUiXSwianRpIjoiMTc0YmFkMzMtY2I0ZS00ZjM5LTlhYjktMjVkYjIxNGJiYjczIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQifQ.2wyxpuQhOaK2npkSHg075At7tiQl4bPGmYUMJEq-EBM",
    "expires_in": 35999,
    "scope": "all",
    "jti": "5adc51c8-bda0-4db3-9159-41e6f127f37b"
}

可以通过以下方式使用刷新令牌重新获得令牌,refresh_token后面跟的是刚获取到的刷新令牌的值

http://localhost:8080/oauth/token?client_id=appkey&client_secret=appsecret&grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI1YWRjNTFjOC1iZGEwLTRkYjMtOTE1OS00MWU2ZjEyN2YzN2IiLCJleHAiOjE2NDEzMzE5MDYsImF1dGhvcml0aWVzIjpbInN5czpyZXM6Y3JlYXRlIiwic3lzOnJlczpsaXN0Iiwic3lzOnJlczpkZWxldGUiXSwianRpIjoiMTc0YmFkMzMtY2I0ZS00ZjM5LTlhYjktMjVkYjIxNGJiYjczIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQifQ.2wyxpuQhOaK2npkSHg075At7tiQl4bPGmYUMJEq-EBM

就可以获得新的令牌了

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDEyOTY0OTEsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI4MTAyMDJjZC0yNDM3LTQ4MTItOGJkNi0zOWEwODNiN2I3MDMiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.2qldnzS_eJ0yqSobMMdZyNIbCExcC-E6YJt8-utu9eI",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI4MTAyMDJjZC0yNDM3LTQ4MTItOGJkNi0zOWEwODNiN2I3MDMiLCJleHAiOjE2NDEzMzE5MDYsImF1dGhvcml0aWVzIjpbInN5czpyZXM6Y3JlYXRlIiwic3lzOnJlczpsaXN0Iiwic3lzOnJlczpkZWxldGUiXSwianRpIjoiMTc0YmFkMzMtY2I0ZS00ZjM5LTlhYjktMjVkYjIxNGJiYjczIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQifQ.14mCgt6PP0Zon3OwwUCAkS4XgNPGIgASoRajKhwaxac",
    "expires_in": 35999,
    "scope": "all",
    "jti": "810202cd-2437-4812-8bd6-39a083b7b703"
}

注意:访问令牌和刷新令牌是不能混用的,通过解码可以发现,刷新令牌携带的信息比访问令牌多了一个ati属性,可能就是据此来区分两个令牌的

  • 可以通过下面的地址来检查令牌的有效性

http://localhost:8080/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDEyOTY0OTEsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI4MTAyMDJjZC0yNDM3LTQ4MTItOGJkNi0zOWEwODNiN2I3MDMiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.2qldnzS_eJ0yqSobMMdZyNIbCExcC-E6YJt8-utu9eI

如果令牌尚在有效期,则返回如下内容

{
    "user_name": "admin",
    "scope": [
        "all"
    ],
    "active": true,
    "exp": 1641296491,
    "authorities": [
        "sys:res:create",
        "sys:res:list",
        "sys:res:delete"
    ],
    "jti": "810202cd-2437-4812-8bd6-39a083b7b703",
    "client_id": "gateway-client"
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

水晶心泉

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值