一个简单的springboot集成oauth2 - 权限认证

概念

客户应用: 通常是一个web或者手机,他需要访问用户的受保护资源
资源服务器::是一个web站点或者web service API,用户的受保护数据保存在这个地方
授权服务器: 在客户应用成功认证并获得授权之后,想客户应用颁发访问令牌Access Token
资源拥有者:资源的拥有人,也就是用户。他可以想要分享给第三方应用的一些操作

一、引入核心包

<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
       <groupId>org.springframework.security.oauth</groupId>
       <artifactId>spring-security-oauth2</artifactId>
</dependency>

二、使用mysql数据库,引入jar包

<!--spring jpa-->
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql-->
<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
</dependency>

三、建表

/*
Navicat MySQL Data Transfer

Source Server         : mysql本地
Source Server Version : 50096
Source Host           : localhost:3306
Source Database       : test

Target Server Type    : MYSQL
Target Server Version : 50096
File Encoding         : 65001

Date: 2021-05-30 18:37:06
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for authority
-- ----------------------------
DROP TABLE IF EXISTS `authority`;
CREATE TABLE `authority` (
  `id` bigint(11) NOT NULL COMMENT '权限id',
  `authority` varchar(255) default NULL COMMENT '权限',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for clientdetails
-- ----------------------------
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (
  `appId` varchar(128) NOT NULL,
  `resourceIds` varchar(128) default NULL,
  `appSecret` varchar(128) default NULL,
  `scope` varchar(128) default NULL,
  `grantTypes` varchar(128) default NULL,
  `redirectUrl` varchar(128) default NULL,
  `authorities` varchar(128) default NULL,
  `access_token_validity` int(11) default NULL,
  `refresh_token_validity` int(11) default NULL,
  `additionalInformation` varchar(4096) default NULL,
  `autoApproveScopes` varchar(128) default NULL,
  PRIMARY KEY  (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for credentials
-- ----------------------------
DROP TABLE IF EXISTS `credentials`;
CREATE TABLE `credentials` (
  `id` bigint(11) NOT NULL COMMENT '凭证id',
  `enabled` tinyint(1) NOT NULL COMMENT '是否可用',
  `name` varchar(255) NOT NULL COMMENT '用户名',
  `password` varchar(255) NOT NULL COMMENT '密码',
  `version` int(11) default NULL COMMENT '版本号',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for credentials_authorities
-- ----------------------------
DROP TABLE IF EXISTS `credentials_authorities`;
CREATE TABLE `credentials_authorities` (
  `credentials_id` bigint(20) NOT NULL COMMENT '凭证id',
  `authorities_id` bigint(20) NOT NULL COMMENT '权限id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
  `token_id` varchar(128) default NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(128) default NULL,
  `client_id` varchar(128) default NULL,
  `authentication` blob,
  `refresh_token` varchar(128) default NULL,
  PRIMARY KEY  (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
  `userId` varchar(128) default NULL,
  `clientId` varchar(128) default NULL,
  `scope` varchar(128) default NULL,
  `status` varchar(10) default NULL,
  `expiresAt` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  `lastModifiedAt` timestamp NOT NULL default '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(128) NOT NULL,
  `resource_ids` varchar(128) default NULL,
  `client_secret` varchar(128) default NULL,
  `scope` varchar(128) default NULL,
  `authorized_grant_types` varchar(128) default NULL,
  `web_server_redirect_uri` varchar(128) default NULL,
  `authorities` varchar(128) default NULL,
  `access_token_validity` int(11) default NULL,
  `refresh_token_validity` int(11) default NULL,
  `additional_information` varchar(4096) default NULL,
  `autoapprove` varchar(128) default NULL,
  PRIMARY KEY  (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
  `token_id` varchar(128) default NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(128) default NULL,
  `client_id` varchar(128) default NULL,
  PRIMARY KEY  (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code` varchar(128) default NULL,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(128) default NULL,
  `token` blob,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
  `menu_id` int(11) NOT NULL auto_increment COMMENT '菜单ID',
  `name` varchar(32) NOT NULL COMMENT '名称',
  `url` varchar(128) default NULL COMMENT 'url',
  `method` varchar(32) default NULL COMMENT '请求方法',
  `create_time` timestamp NULL default NULL COMMENT '创建时间',
  `update_time` timestamp NULL default NULL COMMENT '更新时间',
  `create_user` int(11) default NULL COMMENT '创建用户',
  `update_user` int(11) default NULL COMMENT '更新用户',
  PRIMARY KEY  (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='菜单权限表';

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `role_id` int(11) NOT NULL auto_increment,
  `role_name` varchar(64) NOT NULL,
  `role_desc` varchar(190) default NULL,
  `create_time` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  `update_time` timestamp NULL default NULL,
  `create_user` int(11) default NULL COMMENT '创建用户',
  `update_user` int(11) default NULL COMMENT '更新用户',
  PRIMARY KEY  (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
  `id` int(11) NOT NULL auto_increment,
  `role_id` int(11) NOT NULL COMMENT '角色id',
  `menu_id` int(11) NOT NULL COMMENT '资源id',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Table structure for sys_role_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_user`;
CREATE TABLE `sys_role_user` (
  `id` int(11) NOT NULL auto_increment,
  `user_id` int(11) NOT NULL COMMENT '用户id',
  `role_id` int(11) NOT NULL COMMENT '角色id',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `user_id` int(11) NOT NULL auto_increment COMMENT '主键ID',
  `username` varchar(64) NOT NULL COMMENT '用户名',
  `password` varchar(255) NOT NULL,
  `create_time` timestamp NULL default NULL COMMENT '创建时间',
  `update_time` timestamp NULL default NULL COMMENT '修改时间',
  `create_user` int(11) default NULL COMMENT '创建用户',
  `update_user` int(11) default NULL COMMENT '更新用户',
  PRIMARY KEY  (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户表';

四、代码架构

 五、核心类

WebSecurityConfig

主要是开启拦截,设置数据源UserDetailsService 

package com.vesus.springbootoauth2.config;

import com.vesus.springbootoauth2.service.impl.CustomUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    UserDetailsService customUserService(){
        return new CustomUserService();
    }

    //配置全局设置
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //设置UserDetailsService以及密码规则
        auth.userDetailsService(customUserService());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/hello") ;
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean() ;
    }

    //开启全局方法拦截
    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
    public static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
        @Override
        protected MethodSecurityExpressionHandler createExpressionHandler() {
            return new OAuth2MethodSecurityExpressionHandler();
        }

    }
}

ResourceServerConfiguration

资源配置和服务配置

package com.vesus.springbootoauth2.config;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    private Logger logger = LoggerFactory.getLogger(ResourceServerConfiguration.class);

    @Autowired
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint ;

    @Bean
    public CustomLogoutSuccessHandler customLogoutSuccessHandler(){
        return new CustomLogoutSuccessHandler();
    } ;

    @Override
    public void configure(HttpSecurity http) throws Exception {
       logger.info("=========================111111111=========");
       http.exceptionHandling()
               .authenticationEntryPoint(customAuthenticationEntryPoint)
               .and()
               .logout()
               .logoutUrl("/oauth/logout")
               .logoutSuccessHandler(customLogoutSuccessHandler())
               .and()
               
               .authorizeRequests()
               .antMatchers("/hello/","/oauth/token").permitAll()
               .antMatchers("/**").authenticated();
    }
}

AuthorizationServerConfiguration

配置存储token的方式及配置token验证的相关信息

package com.vesus.springbootoauth2.config;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements EnvironmentAware {

    private Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);

    private static final String ENV_OAUTH = "authentication.oauth.";
    private static final String PROP_CLIENTID = "clientid";
    private static final String PROP_SECRET = "secret";
    private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";

    private RelaxedPropertyResolver propertyResolver ;

    @Autowired
    private DataSource dataSource ;

    @Bean
    public TokenStore tokenStore(){

        //这个是基于JDBC的实现,令牌(Access Token)会保存到数据库
        return new JdbcTokenStore(dataSource);
    }

    @Autowired
    @Qualifier("authenticationManagerBean")//认证方式
    private AuthenticationManager authenticationManager ;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore())
                .authenticationManager(authenticationManager) ;
    }
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()") //allow check token
                .allowFormAuthenticationForClients();
    }

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("yaoge")//client_id用来标识客户的Id
                .secret("123456")//secret客户端安全码
                .scopes("all") //允许授权范围
                .authorities("ROLE_ADMIN","ROLE_USER")//客户端可以使用的权限
                .authorizedGrantTypes("client_credentials","password", "refresh_token")//允许授权类型
                .accessTokenValiditySeconds(1200)
                .refreshTokenValiditySeconds(50000);

    }


    @Override
    public void setEnvironment(Environment environment) {
        //获取到前缀是"authentication.oauth." 的属性列表值.
        this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
    }
}

CustomLogoutSuccessHandler

package com.vesus.springbootoauth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {

    private static final String BEARER_AUTHENTICATION = "Bearer ";
    private static final String HEADER_AUTHORIZATION = "authorization";

    @Autowired
    private TokenStore tokenStore ;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String token = request.getHeader(HEADER_AUTHORIZATION);
        if (token!=null&&token.startsWith(BEARER_AUTHENTICATION)){
            OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token.split(" ")[0]);
            if (oAuth2AccessToken!=null){
                tokenStore.removeAccessToken(oAuth2AccessToken);
            }
        }

        response.setStatus(HttpServletResponse.SC_OK);
    }
}

CustomAuthenticationEntryPoint

package com.vesus.springbootoauth2.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final Logger log = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);


    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.info("Pre-authenticated entry point called. Rejecting access");
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Access Denied");
    }
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy
#debug=true
authentication.oauth.clientid=clientid
authentication.oauth.secret=secret
authentication.oauth.tokenValidityInSeconds=18000
security.oauth2.resource.filter-order = 3

六、启动验证

先获取token

http://localhost:8080/oauth/token?username=user_1&password=123456&grant_type=password&client_id=yaoge&client_secret=1234

 请求接口

http://localhost:8080/abc?access_token=046ca7ae-b6ec-41a4-ae7d-3bd95756d6bb

 项目路径:https://gitee.com/zhouzhiyao2/springboot-oauth2.git

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然可以,以下是一份关于您的Java SpringBoot项目的简介描述: 探索JavaSpringBoot魔法 亲爱的开发者们,准备好一起踏上这次深入JavaSpringBoot之旅了吗?如果您正在寻找一个现代化、高效且易于扩展的开发框架,那么这个SpringBoot项目将是您的理想选择。 主要特点: 快速开发:SpringBoot通过自动配置和约定大于配置的原则,大大简化了项目设置和开发过程。 易于扩展:SpringBoot与生俱来的灵活性使您可以轻松集成各种服务和数据库,以满足您日益增长的需求。 安全性:内置的安全特性,如OAuth2和Spring Security,确保您的应用程序安全无虞。 微服务支持:SpringBoot是微服务架构的理想选择,可以帮助您构建模块化、可扩展的应用程序。 社区支持:全球的开发者社区意味着您可以在遇到问题时找到大量的资源和支持。 无论您是初出茅庐的Java新手,还是经验丰富的开发者,这个项目都将为您提供一个深入了解SpringBoot的机会。无论您是想学习新的技能,还是想提高现有应用程序的性能,这个项目都将是您的宝贵资源。 内容概览 项目设置和初始化 控制器设计和实现 数据持久化与数据库集成 安全性和身份验证 性能优化和监控 部署和生产环境考虑 现在,是时候让您的Java技能得到充分发挥,并掌握SpringBoot的魔法了!这个项目将带领您从基础到高级,探索SpringBoot的每一个角落。
好的,针对您的问题,我可以给您提供以下步骤: 1. 添加依赖 在 pom.xml 文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.0.RELEASE</version> </dependency> ``` 2. 配置认证服务器 在 Spring Boot 应用程序中,您可以使用 @EnableAuthorizationServer 注解来启用 OAuth2 认证服务器。以下是一个示例配置: ``` @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private DataSource dataSource; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } } ``` 3. 配置资源服务器 在 Spring Boot 应用程序中,您可以使用 @EnableResourceServer 注解来启用 OAuth2 资源服务器。以下是一个示例配置: ``` @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/**").authenticated() .anyRequest().permitAll(); } } ``` 4. 配置安全 在 Spring Boot 应用程序中,您可以使用 application.yml 或 application.properties 文件来配置安全。以下是一个示例配置: ``` security: oauth2: client: client-id: my-client client-secret: my-secret access-token-uri: http://localhost:8080/oauth/token user-authorization-uri: http://localhost:8080/oauth/authorize resource: user-info-uri: http://localhost:8080/user ``` 以上就是集成 OAuth2 的基本步骤,您可以根据自己的需求进行修改和扩展。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值