网关

api-gateway

api-gateway是一款轻量级、高性能、易扩展的基于zuul的网关产品,提供API的统一管理服务、涵盖API发布、管理、运维的全生命周期管理。对内辅助用户简单、快速、低成本、低风险的实现微服务聚合、前后端分离、系统集成等功能;对外面向合作伙伴、开发者开放服务。通过使用API-Gateway,我们能快速帮助用户实现传统ESB面临的主要场景,又能满足新型业务场景(移动应用等)所需的高性能、安全、可靠等要求。

通用网关设计

软负载ZUUL

网关活动图

api-gateway在项目中的位置

api gateway作用

  • 简化客户端调用复杂度
    在微服务架构模式下后端服务的实例数一般是动态的,对于客户端而言,很难发现动态改变的服务实例的访问地址信息。因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入API Gateway作为轻量级网关,同时API Gateway中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。
  • 数据裁剪以及聚合
    通常而言不同的客户端在显示时对于数据的需求是不一致的,比如手机端或者Web端又或者在低延迟的网络环境或者高延迟的网络环境。因此为了优化客户端的使用体验,API Gateway可以对通用性的响应数据进行裁剪以适应不同客户端的使用需求,同时还可以将多个API调用逻辑进行聚合,从而减少客户端的请求数,优化客户端用户体验。
  • 多渠道支持
    当然我们还可以针对不同的渠道和客户端提供不同的API Gateway,对于该模式的使用由另外一个大家熟知的方式叫Backend for front-end,在Backend for front-end模式当中,我们可以针对不同的客户端分别创建其BFF。
  • 遗留系统的微服务改造
    对于遗留系统而言进行微服务改造通常是由于原有的系统存在或多或少的问题,比如技术债务,代码质量,可维护性,可扩展性等等。API Gateway的模式同样适用于这一类遗留系统的改造,通过微服务化的改造逐步实现对原有系统中的问题的修复,从而提升对于原有业务相应力的提升。通过引入抽象层,逐步使用新的实现替换旧的实现。

    在Spring Cloud体系中,Spring Cloud Zuul就是提供负载均衡,反向代理,权限认证的一个API Gateway。

api-gateway代码分析

开启ZUUL

Zuul提供的功能

  • 认证鉴权-可以识别访问资源的每一个请求,拒绝不满足的请求
  • 监控埋点,跟踪有意义的数据并统计,以便生成有意义的生产视图
  • 熔断限流,为每一个请求分配容量,并丢弃超过限制的请求
  • 抗压设计,线程池隔离增大并发能力

网关的2层超时调优

hystrix ribbon活动图

hystrix ribbon配置

#设置最大容错超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 90000
#设置最大超时时间
ribbon:  eager-load:enabled: trueServerListRefreshInterval: 10  #刷新服务列表源的间隔时间httpclient:enabled: falseokhttp:enabled: true  ReadTimeout: 90000  ConnectTimeout: 90000 OkToRetryOnAllOperations: trueMaxAutoRetries: 1MaxAutoRetriesNextServer: 1

源码分析

zuul内部代码

类图

参考:https://www.jianshu.com/p/295e51bc1518

zuul基于eureka的服务发现路由

路由注册相关逻辑

org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator

启动时配置mapping等信息

zuul转发逻辑

以访问http://127.0.0.1:9200/api-user/users-anon/login?username=admin为例

  • 通过/api-user/users-anon/login查映射表
  • 找到ZuulRoute映射关系
  • 执行pre过滤器org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter处理是否增加请求头等信息
  • 执行route过滤器
  • org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter

总结

  • pre:这种过滤器在请求被转发之前调用,一般用来实现身份验证等
  • routing:这种路由是用来路由到不同的后端服务的,底层可以使用httpclient或者ribbon请求微服务
  • post:当请求转发到微服务以后,会调用当前类型的过滤器。通常用来为响应天啊及标椎的HTTP Header、收集统计信息,等
  • error:当发生错误是执行的过滤器

网关自定义过滤器


  • brave.servlet.TracingFilter :生成traceId
  • com.open.capacity.common.filter.TraceContextFilter:传递traceId
  • com.open.capacity.client.filter.AccessFilter:传递token
  • com.open.capacity.client.filter.RequestStatFilter::传递traceId
  • com.open.capacity.client.filter.ResponseStatFilter:响应头增加traceId

基于阿波罗配置中心的动态路由

参考代码:https://gitee.com/owenwangwen/config-center/tree/master/apollo-gateway

阿波罗官方已吸收此案例在github可下载

https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-zuul/src/main/java/com/ctrip/framework/apollo/use/cases/spring/cloud/zuul

  • 创建项目
  • 创建配置
  • 项目绑定配置

api-gateway 构建资源服务器

<!-- 资源服务器 --><dependency><groupId>com.open.capacity</groupId><artifactId>uaa-client-spring-boot-starter</artifactId></dependency>

uaa-client-spring-boot-starter源码分析

网关认证处理流程图

网关白名单

security:oauth2:ignored:  /test163/** , /api-auth/** , /doc.html ,/test111 ,/api-user/users-anon/login, /api-user/users/save,    /user-center/users-anon/login,/document.html,**/v2/api-docs,/oauth/** ,/login.html ,/user/login,/**/**.css ,/**/**.js   ,/getVersiontoken:store:type: redis

网关统一异常

认证处理核心代码

public Authentication authenticate(Authentication authentication) throws AuthenticationException {if (authentication == null) {throw new InvalidTokenException("Invalid token (token not found)");}String token = (String) authentication.getPrincipal();OAuth2Authentication auth = tokenServices.loadAuthentication(token);if (auth == null) {throw new InvalidTokenException("Invalid token: " + token);}Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");}checkClientDetails(auth);if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();// Guard against a cached copy of the same detailsif (!details.equals(auth.getDetails())) {// Preserve the authentication details from the one loaded by token servicesdetails.setDecodedDetails(auth.getDetails());}}auth.setDetails(authentication.getDetails());auth.setAuthenticated(true);return auth;

授权流程

启用授权

@Beanpublic OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();expressionHandler.setApplicationContext(applicationContext);return expressionHandler;}

网关认证授权总结

  • 访问者(Accessor)需要访问某个资源(Resource)是这个场景最原始的需求,但并不是谁都可以访问资源,也不是任何资源都允许任何人来访问,所以中间我们要加入一些检查和防护
  • 在访问资源的所经之路上,可能遇到细菌,病毒,不管怎么样,对于要防护的资源来说最好的方法就是设关卡点,对于上图的FilterSecurityInvation,MethodIncation,Jointpoint,这些在spring security oauth中统称SecuredObjects
  • 我们知道在哪里设置关卡点最合适,下一步就是设置关卡,对应FileSecurityInterceptor,MethodSecurityInterceptor,AspectSecurityInterceptor,
    这些关卡统一的抽象类是AbstractSecurityInterceptor
  • 有关卡点,关卡了以后,到底谁该拦截谁不应该呢,spring security oauth中由 AccessDecisionManager控制
  • 最后一个问题,这个谁怎么定义,我们总得知道当前这个访问者是谁才能告诉AccessDecisionManager拦截还是放行,在spring security oauth框架中AuthenticationManager将解决访问者身份认证问题,只有确定你在册了,才可以给授权访问。AuthenticationManager,AccessDecisionManager,AbstractSecurityInterceptor属于spring security框架的基础铁三角。
  • 有了以上骨架,真正执行防护任务的其实是SecurityFilterChain中定于的一系列Filter,其中ExceptionTranslationFilter,它负责接待或者送客,如果访问者来访,对方没有报上名来,那么,它就会让访客去登记认证(找AuthenticationManager做认证),如果对方报上名了,但认证失败,那么请重新认证送客,送客的方式是抛出相应的Exception,所以名字叫做ExceptionTranslationFilter。
  • 最后,这个filter序列中可能不满足我们的需求,比如增加验证码,所以我们需要在其中穿插自己的Filter实现类,为定制和扩展Spring Security Oauth的防护体系。
  • spring security内置的filter序列
  • 执行过滤链

网关api权限设计


相同用户,不同应用的权限隔离
客户端模式 :
客户端A 申请的token ,可以访问/api-user/menu/current ,
客户端B 申请的token,不让访问/api-user/menu/current
密码模式:
客户端模式 :
客户端A admin用户 申请的token ,可以访问/api-user/menu/current ,
客户端B admin用户 申请的token,不让访问/api-user/menu/current

参考issue:https://gitee.com/owenwangwen/open-capacity-platform/issues/IRG23

网关引依赖

网关是否开启基于应用隔离,代码注释了,只是基于token的合法性校验,按建议开启是否启用api接口服务权限
OpenAuthorizeConfigManager

游乐场买了通票,有些地方可以随便玩,有些地方另外
单独校验买票
config.anyRequest().authenticated() ;
//这种通票,token校验正确访问
config.anyRequest().access("@rbacService.hasPermission(request, authentication)"); //这种另外
单独校验,适用于网关对api权限校验


通过clientID隔离服务权限

通过应用分配服务权限

网关hystrix-dashboard

api-gateway 应用访问次数控制

oauth_client_details 增加

字段 备注
if_limit 是否需要控制访问次数
limit_count 访问阀值

auth-server 启动

redis存储结构

加载oauth_client_details 到redis

应用访问次数控制过滤器

/*** Created by owen on 2017/9/10. 根据应用 url 限流 oauth_client_details if_limit 限流开关* limit_count 阈值*/
@Component
public class RateLimitFilter extends ZuulFilter {private static Logger logger = LoggerFactory.getLogger(RateLimitFilter.class);private ThreadLocal<Result> error_info = new ThreadLocal<Result>();@Autowiredprivate RedisLimiterUtils redisLimiterUtils;@Autowiredprivate ObjectMapper objectMapper;@ResourceSysClientServiceImpl sysClientServiceImpl;@Overridepublic String filterType() {return "pre";}@Overridepublic int filterOrder() {return 0;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() {try {RequestContext ctx = RequestContext.getCurrentContext();HttpServletRequest request = ctx.getRequest();if (!checkLimit(request)) {logger.error("too many requests!");error_info.set(Result.failedWith(null, 429, "too many requests!"));serverResponse(ctx, 429);return null;}} catch (Exception e) {e.printStackTrace();}return null;}/**** 统一禁用输出* * @param ctx* @param ret_message*            输出消息* @param http_code*            返回码*/public void serverResponse(RequestContext ctx, int http_code) {try {ctx.setSendZuulResponse(false);outputChineseByOutputStream(ctx.getResponse(), error_info);ctx.setResponseStatusCode(http_code);} catch (IOException e) {StackTraceElement stackTraceElement= e.getStackTrace()[0];logger.error("serverResponse:" + "---|Exception:" +stackTraceElement.getLineNumber()+"----"+ e.getMessage());}}/*** 使用OutputStream流输出中文* * @param request* @param response* @throws IOException*/public void outputChineseByOutputStream(HttpServletResponse response, ThreadLocal<Result> data) throws IOException {/*** 使用OutputStream输出中文注意问题: 在服务器端,数据是以哪个码表输出的,那么就要控制客户端浏览器以相应的码表打开,* 比如:outputStream.write("中国".getBytes("UTF-8"));//使用OutputStream流向客户端浏览器输出中文,以UTF-8的编码进行输出* 此时就要控制客户端浏览器以UTF-8的编码打开,否则显示的时候就会出现中文乱码,那么在服务器端如何控制客户端浏览器以以UTF-8的编码显示数据呢?* 可以通过设置响应头控制浏览器的行为,例如: response.setHeader("content-type",* "text/html;charset=UTF-8");//通过设置响应头控制浏览器以UTF-8的编码显示数据*/OutputStream outputStream = response.getOutputStream();// 获取OutputStream输出流response.setHeader("content-type", "application/json;charset=UTF-8");// 通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码/*** data.getBytes()是一个将字符转换成字节数组的过程,这个过程中一定会去查码表,* 如果是中文的操作系统环境,默认就是查找查GB2312的码表, 将字符转换成字节数组的过程就是将中文字符转换成GB2312的码表上对应的数字* 比如: "中"在GB2312的码表上对应的数字是98 "国"在GB2312的码表上对应的数字是99*//*** getBytes()方法如果不带参数,那么就会根据操作系统的语言环境来选择转换码表,如果是中文操作系统,那么就使用GB2312的码表*/String msg = objectMapper.writeValueAsString(data.get());byte[] dataByteArr = msg.getBytes("UTF-8");// 将字符转换成字节数组,指定以UTF-8编码进行转换outputStream.write(dataByteArr);// 使用OutputStream流向客户端输出字节数组}public boolean checkLimit(HttpServletRequest request) {// 解决zuul token传递问题Authentication user = SecurityContextHolder.getContext().getAuthentication();if (user != null) {if (user instanceof OAuth2Authentication) {try {OAuth2Authentication athentication = (OAuth2Authentication) user;OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) athentication.getDetails();String clientId = athentication.getOAuth2Request().getClientId();Map client = sysClientServiceImpl.getClient(clientId);if(client!=null){String flag = String.valueOf(client.get("if_limit") ) ;if("1".equals(flag)){String accessLimitCount =  String.valueOf(client.get("limit_count") );if (!accessLimitCount.isEmpty()) {Result result = redisLimiterUtils.rateLimitOfDay(clientId, request.getRequestURI(),Long.parseLong(accessLimitCount));if (-1 == result.getResp_code()) {logger.error("token:" + details.getTokenValue() + result.getResp_msg());// ((ResultMsg)// this.error_info.get()).setMsg("clientid:" +// client_id + ":token:" + accessToken + ":" +// result.getMsg());// ((ResultMsg) this.error_info.get()).setCode(401);return false;}}}}} catch (Exception e) {StackTraceElement stackTraceElement= e.getStackTrace()[0];logger.error("checkLimit:" + "---|Exception:" +stackTraceElement.getLineNumber()+"----"+ e.getMessage());}}}return true;}
}

RedisLimiterUtils核心类


@Component
public class RedisLimiterUtils {public static final String API_WEB_TIME_KEY = "time_key:";public static final String API_WEB_COUNTER_KEY = "counter_key:";private static final String EXCEEDS_LIMIT = "规定的时间内超出了访问的限制!";private static Logger logger = LoggerFactory.getLogger(RedisLimiterUtils.class);@ResourceRedisTemplate<Object, Object> redisTemplate;@Resource(name = "stringRedisTemplate")ValueOperations<String, String> ops;@Resource(name = "redisTemplate")ValueOperations<Object, Object> objOps;@ResourceRedisUtil redisUtil;public Result IpRateLimiter(String ip, int limit, int timeout) {String identifier = UUID.randomUUID().toString();String time_key = "time_key:ip:" + ip;String counter_key = "counter_key:ip:" + ip;if (!redisUtil.hasKey(time_key) || redisUtil.getExpire(time_key) <= 0) {redisUtil.set(time_key, identifier, timeout);redisUtil.set(counter_key, 0);}if (redisUtil.hasKey(time_key) && redisUtil.incr(counter_key, 1) > limit) {logger.info(EXCEEDS_LIMIT);return Result.failedWith(null, -1, EXCEEDS_LIMIT);}return Result.succeedWith(null, 0,  "调用次数:" + this.ops.get(counter_key) );}public Result clientRateLimiter(String clientid, int limit, int timeout) {String identifier = UUID.randomUUID().toString();String time_key = "time_key:clientid:" + clientid;String counter_key = "counter_key:clientid:" + clientid;if (!redisUtil.hasKey(time_key) || redisUtil.getExpire(time_key) <= 0) {redisUtil.set(time_key, identifier, timeout);redisUtil.set(counter_key, 0);}if (redisUtil.hasKey(time_key) && redisUtil.incr(counter_key, 1) > limit) {logger.info(EXCEEDS_LIMIT);return Result.failedWith(null, -1, EXCEEDS_LIMIT);}return Result.succeedWith(null, 0,  "调用次数:" + this.ops.get(counter_key) );}public Result urlRateLimiter(String path, int limit, int timeout) {String identifier = UUID.randomUUID().toString();String time_key = "time_key:path:" + path;String counter_key = "counter_key:path:" + path;if (!redisUtil.hasKey(time_key) || redisUtil.getExpire(time_key) <= 0) {redisUtil.set(time_key, identifier, timeout);redisUtil.set(counter_key, 0);}if (redisUtil.hasKey(time_key) && redisUtil.incr(counter_key, 1) > limit) {logger.info(EXCEEDS_LIMIT);return Result.failedWith(null, -1, EXCEEDS_LIMIT);}return Result.succeedWith(null, 0,  "调用次数:" + this.ops.get(counter_key) );}public Result clientPathRateLimiter(String clientid, String access_path, int limit, int timeout) {String identifier = UUID.randomUUID().toString();LocalDate today = LocalDate.now();String time_key = "time_key:clientid:" + clientid + ":path:" + access_path;String counter_key = "counter_key:clientid:" + clientid + ":path:" + access_path;if (!redisUtil.hasKey(time_key) || redisUtil.getExpire(time_key) <= 0) {redisUtil.set(time_key, identifier, timeout);redisUtil.set(counter_key, 0);}if (redisUtil.hasKey(time_key) && redisUtil.incr(counter_key, 1) > limit) {logger.info(EXCEEDS_LIMIT);return Result.failedWith(null, -1, EXCEEDS_LIMIT);}return Result.succeedWith(null, 0,  "调用次数:" + this.ops.get(counter_key) );}public Result rateLimitOfDay(String clientid, String access_path, long limit) {String identifier = UUID.randomUUID().toString();LocalDate today = LocalDate.now();String time_key = "time_key:date:" + today + ":clientid:" + clientid + ":path:" + access_path;String counter_key = "counter_key:date:" + today + ":clientid:" + clientid + ":path:" + access_path;if (!redisUtil.hasKey(time_key) || redisUtil.getExpire(time_key) <= 0) {//当天首次访问,初始化访问计数=0,有效期24hredisUtil.set(time_key, identifier, 24 * 60 * 60);redisUtil.set(counter_key, 0);}//累加访问次数, 超出配置的limit则返回错误if (redisUtil.incr(counter_key, 1) > limit) {logger.info("日内超出了访问的限制!");return Result.failedWith(null, -1, "日内超出了访问的限制!");}return Result.succeedWith(null, 0,  "调用总次数:" + this.ops.get(counter_key) );}public Result acquireRateLimiter(String clientid, String access_path, int limit, int timeout) {String identifier = UUID.randomUUID().toString();LocalDate today = LocalDate.now();String time_key = "time_key:date:" + today + ":clientid:" + clientid + ":path:" + access_path;String counter_key = "counter_key:date:" + today + ":clientid:" + clientid + ":path:" + access_path;if (!redisUtil.hasKey(time_key) || redisUtil.getExpire(time_key) <= 0) {redisUtil.set(time_key, identifier, timeout);redisUtil.set(counter_key, 0);}if (redisUtil.hasKey(time_key) && redisUtil.incr(counter_key, 1) > limit) {logger.info(EXCEEDS_LIMIT);return Result.failedWith(null, -1, EXCEEDS_LIMIT);}return Result.succeedWith(null, 0,  "调用次数:" + this.ops.get(counter_key) );}public void save(String tokenType, String Token, int timeout) {redisUtil.set(tokenType, Token, timeout);}public String getToken(String tokenType) {return redisUtil.get(tokenType).toString();}public void saveObject(String key, Object obj, long timeout) {redisUtil.set(key, obj, timeout);}public void saveObject(String key, Object obj) {redisUtil.set(key, obj);}public Object getObject(String key) {return redisUtil.get(key);}public void removeObject(String key) {redisUtil.del(key);}
}

生产软负载NGINX构建ZUUL集群

pom核心依赖

springcloud api-gateway详解相关推荐

  1. SpringCloud分布式架构详解

    SpringCloud分布式架构详解 1. SpringCloud架构概述 1.1 SpringCloud架构简介 1.2 SpringBoot与SpringCloud依赖关系 1.3 SpringC ...

  2. php tongjiapi 使用_Kayako REST API使用详解一

    Kayako是PHP中非常流行的客服处理系统,包含工单模板.在线聊天模块.新闻模板.知识库模块.疑难解答模块以及电邮处理模块. 为什么REST API? REST 是英文 Representation ...

  3. 最全的jquery datatables api 使用详解

    https://www.cnblogs.com/amoniyibeizi/p/4548111.html 最全的jquery datatables api 使用详解 学习可参考:http://www.g ...

  4. ETCD v3 restful api 使用详解

    ETCD v3 restful api 使用详解 网上已经有很多关于v2接口的使用了,类型下面的请求方式,本文就主要讲解v3版本的restful api的使用方式. //V2版本curl http:/ ...

  5. android Camera2 API使用详解

    原文:android Camera2 API使用详解 由于最近需要使用相机拍照等功能,鉴于老旧的相机API问题多多,而且新的设备都是基于安卓5.0以上的,于是本人决定研究一下安卓5.0新引入的Came ...

  6. c 语言获取系统时间并打印机,C获取打印机状态API函数详解.docx

    C获取打印机状态API函数详解 using System;using System.Collections.Generic;using System.ComponentModel;using Syst ...

  7. matlab ext2int函数,Ext2 核心 API 中文详解.pdf

    Ext2 核心 API 中文详解 Andyu QQ Ext2 核心 API 中文详解 序.关于Ext2 核心API 1.关于EXT 2.02 为联合Adobe, Ext2.02 当中重要的一项便是针对 ...

  8. React Native - Keyboard API使用详解(监听处理键盘事件)

    参考: React Native - Keyboard API使用详解(监听处理键盘事件) 当我们点击输入框时,手机的软键盘会自动弹出,以便用户进行输入. 但有时我们想在键盘弹出时对页面布局做个调整, ...

  9. 【Java 8 新特性】Java 8 Util API: StringJoiner 详解 | 拼接字符串添加分隔符、前缀和后缀

    Java 8 Util API: StringJoiner 详解 StringJoiner(CharSequence d) StringJoiner.add(CharSequence element) ...

  10. openGL API glVertexAttribPointer详解

    openGL API glVertexAttribPointer详解 文章目录 openGL API glVertexAttribPointer详解 一.官方文档 二.翻译 例子 运行结果 代码下载 ...

最新文章

  1. 近期活动盘点:数据科学研究院论坛“人文社科专场、全球最大的免费编程社区公开课、DeeCamp2019:实战AI 铸造定雨神针...
  2. linux开机自动启动
  3. python中的内置函数怎么学_python内部函数学习(九)
  4. leetcode-sort-colors
  5. python输出图像plt_Matplotlib(pyplot)savefig输出空白图像
  6. iview中position: 'fixed'最顶层z-index
  7. 将Matlab程序打包成.exe独立可执行程序
  8. 原驱动天空_万能驱动助理 v5.29 全系列正式版 [2013元旦贺岁版]
  9. 【Proteus仿真】51单片机+DAC0832+LCD1602制作LM317数控直流电源
  10. EasyDarwin开源摄像机访问EasyCamera中海康摄像头语音对讲和云台控制转发实现
  11. PyQt5 -- 安装与发布
  12. Linux网络编程 -- Linux常用工具的使用(vim、gcc、gdb、makefile、shell)
  13. 如何检查SFP光模块的光信号强度?
  14. python爬新闻并保存csv_Python简单爬虫导出CSV文件的实例讲解
  15. Building designing
  16. 数据分析进阶必看干货!销售额下滑详细分析案例
  17. 国家一级建造师——工程经济——第一章——第二节
  18. P4094 [HEOI2016/TJOI2016]字符串 [SA + 主席树]
  19. JS验证身份证号地区码及最后一位校验码
  20. 1.2 安装wdcp

热门文章

  1. SAP生产订单预留相关的备忘录
  2. FUNCTION MODULE 'LDB_PROCESS' 实例讲解
  3. POPUP_TO_CONFIRM_WITH_VALUE
  4. linux图片添加滤镜,PhotoFlare开源图像和照片编辑器,附在Ubuntu 18.04下的安装方法...
  5. jquery找祖先包含_Jquery的parent和parents(找到某一特定的祖先元素)
  6. 相继平均法matlab代码_模式识别matlab编程:用k次平均法将20个样本分成2类
  7. hp designiet 500_2020年HP学院壁纸
  8. linux 操作系统详解,Linux操作系统详解
  9. python 自定义异常和主动抛出异常(raise)的操作
  10. python if条件判断和while循环 练习题