SpringCloud整合Sa-token

1、目的

之前写项目一致都使用的是 SpringSecurity ,但总是感觉SpringSecurity用起来比较繁琐,所以就打算使用sa-token 进行权限校验以及 登录验证,角色校验,集成比较简单.

2、设计思路

整个项目 使用SpringCloud GateWay 进行网关,统一将服务注册到nacos 上去

​ 即 mall_auth : 提供用户登录的功能

​ mall-mbg: 提供对数据库的查询操作

​ mall_common: 公共的方法

​ mall_product: 提供产品查询方法

​ gateway_test : 充当网关功能,对请求做权限校验然后对请求进行分发

image.png

3、实现过程

1、 父项目 pom 依赖

```java 4.0.0

<groupId>org.example</groupId>
<artifactId>test_sa_token</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>


<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>



<properties>
    <mybatis.version>3.5.9</mybatis.version>
    <mysql-connector.version>8.0.29</mysql-connector.version>
    <spring-cloud.version>2021.0.3</spring-cloud.version>
    <spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
    <mybatis-generator.version>1.4.1</mybatis-generator.version>
    <pagehelper-starter.version>1.4.2</pagehelper-starter.version>
</properties>

<!-- 对应的子模块 -->
<modules>
    <module>gateway_test</module>
    <module>mall_auth</module>
    <module>mall_common</module>
    <module>mall_product</module>
</modules>


<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!--feign 连接池 -->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-okhttp</artifactId>
    </dependency>

    <!-- feign 中间件 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>


    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
        <version>3.1.0</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- redis 服务 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>


    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

</dependencies>

<dependencyManagement>
    <dependencies>

        <!--Spring Cloud 相关依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--Spring Cloud Alibaba 相关依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>


        <!-- MyBatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>${mybatis.version}</version>
        </dependency>

        <!-- MyBatis 生成器 -->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>${mybatis-generator.version}</version>
        </dependency>

        <!--MyBatis分页插件starter-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>${pagehelper-starter.version}</version>
        </dependency>

        <!--Mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector.version}</version>
        </dependency>
    </dependencies>



</dependencyManagement>

```

2、 gateway_test 项目

pom.xml

```java 4.0.0 org.example test satoken 1.0-SNAPSHOT com.example gateway test 0.0.1-SNAPSHOT gatewaytest 用于测试sa_token 1.8 org.springframework.cloud spring-cloud-starter-gateway

<dependency>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>mall_common</artifactId>
    </dependency>







    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-dao-redis-jackson</artifactId>
        <version>1.34.0</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>


    <!-- 对应的webFlux sa_token -->
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
        <version>1.34.0</version>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

```

application.yml 配置

```java server: port: 8201

spring: # redis 相关配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0

mvc: pathmatch: matching-strategy: antpathmatcher cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true # 使用小写的service-id routes: # 配置路由路径 - id: mall-auth # 认证中心 uri: lb://mall-auth predicates: - Path=mall-auth/* filters: - StripPrefix=1 - id: mall-product # 产品模块 uri: lb://mall-product predicates: - Path=/mall-product/* filters: - StripPrefix=1

sa-token: # token 的名称 同时也是cookie 名称 token-name: satoken # token 的有效期 单位s 默认为30天, -1代表永不过期 timeout: 2592000 # token 临时有效期(指定时间内无操作或者视为token过期) 单位 秒 activity-timeout: -1 # 是否允许同一账号并发登录(为true 时允许一起登录, 为false 时新登录时挤掉旧登录) is-concurrent: true # 在多人登录同一账号时, 是否共用同一个token(为true 的时候所有登录通用同一个token,为false 时每次登录新建一个 token) is-share: false # token 风格 token-style: uuid

```

bootstrap.yml 文件

```java spring: profiles: active: dev application: name: mall-gateway

```

增加SaTokenConfigure 类用于sa-token 的配置

```java package com.example.gateway_test.config;

import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.reactor.filter.SaReactorFilter; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;

/* * [Sa-Token 权限认证] 全局配置类 */ @Configuration public class SaTokenConfigure { // 注册 Sa-Token全局过滤器 @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 拦截地址 .addInclude("/") / 拦截全部path */ // 开放地址 .addExclude("/favicon.ico") // 鉴权方法:每次访问进入 .setAuth(obj -> { // 登录校验 -- 拦截所有路由,并排除/account/user/doLogin用于开放登录 SaRouter.match("/", "/mall-auth/auth/token", r -> StpUtil.checkLogin());

// // 权限认证 -- 不同模块, 校验不同权限

/*           SaRouter.match("/mall-product/**", r -> StpUtil.checkRole("normal_role"));

                SaRouter.match("/admin/**", r -> StpUtil.checkRole("system_role"));
              SaRouter.match("/account/**", r -> StpUtil.checkRole("user"));
                SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
                SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
                SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));*/
                // 更多匹配 ...  */
            })
            .setBeforeAuth(obj -> {
                // ------设置跨域响应头
                SaHolder.getResponse()
                        // 允许指定域访问跨域资源
                        .setHeader("Access-Control-Allow-Origin", "*")
                        // 允许所有请求方式
                        .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
                        // 有效时间
                        .setHeader("Access-Control-Max-Age", "3600")
                        // 允许的header参数
                        .setHeader("Access-Control-Allow-Headers", "*");

            })
            // 异常处理方法:每次setAuth函数出现异常时进入
            .setError(e -> {
                return SaResult.error(e.getMessage());
            })
            ;
}

}

```

增加 StpInterfaceImpl 实现了sa-token 对角色与权限的查询

```java package com.example.gateway_test.config;

import cn.dev33.satoken.stp.StpInterface; import com.example.common.api.RedisKey; import com.example.common.util.RedisUtil; import org.springframework.stereotype.Component;

import javax.annotation.Resource; import java.util.ArrayList; import java.util.List;

/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 14:09 * @Description 对权限进行校验 */ @Component public class StpInterfaceImpl implements StpInterface {

@Resource
private RedisUtil redisUtil;


@Override
public List<String> getPermissionList(Object loginId, String loginType) {
    // 根据用户的id 来查询出对应的
    Integer userId = Integer.valueOf(String.valueOf(loginId));
    List<String> permissionList = redisUtil.getCacheList(RedisKey.getUserPermissionList(userId));
    return permissionList;
}

@Override
public List<String> getRoleList(Object loginId, String loginType) {
    // 获取缓存
    Integer userId = Integer.valueOf(String.valueOf(loginId));
    List<String> roleList = redisUtil.getCacheList(RedisKey.getUserRoleList(userId));
    return roleList;
}

}

```

增加GlobalException 用于捕获全局异常类

```java package com.example.gateway_test.exception;

import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; import com.example.common.api.CommonResult; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody;

/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 13:40 * @Description 拦截全局异常类 */ @Slf4j public class GlobalException {

@ResponseBody
@ExceptionHandler
public CommonResult<Object> handlerException(Exception ex) {

    // 异常信息
    log.error("gateway服务异常信息---{}",ex.getMessage(),ex);

    CommonResult<Object> objectCommonResult = null;

    if(ex  instanceof NotLoginException) {
        objectCommonResult = CommonResult.unauthorized("未登录");
    } else if(ex instanceof NotRoleException) {
        objectCommonResult = CommonResult.forbidden("没有角色");
    } else if(ex instanceof NotPermissionException) {
        objectCommonResult = CommonResult.forbidden("没有权限");
    } else {
        objectCommonResult = CommonResult.failed(ex.getMessage());
    }

    return objectCommonResult;
}

}

```

其项目主类

```java package com.example.gateway_test;

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Import;

@EnableDiscoveryClient @SpringBootApplication @Import(value = { com.example.common.util.RedisUtil.class }) public class GatewayTestApplication {

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

}

```

3、 mall_auth 项目

这个模块处理用户登录请求

增加LoginController 类用于处理登录请求 ```java package com.example.mall_auth.controller;

import com.example.common.api.CommonResult; import com.example.demo.pojo.dto.LoginUserDto; import com.example.demo.pojo.model.User; import com.example.mall_auth.service.LoginService; import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/** * @author zhangyang * @version 1.0 * @Date 2023/5/16 19:55 * @Description */

@RequestMapping("/auth") @RestController public class LoginController {

@Resource
   private LoginService loginService;


   @PostMapping("/token")
   public CommonResult<LoginUserDto> login(@RequestBody User user) {
       return CommonResult.success(loginService.login(user.getUsername(), user.getPassword()));
   }

}

```

LoginService 接口

```java package com.example.mall_auth.service;

import com.example.demo.pojo.dto.LoginUserDto;

/** * @author zhangyang * @version 1.0 * @Date 2023/5/16 20:03 * @Description */ public interface LoginService {

/**
 * 使用sa-token 进行登录
 * @param username
 * @param password
 * @return
 */
LoginUserDto login(String username, String password);

}

```

LoginServiceImpl 接口实现类

```java package com.example.mall_auth.service.impl;

import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import com.example.demo.mapper.UserMapper; import com.example.demo.pojo.dto.LoginUserDto; import com.example.demo.pojo.model.User; import com.example.mall_auth.service.LoginService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/** * @author zhangyang * @version 1.0 * @Date 2023/5/16 20:03 * @Description 登录接口 */ @Slf4j @Service public class LoginServiceImpl implements LoginService {

@Resource
private UserMapper userMapper;



@Override
public LoginUserDto login(String username, String password) {

    User user = userMapper.selectUmsByUserName(username);
    if(user!=null && user.getPassword().compareTo(password) ==0) {
        // 登录
        // 用用户id 做标识,然后将生成的token 写入到缓存中
        StpUtil.login(user.getId());

        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();

        LoginUserDto loginUserDto = new LoginUserDto();
        loginUserDto.setUser(user);
        loginUserDto.setToken(tokenInfo);
        return  loginUserDto;
    }
    log.error("该用户---{}---登录失败",username);
    return null;
}

}

```

对应的pom 依赖

```java

server: port: 8202

spring: datasource: url: jdbc:mysql://localhost:3306/sa?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 cloud: nacos: config: file-extension: yaml server-addr: http://localhost:8848 discovery: server-addr: http://localhost:8848 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 sa-token: # token 的名称 同时也是cookie 名称 token-name: satoken # token 的有效期 单位s 默认为30天, -1代表永不过期 timeout: 100 # token 临时有效期(指定时间内无操作或者视为token过期) 单位 秒 activity-timeout: -1 # 是否允许同一账号并发登录(为true 时允许一起登录, 为false 时新登录时挤掉旧登录) is-concurrent: true # 在多人登录同一账号时, 是否共用同一个token(为true 的时候所有登录通用同一个token,为false 时每次登录新建一个 token) is-share: false # token 风格 token-style: uuid mybatis: mapper-locations: - classpath:dao/.xml - classpath:mapper/*.xml

feign请求

feign: okhttp: enabled: true client: config: default: connectTimeout: 5000 readTimeout: 5000 loggerLevel: basic

```

pom.xml 文件内容

```java 4.0.0 org.example test satoken 1.0-SNAPSHOT

<groupId>com.example</groupId>
<artifactId>mall_auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mall_auth</name>
<description>mall_auth实现用户登录接口</description>
<properties>
    <java.version>1.8</java.version>
</properties>

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

    <dependency>
        <groupId>com.example</groupId>
        <artifactId>mall-mbg</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>


    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-spring-boot-starter</artifactId>
        <version>1.34.0</version>
    </dependency>


    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>


    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-dao-redis-jackson</artifactId>
        <version>1.34.0</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>mall_common</artifactId>
        <version>1.0-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

```

4、实现效果

使用postman 发送 查询product 的信息

image.png

image.png

得到的结果为token 无效,说明经过网关的时候被sa-token 给拦截住了。

此时我们应该访问 mall_auth 的登录服务

image.png

此时我们会发现本地的redis服务中

图片转存失败,建议将图片保存下来直接上传

image.png 多出了对应的token 缓存信息。

那么此时将token信息写入到header 中,我们再次访问product 服务的时候

image.png

就可以访问到对应的结果了。

5、 功能扩展

上面实现了对登录的拦截。我们还可以利用sa-token 在网关上做权限与角色的校验。使用RABC 对登录的用户做权限与角色的判断。

我们可以在 mallauth 启动的时候将用户信息注入到redis 中,然后在 gatewaytest 服务中的StpInterfaceImpl 中实现通过userId 到缓存中查询对应的权限以及角色集合的代码。

mall_auth 中的 PermissionInit 代码

```java package com.example.mall_auth.init;

import com.alibaba.nacos.common.utils.MapUtils; import com.example.common.api.RedisKey; import com.example.common.util.RedisUtil; import com.example.mall_auth.service.impl.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils;

import javax.annotation.Resource; import java.util.List; import java.util.Map;

/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 14:41 * @Description */ @Slf4j @Component public class PermissionInit implements ApplicationRunner {

@Resource
private UserService userService;


@Resource
private RedisUtil redisUtil;


@Override
public void run(ApplicationArguments args) {
    log.warn("初始化数据补齐--");

    // 注入对应的角色信息
    Map<Integer, List<String>> userRoleMap = userService.findAllUserRoleList();
    if (MapUtils.isNotEmpty(userRoleMap)) {
        // 查询
        for (Map.Entry<Integer, List<String>> entry : userRoleMap.entrySet()) {
            Integer userId = entry.getKey();
            List<String> roleList = entry.getValue();
            if (!CollectionUtils.isEmpty(roleList)) {
                redisUtil.setCacheList(RedisKey.getUserRoleList(userId), roleList);
            }
        }
    }

    // 注入对应的权限信息
    Map<Integer,List<String>> userPermissionMap = userService.findAllUserPermission();
    if(MapUtils.isNotEmpty(userPermissionMap)) {
        // 查询出所有的权限
        for(Map.Entry<Integer,List<String>> entry : userPermissionMap.entrySet()) {
            Integer userId = entry.getKey();
            List<String> permissionList = entry.getValue();
            if(!CollectionUtils.isEmpty(permissionList)) {
                redisUtil.setCacheList(RedisKey.getUserPermissionList(userId),permissionList);
            }
        }
    }
}

}

```

然后gateway_test 中的 StpInterfaceImpl 代码

```java package com.example.gateway_test.config;

import cn.dev33.satoken.stp.StpInterface; import com.example.common.api.RedisKey; import com.example.common.util.RedisUtil; import org.springframework.stereotype.Component;

import javax.annotation.Resource; import java.util.ArrayList; import java.util.List;

/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 14:09 * @Description 对权限进行校验 */ @Component public class StpInterfaceImpl implements StpInterface {

@Resource
private RedisUtil redisUtil;


@Override
public List<String> getPermissionList(Object loginId, String loginType) {
    // 根据用户的id 来查询出对应的
    Integer userId = Integer.valueOf(String.valueOf(loginId));
    List<String> permissionList = redisUtil.getCacheList(RedisKey.getUserPermissionList(userId));
    return permissionList;
}

@Override
public List<String> getRoleList(Object loginId, String loginType) {
    // 获取缓存
    Integer userId = Integer.valueOf(String.valueOf(loginId));
    List<String> roleList = redisUtil.getCacheList(RedisKey.getUserRoleList(userId));
    return roleList;
}

}

```

那么我们就可以修改SaTokenConfigure 中的 权限认证部分的代码

java SaRouter.match("/mall-product/**", r -> StpUtil.checkRole("normal_role"));

表示 当前登录的用户如果需要访问 mall-product 的服务则需要拥有normal_role 角色

java SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));

表示 当前登录的用户如果需要访问 goods 服务则需要拥有goods 的权限

项目源码

https://github.com/to1233/springcloudlearn/tree/main/testsatoken

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值