API服务接口签名代码与设计,如果你的接口不走SSL的话?
在看下面文章之前,我们先问几个问题
- rest 服务为什么需要签名?
- 签名的几种方式?
- 我认为的比较方便的快捷的签名方式(如果有大神持不同意见,可以交流!)?
- 怎么实现验签过程 ?
- 开放式open api sign怎么设计 (openkey 和 openid 的设计) ?
- 在一个服务中,有些接口不需要签名,接口怎么滤过签名 ?
我认为好的签名设计,应该要解决以上问题。
一: Rest 服务为什么需要签名?
在介绍签名之前,我们先对服务进行分一分,我们的服务从内网以及外网角度分为:内网服务以及开放型外网服务两大类
- 内网服务,我们认为它是可靠安全,受局域网的防火墙保护,内网型的服务,我们不开放出INTERNET 访问。
- 暴露在外网型的服务,我们认为是它本质是提供到INTERNET 网络允许访问的服务。我们认为它是不可靠的,不安全的。
外网型的服务,我们通常面临两个问题:收到恶意请求和数据安全(如果你不是通过SSL走的话) 的问题。
在恶意请求方面,又涉及到恶意高频请求以及数据拦截窜篡改请求。
数据安全方面涉及到,网络传输的数据如果被拦截,涉及到客户隐私数据被窃取等相关问题。
因此,为了解决上述问题,伟大的 服务 “签名” 就诞生了。 你可以这么认为:签名 就是 请求当前业务接口的 前提钥匙。通过软实施实现。
签名如何解决上述问题:
- 恶意请求: 我们知道,签名在设计上面具有防篡改性质,如果这一点没有实现,那么就会失去签名的意义。被拦截的请求,修改请求报文后,再次发送,将会被服务端 验签 过程中 检测到,直接打回--我们通常说是验签失败
- 客户隐私:客户隐私数据的保护,加签后的接口,只能请求当前的相同请求报文的请求,而不能尝试请求被篡改后报文的请求。如果数据被拦截,也只能是当前此条数据客户隐私被泄露。因此,如果要绝对的保护客户隐私的话,还有对报文数据进行加密。这样,我们就可以做到数据安全级别较高的接口。下面的文章将对具体实现过程展开。
- 高频请求的保护,如果签名产生的uuid 加上 时间戳,就可以解决高频请求的容错限流等问题.
因此签名尤其变为重要。
我上面的标题,如果你的接口不走SSL的话,你的外网接口就需要走上述这些事情,为了你接口安全而考虑。
二:签名的几种方式
签名的几种方式:我们通常见到的有 SHA 加密签名,MD5 签名。我个人比较推崇的是 MD5 加签签名。
原因:简单,易懂,跨语言平台型强,通用性强。尤其是.NET 与 JAVA 跨语言的的接口签名对接时。因为JAVA 的 SHA 版本有很多中,而更甚的是,有些 SHA 在某些银行还被改过,形成自己私有的版本。如果:
你要对接他们的 他们的接口,你必须使用JAVA 语言. 然而 MD5 的算法比较统一。只要 确认 对方的最简单的 字符串 123 MD5 值跟 你 这边的 MD5 值一样。就可以保证 底层算法 的一致 性,就 可以采用上述的加签方式。
MD5 加签原理:
我们假设有这么一个统一入参结构的请求报文
请求对象协议结构 |
类型 |
说明 |
object |
object |
说明:请求的业务参数(包装对象),各接口不同的参数 二:包装对象中实体中特殊业务字段中的具体格式要求: ① 如果业务对象参数是时间类型的, 将时间参数转成时间戳(当前时间与'1970-01-01'精确到毫秒,类型Long) ② 业务中的浮点型使用字符串定义传送(避免不同跨语言造成序列化形成的浮点位数不一致性) |
time |
long |
当前时间的时间戳:datekong(当前时间与'1970-01-01'相对值,精确到毫秒) |
sign |
string |
sign=MD5(openkey+ time+ JsonConvert.SerializeObject(object)) 备注:OpenKey: 分配给调用方的key值,此值无需暴露在网络中传输。
|
在我们构建传送报文的时候,我们看到有一个字段: sign 是 由 服务方分配给客户端一个 秘钥字符串 再加上 报文中 ( time 时间戳+ objcet 业务参序列化)相加后的字符串 后 MD5值。
我们这里 sign 的形成有两个关键点:
第一: sign 值形成的算法,我这边算法暂时是 :sign= MD5(openkey+ time+ JsonConvert.SerializeObject(object))
第二: sign 分配给客户端的秘钥值—openkey
如下加签请求伪代码:
1 namespace T.API
2 {
3
4
5 /// <summary>
6 /// 请求的报文对象
7 /// </summary>
8 public class SendObject
9 {
10 /// <summary>
11 /// 发送实体对象
12 /// </summary>
13 public object @object { get; set; }
14
15 /// <summary>
16 /// 签名
17 /// </summary>
18
19 public string sign { get; set; }
20 /// <summary>
21 /// 当前请求的时间戳
22 /// </summary>
23 public long? time { get; set; }
24
25 /// <summary>
26 /// 用户id
27 /// </summary>
28 public int userId { get; set; }
29 }
30
31 /// <summary>
32 /// 接收到的报文对象
33 /// </summary>
34 public class ReciveObject
35 {
36 /// <summary>
37 /// 发送实体对象
38 /// </summary>
39 public object @object { get; set; }
40
41 /// <summary>
42 /// 服务请求响应值 code 为 1:请求成功 ,请求无异常
43 /// 当code 为 "1" 的情况下,下面的RevRep 对象中的 message 字段 90% 的场景为空,
44 /// 如果有必要赋值视双方业务场景而定;
45 /// code为 0:我方程序异常/业务性质失败/接口参数校验失败,
46 /// 当 code 为 "0"的情况下,下面message字段包装了异常/失败信息。
47 /// </summary>
48
49 public int code { get; set; }
50
51
52 /// <summary>
53 /// 请求响应的错误消息/或者其他业务场景响应提示信息
54 /// </summary>
55 public int message { get; set; }
56 }
57
58
59 /// <summary>
60 /// 上面 Req 对象中的object 封装字段具体实体定义
61 /// </summary>
62 public class ObjectEntity
63 {
64
65 public string orderNum { get; set; }
66
67 /// <summary>
68 /// 如果参数是浮点型,在实体中定义成字符串类型.
69 /// </summary>
70 public string orderMoney { get; set; }
71
72 /// <summary>
73 /// 如果参数是时间类型的,在实体中定义成long 时间戳类型
74 /// </summary>
75 public long? orderTime { get; set; }
76 }
77
78
79
80
81 /// <summary>
82 /// 请求示例代码
83 /// </summary>
84 public class RequestDemo
85 {
86
87 /// <summary>
88 /// 请求示例,调用方请求
89 /// </summary>
90
91 public static void Request()
92 {
93
94 //服务端分配给调用方:openkey
95 string openKey = "455853655-7dff-5585545-a1c3-7778887"; //
96
97 //定义发送对象
98 SendObject sendobject = new SendObject();
99 //定义请求时间戳
100 long? reqtime= DateTime.Now.ToSafeDateTime().ToSafeDataLong();// 赋值
101 sendobject.time = reqtime;
102 try
103 {
104 //定义以及赋值业务实体
105 ObjectEntity objectEntity = new ObjectEntity();
106 objectEntity.orderNum = "20200506071001";
107 objectEntity.orderTime = DateTime.Now.ToSafeDateTime().ToSafeDataLong();
108 objectEntity.orderMoney = "526.00";
109
110 //将定义好的业务实体塞入SendObject的object字段中.
111 sendobject.@object = objectEntity;
112
113 //加签并且赋值签名
114 sendobject.sign = sign(reqtime, openkey,JsonConvert.SerializeObject(sendobject.@object));
115
116
117 RestRequest rq = new RestRequest(Method.POST);
118
119 rq.Method = Method.POST; //请求设置为POST
120
121 rq.AddHeader(" Content-Type", "application/json;charset=utf-8"); //头部塞入Content-Type
122 rq.AddParameter("application/json", JsonConvert.SerializeObject(sendobject), ParameterType.RequestBody);
123
124
125 RestClient restclient = new RestClient { BaseUrl = new Uri("http://xx.xx.xx.xx:5021") }; //调用地址
126 TaskCompletionSource<IRestResponse> tcs = new TaskCompletionSource<IRestResponse>();
127 restclient.ExecuteAsync(rq, r =>
128 {
129 tcs.SetResult(r);
130 });
131 IRestResponse respones = tcs.Task.Result; // 请求返回的数据
132
133 //如果请求状态正常
134 if ((int)respones.StatusCode == 200)
135 {
136 ReciveObject recive = JsonConvert.DeserializeObject<ReciveObject>(respones.Content);
137 if (recive.code == 1)
138 {
139 //处理业务
140 }
141 else
142 {
143 //处理业务
144 }
145
146
147
148 }
149 else
150 {
151 throw new Exception("调用异常通讯状态:${respones.StatusCode}");
152
153 }
154
155
156
157
158
159
160 }
161 catch (Exception ex)
162 {
163
164 }
165
166 }
167
168
169 /// <summary>
170 /// Md5 方法
171 /// </summary>
172 public static string MD5(string md5orgincontent)
173 {
174
175 string md5result = string.Empty;
176 if (string.IsNullOrEmpty(md5result)) return md5result;
177 StringBuilder sb = new StringBuilder();
178
179 MD5 md5 = new MD5CryptoServiceProvider();
180 byte[] s = md5.ComputeHash(Encoding.UTF8.GetBytes(md5orgincontent));
181 md5.Clear();
182 for (int i = 0; i < s.Length; i++)
183 {
184 sb.Append(s[i].ToString("x2"));
185 }
186 md5result=sb.ToString();
187 return md5result;
188
189
190 }
191
192
193 /// <summary>
194 /// 加签
195 /// </summary>
196 /// <param name="time">时间戳</param>
197 /// <param name="openkey">服务端分配给调用方:openkey</param>
198 /// <param name="szobject">参与加签的object的json序列化字符串</param>
199 /// <returns></returns>
200 public static string sign(long? time, string openkey, string szobject)
201 {
202
203
204 string signresult = string.Empty;
205 var signcontent = openkey+time.ToSafeString()+szobject;
206 signresult = MD5(signcontent);
207 return signresult;
208
209 }
210
211
212
213
214 }
215
216 }
服务端验签原理:
服务端通过 服务端定义接口拦截器或者全局过拦截器。 接口接收到的报文是上述表格的报文结构后,做如下事情
1: 同样:接拦截器中,做同样的事情: 秘钥字符串 再加上 报文传送过来的 ( time 时间戳+ objcet 业务参序列化)相加后的字符串 MD5值 ,我将此值 为 service_sign
2:将服务端的 service_sign 值跟 报文中的 sign 进行比对,如果发现不匹配(假设在双方算法一直,openkey 一致的情况下):报文被篡改,签名验证不通过
我在这里贴出服务端验签 C# 代码:其他语言可以参考:
- 服务端先定义一个接收报文的对象:
1 [JsonObject(MemberSerialization.OptIn)]
2 public class ResultRequset : BaseRequestEntity
3 {
4 [JsonProperty]
5 public object @object { get; set; }
6 public virtual string openKey{ get; set; }
7 /// <summary>
8 /// 服务端加签:此值将于传送过来的 sign 值最终进行比对/// </summary>
9 public override string checkedSign
10 {
11 get
12 {
13 var orgin =this.time.ToString() + openKey+ JsonConvert.SerializeObject(@object);
14 return EntitySign.To32Md5(orgin);
15 }
16 }
17
18 /// <summary>
19 /// 签名验证
20 /// </summary>
21 /// <returns></returns>
22 public Result CheckedSign()
23 {
24 Result r = new Result();
25 if (this.sign == checkedSign)
26 {
27 r.code = 1;
28 return r;
29 }
30 else
31 {
32 r.code = 0;
33 r.message = "延签失败!";
34 }
35 return r;
36 } - 然后构建一个拦截器,拦截器的工作如下所示 NETFramework 代码,其他语言可以参考:
1 public class OpenSignAttribute : ActionFilterAttribute
2 {
3 public Type RequestType { get; set; }
4
5 public override void OnActionExecuting(HttpActionContext actionContext)
6 {
7 HttpContent content = actionContext.Request.Content;
8 var gloablkey = string.Empty;
9 ResultRequset resultRequset = new ResultRequset();
10 foreach (KeyValuePair<string, object> obj in actionContext.ActionArguments)
11 {
12
13 resultRequset = (ResultRequset)obj.Value; //第一步:获取报文数据,强制转换到 上面定义的 ResultRequset 报文接收对象
14 }
15 Result re = resultRequset.CheckedSign(); //第二步: 服务端进行加签并且验签
16 if (re.code == 1)
17 {
18 base.OnActionExecuting(actionContext);
19 }
20 else
21 {
22 actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, re);
23 }
24 }
25 }
- 我们看一下接口定义- 给 接口 打上 OpenSign 标签,并且使用 ResultRequset 来接收对方过来的报文数据。
1 [HttpPost]
2
3 [OpenSign]
4 public ResultRequset test([FromBody]ResultRequset obj)
5 {
6
7 }
上述我们基本上形成了 MD5 加签和验签的逻辑过程。那么上述的这这个过程还是有个缺陷,就是文章一开头要解决的一个问题,开放式open api sign怎么设计 (openkey 和 openid 的设计) ?
也就是说:上述的 demo 的openkey 在是死的,如果我们想 服务端分配给每个调用方的openkey 都不一样,怎么办?
其实原理很简单:我们在增加一个 openid 概念:openid 是服务端分配给对调用方的唯一标识,openkey 是我们分配调用方参与加签的 钥匙。
怎么做呢:
1:调用方: openid 一定要让对方 放入 HTTP HEADER 里面 传送到服务端。openkey 是参与加密,不需要传送。
2:服务端:在接收到 调用方 传送过来的 openid后,通过查库或者其他方式 查出 openid 对应的 openkey, 然后将查到的openkey 参与服务端验签算法。
上述原理,也就是我们通常看到的ALI,腾讯,或者其他第三方提供出来的 API 为什么需要分配一个OPENID,OPENKEY 的原因,或许有些厂商不是这种叫法。但是原理都是这样。
在贴出改造代码之前,我们还需要解决一个问题:就是 服务端在“接收到 调用方 传送过来的 openid后,通过查库或者其他方式 查出 openid 对应的 openkey, 然后将查到的openkey 参与服务端验签算法” 这里的蓝色字体标注的具体怎么查,这对
openid,openkey 配置对 怎么配置在服务端(有可能存库,有可能放在配置文件中)可能每个服务端都不太一样,我们把这层也抽象出来。让接口标签指定。
我们代码再次改造,如下所示:
- 我们先定义一个查找方式的接口:
1 public interface ISingSecret
2 {
3 string OpenId(Microsoft.AspNetCore.Http.HttpRequest request =null);
4
5 string OpenKey(string OpenId);
6 }
- 服务端接收对象改造:
1 [JsonObject(MemberSerialization.OptIn)]
2 public class ResultRequset
3 {
4 [JsonProperty]
5 public object @object { get; set; }
6
7
8 /// <summary>
9 /// 可以覆盖此KEY的方式
10 /// </summary>
11 public virtual string openKey{ get; set; }
12 /// <summary>
13 /// 开放平台所使用的分配给客户的OPENID
14 /// </summary>
15 [JsonProperty]
16 public string openId
17 {
18
19 get;
20
21 set;
22 }
23 /// <summary>
24 /// 获取签名
25 /// </summary>
26 public override string checkedSign
27 {
28 get
29 {
30 var orgin = singContent;
31
32 return EntitySign.To32Md5(orgin);
33 }
34 }
35
36
37 /// <summary>
38 /// 用户ID 登入人ID
39 /// </summary>
40 [JsonIgnore]
41 public string singContent
42 {
43 get { return openKey+ this.time.ToString() + JsonConvert.SerializeObject(@object); }
44 }/// <summary>
45 /// 签名验证
46 /// </summary>
47 /// <returns></returns>
48 public Result CheckedSign()
49 {
50 Result r = new Result();
51
52 if (this.sign == checkedSign)
53 {
54 r.code = 1;
55 return r;
56 }
57 else
58 {
59 r.code = 0;
60
61 r.message = "签名验证失败!";
62 LogService.Default.Debug("签名验证失败---"+"框架签名" + checkedSign.ToSafeString("")+"-------网络签名:"+ sign.ToSafeString("") + "--------签名信息:" + singContent);
63 }
64 return r;
65 }
66 }
- 服务端拦截器改造:
1 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
2 public class CentralSign: ActionFilterAttribute
3 {
4
5
6
7 private Type ISingRealization { get; set; }
8
9 private ISingSecret singRealization { get; set; } //关键代码:由服务端实现通过openid 查出openkey 的具体逻辑.
10
11 public CentralSign(Type ISingSecret) // 关键代码: 定义带构造函数的 接口标签属性 .
12 {
13 this.ISingRealization = ISingSecret;
14 if (ISingRealization != null)
15 {
16 //获取类的初始化参数信息
17 ConstructorInfo obj = ISingRealization.GetConstructor(System.Type.EmptyTypes);
18 singRealization = (ISingSecret)Activator.CreateInstance(ISingRealization); //实例化对象
19
20 }
21 }
22
23 public override void OnActionExecuting(ActionExecutingContext actionContext)
24 {
25 var content = actionContext.HttpContext.Request;
26 var gloablkey = string.Empty;
27
28 ResultRequset resultRequset = new ResultRequset();
29 foreach (KeyValuePair<string, object> obj in actionContext.ActionArguments)
30 {
31 resultRequset = (ResultRequset)obj.Value;
32 }
33
34
35 Result re = new Result();
36 if (resultRequset == null)
37 {
38 re.code = 0;
39 re.message = "传值不能为空";
40 actionContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
41 actionContext.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(re));
42
43 }
44 else
45 {
46 if (singRealization != null)
47 {
48 var openId = singRealization.OpenId(actionContext.HttpContext.Request); // 关键代码: 通过 ISingSecret.OpenId() 方法,获取到对应调用方传送过来的 openid
49
50 resultRequset.publicApikey = singRealization.OpenKey(openId); // 关键代码: 通过 ISingSecret.OpenId() 方法,获取到对应调用方传送过来的 openid
51
52 }
53 re = resultRequset.CheckedSign();
54 if (re.code == 1)
55 {
56 base.OnActionExecuting(actionContext);
57 }
58 else
59 {
60
61 HandleUnauthorizedRequest(actionContext);
62
63 }
64 }
65
66
67 }
68
69 protected void HandleUnauthorizedRequest(ActionExecutingContext actionContext)
70 {
71 var r = new JsonResult("签名失败,访问受限.");
72
73 r.StatusCode = (int)HttpStatusCode.BadRequest;
74 actionContext.Result =r;
75 return;
76 }
77 }
- 服务端接口定义改造:
1 [HttpPost]
2 [CentralSign(typeof(OpenSign))]
3 public Result SignatureSample([FromBody]ResultRequset result)
4 {
5 var str = result.@object.ToSafeString("");
6 Result re = new Result() { code = 1,message="签名验证成功!"};
7 re.@object = str;
8 return re;
9 }上面接口定义 打上了 [CentralSign(typeof(OpenSign))] 标签,CentralSign 接收了一个 OpenSign Type 对象类型。根据上面的代码,我们知道,OpenSign 实现了 ISingSecret 逻辑。我们具体看下 OpenSign 具体实现:
- OpenSign 实现 ISingSecret 逻辑代码:
1 public class OpenSign : ISingSecret
2 {
3 public string OpenId(HttpRequest request)
4 {
5 return Header.GetHeaderValue(request,"openId");
6 }
7
8 public string OpenKey(string OpenId)
9 {
10 return ConfigManage.JsonConfigMange.GetInstance().AppSettings[OpenId];
11 }
12 }
这样我们就整体上完成了我们所需要的 框架性 服务接口签名认证代码。 上面的代码 在Bitter.Frame 框架 服务签名模块中有, Bitter.Frame 代码还在整理中 。后续会贴出来给大家。
API服务接口签名代码与设计,如果你的接口不走SSL的话?的更多相关文章
- 【转】App开放接口api安全性—Token签名sign的设计与实现
前言 在app开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些敏感的数据,所以对这些接口需要进行身份的认证,那么这就需要用户提供一些信息,比如用户名密码等, ...
- App开放接口api安全性—Token签名sign的设计与实现
前言 在app开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些敏感的数据,所以对这些接口需要进行身份的认证,那么这就需要用户提供一些信息,比如用户名密码等, ...
- App开放接口API安全性 — Token签名sign的设计与实现
在app开放接口API的设计中,避免不了的就是安全性问题. 一.https协议 对于一些敏感的API接口,需要使用https协议. https是在http超文本传输协议加入SSL层,它在网络间通信是加 ...
- 设计原则:接口隔离原则(ISP)
接口隔离原则的英文是Interface Segregation Principle,缩写就是ISP.与里氏替换原则一样其定义同样有两种 定义1: Clients should not be force ...
- 开放接口/RESTful/Api服务的设计和安全方案详解
一.总体思路 这个涉及到两个方面问题:一个是接口访问认证问题,主要解决谁可以使用接口(用户登录验证.来路验证)一个是数据数据传输安全,主要解决接口数据被监听(HTTPS安全传输.敏感内容加密.数字签名 ...
- 开放接口/RESTful/Api服务的设计和安全方案
总体思路 这个涉及到两个方面问题:一个是接口访问认证问题,主要解决谁可以使用接口(用户登录验证.来路验证)一个是数据数据传输安全,主要解决接口数据被监听(HTTPS安全传输.敏感内容加密.数字签名) ...
- 微信小程序的Web API接口设计及常见接口实现
微信小程序给我们提供了一个很好的开发平台,可以用于展现各种数据和实现丰富的功能,通过小程序的请求Web API 平台获取JSON数据后,可以在小程序界面上进行数据的动态展示.在数据的关键 一环中,我们 ...
- Atitit.uml2 api 的编程代码实现设计uml开发 使用eclipse jar java 版本
Atitit.uml2 api 的编程代码实现设计uml开发 使用eclipse jar java 版本 1. clipse提供了UML的底层Java包, 1 2. MDTUML2Getting St ...
- 文华财经赢顺外盘期货行情数据API接口开放代码
文华财经赢顺外盘期货行情数据API接口开放代码 怎么才能获取到外盘期货行情数据API接口呢?不少朋友就会考虑到文华财经行情API接口,本身文华财经就是一个软件提供商,提供行情API接口也 ...
随机推荐
- asp.net mvc ajax文件上传
前台页面提交文件 <!DOCTYPE html> <html> <head> <meta name="viewport" content= ...
- IIS重写2.0 IIS伪静态 下载地址
IIS重写2.0 IIS伪静态 下载地址 https://www.iis.net/downloads/microsoft/url-rewrite#additionalDownloads Downloa ...
- matplotlib学习日记(六)-箱线图
(一)箱线图---由一个箱体和一对箱须组成,箱体是由第一个四分位数,中位数和第三四分位数组成,箱须末端之外的数值是离散群,主要应用在一系列测量和观测数据的比较场景 import matplotlib ...
- python初学者-使用for循环用四位数组成不同的数
digits = (1,2,3,4) for i in digits: for j in digits: if j==i: continuefor k in digits: if k==i or k= ...
- Astra示例程序库正式上线啦
新上线的Astra示例程序库提供了基于多种编程语言和框架使用Astra的例子.借助这个示例程序库,你可以在短时间内建构起数据库.创建多个表.装载示例数据并部署基于Cassandra的应用程序. 什么是 ...
- Java学习_反射
什么是反射? 反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息. 反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法. JAVA反射机制是在运行状 ...
- RocketMQ(九):主从同步的实现
分布式系统的三大理论CAP就不说了,但是作为分布式消息系统的rocketmq, 主从功能是最最基础的了.也许该功能现在已经不是很常用了,但是对于我们理解一些分布式系统的常用工作原理还是有些积极意义的. ...
- 微信网页授权多次回调code请求
最近在做微信网页授权的时候遇到一个问题如果直接从后台把微信授权的url参数什么的拼装好,然后直接redirect 这个url 会导致时不时的多次请求回调的url .网上说是因为网络原因,如果10s没有 ...
- JavaDailyReports10_10
1.4.2 键盘事件的处理 KeyListener 接口实现了处理键盘事件 KeyEvent 对象描述键盘事件的相关信息. KeyListener 接口有三个方法:KeyPressed K ...
- Spring IOC 笔记
什么是IOC与DI IOC(inversion of control) 它描述的其实是一种面向对象编程中的设计原则,用来降低代码之间的耦合度, 而DI(dependency Injection)依赖注 ...