oauth2统一认证授权_authenticationsuccessevent oauth2.0 统一认证-程序员宅基地

技术标签: 笔记  

oauth2.0统一认证授权

1.认证与授权

​ 最近学习整理了下认证授权相关实现,下面是大概的一些理解与学习过程。在分布式系统中每个服务都需要认证,授权。如果每个服务都实现一套认证授权的逻辑就会显得冗余,考虑到分布式系统共享性的特点,我们可以独立一个授权服务出来,可以对内部系统或者第三方应用提供认证。

1.1统一认证授权:

​ 提供独立的认证服务,统一处理认证授权。不论是什么用户还是不同种类的客户端,例如小程序,APP,web,都采用一致的认证,权限,会话机制。同时保持开放性,可以接入第三方外部应用。

2.OAuth2.0协议

​ Oauth是一个开放标准协议,他允许用户授权第三方应用访问他们存储在服务提供者上的信息。不需要提供用户名密码等敏感信息,而只需要提供服务提供者允许且用户(客户端)需要的信息。

(1).授权码模式

​ 最常见的例子就是我们平时微信支付宝的第三方登录。用户在浏览器访问网站A.如果该网站接入了微信认证。那么他可以申请微信认证。微信发放授权码,令牌等一系列操作,然后网站A持有该令牌去获取微信用户的信息。

在这里插入图片描述
1、客户端请求第三方授权用户进入网站A的登录页面,点击微信的图标以微信账号登录系统,点击“微信”出现一个二维码,此时用户扫描二维码,开始给网站A授权。

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

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

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

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

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

以上流程只是oauth2.0的一种模式,即授权码模式。也是最oauth2中最安全的一种模式。授权码模式是oauth2中最安全的一种模式。观察上面的流程可以发现token始终没有经过浏览器也没有暴露密码,一切授权操作都是交给客户端的后台进行。因此这种模式对于第三方应用接入应用适用。

(2)密码模式:

在这里插入图片描述
流程:
1.用户想访问客户端,直接提供用户名密码给客户端。
2.客户端将用户名,密码交给授权服务直接获取令牌。
密码模式相对于授权码模式简单一些,但没有授权码模式那么安全,因为他讲用户名密码暴露给了客户端。这种模式一般只适用于客户端是我们内部开发的情况,对于接入外部应用就不行了。

3.Spring Cloud Security OAuth2

​ spring-Security-Oauth2是springcloud对oauth2的实现,oauth2的服务提供了两个服务。授权服务(Authorization Server)与资源服务(Resource server)

3.1授权服务

​ 授权服务(UAA) (Authorization Server)应包含对接入端以及登入用户的合法性进行验证并颁发token等功能。

AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。
TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。

3.1.1授权服务搭建

​ 在父工程下建立security-uaa授权服务。
在这里插入图片描述
创建授权服务配置类: 用@EnableAuthorizationServer注解并且实现AuthorizationServerConfigurerAdapte配置授权服务。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    
}

AuthorizationServerConfigurerAdapter要求配置以下几个类:这几个对象由spring-oauth2进行管理,他们会被spring传入AuthorizationServerConfigurer中进行配置。

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    
    public AuthorizationServerConfigurerAdapter() {
    
    }

    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    }

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    }

    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
    }
}

ClientDetailsServiceConfigurer:用来配置客户端详情服务,客户端的详细信息都在这里进行配置。客户端的信息在这里可以被写死,也可以将他们保存在数据库中。简单的来说这里就是来配置允许认证的客户端。

**AuthorizationServerEndpointsConfigurer:**用来配置令牌访问端点与令牌服务,申请令牌的url。令牌发放生成规则。

AuthorizationServerSecurityConfigurer:令牌的安全约束,访问令牌的安全约束。

3.1.2配置客户端详细信息

ClientDetailsServiceConfigurer可以将客户端详细信息配置在数据库,也可以配置在内存中(clientDetailsService),

clientDetailsService:clientDetailsService负责查找clientDetails

clientDetails:clientDetails中包括以下几个属性。

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

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

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

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

​ authorities:此客户端可以使用的权限

使用内存方式存储userDetails:

    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
    
        //clients.withClientDetails(clientDetailsService); //存储在数据库中
        clients.inMemory()// 存储在内存中
                .withClient("c1")// client_id,客户端id,
                .secret(new BCryptPasswordEncoder().encode("secret"))//客户端密钥
                .resourceIds("res1","res2")//资源列表这里是一个列表可以配置多个
                .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com"); //并且返回授权码
    }

客户端的信息可以采用存储在数据库中的形式:
在这里插入图片描述

    @Autowired
    PasswordEncoder passwordEncoder;

    //将客户端信息存储到数据库
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
    
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
    
        clients.withClientDetails(clientDetailsService);}
3.1.3配置令牌管理与访问端点

​ (1).配置令牌管理

​ 配置令牌存储单独写一个配置类进行配置,注入到spring容器中。

@Configuration
public class TokenConfig {
    

    //采用对称加密的方式生成令牌,统一在网关层进行校验
    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
    
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    //配置令牌生成器
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
    
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }
}

(2).定义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;
    }

配置授权码服务:AuthorizationServerEndpointsConfigurer 这个对象的实例可以完成令牌服务以及令牌endpoint配置。可以通过设置如下属性来决定支持的授权类型。

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

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

@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
    
    return new JdbcAuthorizationCodeServices(dataSource);//设置授权码模式的授权码如何存取
}

这里我们使用授权码模式,必须设置authorizationCodeServices

	@Autowired
    private AuthorizationCodeServices authorizationCodeServices; 
    @Autowired
    private AuthenticationManager authenticationManager;
@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    
        endpoints
                .authenticationManager(authenticationManager)//认证管理器
                .authorizationCodeServices(authorizationCodeServices)//授权码服务
                .tokenServices(tokenService())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
3.1.4配置令牌访问约束

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

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security){
    
                security
                 //oauth/token_key是公开
                .tokenKeyAccess("permitAll()")                    
                 //oauth/check_token公开,开启token检查:资源服务拿到令牌可以请求oauth/check_token进行验证,如果采用jwt令牌的方式这里可以不用开启
                .checkTokenAccess("permitAll()") 
                    //表单认证(申请令牌)
                .allowFormAuthenticationForClients();				
    }

1.tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个
endpoint完全公开。
2.checkToken这个endpoint完全公开
3.允许表单认证

3.2资源服务配置

新建一个服务
在这里插入图片描述

创建资源服务配置类

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends
ResourceServerConfigurerAdapter {
    
    //资源的标识,这里与授权服务中clientDetailsService中配置的resourceIds("res1","res2")相对应
	public static final String RESOURCE_ID = "res1";
    
    //这里配置资源服务
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
    
    resources.resourceId(RESOURCE_ID)
    .tokenServices(tokenService())
    .stateless(true);
}
   //这里的配置类似于HttpSecurity访问权限等一系列配置
@Override
public void configure(HttpSecurity http) throws Exception {
    
    http
    .authorizeRequests()
        //这里配置授权服务中允许访问的范围。
    .antMatchers("/**").access("#oauth2.hasScope('all')")
    .and().csrf().disable()
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

4.统一认证授权

在这里插入图片描述
流程:
接入方经过网关请求授权服务授权,授权服务验证,并返回token给接入应用
接入方携带token访问资源。
在网关层,会校验该接入方的访问权限,并且解析token为明文,将该token下发给对应资源服务
在资源服务拿到token后,还会继续根据token中的身份信息来校验是否有权限访问对应资源。

4.1搭建注册中心

所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务。
在这里插入图片描述

配置application.yml

spring:
  application:
    name: security_discovery
server:
  port: 53000 #启动端口
eureka:
  server:
    enable-self-preservation: false    #关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保护,导致别人还认为是好用的服务
  client:
    register-with-eureka: false  #false:不作为一个客户端注册到注册中心
    instance-info-replication-interval-seconds: 10
    serviceUrl:
      defaultZone: http://localhost:${
    server.port}/eureka/

4.2搭建网关

​ 认证服务器生成jwt令牌,所有请求都会通过网关层校验。在网关层拦截请求获取令牌解析,并转发给对应的服务。这样各资源服务就不需要关注令牌解析了。网关就从当了上文中资源服务器的角色。

在这里插入图片描述

网关采用zuul,统一认证服务与资源服务都是网关下微服务,需要在网关上新增路由配置

zuul.routes.uaa‐service.stripPrefix = false
zuul.routes.uaa‐service.path = /uaa/**
zuul.routes.user‐service.stripPrefix = false
zuul.routes.user‐service.path = /source/**
4.2.1网关转发token

1.从请求中获取令牌内容。

2.组装明文token,转发给微服务,放入请求头中

public class AuthFilter extends ZuulFilter {
    

    @Override
    public String filterType() {
    
        return "pre";
    }

    @Override
    public int filterOrder() {
    
        return 0;
    }

    @Override
    public boolean shouldFilter() {
    
        return true;
    }

    @Override
    public Object run() throws ZuulException {
    
        RequestContext currentContext = RequestContext.getCurrentContext();
        //从Security上下文中拿到用户身份对象
        SecurityContext authentication = SecurityContextHolder.getContext();
        //判断其是否是oauth授予的身份信息
        if(!(authentication instanceof OAuth2Authentication)){
    
            return null;
        }

        OAuth2Authentication oAuth2Authentication =(OAuth2Authentication) authentication;

        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();

//        取出用户身份信息
        String principal = userAuthentication.getName();

//        取出用户权限
        List<String> authorities =  new ArrayList<>();

        Collection<? extends GrantedAuthority> authorities1 = userAuthentication.getAuthorities();

        for(GrantedAuthority authentication1:authorities1 ) {
    
            authorities.add(authentication1.getAuthority());
        }

        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);

        if(userAuthentication != null) {
    
            jsonToken.put("principal" ,principal);
            jsonToken.put("authorities",authorities);
        }
//        把用户身份信息和授权信息放在json中,加入http的header中,转发给微服务
        //对其进行base64编码
        currentContext.addZuulRequestHeader("json-token",EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
        return null;
    }
}

配置令牌配置类:

在授权服务中我配置了采用对称加密方式生成令牌的配置类,这里直接拷贝到网关服务。

public class TokenConfig {
    

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
    
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
    
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }
4.2.2配置资源服务

主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要什么样的权限才能访问某个微服务

@Configuration
public class ResouceServerConfig  {
    

    //资源的标识,这里与授权服务中clientDetailsService中配置的resourceIds("res1","res2")相对应
    public static final String RESOURCE_ID = "res1";

    //这里是对授权服务请求进行处理
    @Configuration
    @EnableResourceServer
    public class UAAServerConfig extends ResourceServerConfigurerAdapter {
    
        @Autowired
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
    
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
    
            //这里配置只要是授权服务的请求都不进行拦截
            http.authorizeRequests()
                 .antMatchers("/uaa/**").permitAll();
        }
    }

    //这里是对3.2中的资源服务的请求进行处理
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        @Autowired
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
    
            //这里对令牌进行解析
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
    
            http
                    .authorizeRequests()
                	//这里配置授权服务中允许访问的范围。
                    .antMatchers("/resource/**").access("#oauth2.hasScope('ROLE_API')");
        }
    }
    //配置其它的资源服务..
}
4.4.3用户权限拦截

在下游微服务拿到token后,应该怎么实现权限拦截?自己解析明文token,自己实现一套访问策略?在这里还是可以使用spring security来认证token,在资源服务中新建filter,用于从请求中获取token并解析,拿到用户身份信息与权限,将其放入springsecurity上下文中。

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    
            //解析出头中的token
        String token = httpServletRequest.getHeader("json-token");
        if(token!=null){
    
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            //将token转成json对象
            JSONObject jsonObject = JSON.parseObject(json);
            //用户身份信息
            UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
            //用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
            //将用户信息和权限填充 到用户身份token对象中
            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            //将authenticationToken填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            
        }
        filterChain.doFilter(httpServletRequest,httpServletResponse);

    }
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39271545/article/details/118197556

智能推荐

Docker 快速上手学习入门教程_docker菜鸟教程-程序员宅基地

文章浏览阅读2.5w次,点赞6次,收藏50次。官方解释是,docker 容器是机器上的沙盒进程,它与主机上的所有其他进程隔离。所以容器只是操作系统中被隔离开来的一个进程,所谓的容器化,其实也只是对操作系统进行欺骗的一种语法糖。_docker菜鸟教程

电脑技巧:Windows系统原版纯净软件必备的两个网站_msdn我告诉你-程序员宅基地

文章浏览阅读5.7k次,点赞3次,收藏14次。该如何避免的,今天小编给大家推荐两个下载Windows系统官方软件的资源网站,可以杜绝软件捆绑等行为。该站提供了丰富的Windows官方技术资源,比较重要的有MSDN技术资源文档库、官方工具和资源、应用程序、开发人员工具(Visual Studio 、SQLServer等等)、系统镜像、设计人员工具等。总的来说,这两个都是非常优秀的Windows系统镜像资源站,提供了丰富的Windows系统镜像资源,并且保证了资源的纯净和安全性,有需要的朋友可以去了解一下。这个非常实用的资源网站的创建者是国内的一个网友。_msdn我告诉你

vue2封装对话框el-dialog组件_<el-dialog 封装成组件 vue2-程序员宅基地

文章浏览阅读1.2k次。vue2封装对话框el-dialog组件_

MFC 文本框换行_c++ mfc同一框内输入二行怎么换行-程序员宅基地

文章浏览阅读4.7k次,点赞5次,收藏6次。MFC 文本框换行 标签: it mfc 文本框1.将Multiline属性设置为True2.换行是使用"\r\n" (宽字符串为L"\r\n")3.如果需要编辑并且按Enter键换行,还要将 Want Return 设置为 True4.如果需要垂直滚动条的话将Vertical Scroll属性设置为True,需要水平滚动条的话将Horizontal Scroll属性设_c++ mfc同一框内输入二行怎么换行

redis-desktop-manager无法连接redis-server的解决方法_redis-server doesn't support auth command or ismis-程序员宅基地

文章浏览阅读832次。检查Linux是否是否开启所需端口,默认为6379,若未打开,将其开启:以root用户执行iptables -I INPUT -p tcp --dport 6379 -j ACCEPT如果还是未能解决,修改redis.conf,修改主机地址:bind 192.168.85.**;然后使用该配置文件,重新启动Redis服务./redis-server redis.conf..._redis-server doesn't support auth command or ismisconfigured. try

实验四 数据选择器及其应用-程序员宅基地

文章浏览阅读4.9k次。济大数电实验报告_数据选择器及其应用

随便推点

灰色预测模型matlab_MATLAB实战|基于灰色预测河南省社会消费品零售总额预测-程序员宅基地

文章浏览阅读236次。1研究内容消费在生产中占据十分重要的地位,是生产的最终目的和动力,是保持省内经济稳定快速发展的核心要素。预测河南省社会消费品零售总额,是进行宏观经济调控和消费体制改变创新的基础,是河南省内人民对美好的全面和谐社会的追求的要求,保持河南省经济稳定和可持续发展具有重要意义。本文建立灰色预测模型,利用MATLAB软件,预测出2019年~2023年河南省社会消费品零售总额预测值分别为21881...._灰色预测模型用什么软件

log4qt-程序员宅基地

文章浏览阅读1.2k次。12.4-在Qt中使用Log4Qt输出Log文件,看这一篇就足够了一、为啥要使用第三方Log库,而不用平台自带的Log库二、Log4j系列库的功能介绍与基本概念三、Log4Qt库的基本介绍四、将Log4qt组装成为一个单独模块五、使用配置文件的方式配置Log4Qt六、使用代码的方式配置Log4Qt七、在Qt工程中引入Log4Qt库模块的方法八、获取示例中的源代码一、为啥要使用第三方Log库,而不用平台自带的Log库首先要说明的是,在平时开发和调试中开发平台自带的“打印输出”已经足够了。但_log4qt

100种思维模型之全局观思维模型-67_计算机中对于全局观的-程序员宅基地

文章浏览阅读786次。全局观思维模型,一个教我们由点到线,由线到面,再由面到体,不断的放大格局去思考问题的思维模型。_计算机中对于全局观的

线程间控制之CountDownLatch和CyclicBarrier使用介绍_countdownluach于cyclicbarrier的用法-程序员宅基地

文章浏览阅读330次。一、CountDownLatch介绍CountDownLatch采用减法计算;是一个同步辅助工具类和CyclicBarrier类功能类似,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。二、CountDownLatch俩种应用场景: 场景一:所有线程在等待开始信号(startSignal.await()),主流程发出开始信号通知,既执行startSignal.countDown()方法后;所有线程才开始执行;每个线程执行完发出做完信号,既执行do..._countdownluach于cyclicbarrier的用法

自动化监控系统Prometheus&Grafana_-自动化监控系统prometheus&grafana实战-程序员宅基地

文章浏览阅读508次。Prometheus 算是一个全能型选手,原生支持容器监控,当然监控传统应用也不是吃干饭的,所以就是容器和非容器他都支持,所有的监控系统都具备这个流程,_-自动化监控系统prometheus&grafana实战

React 组件封装之 Search 搜索_react search-程序员宅基地

文章浏览阅读4.7k次。输入关键字,可以通过键盘的搜索按钮完成搜索功能。_react search