互联网时代,安全是永恒的主题,威胁无处不在,哪怕是在企业内网。
APDPlat充分考虑到了安全的问题:
首先,在浏览器中对用户密码加入复杂字符({用户信息})之后进行加密(Secure Hash Algorithm,SHA-512,as defined in FIPS 180-2),在服务器端加入用户名和复杂字符之后再次加密,提高破解复杂度;
其次,在浏览器和服务器之间采用安全通道(HTTPS)传输用户信息,避免信息泄露。
再次,安全和易用相互矛盾,不同的应用需要的平衡点不一样,APDPlat充分考虑到了这个问题,提供了可配置的用户密码安全策略,以满足不同的需求。
下面详细介绍相关的设计和实现:
首先,在浏览器对用户的登陆密码进行加密,使用的sha512.js来源于http://pajhome.org.uk/crypt/md5/:
j_password=hex_sha512(j_password+'{用户信息}');
在服务器端对新增用户、修改密码、重置密码时候的密码进行加密,使用类org.apdplat.module.security.service.PasswordEncoder的encode方法:
/** * 用户密码双重加密: * 1、使用SHA-512算法,salt为user.getMetaData(),即:用户信息 * 2、使用SHA-256算法,salt为saltSource.getSalt(user),即:用户名+APDPlat应用级产品开发平台的作者是杨尚川,联系方式(邮件:)(QQ:281032878) * @author 杨尚川 */ public class PasswordEncoder { public static String encode(String password,User user){ password = new ShaPasswordEncoder(512).encodePassword(password,user.getMetaData()); SaltSource saltSource = SpringContextUtils.getBean("saltSource"); return new ShaPasswordEncoder(256).encodePassword(password,saltSource.getSalt(user)); } public static void main(String[] args){ User user = new User(); user.setUsername("admin"); user.setPassword("admin"); String password = new ShaPasswordEncoder(512).encodePassword(user.getPassword(),user.getMetaData()); System.out.println("Step 1 use SHA-512: "+password+" length:"+password.length()); SaltSource saltSource = new APDPlatSaltSource(); password = new ShaPasswordEncoder(256).encodePassword(password,saltSource.getSalt(user)); System.out.println("Step 2 use SHA-256: "+password+" length:"+password.length()); } }
/** * 用户salt服务,salt为: * 用户名+APDPlat应用级产品开发平台的作者是杨尚川,联系方式(邮件:[email protected])(QQ:281032878) * @author 杨尚川 */ @Service("saltSource") public class APDPlatSaltSource implements SaltSource{ @Override public Object getSalt(UserDetails user) { //变化的用户名+固定的字符串 String text = user.getUsername()+"APDPlat应用级产品开发平台的作者是杨尚川,联系方式(邮件:[email protected])(QQ:281032878)"; return text; } }
用户在登陆的时候,浏览器进行一次加密,服务器进行二次加密,服务器加密的spring security配置如下:
<s:authentication-manager alias="authenticationManager"> <s:authentication-provider user-service-ref="userDetailsServiceImpl" > <s:password-encoder hash="sha-256"> <s:salt-source ref="saltSource"/> </s:password-encoder> </s:authentication-provider> </s:authentication-manager>
其次,使用HTTPS安全通道,在配置文件config.local.properties中指定channel.type的值为https:
#指定数据传输通道,可选值为http或https channel.type=https http.port=8080 https.port=8443
再次,提供了可配置的用户密码安全策略,在配置文件config.local.properties中指定user.password.strategy的值,可在安全和易用之间进行平衡,这里的值为相应策略类的spring bean name:
#用户密码安全策略 user.password.strategy=passwordLengthStrategy;passwordComplexityStrategy
接下来看看跟用户密码安全策略相关的设计与实现:
用户密码安全策略接口:
/** * 用户密码安全策略 * @author 杨尚川 */ public interface PasswordStrategy { /** * 检查用户的密码是否符合安全策略 * @param password 用户密码 * @throws PasswordInvalidException 不合法的原因包含在异常里面 */ public void check(String password) throws PasswordInvalidException; }
APDPlat默认提供了2个安全策略,分别是密码长度安全策略和密码复杂性安全策略。
1、密码长度安全策略:
/** * 密码长度安全策略 * 密码长度必须大于等于6 * @author 杨尚川 */ @Service public class PasswordLengthStrategy implements PasswordStrategy{ private static final APDPlatLogger LOG = APDPlatLoggerFactory.getAPDPlatLogger(PasswordLengthStrategy.class); @Override public void check(String password) throws PasswordInvalidException { if(StringUtils.isBlank(password) || password.length() < 6){ String message = "密码长度必须大于等于6"; LOG.error(message); throw new PasswordInvalidException(message); } LOG.info("密码符合安全策略"); } }
2、密码复杂性安全策略
/** * 密码复杂性安全策略: * 1、密码不能为空 * 2、密码不能全是数字 * 3、密码不能全是字符 * @author 杨尚川 */ @Service public class PasswordComplexityStrategy implements PasswordStrategy{ private static final APDPlatLogger LOG = APDPlatLoggerFactory.getAPDPlatLogger(PasswordComplexityStrategy.class); @Override public void check(String password) throws PasswordInvalidException { if(StringUtils.isBlank(password)){ String message = "密码不能为空"; LOG.error(message); throw new PasswordInvalidException(message); } if(StringUtils.isNumeric(password)){ String message = "密码不能全是数字"; LOG.error(message); throw new PasswordInvalidException(message); } if(StringUtils.isAlpha(password)){ String message = "密码不能全是字符"; LOG.error(message); throw new PasswordInvalidException(message); } LOG.info("密码符合安全策略"); } }
有了不同的策略之后,还需要一个类来执行配置文件指定的策略:
/** * 密码安全策略执行者 * 根据配置项user.password.strategy * 指定的spring bean name * 分别执行指定的策略 * @author 杨尚川 */ @Service public class PasswordStrategyExecuter implements PasswordStrategy, ApplicationListener { private static final APDPlatLogger LOG = APDPlatLoggerFactory.getAPDPlatLogger(ApplicationListener.class); private final List<PasswordStrategy> passwordStrategys = new LinkedList<>(); @Override public void check(String password) throws PasswordInvalidException { for(PasswordStrategy passwordStrategy : passwordStrategys){ passwordStrategy.check(password); } } @Override public void onApplicationEvent(ApplicationEvent event){ if(event instanceof ContextRefreshedEvent){ LOG.info("spring容器初始化完成,开始解析PasswordStrategy"); String strategy = PropertyHolder.getProperty("user.password.strategy"); if(StringUtils.isBlank(strategy)){ LOG.info("未配置user.password.strategy"); return; } LOG.info("user.password.strategy:"+strategy); String[] strategys = strategy.trim().split(";"); for(String item : strategys){ PasswordStrategy passwordStrategy = SpringContextUtils.getBean(item.trim()); if(passwordStrategy != null){ passwordStrategys.add(passwordStrategy); LOG.info("找到PasswordStrategy:"+passwordStrategy); }else{ LOG.info("未找到PasswordStrategy:"+passwordStrategy); } } } } }
有了执行安全策略的服务之后,需要在新增用户、修改密码、重置密码的地方验证用户密码的安全,在用户服务类中进行验证APDPlat_Core/src/main/java/org/apdplat/module/security/service/UserService.java:
首先,注入密码安全策略执行者:
@Resource(name="passwordStrategyExecuter") private PasswordStrategyExecuter passwordStrategyExecuter;
其次,在新增用户的时候验证密码是否符合安全策略:
//先对用户的密码策略进行验证 try{ passwordStrategyExecuter.check(model.getPassword()); }catch(PasswordInvalidException e){ throw new RuntimeException(e.getMessage()); } LOG.debug("加密用户密码"); model.setPassword(PasswordEncoder.encode(model.getPassword(), model));
再次,在修改密码的时候验证密码是否符合安全策略:
for(Property property : properties){ if("password".equals(property.getName().trim())){ //先对用户的密码策略进行验证 try{ passwordStrategyExecuter.check(property.getValue().toString()); }catch(PasswordInvalidException e){ throw new RuntimeException(e.getMessage()); } property.setValue(PasswordEncoder.encode(property.getValue().toString(),user)); break; } }
//先对用户的密码策略进行验证 try{ passwordStrategyExecuter.check(newPassword); }catch(PasswordInvalidException e){ result.put("success", false); result.put("message", e.getMessage()); LOG.error(e.getMessage()); return result; } oldPassword=PasswordEncoder.encode(oldPassword.trim(),user); if(oldPassword.equals(user.getPassword())){ user.setPassword(PasswordEncoder.encode(newPassword.trim(),user)); serviceFacade.update(user); message = "修改成功"; result.put("success", true); result.put("message", message); LOG.info(message); }else{ message = "修改失败,旧密码错误"; result.put("success", false); result.put("message", message); LOG.error(message); }
最后,在重置密码的时候验证密码是否符合安全策略:
//先对用户的密码策略进行验证 try{ passwordStrategyExecuter.check(password); }catch(PasswordInvalidException e){ LOG.error(e.getMessage()); return e.getMessage(); } int success = 0; for(int id : ids){ User user = serviceFacade.retrieve(User.class, id); if(user == null){ LOG.error("ID为 "+id+" 的用户不存在,无法为其重置密码"); continue; } if(PropertyHolder.getBooleanProperty("demo") && "admin".equals(user.getUsername())){ LOG.error("演示版本不能重置admin用户的密码"); continue; } //设置新密码 user.setPassword(PasswordEncoder.encode(password, user)); //同步到数据库 serviceFacade.update(user); success++; }
后记(说明APDPlat中密码安全策略的重要性):
假设用户密码明文为:123456,我们使用http://pajhome.org.uk/crypt/md5/的MD5计算功能进行密文的计算,使用http://www.cmd5.com/的解密功能进行明文的计算。
1、明文123456计算得到密文为:e10adc3949ba59abbe56e057f20f883e,然后对密文进行解密,立马得到明文:123456。
2、我们加大密码长度,在密码后面填充3个0,把密码变为123456000,计算得到密文为3bc2fbdd89ef79f3dbfbaf1f2132baa1,然后对密文进行解密,解密页面提示:已查到,这是一条付费记录,密文类型:md5。说明还是能解密。
3、我们加大密码复杂度,在每两个密码之间填充更多的复杂字符,把密码变为{~$(!APDPlat)1(是)2(一个)3(应用级产品)4(开发平台)5(帮助您)6(快速开发企业级应用程序)$~},计算得到密文为:59f1cade9738167f3b4070e29da5af2e,然后对密文进行解密,解密页面提示:未查到,已加入本站后台解密。要想破解这么复杂的密码不是容易的事,何况APDPlat采用了比MD5更安全的SHA-512加密算法。
从上面的分析可以看到,保证密码安全并不容易,为了防止用户密码在网络传输中被监听泄露,我们使用HTTPS安全通道,为了避免用户的明文密码在网络上传输,先在客户端进行了加密,当然了,在客户端的加密规则很容易就暴露了,我们需要在服务器端再进行一次加密,也就是把客户端加密过后的密文当做明文,这样最终的用户密码密文就很难再破解出来了。
总结一下,用户密码主要面临两方面的威胁:一是在用户登陆的时候,需要将密码提交给服务器以验证身份,用户的密码有可能在网络传输过程中被监听导致泄露,用户在输入密码的时候也有可能被旁边的人看到,用户输入的密码也有可能被计算机中的病毒木马窃取;二是存储在服务器端数据库中的用户密码有可能被网站工作人员或黑客获取到,如果存储的用户密码未加密而是明文存储,那么就相当危险了,就算是加密了,如果采用的算法有缺陷(王小云为首的研发团队破译了MD5和SHA-1)且密码过于简单,也有可能被破解。
参考资料:
1、http://www.cmd5.com/
2、http://pajhome.org.uk/crypt/md5/
3、http://zh.wikipedia.org/wiki/%E7%8E%8B%E5%B0%8F%E9%9B%B2
APDPlat托管在Github