SpringSecurity OAuth2.0认证授权-part2

此篇文章包含oauth2项目搭建、整合jwt、授权方式测试;

篇幅过长,拆分为:

part1: 认证授权原理回顾及分布式系统认证方案;

part2: oauth2项目搭建、授权方式测试;

part3: 整合jwt、网关、注册中心 完善整套OAuth2.0

------------------------------------------------------------------------------------

3. OAuth2.0

3.1 OAuth2.0介绍

OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向 后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服 务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。

Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。

参考:oAuth_百度百科

Oauth协议:RFC 6749 - The OAuth 2.0 Authorization Framework 

下边分析一个Oauth2认证的例子,通过例子去理解OAuth2.0协议的认证流程,本例子是某奇艺网站使用微信 认证的过程,这个过程的简要描述如下:

用户借助微信认证登录某奇艺网站,用户就不用单独在某奇艺注册用户,怎么样算认证成功吗?某奇艺网站需要成功从微信获取用户的身份信息则认为用户认证成功,那如何从微信获取用户的身份信息?用户信息的 拥有者是用户本人,微信需要经过用户的同意方可为某奇艺网站生成令牌,某奇艺网站拿此令牌方可从微 信获取用户的信息。

1、客户端请求第三方授权 用户进入黑马程序的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。

 点击“微信”出现一个二维码,此时用户扫描二维码,开始给某奇异授权

 2、资源拥有者同意给客户端授权 资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微 信会询问用户是否给授权某奇异访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会 颁发一个授权码,并重定向到某奇异的网站。

3、客户端获取到授权码,请求认证服务器申请令牌 此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。

4、认证服务器向客户端响应令牌 微信认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。 此交互过程用户看不到,当客户端拿到令牌后,用户在黑马程序员看到已经登录成功。

5、客户端请求资源服务器的资源 客户端携带令牌访问资源服务器的资源。 黑马程序员网站携带令牌请求访问微信服务器获取用户的基本信息。

6、资源服务器返回受保护资源 资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。

以上认证授权详细的执行流程如下:

通过上边的例子我们大概了解了OAauth2.0的认证过程,下边我们看OAuth2.0认证流程:

引自OAauth2.0协议rfc6749 RFC 6749 - The OAuth 2.0 Authorization Framework 

OAauth2.0包括以下角色:

1、客户端

本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏 览器端)、微信客户端等。

2、资源拥有者

通常为用户,也可以是应用程序,即该资源的拥有者。

3、授权服务器(也称认证服务器)

用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。

4、资源服务器

存储资源的服务器,本例子为微信存储的用户信息。

现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会 给准入的接入方一个身份,用于接入时的凭据:

client_id:客户端标识 client_secret:客户端秘钥

因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端

3.2 Spring Cloud Security OAuth2

3.2.1 环境介绍

Spring-Security-OAuth2是对OAuth2的一种实现,并且跟我们之前学习的Spring Security相辅相成,与Spring Cloud体系的集成也非常便利,接下来,我们需要对它进行学习,最终使用它来实现我们设计的分布式认证授权解 决方案。

OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用 同一个授权服务的多个资源服务。

授权服务 (Authorization Server)应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌 的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的endpoints:

  • AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。

  • TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。 资源服务 (Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴 权等,下面的过滤器用于实现 OAuth 2.0 资源服务:

  • OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。

认证流程如下:

1、客户端请求oauth授权服务进行认证。

2、认证通过后由oauth颁发令牌。

3、客户端携带令牌Token请求资源服务。

4、资源服务校验令牌的合法性,合法即返回资源信息。

3.2.2 环境搭建

3.2.2.1 父工程

创建maven工程作为父工程,依赖如下:

<?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.panghl</groupId>
    <artifactId>oauth2-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>security-eureka</module>
        <module>security-gateway</module>
        <module>security-order</module>
        <module>security-auth</module>
    </modules>
    <packaging>pom</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
    </parent>
​
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
​
    <dependencyManagement>
        <dependencies>
​
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
​
​
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
                <scope>provided</scope>
            </dependency>
​
            <dependency>
                <groupId>javax.interceptor</groupId>
                <artifactId>javax.interceptor-api</artifactId>
                <version>1.2.2</version>
            </dependency>
​
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
            </dependency>
​
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.0</version>
            </dependency>
​
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.21</version>
            </dependency>
​
​
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.9.RELEASE</version>
            </dependency>
​
​
            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.2.RELEASE</version>
            </dependency>
​
​
        </dependencies>
    </dependencyManagement>
​
​
    <build>
        <finalName>${project.name}</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <!--<plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>-->
​
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
​
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <encoding>utf-8</encoding>
                    <useDefaultDelimiters>true</useDefaultDelimiters>
                </configuration>
            </plugin>
        </plugins>
    </build>
​
</project>

3.2.2.2 创建oauth授权服务工程

1、创建security-auth

创建security-auth作为授权服务工程,依赖如下:

<?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>oauth2-parent</artifactId>
        <groupId>com.panghl</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>com.panghl</groupId>
    <artifactId>security-auth</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
​
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
​
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>
​
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
​
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
​
</project>

工程结构如下:

2、启动类

/**
 * @Author panghl
 * @Date 2022/2/20
 * 授权服务
 **/
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = {"com.panghl.auth"})
public class OauthServer {
​
    public static void main(String[] args) {
        SpringApplication.run(OauthServer.class,args);
    }
}

3、配置文件

在resources下创建application.yml

server:
  port: 8005
spring:
  application:
    name: security-auth
​
  datasource:
    url: jdbc:mysql://192.168.31.66:3306/oauth2?serverTimezone=UTC&useSSL=false&autoReconnect=true&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf8
    username: root
    password: password
  main:
    allow-bean-definition-overriding: true
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://127.0.0.1:8001/eureka
​
feign:
  hystrix:
    enabled: true

3.2.2.3 创建Order资源服务工程

本工程为Order订单服务工程,访问本工程的资源需要认证通过。

本工程的目的主要是测试认证授权的功能,所以不涉及订单管理相关业务。

1、创建Order工程

<?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>oauth2-parent</artifactId>
        <groupId>com.panghl</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>com.panghl</groupId>
    <artifactId>security-order</artifactId>
    <dependencies>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>
​
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
​
​
    </dependencies>
​
</project>

2、工程结构

3、配置文件

server:
  port: 8004
spring:
  application:
    name: security-order
​
  datasource:
    url: jdbc:mysql://192.168.31.66:3306/oauth2?serverTimezone=UTC&useSSL=false&autoReconnect=true&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf8
    username: root
    password: password
  main:
    allow-bean-definition-overriding: true
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://127.0.0.1:8001/eureka

DB环境准备

CREATE DATABASE `oauth2`

CREATE TABLE `oauth_client_details` (
  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端标\r\n识',
  `resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '接入资源列表',
  `client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '客户端秘钥',
  `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `access_token_validity` int DEFAULT NULL,
  `refresh_token_validity` int DEFAULT NULL,
  `additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `archived` tinyint DEFAULT NULL,
  `trusted` tinyint DEFAULT NULL,
  `autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='接入客户端信息';

INSERT INTO oauth2.oauth_client_details
(client_id, resource_ids, client_secret, `scope`, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, create_time, archived, trusted, autoapprove)
VALUES('c1', 'res1', '$2a$10$ef4BQKzyliCf9db9DUKYme83iPbCBP9ZMXpBamK1E94kWquv3Qx7q', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', NULL, 7200, 259200, NULL, '2022-02-20 08:15:12', 0, 0, 'false');
INSERT INTO oauth2.oauth_client_details
(client_id, resource_ids, client_secret, `scope`, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, create_time, archived, trusted, autoapprove)
VALUES('c2', 'res2', '$2a$10$ef4BQKzyliCf9db9DUKYme83iPbCBP9ZMXpBamK1E94kWquv3Qx7q', 'ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', NULL, 31536000, 2592000, NULL, '2022-02-20 08:15:13', 0, 0, 'false');


-- oauth2.oauth_code definition

CREATE TABLE `oauth_code` (
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `authentication` blob,
  KEY `code_index` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;

CREATE TABLE `t_user` (
  `id` bigint NOT NULL COMMENT '用户id',
  `username` varchar(64) NOT NULL,
  `password` varchar(64) NOT NULL,
  `fullname` varchar(255) NOT NULL COMMENT '用户姓名',
  `mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
INSERT INTO oauth2.t_user
(id, username, password, fullname, mobile)
VALUES(1, '张三', '$2a$10$cRGf/jYto/ciEB14IyJxreq1ZG7GVjN.Bm9NcktzFx4gJ8/LOsyAq', '111', '11');

-- oauth2.t_role definition

CREATE TABLE `t_role` (
  `id` varchar(32) NOT NULL,
  `role_name` varchar(255) DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `status` char(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
INSERT INTO oauth2.t_role
(id, role_name, description, create_time, update_time, status)
VALUES('1', '管理员', NULL, NULL, NULL, '');
-- oauth2.t_user_role definition

CREATE TABLE `t_user_role` (
  `user_id` varchar(32) NOT NULL,
  `role_id` varchar(32) NOT NULL,
  `create_time` datetime DEFAULT NULL,
  `creator` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
INSERT INTO oauth2.t_user_role
(user_id, role_id, create_time, creator)
VALUES('1', '1', NULL, NULL);
-- oauth2.t_permission definition

CREATE TABLE `t_permission` (
  `id` varchar(32) NOT NULL,
  `code` varchar(32) NOT NULL COMMENT '权限标识符',
  `description` varchar(64) DEFAULT NULL COMMENT '描述',
  `url` varchar(128) DEFAULT NULL COMMENT '请求地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
INSERT INTO oauth2.t_permission
(id, code, description, url)
VALUES('1', 'p1', '测试资源
1', '/r/r1');
INSERT INTO oauth2.t_permission
(id, code, description, url)
VALUES('2', 'p3', '测试资源2', '/r/r2');
-- oauth2.t_role_permission definition

CREATE TABLE `t_role_permission` (
  `role_id` varchar(32) NOT NULL,
  `permission_id` varchar(32) NOT NULL,
  PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

INSERT INTO oauth2.t_role_permission
(role_id, permission_id)
VALUES('1', '1');
INSERT INTO oauth2.t_role_permission
(role_id, permission_id)
VALUES('1', '2');

3.2.2.授权服务器配置

3.2.2.1 EnableAuthorizationServer

可以用 @EnableAuthorizationServer 注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权 服务器。

在Config包下创建AuthorizationServer: [配置信息后面会说明]

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    //客户端详情服务
    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 令牌管理服务
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService); // 客户端信息服务
        service.setSupportRefreshToken(true);// 是否产生刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        // 令牌增强
        //TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        //tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
        //service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        //设置授权码模式的授权码如何存取,暂时采用内存方式
        return new InMemoryAuthorizationCodeServices();
    }


    /**
     * 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在
     * 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // clients.withClientDetails(clientDetailsService);
        // 暂时使用内存方式
        clients.inMemory()// 使用in‐memory存储
                .withClient("c1")// client_id
                .secret(new BCryptPasswordEncoder().encode("secret")) //客户端密钥
                .resourceIds("res1") //资源列表
                .authorizedGrantTypes("authorization_code",
                        "password","client_credentials","implicit","refresh_token")
                // 该client允许的授权类型 uthorization_code,password,refresh_token,implicit,client_credentials
               .scopes("all")// 允许的授权范围
                .autoApprove(false)// false 跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }

    /**
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     * /oauth/authorize:授权端点。
     * /oauth/token:令牌端点。
     * /oauth/confirm_access:用户确认授权提交端点。
     * /oauth/error:授权服务错误信息端点。
     * /oauth/check_token:用于资源服务访问的令牌解析端点。
     * /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //密码模式需要
                .authorizationCodeServices(authorizationCodeServices) //授权码模式需要
                .tokenServices(tokenService()) // 令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST); //允许POST提交
    }


    /**
     * 用来配置令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")  //tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。
                .checkTokenAccess("permitAll()") //checkToken这个endpoint完全公开
                .allowFormAuthenticationForClients();  // 允许表单认证
    }
    
}

AuthorizationServerConfigurerAdapter要求配置以下几个类,这几个类是由Spring创建的独立的配置对象,它们 会被Spring传入AuthorizationServerConfigurer中进行配置。

ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。

AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。

AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.

3.2.2.2.配置客户端详细信息

ClientDetailsServiceConfigurer 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService), ClientDetailsService负责查找ClientDetails,而ClientDetails有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的Id。

  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。

  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。

  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。

  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户 端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现 ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

/**
 * 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在
 * 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
 * @param clients
 * @throws Exception
 */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     clients.withClientDetails(clientDetailsService);
    // 暂时使用内存方式
    clients.inMemory()// 使用in‐memory存储
            .withClient("c1")// client_id
            .secret(new BCryptPasswordEncoder().encode("secret")) //客户端密钥
            .resourceIds("res1") //资源列表
            .authorizedGrantTypes("authorization_code",
                    "password","client_credentials","implicit","refresh_token")
            // 该client允许的授权类型 uthorization_code,password,refresh_token,implicit,client_credentials
            .scopes("all")// 允许的授权范围
            .autoApprove(false)// false 跳转到授权页面
            //加上验证回调地址
            .redirectUris("http://www.baidu.com");
}

3.2.2.4.管理令牌

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来 加载身份信息,里面包含了这个令牌的相关权限。

自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类, 里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时 候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了 所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的 令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都 实现了TokenStore接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

  • JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。

  • JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对 于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授 权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

1、定义TokenConfig

在config包下定义TokenConfig,我们暂时先使用InMemoryTokenStore,生成一个普通的令牌。

@Configuration
public class TokenConfig {
    // 令牌存储策略
    @Bean
    public TokenStore tokenStore() {
        // 内存方式,生成普通令牌
        return new InMemoryTokenStore();
    }
}

2、定义AuthorizationServerTokenServices

在AuthorizationServer中定义AuthorizationServerTokenServices

    @Autowired
    private TokenStore tokenStore;
    //客户端详情服务
    @Autowired
    private ClientDetailsService clientDetailsService;
    
// 令牌管理服务
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService); // 客户端信息服务
        service.setSupportRefreshToken(true);// 是否产生刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

3.2.2.5.令牌访问端点配置

AuthorizationServerEndpointsConfigurer 这个对象的实例可以完成令牌服务以及令牌endpoint配置。

配置授权类型(Grant Types)

  • AuthorizationServerEndpointsConfigurer 通过设定以下属性决定支持的授权类型(Grant Types):

  • authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置 这个属性注入一个 AuthenticationManager 对象。

  • userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对 象),当你设置了这个之后,那么 "refresh_token" 即刷新令牌授权类型模式的流程中就会包含一个检查,用 来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。

  • authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对 象),主要用于 "authorization_code" 授权码类型模式。

  • implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。

  • tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并 且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的 需求的时候,才会考虑使用这个。

配置授权端点的URL(Endpoint URLs):

AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点URL链 接,它有两个参数:

第一个参数:String 类型的,这个端点URL的默认链接。

第二个参数:String 类型的,你要进行替代的URL链接。

以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的 第一个参数:

  • /oauth/authorize:授权端点。

  • /oauth/token:令牌端点。

  • /oauth/confirm_access:用户确认授权提交端点。

  • /oauth/error:授权服务错误信息端点。

  • /oauth/check_token:用于资源服务访问的令牌解析端点。

  • /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。

需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问.

在AuthorizationServer配置令牌访问端点

@Autowired
private AuthorizationCodeServices authorizationCodeServices;

@Autowired
private AuthenticationManager authenticationManager;

    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        //设置授权码模式的授权码如何存取,暂时采用内存方式
        //return new InMemoryAuthorizationCodeServices();
        return new JdbcAuthorizationCodeServices(dataSource);
    }
    /**
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     * /oauth/authorize:授权端点。
     * /oauth/token:令牌端点。
     * /oauth/confirm_access:用户确认授权提交端点。
     * /oauth/error:授权服务错误信息端点。
     * /oauth/check_token:用于资源服务访问的令牌解析端点。
     * /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //密码模式需要
                .authorizationCodeServices(authorizationCodeServices) //授权码模式需要
                .tokenServices(tokenService()) // 令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST); //允许POST提交
    }

3.2.2.6.令牌端点的安全约束

AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束,在 AuthorizationServer中配置如下.

/**
 * 用来配置令牌端点的安全约束
 * @param security
 * @throws Exception
 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security
            .tokenKeyAccess("permitAll()")  //tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。
            .checkTokenAccess("permitAll()") //checkToken这个endpoint完全公开
            .allowFormAuthenticationForClients();  // 允许表单认证
}

(1)tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。

(2)checkToken这个endpoint完全公开

(3) 允许表单认证

授权服务配置总结:授权服务配置分成三大块,可以关联记忆。

既然要完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。

既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的 token。

既然暴露除了一些endpoint,那对这些endpoint可以定义一些安全上的约束等。

3.2.2.7 web安全配置

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //认证管理器
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }

}

3.2.3 授权码模式

授权码模式交互图:

(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:

http://localhost:8005/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

参数列表如下:

client_id:客户端准入标识。

response_type:授权码模式固定为code。

scope:客户端权限。

redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。

百度一下,你就知道

(3)授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。

(4)客户端拿着授权码向授权服务器索要访问access_token,请求如下:

localhost:8005/oauth/token?code=MQzXkX&client_id=c1&client_secret=secret&username=zhangsan&password=123&grant_type=authorization_code&redirect_uri=http://www.baidu.com

参数列表如下

  • client_id:客户端准入标识。

  • client_secret:客户端秘钥。

  • grant_type:授权类型,填写authorization_code,表示授权码模式

  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。

  • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

(5)授权服务器返回令牌(access_token)

这种模式是四种模式中最安全的一种模式。一般用于client是Web服务器端应用或第三方的原生App调用资源服务 的时候。因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大 限度的减小了令牌泄漏的风险。

3.2.3.2 测试

 

  

3.2.4 简化模式

3.2.4.1 简化模式介绍

简化模式交互图:

(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:

http://localhost:8005//oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

参数描述同授权码模式 ,注意response_type=token,说明是简化模式。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。

3)授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览 器。

https://www.baidu.com/#access_token=6257f954-3006-4e0d-a362-1b0ec67a3ac9&token_type=bearer&expires_in=7033

注:fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fragment 的开头, 其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要 知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。 一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。

3.2.5 密码模式

3.2.5.1 授权码模式介绍

密码模式交互图:

(1)资源拥有者将用户名、密码发送给客户端

(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:

http://localhost:8005/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=%E5%BC%A0%E4%B8%89&password=123

参数列表如下:

  • client_id:客户端准入标识。

  • client_secret:客户端秘钥。

  • grant_type:授权类型,填写password表示密码模式 username:资源拥有者用户名。

  • password:资源拥有者密码。

(3)授权服务器将令牌(access_token)发送给client

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我 们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

3.2.5.2 测试

3.2.6 客户端模式

3.2.6.1 客户端模式介绍

(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)

(2)确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:

http://localhost:8005/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

参数列表如下:

  • client_id:客户端准入标识。

  • client_secret:客户端秘钥。

  • grant_type:授权类型,填写client_credentials表示客户端模式 这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因 此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。

3.2.6.2 测试

3.2.7 资源服务器测试

3.2.7.1 资源服务器配置

@EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个 配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个 对象的实例),下面是一些可以配置的属性:

ResourceServerSecurityConfigurer中主要包括:

  • tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。

  • tokenStore:TokenStore类的实例,指定令牌如何访问,与tokenServices配置可选

  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。

  • 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。 HttpSecurity配置这个与Spring Security类似:

  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。

  • 通过http.authorizeRequests()来设置受保护资源的访问规则

  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链

编写ResouceServerConfig:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    public static final String RESOURCE_ID = "res1";


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID) //资源id
                .tokenServices(tokenService())//验证令牌的服务
                .stateless(true);
    }




    //资源服务令牌解析服务
    @Bean
    public ResourceServerTokenServices tokenService() {
//使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:8005/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }


    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                .and().csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

}

3.2.7.2 验证token

ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的 话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题。如果 你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它 知道如何对令牌进行解码。

令牌解析方法: 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式 使用 RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token

使用授权服务的 /oauth/check_token 端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问, 这在咱们授权服务配置中已经提到了,下面是一个例子,在这个例子中,我们在授权服务中配置了 /oauth/check_token 和 /oauth/token_key 这两个端点:

 在资源 服务配置RemoteTokenServices ,在ResouceServerConfig中配置:

@Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID) //资源id
                .tokenServices(tokenService())//验证令牌的服务
                .stateless(true);
    }

    //资源服务令牌解析服务
    @Bean
    public ResourceServerTokenServices tokenService() {
//使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:8005/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }

3.2.7.3 编写资源

在controller包下编写OrderController,此controller表示订单资源的访问类:

@RestController
public class OrderController {

    @GetMapping(value = "/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1() {
        return "访问资源1";
    }

}

3.2.7.4 添加安全访问控制

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
 .antMatchers("/r/r1").hasAuthority("p2")
 .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
        ;
    }
}

3.2.7.5 测试

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值