从事Android工作4年以来,只有前1年不到的时间是用C++在开发东西(主要是开发DLNA组件,目前我已将它们全部开源,参考http://blog.csdn.net/innost/article/details/40216763),后面的工作几近都在用Java。自以为Java相干的东西都见过了,可前段时间有个朋友给我花了1个多小时讲授他们某套系统的安全部系结构,其中触及到很多专业术语,比如Message Digest(消息摘要)、Digital Signature(数字签名)、KeyStore(恕我不知道翻译成甚么好,还是用英文原称吧)、CA(Certificate Authority)等。我当时脑袋就大了,尼玛弄Java这么久,历来没接触过啊。为此,我特地到AndroidFramework代码中查询了下,Android平台里与之相干的东西还有1个KeyChain。
原来,上述内容都属于Java世界中1个早已存在的知识模块,那就是JavaSecurity。Java Security包括很多知识点,常见的有MD5,DigitalSignature等,而Android在Java Seurity以外,拓展了1个android.security包,此包中就提供了KeyChain。
本文将介绍Java Security相干的基础知识,然后介绍下Android平台上与之相干的使用处景。
实际上,在1些金融,银行,电子支付方面的利用程序中,JavaSecurity使用的地方非常多。
https://code.csdn.net/Innost/androidsecuritydemo 目前已公然。
Java Security实际上是Java平台中1个比较独立的模块。除软件实现上内容外,它实际上对应了1系列的规范。从Java2开始,Java Security包括主要3个重要的规范:
在上述3个子模块或规范中,JCE是JavaSecurity的大头,其他两个子模块JSSE和JAAS都依赖于它,比如SSL/TLS在工作进程中需要使用密钥对数据进行加解密,那末密钥的创建和使用就依托JCE子模块了。
另外,既然和安全相干,那末对安全敏感的相干部门或政府肯定会有所干涉。Java是在美国被发明的,所以美国政府对Java Security方面的出口(比如哪些模块,哪些功能能给其他国家使用)有相干的限制。例如,不允许出口的JCE(从软件实现上看,可能就是从Java官网上下载到的几个Jar包文件)支持1些高级的加解密功能(比如在密钥长度等方面有所限制)。
注意,Java Security包括的内容非常多,而本文将重点关注Java SecurityAPI与其使用方法方面的知识。对Java Security其他细节感兴趣的同学可在浏览完本文后,再浏览参考文献[1]。
介绍JCE之前,先来说解下JCE的设计架构。JCE在设计时重点关注两个问题:
对独立性而言,1个最通用的做法就是把定义和实现通过抽象类或基类的方式进行解耦合。在JCE中:
注意,可互操作性是指Provider A实现的MD5值,能被Provider B辨认。明显,这个要求是公道的。
图1所示为JCE中1些类的定义:
图1 JCE中1些类的定义
图1展现了JCE中的1些类定义。大家先关注左下角的Provider和Security类:
注意,由于历史缘由,JCE的Service分散定义在好些个Package中:
上述这两个package都属于JCE的内容。从使用者的角度来看,其实不太需要关注它们到底定义在哪一个Package中(代码中通过1句import xxx就能够把对应的包引入,后续编写代码时候,直接使用相干类就行了)。
BTW,个人感觉Java类定义非常多,而且有些类和它们所在包的关系仿佛有些混乱。
前面已说过,JCE框架对使用者提供的是基础类(抽象类或接口类),而具体实现需要有地方注册到JCE框架中。所以,我们来看看Android平台中,JCE框架都注册了哪些Provider:
[-->Security.java]
static {
boolean loaded = false;
try {
//从资源文件里搜索是不是有security.properties定义,Android平台是没有这个文件的
InputStreamconfigStream =
Security.class.getResourceAsStream("security.properties");
InputStream input = newBufferedInputStream(configStream);
secprops.load(input);
loaded = true;
configStream.close();
}......
if (!loaded) {//注册默许的Provider
registerDefaultProviders();
}
......
}
private static void registerDefaultProviders() {
/*
JCE对Provider的管理非常简单,就是将Provider类名和对应的优先级存在属性列表里
比以下面的:OpenSSLProvider,它对应的优先级是1.
优先级是甚么意思呢?前面说过,不同的Provider可以实现同1个功能集合,比如都实现
MessageDigestSpi。那末用户在创建MessageDigest的实例时,如果没有指明Provider名,
JCE默许从第1个(依照优先级,由1到n)Provider开始搜索,直到找到第1个实现了
MessageDigestSpi的Provider(比如Provider X)为止。然后,MessageDigest的实例
就会由Provider X创建。图2展现了1个例子
*/
secprops.put("security.provider.1",
"com.android.org.conscrypt.OpenSSLProvider");
secprops.put("security.provider.2",
"org.apache.harmony.security.provider.cert.DRLCertFactory");
secprops.put("security.provider.3",
"com.android.org.bouncycastle.jce.provider.BouncyCastleProvider");
secprops.put("security.provider.4",
"org.apache.harmony.security.provider.crypto.CryptoProvider");
//和JSSE有关
secprops.put("security.provider.5",
"com.android.org.conscrypt.JSSEProvider");
}
图2展现了Provider优先级使用的例子:
图2 Provider优先级示例
图2中:
注意,图2中的SHA⑴/256和MD5都是MessageDigest的1种算法,本文后面会介绍它们。
Android平台中,每一个利用程序启动时都会注册1个类型为“AndroidKeyStoreProvider”的对象。这是在ActivityThread中完成的,代码以下所示:
[-->ActivityThread.java::main]
public static void main(String[] args) {
......
Security.addProvider(new AndroidKeyStoreProvider());
......
}
来看看AndroidKeyStoreProvider是甚么,代码以下所示:
[-->AndroidKeyStoreProvider::AndroidKeyStoreProvider]
public class AndroidKeyStoreProvider extends Provider {
public static final StringPROVIDER_NAME = "AndroidKeyStore";
publicAndroidKeyStoreProvider() {
super(PROVIDER_NAME, 1.0,"Android KeyStore security provider");
put("KeyStore." + AndroidKeyStore.NAME,
AndroidKeyStore.class.getName());
put("KeyPairGenerator.RSA",AndroidKeyPairGenerator.class.getName());
}
}
AndroidKeyStoreProvider很简单,但是看上去还是不明白它是干甚么的?其实,这个问题的更深层次的问题是:Provider是干甚么的?
固然,Provider的内容和功能比这要复杂,不过我们对Provider的实现没甚么兴趣,大家只要知道它存储了1系列的属性key和value就能够了。JCE会根据情况去查询这些key和对应的value。
来个例子,看看Android系统上都有哪些Provider:
[-->DemoActivity.java::testProvider]
void testProvider(){
e(TAG, "***Begin TestProviders***");
//获得系统所有注册的Provider
Provider[] providers = Security.getProviders();
for(Providerprovider:providers){
//输出Provider名
e(TAG,"Provider:" + provider+" info:");
//前面说了,provider其实就是包括了1组key/value值。下面将打印每一个Provider的
//这些信息
Set<Entry<Object,Object>>allKeyAndValues = provider.entrySet();
Iterator<Entry<Object, Object>> iterator =allKeyAndValues.iterator();
while(iterator.hasNext()){
Entry<Object,Object> oneKeyAndValue =iterator.next();
Object key = oneKeyAndValue.getKey();
Object value =oneKeyAndValue.getValue();
//打印Key的类型和值
e(TAG,"===>" + "Keytype="+key.getClass().getSimpleName()+"
Key="+key.toString());
//打印Value的类型和值
e(TAG,"===>" + "Valuetype="+value.getClass().getSimpleName()+"
Value="+value.toString());
}
}
e(TAG, "***End TestProviders*** ");
}
在控制台中,通过adb logcat | grep ASDemo就能够显示testProvider的输出信息了,如图3所示:
图3 testProvider输出示例
图3打出了AndroidOpenSSLprovider的信息:
了解完JCE框架后,我们分别来介绍JCE中的1些重要Service。
谈到安全,大家第1想到的就是密钥,即Key。那末大家再仔细想一想下面这两个问题:
图4解释了上述问题:
图4 Key示意
图4中:
在安全领域中,Key分为两种:
图5所示为JCE中Key相干的类和继承关系。
图5 JCE Key相干类
图5中:
先来看对称key的创建和导入(也就是把Key的书面表达导入到程序中并生成Key对象)
[-->DemoActivity.java::testKey]
{//对称key即SecretKey创建和导入
//假定双方约定使用DES算法来生成对称密钥
e(TAG,"==>secret key: generated it using DES");
KeyGeneratorkeyGenerator = KeyGenerator.getInstance("DES");
//设置密钥长度。注意,每种算法所支持的密钥长度都是不1样的。DES只支持64位长度密钥
//(或许是算法本身的限制,或是不同Provider的限制,或是政府管制的限制)
keyGenerator.init(64);
//生成SecretKey对象,即创建1个对称密钥
SecretKey secretKey = keyGenerator.generateKey();
//获得2进制的书面表达
byte[] keyData =secretKey.getEncoded();
//平常使用时,1般会把上面的2进制数组通过Base64编码转换成字符串,然后发给使用者
String keyInBase64 =Base64.encodeToString(keyData,Base64.DEFAULT);
e(TAG,"==>secret key: encrpted data ="+ bytesToHexString(keyData));
e(TAG,"==>secrety key:base64code=" + keyInBase64);
e(TAG,"==>secrety key:alg=" + secretKey.getAlgorithm());
//假定对方收到了base64编码后的密钥,首先要得到其2进制表达式
byte[] receivedKeyData =Base64.decode(keyInBase64,Base64.DEFAULT);
//用2进制数组构造KeySpec对象。对称key使用SecretKeySpec类
SecretKeySpec keySpec =new SecretKeySpec(receivedKeyData,”DES”);
//创建对称Key导入用的SecretKeyFactory
SecretKeyFactorysecretKeyFactory = SecretKeyFactory.getInstance(”DES”);
//根据KeySpec还原Key对象,即把key的书面表达式转换成了Key对象
SecretKey receivedKeyObject = secretKeyFactory.generateSecret(keySpec);
byte[]encodedReceivedKeyData = receivedKeyObject.getEncoded();
e(TAG,"==>secret key: received key encoded data ="
+bytesToHexString(encodedReceivedKeyData));
如果1切正常的话,红色代码和绿色代码打印出的2进制表示应当完全1样。此测试的结果如图6所示:
图6 SecretKey测试结果
此处有几点说明:
注意,本文会讨论太多算法相干的内容。
再来看KeyPair的用例:
[-->DemoActivity.java::KeyPair测试]
{//public/private key test
e(TAG, "==>keypair: generated it using RSA");
//使用RSA算法创建KeyPair
KeyPairGeneratorkeyPairGenerator = KeyPairGenerator.getInstance("RSA");
//设置密钥长度
keyPairGenerator.initialize(1024);
//创建非对称密钥对,即KeyPair对象
KeyPair keyPair =keyPairGenerator.generateKeyPair();
//获得密钥对中的公钥和私钥对象
PublicKey publicKey =keyPair.getPublic();
PrivateKey privateKey =keyPair.getPrivate();
//打印base64编码后的公钥和私钥值
e(TAG,"==>publickey:"+bytesToHexString(publicKey.getEncoded()));
e(TAG, "==>privatekey:"+bytesToHexString(privateKey.getEncoded()));
/*
现在要斟酌如何把公钥传递给使用者。虽然可以和对称密钥1样,把2进制数组取出来,但是
对非对称密钥来讲,JCE不支持直接通过2进制数组来还原KeySpec(多是算法不支持)。
那该怎样办呢?前面曾说了,除直接还原2进制数组外,还可以通过具体算法的参数来还原
RSA非对称密钥就得使用这类方法:
1 首先我们要获得RSA公钥的KeySpec。
*/
//获得RSAPublicKeySpec的class对象
Class spec = Class.forName("java.security.spec.RSAPublicKeySpec");
//创建KeyFactory,并获得RSAPublicKeySpec
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKeySpecrsaPublicKeySpec =
(RSAPublicKeySpec)keyFactory.getKeySpec(publicKey, spec);
//对RSA算法来讲,只要获得modulus和exponent这两个RSA算法特定的参数就能够了
BigInteger modulus =rsaPublicKeySpec.getModulus();
BigInteger exponent =rsaPublicKeySpec.getPublicExponent();
//把这两个参数转换成Base64编码,然后发送给对方
e(TAG,"==>rsa pubkey spec:modulus="+
bytesToHexString(modulus.toByteArray()));
e(TAG,"==>rsa pubkey spec:exponent="+
bytesToHexString(exponent.toByteArray()));
//假定接收方收到了代表modulus和exponent的base64字符串并得到了它们的2进制表达式
byte[] modulusByteArry =modulus.toByteArray();
byte[] exponentByteArry =exponent.toByteArray();
//由接收到的参数构造RSAPublicKeySpec对象
RSAPublicKeySpecreceivedKeySpec = new RSAPublicKeySpec(
newBigInteger(modulusByteArry),
new BigInteger(exponentByteArry));
//根据RSAPublicKeySpec对象获得公钥对象
KeyFactoryreceivedKeyFactory = keyFactory.getInstance("RSA");
PublicKey receivedPublicKey =
receivedKeyFactory.generatePublic(receivedKeySpec);
e(TAG, "==>received pubkey:"+
bytesToHexString(receivedPublicKey.getEncoded()));
}
如果1切正常的话,上述代码中红色和黑色代码段将输出完全1样的公钥2进制数据。如图7所示:
图7 KeyPair测试示意图
在Android平台的JCE中,非对称Key的经常使用算法有“RSA”、“DSA”、“Diffie?Hellman”、“Elliptic Curve (EC)”等。
我自己在学习Key的时候,最迷惑的就是前面提到的两个问题:
Key是甚么?虽然“密钥”这个词常常让我联想到实际生活中的钥匙,但是在学习JavaSecurity之前,我1直不知道在代码中(或编程时),它究竟是个甚么玩意。并且,它到底怎样创建。
创建Key以后,我怎样把它传递给其他人。就好比钥匙1样,你总得给个实物给人家吧?
现在来看这两个问题的总结性回答:):
不理解上述内容的同学,请把实例代码再仔细看看!
JCE中,Certificates(是复数喔!)是证书之意。“证书”也是1个平常生活中经常使用的词,但是在JCE中,或说在Java Security中,它有甚么用呢?
这个问题的答案还是和Key的传递有关系。前面例子中我们说,创建密钥的人1般会把Key的书面表达情势转换成Base64编码后的字符串发给使用者。使用者再解码,然后还原Key就能够用了。
上面这个流程本身没有甚么隐患,但是,是否是随便1个人(1个组织,1个机构等等等等)给你发1个key,你就敢用呢?简单点说,你怎样判断是不是该信任给你发Key的某个人或某个机构呢?
好吧,这就好比现实生活中有个人说自己是警察,那你肯定得要他取出警官证或甚么别的东西来证明他是警察。这个警官证的作用就是证书的作用。
1般而言,我们会把Key的2进制表达式放到证书里,证书本身再填上其他信息(比如此证书是谁签发的,甚么时候签发的,有效期多久,证书的数字签名等等)。
初看起来,好像证书比直接发base64字符串要正规点,1方面它包括了证书签发者的信息,而且还有数字签名以避免内容被篡改。
但是,证书解决了信任的问题吗?很明显是没有的。由于证书是谁都可以制作的。既然不是人人都可以相信,那末,也不是随意甚么证书都可以被信任。
怎样办?先来看现实生活中是怎样解决信任问题的。
现实生活中也有很多证书,大到房产证、身份证,小到离职证明、介绍信。对方怎样判断你拿的这些证是真实有效的呢?
对头,看证书是谁/或哪一个机构的“手墨”!比如,结婚证首先要看是否是民政局的章。那....民政局是不是应当被信任呢???
好吧。关于民政局是不是应当被信任的这个问题在技术上基本无解,它是1个死循环。由于,即便能找到另外1个机构证明民政局合法有效,但这个问题仍然会顺流而上,说民政局该被信任的那个机构其本身是不是能被信任呢?....此问题终究会没完没了的问下去。
那怎样办?没甚么好办法。只要大家公认民政局能被信任,就能够了。
同理,对证书的是不是可信任问题而言:
客户拿到证书a后,首先要检查下:
......唧唧歪歪半天,其实关于证书的核心问题就1个:
证书背后常常是1个证书链。
.......
为了方便,系统(PC,Android,乃至阅读器)等都会把1些顶级CA(也叫Root CA,即根CA)的证书默许集成到系统里。这些RootCA用作自己身份证明的证书(包括该CA的公钥等信息)叫根证书。根证书理论上是需要被信任的。以Android为例,它在libcore/luni/src/main/files/cacerts下放了150多个根证书(以Android 4.4为例),如图8所示:
图8 Android自带的根证书文件
我们随意打开1个根证书文件看看,以下所示:
[某证书文件的内容,用记事本打开便可]
-----BEGIN CERTIFICATE-----
MIID5TCCAs2gAwIBAgIEOeSXnjANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMC
VVMxFDASBgNVBAoTC1dlbGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBD
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEvMC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9v
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDAxMDExMTY0MTI4WhcNMjEwMTE0
MTY0MTI4WjCBgjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1dlbGxzIEZhcmdvMSww
KgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEvMC0G
A1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVqDM7Jvk0/82bfuUER84A4n13
5zHCLielTWi5MbqNQ1mXx3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHE
SxP9cMIlrCL1dQu3U+SlK93OvRw6esP3E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4O
JgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5OEL8pahbSCOz6+MlsoCu
ltQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4jsNtlAHCE
AQgAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XAgMB
AAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8wTAYDVR0gBEUwQzBBBgtghkgBhvt7hwcB
CzAyMDAGCCsGAQUFBwIBFiRodHRwOi8vd3d3LndlbGxzZmFyZ28uY29tL2NlcnRw
b2xpY3kwDQYJKoZIhvcNAQEFBQADggEBANIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo
7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrvm+0fazbuSCUlFLZWohDo7qd/
0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0ROhPs7fpvcmR7
nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx
x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKbWV01QIroTmMatukgalHizqSQ
33ZwmVxwQ023tqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s=
-----END CERTIFICATE-----
Certificate: #下面是证书的明文内容
Data:
Version: 3 (0x2)
Serial Number:971282334 (0x39e4979e)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, O=Wells Fargo, OU=Wells Fargo CertificationAuthority, CN=Wells Fargo Root Certificate Authority
Validity
Not Before: Oct11 16:41:28 2000 GMT
Not After : Jan14 16:41:28 2021 GMT
Subject: C=US, O=Wells Fargo, OU=Wells Fargo CertificationAuthority, CN=Wells Fargo Root Certificate Authority
Subject Public Key Info:#Public Key的KeySpec表达式
Public Key Algorithm: rsaEncryption #PublicKey的算法
Public-Key: (2048 bit)
Modulus:
00:d5:a8:33:3b:26:f9:34:ff:cd:9b:7e:e5:04:47:
ce:00:e2:7d:77:e7:31:c2:2e:27:a5:4d:68:b9:31:
ba:8d:43:59:97:c7:73:aa:7f:3d:5c:40:9e:05:e5:
a1:e2:89:d9:4c:b8:3f:9b:f9:0c:b4:c8:62:19:2c:
45:ae:91:1e:73:71:41:c4:4b:13:fd:70:c2:25:ac:
22:f5:75:0b:b7:53:e4:a5:2b:dd:ce:bd:1c:3a:7a:
c3:f7:13:8f:26:54:9c:16:6b:6b:af:fb:d8:96:b1:
60:9a:48:e0:25:22:24:79:34:ce:0e:26:00:0b:4e:
ab:fd:8b:ce:82:d7:2f:08:70:68:c1:a8:0a:f9:74:
4f:07:ab:a4:f9:e2:83:7e:27:73:74:3e:b8:f9:38:
42:fc:a5:a8:5b:48:23:b3:eb:e3:25:b2:80:ae:96:
d4:0a:9c:c2:78:9a:c6:68:18:ae:37:62:37:5e:51:
75:a8:58:63:c0:51:ee:40:78:7e:a8:af:1a:a0:e1:
b0:78:9d:50:8c:7b:e7:b3:fc:8e:23:b0:db:65:00:
70:84:01:08:00:14:6e:54:86:9a:ba:cc:f9:37:10:
f6:e0:de:84:2d:9d:a4:85:37:d3:87:e3:15:d0:c1:
17:90:7e:19:21:6a:12:a9:76:fd:12:02:e9:4f:21:
5e:17
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 BasicConstraints: critical
CA:TRUE
X509v3Certificate Policies:
Policy:2.16.840.1.114171.903.1.11
CPS:http://www.wellsfargo.com/certpolicy
Signature Algorithm: sha1WithRSAEncryption #数字签名,以后再讲
d2:27:dd:9c:0a:77:2b:bb:22:f2:02:b5:4a:4a:91:f9:d1:2d:
be:e4:bb:1a:68:ef:0e:a4:00:e9:ee:e7:ef:ee:f6:f9:e5:74:
a4:c2:d8:52:58:c4:74:fb:ce:6b:b5:3b:29:79:18:5a:ef:9b:
ed:1f:6b:36:ee:48:25:25:14:b6:56:a2:10:e8:ee:a7:7f:d0:
3f:a3:d0:c3:5d:26:ee:07:cc:c3:c1:24:21:87:1e:df:2a:12:
53:6f:41:16:e7:ed:ae:94:fa:8c:72:fa:13:47:f0:3c:7e:ae:
7d:11:3a:13:ec:ed:fa:6f:72:64:7b:9d:7d:7f:26:fd:7a:fb:
25:ad:ea:3e:29:7f:4c:e3:00:57:32:b0:b3:e9:ed:53:17:d9:
8b:b2:14:0e:30:e8:e5:d5:13:c6:64:af:c4:00:d5:d8:58:24:
fc:f5:8f:ec:f1:c7:7d:a5:db:0f:27:d1:c6:f2:40:88:e6:1f:
f6:61:a8:f4:42:c8:b9:37:d3:a9:be:2c:56:78:c2:72:9b:59:
5d:35:40:8a:e8:4e:63:1a:b6:e9:20:6a:51:e2:ce:a4:90:df:
76:70:99:5c:70:43:4d:b7:b6:a7:19:64:4e:92:b7:c5:91:3c:
7f:48:16:65:7b:16:fd:cb:fc:fb:d9:d5:d6:4f:21:65:3b:4a:
7f:47:a3:fb
SHA1 Fingerprint=93:E6:AB:22:03:03:B5:23:28:DC:DA:56:9E:BA:E4:D1:D1:CC:FB:65
关于证书文件,还有1些容易混淆的事情要交代:
下面是1些常见的证书文件格式,1般用文件后缀名标示。
上一篇 技能树之旅: 计算点数与从这开始
下一篇 J2EE--JDBC