自定义简单的微服务认证授权

一、有状态 vs 无状态

在这里插入图片描述
在这里插入图片描述

二、微服务认证方案

1. “处处安全”方案

Oauth2.0 系列文章

代表实现:

  1. Spring Cloud Security
  2. JBoss Keycloak

示例代码:

Spring Cloud Security示例代码1

Spring Cloud Security示例代码2

JBoss Keycloak示例代码

2. 外部无状态、内部有状态方案

在这里插入图片描述

3. “网关认证授权、内部裸奔”方案

在这里插入图片描述

4. “内部裸奔”改进方案

在这里插入图片描述

5. 方案对比与选择

在这里插入图片描述


三、访问控制模型

在这里插入图片描述

RABC访问控制模型

在这里插入图片描述

1. RABC概述

RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般是多对多的关系。(如下图)
在这里插入图片描述

2. RABC对象关系
  1. 权限

    系统的所有权限信息。权限具有上下级关系,是一个树状的结构。如:

    • 系统管理
      • 用户管理
        • 查看用户
        • 新增用户
        • 修改用户
        • 删除用户
  2. 用户

    系统的具体操作者,可以归属于一个或多个角色,它与角色的关系是多对多的关系

  3. 角色

    为了对许多拥有相似权限的用户进行分类管理,定义了角色的概念,例如系统管理员、管理员、用户、访客等角色。角色具有上下级关系,可以形成树状视图,父级角色的权限是自身及它的所有子角色的权限的综合。父级角色的用户、父级角色的组同理可推。

3. RABC关系图

在这里插入图片描述

4. RABC模块图

在这里插入图片描述

5. RABC结构表
CREATE TABLE `tb_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
  `name` varchar(64) NOT NULL COMMENT '权限名称',
  `enname` varchar(64) NOT NULL COMMENT '权限英文名称',
  `url` varchar(255) NOT NULL COMMENT '授权路径',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='权限表';

CREATE TABLE `tb_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
  `name` varchar(64) NOT NULL COMMENT '角色名称',
  `enname` varchar(64) NOT NULL COMMENT '角色英文名称',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='角色表';

CREATE TABLE `tb_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='角色权限表';

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(64) NOT NULL COMMENT '密码,加密存储',
  `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
  `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='用户表';

CREATE TABLE `tb_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户 ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='用户角色表';

四、JWT

1. JWT简介

JWT全称Json web token,是一个开放标准(RFC 7519),用来在各方之间安全地传输信息。JWT可被验证和信任,因为它是数字签名的。

2. 组成

在这里插入图片描述

3. 公式

在这里插入图片描述

4. 定义JWT操作工具类

jjwt GitHub

jjwt 自定义工具类参考

自定义JWT操作工具类
  1. pom.xml

    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-api</artifactId>
      <version>0.10.7</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-impl</artifactId>
      <version>0.10.7</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-jackson</artifactId>
      <version>0.10.7</version>
      <scope>runtime</scope>
    </dependency>
    
  2. 工具类

    @Slf4j
    @RequiredArgsConstructor
    @SuppressWarnings("WeakerAccess")
    @Component
    public class JwtOperator {
        /**
         * 秘钥
         * - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
         */
        @Value("${secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
        private String secret;
        /**
         * 有效期,单位秒
         * - 默认2周
         */
        @Value("${expire-time-in-second:1209600}")
        private Long expirationTimeInSecond;
    
        /**
         * 从token中获取claim
         *
         * @param token token
         * @return claim
         */
        public Claims getClaimsFromToken(String token) {
            try {
                return Jwts.parser()
                    .setSigningKey(this.secret.getBytes())
                    .parseClaimsJws(token)
                    .getBody();
            } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
                log.error("token解析错误", e);
                throw new IllegalArgumentException("Token invalided.");
            }
        }
    
        /**
         * 获取token的过期时间
         *
         * @param token token
         * @return 过期时间
         */
        public Date getExpirationDateFromToken(String token) {
            return getClaimsFromToken(token)
                .getExpiration();
        }
    
        /**
         * 判断token是否过期
         *
         * @param token token
         * @return 已过期返回true,未过期返回false
         */
        private Boolean isTokenExpired(String token) {
            Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }
    
        /**
         * 计算token的过期时间
         *
         * @return 过期时间
         */
        private Date getExpirationTime() {
            return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
        }
    
        /**
         * 为指定用户生成token
         *
         * @param claims 用户信息
         * @return token
         */
        public String generateToken(Map<String, Object> claims) {
            Date createdTime = new Date();
            Date expirationTime = this.getExpirationTime();
    
    
            byte[] keyBytes = secret.getBytes();
            SecretKey key = Keys.hmacShaKeyFor(keyBytes);
    
            return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(createdTime)
                .setExpiration(expirationTime)
                // 你也可以改用你喜欢的算法
                // 支持的算法详见:https://github.com/jwtk/jjwt#features
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
        }
    
        /**
         * 判断token是否非法
         *
         * @param token token
         * @return 未过期返回true,否则返回false
         */
        public Boolean validateToken(String token) {
            return !isTokenExpired(token);
        }
    }
    
  3. 配置

    jwt:
      # 秘钥(各个微服务的秘钥一定要一致)
      secret: aaaaaaaaaaassssssssscxzdfacdg
      # 有效期,单位秒,默认2周
      expire-time-in-second: 1209600
    
  4. 测试使用

    @Autowired
    private JwtOperator jwtOperator;
    
    public static void main(String[] args) {
            // 1. 初始化
            JwtOperator jwtOperator = new JwtOperator();
            jwtOperator.expirationTimeInSecond = 1209600L;
            jwtOperator.secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt";
    
            // 2.设置用户信息
            HashMap<String, Object> objectObjectHashMap = Maps.newHashMap();
            objectObjectHashMap.put("id", "1");
    
            // 测试1: 生成token
            String token = jwtOperator.generateToken(objectObjectHashMap);
            // 会生成类似该字符串的内容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
            System.out.println(token);
    
            // 将我改成上面生成的token!!!
            String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ";
            // 测试2: 如果能token合法且未过期,返回true
            Boolean validateToken = jwtOperator.validateToken(someToken);
            System.out.println(validateToken);
    
            // 测试3: 获取用户信息
            Claims claims = jwtOperator.getClaimsFromToken(someToken);
            System.out.println(claims);
    
            // 将我改成你生成的token的第一段(以.为边界)
            String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
            // 测试4: 解密Header
            byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
            System.out.println(new String(header));
    
            // 将我改成你生成的token的第二段(以.为边界)
            String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk1NDEsImV4cCI6MTU2Njc5OTE0MX0";
            // 测试5: 解密Payload
            byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
            System.out.println(new String(payload));
    
            // 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的
            jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");
    }
    

五、实现认证授权

1. 实现小程序登录

在这里插入图片描述

  1. 前端部分代码

    login(e) {
      const self = this;
      let userInfo = e.mp.detail.userInfo;
      // 登录
      wx.login({
        success: (res) => {
          request(
            LOGIN_URL,
            'POST', {
              code: res.code,
              wxNickname: userInfo.nickName,
              avatarUrl: userInfo.avatarUrl
            }
          ).then(res => {
            console.log('登录成功...', res);
            wx.setStorageSync('token', res.token);
            wx.setStorageSync('user', res.user);
            console.log('user...', res.user);
            wx.showToast({
              title: '登录成功!'
            });
            console.log('user...', res.user);
            self.user = res.user;
          }).catch(error => {
            console.log('error', error);
            reject(error)
          });
        }
      });
    },
    
  2. 后端

    1. pom.xml

      <!-- weixin -->
      <dependency>
          <groupId>com.github.binarywang</groupId>
          <artifactId>weixin-java-miniapp</artifactId>
          <version>3.5.0</version>
      </dependency>
      
    2. configuration

      @Configuration
      public class WxConfig {
      
          @Bean
          public WxMaConfig wxMaConfig() {
              WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
              //appid和secret可在微信公众平台查看
              config.setAppid("xxxxxxxxxxxx");
              config.setSecret("xxxxxxxxxxxxxxxxxxxxxxxxxx");
              return config;
          }
      
          @Bean
          public WxMaService wxMaService(WxMaConfig wxMaConfig) {
              WxMaServiceImpl wxMaService = new WxMaServiceImpl();
              wxMaService.setWxMaConfig(wxMaConfig);
              return wxMaService;
          }
      }
      
    3. Controller代码

      @PostMapping("/login")
      public LoginRespDTO login(@RequestBody UserLoginDTO loginDTO) throws WxErrorException {
          //微信小程序服务端校验是否已经登录的结果
          WxMaJscode2SessionResult result = wxMaService.getUserService().getSessionInfo(loginDTO.getCode());
      
          //微信的openId,用户在微信的唯一标识
          String openid = result.getOpenid();
      
          /**
      	 *看用户是否已经注册到数据库
      	 *如果未注册,插入返回新user
           *如果已经注册,返回user
      	*/
          User user = this.userService.login(loginDTO, openid);
      
          //颁发token
          Map<String,Object> userInfo = new HashMap<>(3);
          userInfo.put("id",user.getId());
          userInfo.put("wxNickname",user.getWxNickname());
          userInfo.put("role",user.getRoles());
          String token = jwtOperator.generateToken(userInfo);
          //日志
          log.info("用户 {} 登录成功,生成的token = {},有效期为 {}",
                  user.getWxNickname(),token,jwtOperator.getExpirationTime());
      
          //构建响应
          return LoginRespDTO.builder().user(
                  UserRespDTO.builder()
                  .id(user.getId())
                  .avatarUrl(user.getAvatarUrl())
                  .bonus(user.getBonus())
                  .wxNickname(user.getWxNickname())
                  .build())
              	.token(JwtTokenRespDTO.builder()
                          .token(token)
                          .expirationTime(jwtOperator.getExpirationTime().getTime())
                          .build()
          ).build();
      }
      
    4. Service代码

      public User login(UserLoginDTO loginDTO,String openId) {
              User user = this.userMapper.selectOne(
                      User.builder().wxId(openId).build()
              );
              if (user == null) {
                  User userToSave = User.builder()
                          .wxId(openId)
                          .bonus(300)
                          .wxNickname(loginDTO.getWxNickname())
                          .avatarUrl(loginDTO.getAvatarUrl())
                          .roles("user")
                          .createTime(new Date())
                          .updateTime(new Date())
                          .build();
                  this.userMapper.insertSelective(
                          userToSave
                  );
                  return userToSave;
              }
              return user;
          }
      

2. AOP实现登录检查状态

  1. 实现登录检查的方式
    在这里插入图片描述

  2. 使用Spring AOP实现登录检查状态

    1. pom.xml

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      
    2. 新建注解

      public @interface CheckLogin {
      }
      
    3. 新建AOP切面,实现加有注解的地方需要进行token验证

      @Aspect
      @Component
      @RequiredArgsConstructor(onConstructor = @__(@Autowired))
      public class AuthAspect {
          private final JwtOperator jwtOperator;
      
          /**
           * 检查登录状态
           * @param point
           * @return
           */
          @Around("@annotation(com.banmingi.nodeapp.contentcenter.auth.CheckLogin)")
          public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
      
                  this.checkToken();
                  return point.proceed();
      
          }
      
          private void checkToken() {
              try {
              //1. 从header里面获取token
              HttpServletRequest request = getHttpServletRequest();
              String token = request.getHeader("X-Token");
      
              //2. 校验token是否合法或在有效期内,如果不合法或已过期,直接抛异常;如果合法或未过期,放行
              Boolean isValid = jwtOperator.validateToken(token);
      
              if (!isValid) {
                  throw new SecurityException("token 不合法!");
              }
              //3. 如果校验成功,就将用户的信息设置到request的attribute里面
              Claims claims = jwtOperator.getClaimsFromToken(token);
              request.setAttribute("id",claims.get("id"));
              request.setAttribute("wxNickname",claims.get("wxNickname"));
              request.setAttribute("role",claims.get("role"));
              } catch (Throwable throwable) {
                  throw new SecurityException("token 不合法!");
              }
          }
      
          /**
           * 获取request
           * @return
           */
          private HttpServletRequest getHttpServletRequest() {
              RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
              ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
              return attributes.getRequest();
          }
      }
      
      
    4. 异常处理

      public class SecurityException extends RuntimeException {
      
          public SecurityException(String message) {
              super(message);
          }
      
          public SecurityException(String message, Throwable cause) {
              super(message, cause);
          }
      }
      
      @RestControllerAdvice
      @Slf4j
      public class GlobalExceptionErrorHander {
          @ExceptionHandler(SecurityException.class)
          public ResponseEntity<ErrorBody> error(SecurityException e) {
              log.warn("发生Security异常",e);
             return new ResponseEntity<ErrorBody>(
                      ErrorBody.builder()
                              .body(e.getMessage())
                              .status(HttpStatus.UNAUTHORIZED.value())
                              .build(),
                      HttpStatus.UNAUTHORIZED
              );
          }
      }
      
      @Data
      @Builder
      @NoArgsConstructor
      @AllArgsConstructor
      class ErrorBody {
          private String body;
          private int status;
      }
      

3. 微服务之间token的传递

1. Feign传递token
  1. 参数传递时使用@RequestHeader注解

    Controller代码:

    @GetMapping("/{id}")
    @CheckLogin
    public ShareDTO findById(@PathVariable Integer id,
                             @RequestHeader("X-Token") String token) {
        return this.shareService.findById(id,token);
    }
    

    Feign Client接口代码:

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id,
    @RequestHeader("X-Token") String token);
    
  2. 使用Feign的RequestInterceptor拦截器

    定义一个类实现RequestInterceptor接口

    public class TokenRelayRequestInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate requestTemplate) {
            //1. 从header里面获取token
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
            HttpServletRequest request = attributes.getRequest();
            String token = request.getHeader("X-Token");
    
            //2. 传递token
            if (StringUtils.isNoneBlank(token))
                requestTemplate.header("X-Token",token);
        }
    }
    

    配置(这里使用全局配置方式,所有Feign接口都会带上token):

    feign:
      client:
        config:
          #全局配置
          default:
            loggerLevel: BASIC
            requestInterceptors:
              - com.banmingi.nodeapp.contentcenter.interceptor.TokenRelayRequestInterceptor
    
2. RestTemplate传递token
  1. 调用exchange()方法

    @GetMapping("/tokenRelay/{userId}")
    public ResponseEntity<UserDTO> tokenRelay(@PathVariable Integer userId, 											   HttpServletRequest request) {
            String token = request.getHeader("X-Token");
    
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.add("X-Token",token);
    
            ResponseEntity<UserDTO> exchange = this.restTemplate
                    .exchange("http://user-center/users/{userId}",
                            HttpMethod.GET,
                            new HttpEntity<>(httpHeaders),
                            UserDTO.class,
                            userId);
        
            return exchange;
        }
    
  2. 使用RestTemplate的ClientHttpRequestIntercept拦截器

    定义一个类实现ClientHttpRequestIntercept接口

    public class TestRestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {
        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            //1. 从header里面获取token
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
            HttpServletRequest httpRequest = attributes.getRequest();
    
            String token = httpRequest.getHeader("X-Token");
    
            HttpHeaders headers = request.getHeaders();
            headers.add("X-Token",token);
    
            //保证请求继续执行
            return execution.execute(request,body);
        }
    }
    

    配置:

    @Bean
        @LoadBalanced
        //@SentinelRestTemplate
        public RestTemplate restTemplate(){
          RestTemplate restTemplate = new RestTemplate();
          restTemplate.setInterceptors(
                  Collections.singletonList(
                          new TestRestTemplateTokenRelayInterceptor()
                  ));
          return restTemplate;
        }
    

4. AOP实现用户权限验证

使用Spring AOP实现用户权限验证
  1. pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
    
  2. 新建注解

    @Retention(RetentionPolicy.RUNTIME)
    public @interface CheckAuthorization {
        String value();
    }
    
  3. 新建AOP切面,实现加有注解的地方需要进行权限验证

    @Aspect
    @Component
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class AuthAspect {
        private final JwtOperator jwtOperator;
    
        /**
         * 检查登录状态
         * @param point
         * @return
         */
        @Around("@annotation(com.banmingi.nodeapp.contentcenter.auth.CheckLogin)")
        public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
    
                this.checkToken();
                return point.proceed();
    
        }
    
        private void checkToken() {
            try {
            //1. 从header里面获取token
            HttpServletRequest request = getHttpServletRequest();
            String token = request.getHeader("X-Token");
    
            //2. 校验token是否合法或在有效期内,如果不合法或已过期,直接抛异常;如果合法或未过期,放行
            Boolean isValid = jwtOperator.validateToken(token);
    
            if (!isValid) {
                throw new SecurityException("token 不合法!");
            }
            //3. 如果校验成功,就将用户的信息设置到request的attribute里面
            Claims claims = jwtOperator.getClaimsFromToken(token);
            request.setAttribute("id",claims.get("id"));
            request.setAttribute("wxNickname",claims.get("wxNickname"));
            request.setAttribute("role",claims.get("role"));
            } catch (Throwable throwable) {
                throw new SecurityException("token 不合法!");
            }
        }
    
        /**
         * 获取request
         * @return
         */
        private HttpServletRequest getHttpServletRequest() {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
            return attributes.getRequest();
        }
    
        /**
         * 权限验证
         * @param point
         * @return
         * @throws Throwable
         */
        @Around("@annotation(com.banmingi.nodeapp.contentcenter.auth.CheckAuthorization)")
        public Object checkAuthorization(ProceedingJoinPoint point) throws Throwable {
            try {
                //1. 验证token是否合法
                this.checkToken();
                //2. 验证用户角色是否匹配
                HttpServletRequest request = getHttpServletRequest();
                String role = (String) request.getAttribute("role");
    
                MethodSignature signature = (MethodSignature) point.getSignature();
                //拿到添加@CheckAuthorization注解的方法
                Method method = signature.getMethod();
                //拿到@CheckAuthorization注解
                CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class);
    
                String value = annotation.value();
                if (!Objects.equals(role,value)) {
                    throw new SecurityException("用户无权访问!");
                }
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                throw new SecurityException("用户无权访问!");
            }
            return point.proceed();
        }
    }
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值