现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程是:
1. 用户用密码登录成功后,服务器返回token给客户端;
2. 客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;
3. 服务器检查token的有效性,有效则返回数据,若无效,分两种情况:
Øtoken错误,这时需要用户重新登录,获取正确的token;
Øtoken过期,这时客户端需要再发起一次认证请求,获取新的token;
然而,此种验证方式存在一个安全性问题:当登录接口被劫持时,黑客就获取到了用户密码和token,后续则可以对该用户做任何事情了。用户只有修改密码才能夺回控制权。那么该如何优化呢?
方法一、url签名
一、签名的设计
原理:用户登录后向服务器提供用户认证信息(如账户和密码),服务器认证完后给客户端返回一个Token令牌,用户再次获取信息时,带上此令牌,如果令牌正确,则返回数据。对于获取Token信息后,访问用户相关接口,客户端请求的url需要带上如下参数:
Ø 时间戳:timestamp;
Ø Token令牌:token;
然后将所有用户请求的参数按照字母排序(包括timestamp,token),然后根据MD5加密(也可是其他加密算法),全部大写,生成sign签名,这就是所说的url签名算法。然后登陆后每次调用用户信息时,带上sign,timestamp,token参数。
例如:原请求
http://www.andy.cn/api/user/update/info.shtml?city=lz
加上时间戳和token后:
http://www.andy.cn/api/user/update/info.shtml?city=lz×tamp=12445323134&token=wefkfjdskfjewfjkjfdfnc
然后根据url参数生成sign,最终的请求如下:
http://www.andy.cn/api/user/update/info.shtml?city=lz&
timestamp=12445323134&token=wefkfjdskfjewfjkjfdfnc&sign=FDK2434JKJFD334FDF2
其最终的原理是减小明文的暴露次数,保证数据安全的访问。
具体实现如下:
1. api请求客户端向服务器端一次发送用户认证信息(用户名和密码),服务器端请求到该请求后,验证用户信息是否正确。
如果正确:则返回一个唯一不重复的字符串(一般为UUID),然后在Redis(任意缓存服务器)中维护Token----Uid的用户信息关系,以便其他api对token的校验。
如果错误:则返回错误码。
2.服务器设计一个url请求拦截规则
(1)判断是否包含timestamp,token,sign参数,如果不含有返回错误码。
(2)判断服务器接到请求的时间和参数中的时间戳是否相差很长一段时间(时间自定义如半个小时),如果超过则说明该 url已经过期(如果url被盗,他改变了时间戳,但是会导致sign签名不相等)。
(3)判断token是否有效,根据请求过来的token,查询redis缓存中的uid,如果获取不到这说明该token已过期。
(4)根据用户请求的url参数,服务器端按照同样的规则生成sign签名,对比签名看是否相等,相等则放行。
(5)此url拦截只需对获取身份认证的url放行(如登陆url),剩余所有的url都需拦截。
3.Token和Uid关系维护
对于用户登录我们需要创建token--uid的关系,用户退出时需要删除token--uid的关系。
二、签名实现
1.获取全部请求参数
2.生成签名
方法二、https协议
一、什么是Https,与Http有哪儿些异同
HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。它是一个URI scheme(抽象标识符体系),句法类同http:体系。用于安全的HTTP数据传输。https:URL表明它使用了HTTP,但HTTPS存在不同于HTTP的默认端口及一个加密/身份验证层(在HTTP与TCP之间)。
Ø https协议需要到ca申请证书,一般免费证书很少,需要交费。http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议;
Ø http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。
Ø http的连接很简单,是无状态的,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
二、 HTTPS解决的问题
1 . 信任主机的问题. 采用https 的server 必须从CA 申请一个用于证明服务器用途类型的证书. 该证书只有用于对应的server 的时候,客户端才信任主机.
2 . 通讯过程中的数据的泄密和被窜改。
三、 Https请求交互过程
1.客户端发送一个request,https服务端把支持的加密算法等以证书的形式返回一个身份信息(包含ca颁发机构和加密公钥等)。
2.获取证书之后,验证证书合法性。
3.随机产生一个密钥,并以证书当中的公钥加密。
4.request https服务端,把用公钥加密过的密钥传送给https服务端。
5.https服务端用自己的密钥解密,获取随机值。
6.之后双方传送数据都用此密钥加密后通信。
四、 服务器端配置
1、生成密钥库和证书:
因使用java环境,下面使用jdk下面的keytool工具来生成相应的密钥库和证书,下面的命令是在windows 7 下面测试通过的,可以直接复制使用
1)创建目录,如d:/sslDemo
2)使用资源管理进入d:/sslDemo,按住shift+右键,弹出菜单,选择"在此处打开命令行".
3)生成服务器证书库
keytool -validity 36500 -genkey -v -alias server -keyalg RSA -keystore server.keystore -dname "CN=www.itjoyee.com,OU=itjoyee.com,O=itjoyee.com,L=Wuhan,ST=HuBei,c=cn" -storepass 123456 -keypass 123456
注: 服务器证书库参数“CN”必须与服务端的IP地址相同,否则会报错,客户端的任意。(有些信息可有可无,例如L,St等)
回车,会提示你输入一些个人信息及组织信息:
输入yes,回车即可。
4)从服务器证书库中导出服务器证书(如果是web服务到此可以结束)
keytool -export -v -alias server -keystore server.keystore -storepass 123456 -rfc -file server.cer
5)生成客户端信任证书库(由服务端证书生成的证书库,客户端使用此证书验证服务端来源可靠)
keytool -import -v -alias server -file server.cer -keystore client.truststore -storepass 123456 -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
6)生成客户端证书库
keytool -validity 36500 -genkeypair -v -alias client -keyalg RSA -storetype PKCS12 -keystore client.p12 -dname "CN=clients.itjoyee.com,OU=jiajianfa,O=jiajianfa,L=Wuhan,ST=HuBei,c=cn" -storepass 123456 -keypass 123456
7)从客户端证书库中导出客户端证书
keytool -export -v -alias client -keystore client.p12 -storetype PKCS12 -storepass 123456 -rfc -file client.cer
8)将客户端证书导入到服务器证书库(使得服务器信任客户端证书,服务器端用此验证客户端的合法性)
keytool -import -v -alias client -file client.cer -keystore server.keystore -storepass 123456
9)查看服务端证书中信任的客户端证书
keytool -list -keystore server.keystore -storepass 123456
2、服务器端配置
由于使用tomcat,下面使用tomcat做为实例配置.
2.1、在tomcat安装目录下新建key目录,将上面生成的server.keystore复制过去.
2.2、编辑tomcat安装目录下的conf目录下的server.xml,如:d:\sslDemo\apache-tomcat-7.0.55\conf\server.xml
找到Connector,修改如下:
3.tomcat下的服务强制使用ssl配置
以ROOT服务为例,修改
D:\sslDemo\apache-tomcat-7.0.55\webapps\ROOT\WEB-INF\web.xml
五、客户端测试(Android工具类)
为了方便测试android下双向认证可以用,生成证书的时候把域名换成服务器的ip地址,验证才可以通过。
public class HttpClientSslHelper {
private static final String KEY_STORE_TYPE_BKS = "bks";
private static final String KEY_STORE_TYPE_P12 = "PKCS12";
private static final String SCHEME_HTTPS = "https";
private static final int HTTPS_PORT = 8444;
private static final String KEY_STORE_CLIENT_PATH = "client.p12";
private static final String KEY_STORE_TRUST_PATH = "client.truststore";
private static final String KEY_STORE_PASSWORD = "123456";
private static final String KEY_STORE_TRUST_PASSWORD = "123456";
private static KeyStore keyStore;
private static KeyStore trustStore;
public static HttpClient getSslHttpClient(Context pContext) {
HttpClient httpsClient = new DefaultHttpClient();
try {
// 服务器端需要验证的客户端证书
keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
// 客户端信任的服务器端证书
trustStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS);
InputStream ksIn = pContext.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
InputStream tsIn = pContext.getResources().getAssets().open(KEY_STORE_TRUST_PATH);
try {
keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ksIn.close();
} catch (Exception ignore) {
}
try {
tsIn.close();
} catch (Exception ignore) {
}
}
SSLSocketFactory socketFactory = new SSLSocketFactory(keyStore, KEY_STORE_PASSWORD, trustStore);
Scheme sch = new Scheme(SCHEME_HTTPS, socketFactory, HTTPS_PORT);
httpsClient.getConnectionManager().getSchemeRegistry().register(sch);
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return httpsClient;
}
}
实例代码:
1 AsyncTask testTask = new AsyncTask() {
@Override
protected Object doInBackground(Object... params) {
try {
HttpClient httpsClient = AppSslApplication.getHttpsClient(MainActivity.this.getBaseContext());
HttpGet httpget = new HttpGet(HTTPS_URL);
HttpResponse response = httpsClient.execute(httpget);
HttpEntity entity = response.getEntity();
Log.e("Response status", response.getStatusLine().toString());
if (entity != null) {
Log.e("Response", "Response content length: " + entity.getContentLength());
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent()));
String text;
while ((text = bufferedReader.readLine()) != null) {
Log.e("Response status", text);
}
bufferedReader.close();
}
httpsClient.getConnectionManager().shutdown();
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
};
testTask.execute();
}