插件开发版|Authing 结合 APISIX 实现统一可配置 API 权限网关

news2024/12/26 11:49:58

当开发者在构建网站、移动设备或物联网应用程序时,API 网关作为微服务架构中不可或缺的控制组件,是流量的核心进出口。通过有效的权限管控,可以实现认证授权、监控分析等功能,提高 API 的安全性、可用性、拓展性以及优化 API 性能。在上一篇文章( Authing 结合 APISIX 实现统一可配置 API 权限网关【快速启动版】)中,演示了通过 Authing 权限管理 + APISIX 实现 API 的访问控制效果,本文将教你如何实现上述能力的具体实践方法

01 关于 Authing

Authing 是国内首款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务。以「API First」作为产品基石,把身份领域所有常用功能都进行了模块化的封装,通过全场景编程语言 SDK 将所有能力 API 化提供给开发者。同时,用户可以灵活的使用 Authing 开放的 RESTful APIs 进行功能拓展,满足不同企业不同业务场景下的身份和权限管理需求

02 关于 APISIX

Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 不仅支持插件动态变更和热插拔,而且拥有众多实用的插件。Apache APISIX 的 OpenID Connect 插件支持 OpenID Connect 协议,用户可以使用该插件让 Apache APISIX 对接 Authing 服务,作为集中式认证网关部署于企业中。

03 业务目标

通过 Authing 权限管理 + APISIX 实现 API 的访问控制

04 如何实现

本文所涉及到的代码已经上传到 Github

Python 插件
https://github.com/fehu-asia/authing-apisix-python-agent

Java Adapter:
https://github.com/fehu-asia/authing-apisix-java-adapter

Java 插件
https://github.com/fehu-asia/authing-apisix-java-agent

4.1 业务架构

系统整体包含了三大部分: Authing 服务集群、Authing 插件适配服务以及 APISIX 网关,本方案建立需要配置和开发的部分有四个部分,Authing API 权限结构配置、APISIX 插件和路由配置、APISIX 插件开发部署以及业务适配服务开发,其中业务适配服务包含了认证和授权的主要逻辑(使用单独服务承载),避免了插件的频繁更新和部署。

这里需要说明的是,之所以采用 Adapter 的方式来实现,是因为插件我们并不希望经常变动,但需求可能是无法避免的需要经常变动,所以我们将具体的鉴权逻辑放在 Adapter ,插件只实现请求转发和根据 Adapter 的返回结果决定是否放行,同时无状态的插件可以让我们实现更多的场景复用和能力扩展,例如进行鉴权结果的缓存实现,后续只需维护 Adapter 即可。

当然我们也可将具体的逻辑放在插件里。

注意,本教程只用于与 APISIX 和 Authing 进行集成,对于生产环境使用,您需要自行开发插件并保证其安全性及可用性等,本文档不承诺此插件可以用于生产环境

  • APISIX 基础环境搭建
git clone https://github.com/apache/apisix-docker.git
cd apisix-docker/example
docker-compose -p docker-apisix up -d

到这里可以使用 docker ps 查看 apisix docker 进程启动状态, 随后访问 localhost:9000 可以进入 dashboard 界面进行路由和插件的配置。

4.2 在 Authing 对 API 进行管理

登录 Authing 官网:www.authing.com ,进行以下操作:

  • 4.2.1 创建应用

配置 Token 签名算法为 RS256 及校验 AccessToken 的方式为 none 。

  • 4.2.2 创建用户

进入 Authing 控制台-用户管理-用户列表-点击创建用户后,可以根据不同方式(用户名、手机号、邮箱)创建测试用户,如下图所示:

  • 4.2.3 创建 API

进入 Authing 控制台-权限管理-创建资源,可以选择创建树数据类型的资源,如下图所示:

  • 4.2.4 创建策略

进入权限管理-数据资源权限-数据策略标签,可以点击创建策略来新建数据访问策略,如下图所示。策略包含了对应的权限空间中定义的数据以及操作,创建后能够基于此策略对不同对象(用户、角色、用户组等)进行授权管理。

  • 4.2.5 API 授权

4.3 APISIX 路由和 SOCK 配置

  • 4.3.1 SOCK 配置

APISIX 使用 unix sock 与插件进程通信,因此需要配置对应的 sock 端口:

需要将宿主机上的 sock 文件挂载到容器里,插件启动的时候会在宿主机上创建这个 sock 文件,此处需要注意的是,若 APISIX 是先于插件启动的,当插件启动后,则需要重启下 APISIX 容器,确保插件先于 APISIX 启动。

文件位置: /apisix-docker/example/docker-compose.yml apisix部分

  apisix:
    image: apache/apisix:latest
    restart: always
    volumes:
      - ./apisix_log:/usr/local/apisix/logs
      - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
      - /tmp/runner.sock:/tmp/runner.sock
  • 4.3.2 路由配置

X-API-KEY: /apisix/apisix-docker/example/apisix_conf/config.yaml

curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/*",
  "plugins": {
    "ext-plugin-pre-req": {
      "conf": [
 {
          "name": "authing_agent",
      "value": "{\"url\": \"{适配服务的访问地址}\",\"user_pool_id\": \"{用户池ID}\",\"user_pool_secret\": \"{用户池密钥}\"}"
    }
      ]
    }
  },
  "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpbin.org:80": 1
        }
    }
}'

ext-plugin-pre-req 是需要启用的插件类型, 在配置 conf 中需要确定两个变量:

“name”: 插件名称
“value”: “{“url”: “适配服务的访问地址”,“user_pool_id”: “用户池ID”,“user_pool_secret”: “用户池密钥”}”

其中,访问地址格式为 {{domain}}:{{port}}/{{path}}

例如:
“{“url”: “http://192.168.1.123:8080/isAllow”,“user_pool_id”: “124u2353h2t24he2u349382u152”,“user_pool_secret”: “6435462313i5412njburh2u34”}”

4.4 APISIX 插件开发和部署

  • 4.4.1 建立插件工程目录

git clone https://github.com/apache/apisix-python-plugin-runner.git
进入目录 make setup
进入目录 make install
进入目录并修改 apisix/plugins/rewrite.py 文件,将请求参数传递到 Authing

  • 4.4.2 编写 Agent (python) 插件代码

可使用其他语言实现例如 Java、Go、Lua

之所以采用 Python 的原因是因为环境初始化比较简单,可以让开发者快速了解 APISIX 的插件的开发机制。

https://apisix.apache.org/docs/apisix/external-plugin/

from typing import Any
from apisix.runner.http.request import Request
from apisix.runner.http.response import Response
from apisix.runner.plugin.core import PluginBase
import json
import requests
import json

def isAllow(request,config):
    return requests.request("POST", 
        config.get("url"), 
        headers={
        'Content-Type': 'application/json'
        }, 
        data=json.dumps({
        "request": request,
        "pluginConfig": config
        }))



class Rewrite(PluginBase):

    def name(self) -> str:
        return "authing_agent"

    def config(self, conf: Any) -> Any:
        return conf

    def filter(self, conf: Any, request: Request, response: Response):
        # 组装 Adapter 请求参数
        authing_request = {
        "uri": request.get_uri(),
        "method": request.get_method(),
        "args":request.get_args(),
        "headers":request.get_headers(),
        "request_id":request.get_id(),
        "host":request.get_var("host"),
        "remote_addr": request.get_remote_addr(),
        "configs": request.get_configs()
        }
        # 接收 Adapter 响应判断是否放行
        authing_response = isAllow(authing_request,eval(conf))
        if authing_response.text != "ok":
            response.set_status_code(authing_response.status_code)
            response.set_body(authing_response.text)            
  • 4.4.3 运行 Agent 插件
nohup make dev & #后台运行 agent 程序

4.5 适配器开发

  • 4.5.1 通信接口设计

启动代理 Authing 服务(自行实现对应接口,以 springboot 为例,接口结构如下)

  • 4.5.2 部分 JAVA 文件列出如下

IsAllowController.java

package cn.authing.apisix.adapter.controller;

import cn.authing.apisix.adapter.entity.APISIXRquestParams;
import cn.authing.sdk.java.client.ManagementClient;
import cn.authing.sdk.java.dto.CheckPermissionDto;
import cn.authing.sdk.java.dto.CheckPermissionRespDto;
import cn.authing.sdk.java.dto.CheckPermissionsRespDto;
import cn.authing.sdk.java.model.ManagementClientOptions;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import com.google.gson.Gson;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Gao FeiHu
 * @version 1.0.0
 * @date 2022.12.22
 * @email gaofeihu@authing.cn
 */
@RestController
@Slf4j
public class IsAllowController {

    /**
     * 用户池 ID
     */
    public static String ACCESS_KEY_ID = "";
    /**
     * 用户池密钥
     */
    public static String ACCESS_KEY_SECRET = "";
    /**
     * Authing SDK
     * See
     * https://docs.authing.cn/v3/reference/
     */
    ManagementClient managementClient;

    /**
     * 初始化 ManagementClient
     *
     * @param ak  用户池 ID
     * @param aks 用户池密钥
     */
    public void init(String ak, String aks) {
        log.info("init ManagementClient ......");
        try {
            // 保存用户池 ID 和密钥
            ACCESS_KEY_ID = ak;
            ACCESS_KEY_SECRET = aks;
            // 初始化
            ManagementClientOptions options = new ManagementClientOptions();
            options.setAccessKeyId(ak);
            options.setAccessKeySecret(aks);
            managementClient = new ManagementClient(options);
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("初始化 managementClient 失败,可能无法请求!");
        }
    }

    /**
     * 是否放行
     *
     * @param apisixRquestParams 请求 body ,包含了 APISIX 插件的配置以及请求上下文
     * @param response           HttpServletResponse
     * @return 200 OK 放行
     * 403 forbidden 禁止访问
     * 500 internal server error 请求错误 可根据实际需求放行或拒绝
     */
    @PostMapping("/isAllow")
    public Object isAllow(@RequestBody APISIXRquestParams apisixRquestParams, HttpServletResponse response) {

        // 请求计时器
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 请求 ID 与 APISIX 一致
        String requestID = apisixRquestParams.getRequest().getRequest_id();

        log.info("{} ==> 请求入参 : {} ", requestID, new Gson().toJson(apisixRquestParams));

        try {
            // 0. 若插件为多实例用于实现不同业务逻辑,此处可对应修改为多实例模式
            if (managementClient == null || !ACCESS_KEY_ID.equals(apisixRquestParams.getPluginConfig().get("user_pool_id"))) {
                init((String) apisixRquestParams.getPluginConfig().get("user_pool_id"), (String) apisixRquestParams.getPluginConfig().get("user_pool_secret"));
            }

            // 1. 拿到 accessToken
            String authorization = (String) apisixRquestParams.getRequest().getHeaders().get("authorization");
            if (!StringUtils.hasLength(authorization)) {
                return result(response, stopWatch, requestID, HttpStatus.HTTP_UNAUTHORIZED, "HTTP_UNAUTHORIZED");
            }

            String accessToken = authorization;
            if (authorization.startsWith("Bearer")) {
                accessToken = authorization.split(" ")[1].trim();
            }


            log.info("{} ==> accessToken : {} ", requestID, accessToken);
            // 2. 解析 accessToken 拿到应用 ID 和用户 ID
            JWSObject parse = JWSObject.parse(accessToken);
            Map<String, Object> payload = parse.getPayload().toJSONObject();
            String aud = (String) payload.get("aud");
            String sub = (String) payload.get("sub");

            // 3. 校验 accessToken
            // 在线校验
            String result = onlineValidatorAccessToken(accessToken, aud);
            log.info("{} ==> accessToken 在线结果 : {} ", requestID, result);
            if (!result.contains("{\"active\":true")) {
                return result(response, stopWatch, requestID, HttpStatus.HTTP_UNAUTHORIZED, "HTTP_UNAUTHORIZED");
            }

//            // 离线校验
//            if (null == offlineValidatorAccessToken(accessToken, aud)) {
//                return result(response, stopWatch, requestID, HttpStatus.HTTP_UNAUTHORIZED, "HTTP_UNAUTHORIZED");
//            }

            // 4. 获取到 APISIX 中的请求方法,对应 Authing 权限中的 action
            String action = apisixRquestParams.getRequest().getMethod();

            // 5. 获取到 APISIX 中的请求路径
            String resource = apisixRquestParams.getRequest().getUri();

            // 6. 去 Authing 请求,判断是否有权限
            // TODO 可在此添加 Redis 对校验结果进行缓存
            CheckPermissionDto reqDto = new CheckPermissionDto();
            reqDto.setUserId(sub);
            reqDto.setNamespaceCode(aud);
            reqDto.setResources(Arrays.asList(resource.substring(1, resource.length())));
            reqDto.setAction(action);
            CheckPermissionRespDto checkPermissionRespDto = managementClient.checkPermission(reqDto);
            log.info(new Gson().toJson(checkPermissionRespDto));

            // 7. 由于我们是单个 resource 校验,所以只需要判断第一个元素即可
            List<CheckPermissionsRespDto> resultList = checkPermissionRespDto.getData().getCheckResultList();
            if (resultList.isEmpty() || resultList.get(0).getEnabled() == false) {
                return result(response, stopWatch, requestID, HttpStatus.HTTP_FORBIDDEN, "HTTP_FORBIDDEN");
            }

            return result(response, stopWatch, requestID, HttpStatus.HTTP_OK, "ok");

        } catch (Exception e) {
            e.printStackTrace();
            log.error("请求错误!", e);
            return result(response, stopWatch, requestID, HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage());
        }
    }


    public String result(HttpServletResponse response, StopWatch stopWatch, String requestID, int status, String msg) {
        stopWatch.stop();
        log.info("{} ==> 请求耗时:{} , 请求出参 : http_status_code={},msg={} ", requestID, stopWatch.getTotalTimeMillis() + "ms", status, msg);
        response.setStatus(status);
        return msg;
    }


    public String onlineValidatorAccessToken(String accessToken, String aud) {
        HashMap<String, Object> paramMap = new HashMap<>();
        paramMap.put("token", accessToken);
        paramMap.put("token_type_hint", "access_token");
        paramMap.put("client_id", aud);
        return HttpUtil.post("https://api.authing.cn/" + aud + "/oidc/token/introspection", paramMap);

    }

    public JWTClaimsSet offlineValidatorAccessToken(String accessToken, String aud) {
        try {
            ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
                    new DefaultJWTProcessor<>();
            JWKSource<SecurityContext> keySource =
                    null;

            keySource = new RemoteJWKSet<>(new URL("https://api.authing.cn/" + aud + "/oidc/.well-known/jwks.json"));

            JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;

            JWSKeySelector<SecurityContext> keySelector =
                    new JWSVerificationKeySelector<>(expectedJWSAlg, keySource);

            jwtProcessor.setJWSKeySelector(keySelector);

            return jwtProcessor.process(accessToken, null);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        } catch (BadJOSEException e) {
            e.printStackTrace();
        } catch (JOSEException e) {
            e.printStackTrace();
        } finally {
            return null;
        }
    }
}

APISIXRquestParams.java

package cn.authing.apisix.adapter.entity;

import lombok.Data;
import lombok.ToString;

import java.util.Map;

/**
 * APISIX 请求实体类
 */
@Data
@ToString
public class APISIXRquestParams {
    /**
     * APISIX 请求上下文
     */
    APISIXRequest request;
    /**
     * 插件配置
     */
    Map<String, Object> pluginConfig;

}

APISIXRequest.java

package cn.authing.apisix.adapter.entity;

import lombok.Data;
import lombok.ToString;

import java.util.Map;

@Data
@ToString
public class APISIXRequest {
    private String uri;
    private String method;
    private String request_id;
    private String host;
    private String remote_addr;
    private Map<String, Object> args;
    private Map<String, Object> headers;
    private Map<String, Object> configs;
}

4.6 访问测试

  • 4.6.1 未认证

  • 4.6.2 无权限

  • 4.6.3 认证通过并成功访问

404 是因为上游服务没有这个接口,但认证和 API 鉴权已经通过

05 总结

如果您需要对 API 进行细颗粒度的管理可以通过本方案来实现,我们可以在 Adapter 实现更加细粒度的 API 访问控制以及更加场景化的权限方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/352755.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【郭东白架构课 模块一:生存法则】06|法则二:拼多多是如何通过洞察用户人性脱颖而出的?

你好&#xff0c;我是郭东白。上节课我们学习了怎么利用马斯洛理论去指导架构设计&#xff0c;尤其是该如何考虑和顺应研发人员的人性。 我们都知道&#xff0c;软件这个虚拟的存在最终是要服务于用户的&#xff0c;所以在软件设计的过程中也要考虑用户的人性。也就是说&#…

二进制中1的个数-剑指Offer-java位运算

一、题目描述编写一个函数&#xff0c;输入是一个无符号整数&#xff08;以二进制串的形式&#xff09;&#xff0c;返回其二进制表达式中数字位数为 1 的个数&#xff08;也被称为 汉明重量).&#xff09;。提示&#xff1a;请注意&#xff0c;在某些语言&#xff08;如 Java&…

chatGPT在软件测试中七大应用方式

chatGPT火得不能再火了&#x1f525;过去两周&#xff0c;国内chatGPT概念股很疯狂&#xff0c;不只是百度、讯飞&#xff0c;有些默默无闻且业绩亏损的公司股价大涨&#xff0c;有1-2个公司连续7个涨停板&#xff0c;不可思议&#xff01;上周&#xff0c;因为微软Bing发布新版…

postman-enterprise-API

Postman 是一个用于构建和使用 API 的 API 平台。Postman 简化了 API 生命周期的每个步骤并简化了协作&#xff0c;因此您可以更快地创建更好的 API。 API存储库 在一个中央平台上围绕您的所有 API 工件轻松存储、编目和协作。Postman 可以存储和管理 API 规范、文档、工作流配…

【Unity资源下载】POLYGON Dungeon Realms - Low Poly 3D Art by Synty

$149.99 Synty Studios 一个史诗般的低多边形资产包&#xff0c;包括人物、道具、武器和环境资产&#xff0c;用于创建一个以奇幻为主题的多边形风格游戏。 模块化的部分很容易在各种组合中拼凑起来。 包包含超过1,118个详细预制件。 主要特点 ◼ ◼ 完全模块化的地下城!包…

“消息驱动、事件驱动、流 ”的消息模型

文章目录背景消息驱动 Message-Driven事件驱动 Event-Driven流 Streaming事件规范标准简介&#xff1a; 本文旨在帮助大家对近期消息领域的高频词“消息驱动&#xff08;Message-Driven&#xff09;&#xff0c;事件驱动&#xff08;Event-Driven&#xff09;和流&#xff08;S…

基于Java+Swing+Mysql实现通讯录管理系统

基于JavaSwingMysql实现通讯录管理系统一、系统介绍二、功能展示1.用户登陆2.查询信息3.新增信息4.修改信息5.删除信息三、数据库四、其他系统实现五、获取源码一、系统介绍 1.登录系统 2.查询信息 3.新增信息 4.修改信息 5.删除信息 运行环境&#xff1a;idea/eclipse、mysq…

erupt框架Ueditor富文本编辑器图片上传出现405异常

最近在研究erupt框架(v1.11.2),当字段使用Ueditor富文本编辑器,在图片上传的时候出现405异常,接口不支持POST请求方式: 根据错误提示找到对应的源码,发现Handler方法只支持GET请求,而图片上传的时候是以POST方式发起请求的; 此时需要修改源码,用自定义的类覆盖jar包中同名的…

Qt系列:调用Edge浏览器示例

背景 需要解决以下几个问题 政府项目新浏览器兼容老系统ActiveX控件&#xff0c;Qt WebEngineView没有直接的实现方案&#xff0c;需要利用Qt的ActiveX兼容模块与浏览器往返多次交互Qt ActiveX未实现COM事件通知官方Win32示例存在滥用lambda函数的嫌疑&#xff0c;lambda函数…

2023年保健饮品行业分析:市场规模不断攀升,年度销额增长近140%

随着人们健康意识的不断增强&#xff0c;我国保健品市场需求持续增长&#xff0c;同时&#xff0c;保健饮品的市场规模也在不断攀升。 根据鲸参谋电商数据显示&#xff0c;2022年度&#xff0c;京东平台上保健饮品的年度销量超60万件&#xff0c;同比增长了约124%&#xff1b;该…

flutter-第1章-配置环境

flutter-第1章-配置环境 本文针对Windows系统。 一、安装Android Studio 从Android Studio官网下载最新版本&#xff0c;一直默认安装就行。 安装完成要下载SDK&#xff0c;可能会需要科学上网。 打开AS&#xff0c;随便创建一个新项目。 点击右上角的SDK Manager 找到SDK…

c#: NetTopologySuite凹凸多边形计算

环境&#xff1a; .net 6.0NetTopologySuite 2.5.0vs2022平面二维 一、夹角计算 1.1 计算向量与x轴正方向的夹角 方法: AngleUtility.Angle(Coordinate p) 下图上的t2即为p&#xff0c;之所以这么写是为了和AngleUtility.AngleBetweenOriented做比较 注意&#xff1a; 结果…

MySQL 服务正在启动.MySQL 服务无法启动.服务没有报告任何错误。请键入 NET HELPMSG 3534 以获得更多的帮助。总结较全 (已解决)

输入以下命令启动mysql&#xff1a; net start mysql出现以下错误提示&#xff1a; MySQL 服务正在启动 .MySQL 服务无法启动。服务没有报告任何错误。请键入 NET HELPMSG 3534 以获得更多的帮助。 出现这个问题的话&#xff0c;一般有几个情况&#xff1a; 一、MySQL安装文…

面试7分看能力,3分靠嘴皮,剩下90分就靠这份Java面试八股文

有句话说的好&#xff0c;面试中7分靠能力&#xff0c;3分靠嘴皮刚开始面试的时候&#xff0c;介绍项目一般都是凸显我们技能的重中之重&#xff0c;它对一次面试的成败可以说具有决定性作用&#xff0c;这就涉及到我们的表达能力了&#xff0c;有人说我其实水平都在线&#xf…

大学生开学买什么,返校必备数码好物推荐

开学还不知道需要准备些什么&#xff0c;这篇开学数码好物&#xff0c;希望能够对你在开学购买的好物有一些帮助&#xff0c;一款好的数码装备&#xff0c;可以让我们在学校学习当中能够用最少的时间&#xff0c;最大的产出&#xff0c;节省时间&#xff0c;提高学习效率&#…

2023-02-17 学习记录--TS-邂逅TS(一)

TS-邂逅TS&#xff08;一&#xff09; 不积跬步&#xff0c;无以至千里&#xff1b;不积小流&#xff0c;无以成江海。&#x1f4aa;&#x1f3fb; 一、TypeScript在线编译器 https://www.typescriptlang.org/play/ 二、类型 1、普通类型 number&#xff08;数值型&#xff…

零信任-360连接云介绍(9)

​360零信任介绍 360零信任又称360连接云安全访问平台(下文简称为&#xff1a;360连接云)&#xff0c;360连接云&#xff0c;是360基于零信任安全理念&#xff0c;以身份为基础、动态访问控制为核心打造的安全访问平台。 通过收缩业务暴露面、自适应增强身份认证、终端持续检…

操作系统(day11)--快表,两级页表

具有快表的地址变换机构 时间局限性&#xff1a;会有大量连续的指令需要访问同一个内存块的数据的情况&#xff08;程序中的循环&#xff09; 空间局限性&#xff1a;一旦程序访问了某个存储单元&#xff0c;在不久之后&#xff0c;其附近的存储单元也很有可能被访问。&#xf…

MySQL(一)服务器连接 库的基本操作

目录 一、连接服务器 二、简单使用 三、校验规则 条件筛选 where 进行order排序 三、查看数据库 使用 show databases&#xff1b;&#xff08;注意分号和最后一个s&#xff09; 显示创建数据库的详情信息&#xff1a;使用show create database test2; 四、修改数据库 五…

干货 | 有哪些安慰剂按钮的设计?

仔细观察我们的生活&#xff0c;你会发现处处都是安慰剂按钮&#xff0c;ATM的点钞声、开启空调的呼呼声&#xff0c;这些都对用户心里产生了有意的引导作用&#xff0c;当你打开了空调按钮&#xff0c;先播放声音会让你感觉你按下的按钮起到了作用。 我们的大脑不喜欢杂乱无章…