一.前言
上一篇文章里介绍了使用自定义的Realm来实现数据的获取,不过,数据的获取依然不是来源于真实的数据库或者是nosql,只是包装了一个方法,假装从这个方法里获取了数据库中的用户信息,然后我们返回了一个SimpleAccount,这个对象携带了用户名与密码,当时我们是明文返回的密码。这样做很显然是不安全的,一旦数据来源被攻破,所有的用户信息都会被泄露。所以这里我们介绍下,常用的密码加密策略。
二.常用加密策略
我们在日常的开发中可能都是用过不同的加密算法,那么这些算法到底有哪些优势呢,他们的特点又是什么呢?这里简单介绍下几种常用的加密算法。
1.MD5
1.什么是MD5
MD5与我们常用的SM2等算法有很大区别,通常我们使用的加密算法都是可以将密文破解的,比如基于对称算法与非对称算法实现的加密方式,但MD5则不行,MD5的典型特点就是不可逆,也就是说我们加密的信息就不能再反向解密出来了,那么可能有人会说网络上有很多MD5在线解密的网站,注意这些并不是真的解密了MD5加密后的密文,而是使用穷举法,对密文进行一一匹配,来实现的破解,这种暴力破解其实很好规避,不过这样牵扯出了MD5的另一个特点,那就是固定的明文多次加密的结果都是一样的。使用MD5算法生成的始终是一个16进制的32位(256bit)的字符串。
2.MD5的应用场景
①应用于加密场景,比如登录中的密码加密。
②应用于签名,用来判定两个文件是否是同一个文件,同一个文件使用MD5加密后的密文肯定是一致的,这样可以防止文件被篡改。
2.AES
3.什么是AES
AES是一种常用的加密算法,该算法是一种对称加密算法,何为对称加密算法呢,就是加密与解密都需要同一个秘钥,我们常用的AES有AES128,AES192,AES256,这些是什么意思呢,这三种AES其实就是指不同的秘钥的长度,三种AES对应的秘钥长度分别是128位,192位,256位,换算成字节就是16,24,32。比如这样一个秘钥:String str= “1234567812345678”,那么使用这个秘钥的AES自然就是,用的AES128了。
4.AES的使用场景
在实际的项目中通常前后端数据的交互都是采用AES加密,这样就可以规避敏感信息的泄露,不过值得说的是通常都不是仅仅对AES进行简单的加密,加密完以后还会对密文进行Base64加密,然后才会进行交互,这样就保证了信息传输的安全性,不过没有百分百的安全,只要是密文能被解出来,就会存在破解的风险,风险都是相对的,我们能做的就是相对安全。
三.使用MD5+盐+hash散列实现登录
我们已经都知道了,使用MD5加密,我们依然可以使用穷举法来实现暴力破解,这样就需要用户设置密码时不能使用简单的密码,简单的密码很容易就会被穷举到,但是用户在使用时是没有这样的意识的,所以就需要我们为用户的密码加点“盐”,让密码变得更咸一点,使密码不会这么容易被破解出来。所谓的这个盐呢,其实就是为密码拼接一串我们随机生成的字符串。此外我们还可以为MD5+盐生成的16进制的值进行hash散列,这样就会让密码更加的安全,即使数据库被攻破,盐与密码都被获取了,散列次数一样还是不会被发现,当然了这样情况也是极少碰到的。
1.如何使用Shiro的MD5,对数据进行加密
那么如何使用Shiro提供的MD5呢,我们看下下面的代码就明白了了
public class Test { public static void main(String[] args) { Md5Hash md5Hash = new Md5Hash("123"); System.out.println(md5Hash.toHex()); Md5Hash md5Hash1 = new Md5Hash("123","12345&8"); System.out.println(md5Hash1.toHex()); Md5Hash md5Hash2 = new Md5Hash("123","12345&8",1024); System.out.println(md5Hash2.toHex()); } }
上面的程序输出结果如下所示:
202cb962ac59075b964b07152d234b70 19d11f5191d12b1c224a7d535e1cb650 cc24303e7e1827dbb9742d1f4efeb0ec Process finished with exit code 0
根据上面的例子,我们可以很形象的看出哈,第一个例子是只使用了MD5进行加密的数据,第二步部分是加上了“盐”的MD5加密,第三部分则是在第二部分的基础上对值进行散列,其实根据这样思路我们可以对密码进行很多种加密,比如对盐进行散列,对盐也进行MD5加密,或者对盐使用Base64,或者是AES等,都可以,我们可以组合出很多的加密,不过其实是没有必要的,没有绝对的安全,但是一般我们使用MD5+“盐”+hash散列就已经可以了。
2.使用MD5+盐+hash散列实现登录
下面我们就来看下使用Shiro实现的登录场景中是如何使用这种加密的方式呢,前面的文章已经介绍过,在密码验证的地方有个密码匹配器,默认的密码匹配器就是做equals比较密码的,既然我们不想使用equals直接比较,那么就需要更换这个密码匹配器,那么我们怎么更换这个密码匹配器呢?
1.密码匹配器在哪里
前面我们已经说过密码的校验是在这里完成的,如下图
该方法的第一行就是密码匹配器了,密码匹配器是该类的一个属性,所以我们想要更换密码匹配器就为这个属性set一个我们想要的密码匹配器就可以实现更换。
2.匹配匹配器在哪里换
我们已经知道密码匹配器在哪里了,那我们想要更换难道是在这个密码匹配器所属的类中更换吗,当然不是我们写的,真正的更换地方应该使我们写的自定义realm中去进行更换,我们通常会为安全管理器设置realm,在设置realm之前我们需要更换密码匹配器,还需要告诉这个realm我们使用哪种加密算法。那么我们对应的代码就应该是下面这个样子了
public class TestAuthenticator2 { public static void main(String[] args) throws Exception{ //定义安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); //定义一个支持MD5+盐+hash散列的密码匹配器 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //告诉密码匹配器密文是哪种加密方式 hashedCredentialsMatcher.setHashAlgorithmName("md5"); //告诉密码匹配器,密码被散列的次数,这个一般定义为1024的倍数,默认一次 hashedCredentialsMatcher.setHashIterations(1024); //定义自己的realm SecondRealm secondRealm = new SecondRealm(); //为realm设置密码匹配器 secondRealm.setCredentialsMatcher(hashedCredentialsMatcher); //为安全管理器设置realm defaultSecurityManager.setRealm(secondRealm); //模拟用户登录场景 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhaoyun","123"); SecurityUtils.setSecurityManager(defaultSecurityManager); try { Subject subject = SecurityUtils.getSubject(); subject.login(usernamePasswordToken); System.out.println("登录成功"); } catch (Exception e) { e.printStackTrace(); } } }
既然我们已经告诉了安全管理器,我数据库存储的是MD5+盐+hash散列实现的密文,那么我们的realm再像以前那么写就不正确了。
3.自定义realm
在我们不对密码进行加密时我们模拟的realm获取的都是明文的用户名与密码返回给了密码匹配器,那么我们想使用MD5+盐+hash散列的方式去实现密码加密,以前的信息返回肯定会报错,那我们该如何写呢,请看下面的代码
public class SecondRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken; String userName = usernamePasswordToken.getUsername(); //获取数据库中的用户信息 // SimpleAccount simpleAccount = new SimpleAccount("zhaoyun","202cb962ac59075b964b07152d234b70","df",this.getName()); SimpleAuthenticationInfo simpleAuthenticationInfo = getSimpleInfo(); //验证用户名与token用户名是否相同 if(simpleAuthenticationInfo.getPrincipals().asList().contains(userName)){ return simpleAuthenticationInfo; }else{ return null; } } private SimpleAuthenticationInfo getSimpleInfo(){ Md5Hash md5Hash = new Md5Hash("123"); Md5Hash md5Hash1 = new Md5Hash("123","12345&8"); Md5Hash md5Hash2 = new Md5Hash("123","12345&8",1024); return new SimpleAuthenticationInfo("zhaoyun",md5Hash2, ByteSource.Util.bytes("12345&8"),this.getName()); } }
有一点要说下在上一个例子中我们实现自定义realm,是去继承AuthenticatingRealm,这个例子里我们是去继承的AuthorizingRealm,这两个Realm一个是做认证的,一个是授权的(第一个认证),而且用作授权的AuthorizingRealm是AhthenticatingRealm的子类,所以我们可以直接继承AuthorizingRealm也是正确的,就像上方的代码,继承了这个类,就要求我们重写认证和授权两块的代码了。授权这部分还未涉及到,我们暂时先不说,还继续看认证的部分。
根据这个方法SimpleAuthenticationInfo(),我们可以看到里面写了三个Md5Hash的对象,第一个是只用了Md5,第二个使用了随机盐,随机盐即使一组随机的字符串,第三个则是使用了MD5+盐+散列1024次生产的密文,
细心的同学可能会发现哈,我们在为realm设置密码匹配器时并没有告诉ream随机盐是多少,只告诉了密码匹配器密文是何种加密方式,需要散列多少次。
4.密码匹配器怎么知道随机盐是多少的
其实很简单,我们在完成用户名验证时,就会返回一个账户信息也就是SimpleAuthenticatingInfo该类的对象,这个对象包含了密码,已经盐,在到达密码匹配器时,会根据是否传了盐来去动态获取盐的值,而不需要我们在用户登录时直接携带者盐,况且用户登录携带着盐也是极不安全的,所以我们并不需要为密码匹配器设置盐。
5.使用MD5+盐+hash散列成功登录
运行上方的代码我们可以得到结果如下:
登录成功 Process finished with exit code 0
感兴趣的大佬可以多试试,这里就不罗列其他的几种密码使用场景了。
四.总结
这篇文章主要介绍了MD5在使用Shiro登录时的的应用场景,我们使用MD5+盐+hash散列的方式,可以很大程度上保证密码的安全性,在日常的开发中其实,密码加你也是这种,就比如我前阵子在做的项目,前后端交互在对参数进行加密时就是使用AES+Base64进行双重加密的。