8 Hjpetstore 安全策略
8.1 不要将密码存成明文
明文的密码,只要记下,写下或存储在计算机中,就不再是密码,它是明文。
企业应用中,密码在数据库需要采用 HASH 算法进行加密, 通常采用 MD5 加密算法。JAVA 通过 JCE 对密码领域提供全面支持
public static String md5(byte[] plainText) throws NoSuchAlgorithmException, UnsupportedEncodingException {
MessageDigest messageDigest = MessageDigest.getInstance("MD5 ");
String md5 = new BigInteger(1, messageDigest.digest(plainText)).toString(16);
if (md5.length() < 32) {
md5 = "0" + md5;
}
messageDigest.reset();
return md5;
}
HASH 算法理论上保证不可逆性,即从一个 HASH 值,理论上很难,至少“很不容易”得到原文 。我们说”很不容易”,是因为黑客们会想尽一切可能的办法的,比如HASH 字典攻击: 他们将收集一个字典库来保存常用的密码的HASH 值。
另一个问题是,如果两个人使用相同的密码,如果皮皮鲁使用'haoboy5258' ,假设另外一个小伙也喜欢上这个密码,并且用在一个低安全性的系统。
'haoboy5258' 的HASH 值可以通过以下命令获得 (windows 的同学可以使用 online tools ):
pprun@pprun-laptop:~$ echo -n 'j2eej2ee' | md5sum
b6dcb0189da62b6b849903dcd57f84be -
HASH 保证对于同一个输入一定会返回同一个输出,这也是HASH 的本意,用于保证内容的没有被串改 。
对于一个低安全的系统,黑客们很容易攻破并获得密码的HASH 值,有了它,通过查找HASH 字典,便可以得到相应的明文了。
这样一来打破了HASH 算法理论上保证不可逆性,即 通过 HASH 值不能知道明文。
为了使黑客更累些,通常的做法是在生成对应的 HASH 值时再带上一个该用户特有的属性,比如 create_time 或者,如果用户名是唯一的,可以用 username, 这个值就是我们通过所说的 salt , 就是在生成 hash 时,撒上把盐,呵呵。
public static String md5(byte[] plainText, byte[] salt) throws NoSuchAlgorithmException, UnsupportedEncodingException {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(salt);
String md5 = new BigInteger(1, messageDigest.digest(plainText)).toString(16);
if (md5.length() < 32) {
md5 = "0" + md5;
}
messageDigest.reset();
if (log.isDebugEnabled()) {
log.debug(new String(plainText, "UTF8") + "[salt=" + new String(salt, "UTF8") + "] 's MD5: " + md5);
}
return md5;
}
这样一来,数据库中保存的是这个 md5 值作为密码,我们在用户 login 时比对的是 用这个值和用他们输入的用户和密码生成的 md5 值,如果相等,则是系统有效用户:
@Override
public User getUser(String username, String password) throws ServiceException {
String passwordMd5 = MessageDigestUtil.md5(password.getBytes("UTF8"), username.getBytes("UTF8"));
return this.userDao.getUser(username, passwordMd5);
}
8.2 使用 Kaptcha
DoS ( Denial of Service) 恶劣的家伙们最常使用的方式之一, 注册,登录,搜索(可选)需要CAPTCHA( 全自动区分计算机和人类的图灵测试) 来应对这种无聊的攻击手段。
我们需要安全,但是我们不能丧失用户友好性。
使用开源的或者自制的 captcha 已经很常见了,我们希望这种captcha 不要把系统的真正用户惹烦了。
目前开源的大多数实现生成的 captcha 的可读性太差,以下所谓的TOP10 IT 公司他们的注册流程中采用的 captcha:
你能在10 秒内看清楚,并且猜对吗?我猜大部分人都有这样的经历,一遍,两遍,三遍... 总是“你所输入的字符与图片显示的不相符 … “
还好 kaptcha 的作者意识到了这一点,以下是 kaptcha 生成的图片:
8.2.1 kaptcha 配置
8.2.1.1KaptchaServlet
在 web.xml 中配置 servlet
<servlet> <servlet-name>Kaptcha</servlet-name> <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Kaptcha</servlet-name> <url-pattern>/kaptcha.jpg</url-pattern> </servlet-mapping>
8.2.1.2 KAPTCHA_SESSION_KEY
对于Spring MVC controller 可以以下代码进行Session 属性 KAPTCHA_SESSION_KEY 进行校验,
private boolean validateCaptcha(HttpServletRequest request) {
String kaptchaExpected = (String) request.getSession().getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
//String kaptchaReceived = userForm.getKaptcha();
String kaptchaReceived = request.getParameter("kaptcha");
if (log.isDebugEnabled()) {
log.debug("Received kaptcha: '" + kaptchaReceived + "' is comparing with Expected kaptcha: '" + kaptchaExpected + "'...");
}
if (kaptchaReceived == null || !kaptchaReceived.equalsIgnoreCase(kaptchaExpected)) {
log.error("Received kaptcha: '" + kaptchaReceived + "' is comparing with Expected kaptcha: '" + kaptchaExpected + "'...");
return false;
}
return true;
}
8.3 使用 https (SSL 通道)
用户敏感信息提交页面需要 https (SSL 通道) ,没有 ssl 的表单提交没有任何安全可言,如果你的系统在用户提交敏感信息时没有使用 https ,请立即file 一个 p0 的 bug 。
事实上采用 https 是非常简单的,因为没有那个 web 服务器不支持这一协议的。
-
在页面中hardcode https url 跳转,然后在处理完成后hardcode http url 跳回
-
采用 filter 或者 interceptor 将所有需要跳转的请求统一在同一个地方处理,然后,
-
在整个session 中继续使用https
-
或者在请求完成时 hardcode 跳转回 http url, 如果工作流不是很复杂的话,也同样可以使用 filter 或者 interceptor 完成返回的逻辑
public class HttpsUrlRewritingInterceptor extends HandlerInterceptorAdapter {
private int sslPort;
public void setSslPort(int sslPort) {
this.sslPort = sslPort;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("https".equals(request.getScheme()) == false) {
// if it is already in https, bypass
StringBuilder sslUrl = new StringBuilder();
sslUrl.append("https://").append(request.getServerName()).append(":").append(sslPort).append(request.getRequestURI());
response.sendRedirect(sslUrl.toString());
}
return true;
}
}
8.4 密码算法及其参数的选择
密码算法或其参数的选择本着够用就行,但要支持可扩展。科学是无止境的,同样,黑客利用技术的手段也一样。再有,过去可靠的算法,并不意味着目前或将来一定可靠。如同 王小云 破解 MD5 和 SHA-1 算法引起美国人恐慌一样。
密码本身是性能的杀手,是人类”自找麻烦“,就象我们给家里安防盗门,公司的配保险柜一样。
我们的选择应该本着够用就行,但一定要保留扩展的余地,这会涉及到
-
密码算法
-
算法参数
-
数据库字段长度的定义
例如:目前 RSA 算法 key 的长度为 1024 已经足矣就会一般性的攻击了。
private static final String ALGORITHM = "RSA";
private static final int keyLength = 1024;
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
keyGen.initialize(keyLength);
KeyPair keyPair = keyGen.generateKeyPair();
return keyPair;
}
8.5 SOA public api key
基于SOA 架构,如果 web service 面象的是大众,是一个 public API ,安全策略将是怎样呢?
SOA 公共API 的访问,用户需要通过注册获取一个 api key ,然后 web serive 调用时作我为请求参数传入。象 Google/Yahoo/Amazon 的 public web service 的方式那样。
我们或许并不想因为这个“自找麻烦”的安全问题失去潜在的用户,但同时我们也要保证系统的安全以及可控性,比如非 VIP 用户, 每天 < 1000 requests
这个public api key ,根据需求,可以是 md5, sha-1( 或许这个不应该在考虑之列了,因为王小云的原因),sha-256, sha-512, 等等:
-
用户提交一个注册请求
-
系统返回一个 api key 给用户,可以通过 email 或者online 的方式
-
用户在访问 web service 时,作为参数传入这个api key
hjpetstore 提供一个 REST 服务:通过关键字,查找宠物店里的出售的宠物列表:
@RequestMapping(value = "/products/{keyword}", method = RequestMethod.GET)
public ModelAndView getProductsByKeyword(
@RequestParam("apikey") String apiKey ,
@RequestParam("page") int page,
@RequestParam("max") int max,
@PathVariable("keyword") String keyword) {
if (log.isDebugEnabled()) {
log.debug("HjpetstoreController is processing request for keyword: " + keyword);
}
Products products = hjpetstoreService.searchProductList(apiKey, keyword, page, max);
ModelAndView mav = new ModelAndView("products");
mav.addObject(products);
return mav;
}
@RequestParam("apikey") String apiKey 限定了 apiKey request parameter 必须存在(default) ,否则 Spring MVC 3 的 DispatcherServlet 根本不会 match 这个方法用来处理请求。
业务逻辑的处理,将检查这个唯一性的 api-key 是否对应系统中的一个用户:
boolean isUserExistingForKey = userDao.isUserExistingForApiKey(apiKey);
if (isUserExistingForKey == false) {
throw new ServiceException("Your apikey is not valid");
}
...
8.6 PCI (Payment Card Industry)
对于高安全系统,例如PCI (Payment Card Industry 支付卡行业数据安全标准) ,安全专家少不了。
如果你身处这样的一个高安全系统,首先恭喜你!
但同时,你要知道身上的责任,公司的信用是建立在系统的安全之上的。同时信用卡安全事故折射这一现实的世界 --对于这样的系统,安全不再是“自找麻烦”。反而有时要把自己当成“黑客”,通过写测试模拟各种攻击场景。
作为工程师,我们必须与安全专家紧密合作,不能有任何冒险的心理。此外,所有安全数据必须跟普通的数据隔离,否则将遭到 PCI 委员会的严厉处罚,一个月的处罚金足以 pay 一个安全顾问一年。
Hjpetstore 已经预备了这一方案,虽然对卡片及用户数据没有与普通数据分隔开,但是,SecurityService 设计成一个 REST 组件,可以单独部署。