- 浏览: 236576 次
- 性别:
- 来自: 上海
文章分类
最新评论
-
shuhucy:
必须赞啊,源码理解的很深,解决一个困扰两天的问题
Spring AOP源码分析(八)SpringAOP要注意的地方 -
sealinesu:
精彩
Spring事务源码分析(一)Spring事务入门 -
whlt20090509:
"WEB-INF/view目录下有一个简单的hell ...
SpringMVC源码总结(一)HandlerMapping和HandlerAdapter入门 -
hai0378:
兄台 算我一个,最近在研究dubbo motan 及 zk ...
ZooKeeper源码研究寻求小伙伴 -
zpkbtzmbw:
看懂了,原理
SpringMVC源码总结(五)Tomcat的URIEncoding、useBodyEncodingForURI和CharacterEncodingFilter
Realm在验证用户身份的时候,要进行密码匹配。最简单的情况就是明文直接匹配,然后就是加密匹配,这里的匹配工作则就是交给CredentialsMatcher来完成的。先看下它的接口方法:
根据用户名获取AuthenticationInfo ,然后就需要将用户提交的AuthenticationToken和AuthenticationInfo 进行匹配。
AuthenticatingRealm从第三篇文章知道是用来进行认证流程的,它有一个属性CredentialsMatcher credentialsMatcher,使用如下:
以上我们知道了CredentialsMatcher所处的认证的位置及作用,下面就要详细看看具体的匹配过程,还是接口设计图:
对于上图的三个分支,一个一个来说。
对于AllowAllCredentialsMatcher:
都返回true,这意味着,只要该用户名存在即可,不用去验证密码是否匹配。
对于PasswordMatcher:
内部使用一个PasswordService 来完成匹配。从上面的匹配过程中,我们了解到了,对于服务器端存储的密码分成String和Hash两种,然后由PasswordService 来分别处理。所以PasswordMatcher 也只是完成了一个流程工作,具体的内容要到PasswordService 来看。
到底什么是Hash呢?
先看下接口图:
看下ByteSource:
就维护了一个byte[]数组。
看下SimpleByteSource的实现:
toHex就是将byte数组准换成16进制形式的字符串。toBase64就是将byte数组进行base64编码。
Hex.encodeToString(getBytes()) 详情如下:
对于一个byte[] data数组,byte含有8位,(0xF0 & data[i]) >>> 4 表示取其高四位的值。如
当data[i]=01001111时,0xF0 & data[i]则为01000000,然后右移四位则变成00000100即为值4,所以DIGITS[(0xF0 & data[i])=DIGITS[4]=4,同理data[i]的低四位变成f。最终的结果为一个byte 01001111变成两个char 4f。
Base64.encodeToString(getBytes()):就稍微比较麻烦,这里不再详细说明。原理的话可以到网上搜下,有很多这样的文章。还是回到ByteSource的接口图,该轮到Hash了。
多添加了三个属性,算法名、盐值、hash次数。
继续看Hash的实现者AbstractHash:
整个过程就是根据源source和salt和hashIterations(hash次数),算出一个新的byte数组。
再来看下是如何生成新数组的:
看到这里就明白了,MessageDigest 是jdk自带的java.security包中的工具,用于对数据进行加密。可以使用不同的加密算法,举个简单的例子,如用md5进行加密。md5是对一个任意的byte数组进行加密变成固定长度的128位,即16个字节。然后这16个字节的展现有多种形式,这就与md5本身没关系了。展现形式如:把加密后的128位即16个字节进行Hex.encodeToString操作,即每个字节转换成两个字符(高四位一个字符,低四位一个字符)。到这个网址http://www.cmd5.com/中去输入字符串"lg",得到的md5("lg",32)的结果为 a608b9c44912c72db6855ad555397470,下面我们就来做出此结果
md5.reset()表示要清空要加密的源数据。digest(byte[])表示将该数据填充到源数据中,然后加密。
md5算出结果byte[] ret后,我们选择的展现形式是Hex.encodeToString(ret)即转换成16进制字符表示。这里的Hex就是借用shiro的Hex。结果如下:
和上面的结果一样,也就是说该网址对md5加密后的结果也是采用转换成16进制字符的展现形式。该网址的md5(lg,16) = 4912c72db6855ad5 则是取自上述结果的中间字符。
简单介绍完md5后,继续回到AbstractHash的hash方法中,就变得很简单。digest.update(salt)方法就是向源数据中继续添加要加密的数据,digest.digest(hashed)内部调用了update方法即先填充数据,然后执行加密过程。
所以这里的过程为:
第一轮: salt和bytes作为源数据加密得到hashed byte数组
第二轮:如果传递进来的hashIterations hash次数大于1的话,要对上述结果继续进行加密
得到最终的加密结果。
AbstractHash对子类留了一个抽象方法public abstract String getAlgorithmName(),用于获取加密算法名称。然而此类被标记为过时,推荐使用它的子类SimpleHash,不过上述原理仍然没有变,不再详细去说,可以自己去查看,Hash终于解释完了,总结一下,就是根据源字节数组、算法、salt、hash次数得到一个加密的byte数组。
回到CredentialsMatcher的实现类PasswordMatcher中,在该类中,对服务器端存储的密码形式分成了两类,一类是String,另一类就是Hash,Hash中包含了加密采用的算法、salt、hash次数等信息。 PasswordMatcher中的PasswordService 来完成匹配过程。我们就可以试想匹配过程:若服务器端存储的密码为Hash a,则我们就能知道加密过程所采用的算法、salt、hash次数信息,然后对原密码进行这样的加密,算出一个Hash b,然后比较a b的byte数组是否一致,这只是推想,然后来看下实际内容:
PasswordService 接口图如下:
HashingPasswordService:继承了PasswordService ,加入了对Hash处理的功能
最终的实现类DefaultPasswordService:
首先还是先了解属性,三个重要属性HashService 、HashFormat、HashFormatFactory 。
HashService接口类图:
将一个HashRequest计算出一个Hash。什么是HashRequest?
就是我们上述所说的那几个重要元素。原密码、salt、hash次数、算法名称。这个计算过程也就是上述AbstractHash的过程。
再看HashService 的子类ConfigurableHashService:
就是可以对上述几个重要元素进行设置。privateSalt和RandomNumberGenerator接下来再说,再看ConfigurableHashService的实现类DefaultHashService:
来看下它是怎么实现将HashRequest变成Hash的:
第一步:获取算法,先获取request本身的算法,如果没有,则使用DefaultHashService 默认的算法,在DefaultHashService 的构造函数中默认使用SHA-512的加密算法。同理对于hash次数也是同样的逻辑。
第二步:获取publicSalt
当HashRequest request本身有salt时,则充当publicSalt直接返回。当没有时,则需要去使用RandomNumberGenerator产生一个publicSalt,当DefaultHashService 的privateSalt 存在或者DefaultHashService 的generatePublicSalt标志为true,都会去产生publicSalt。
第三步:结合publicSalt和privateSalt
第四步:Hash computed = new SimpleHash(algorithmName, source, salt, iterations)这就就是上文我们强调的加密核心,不再说明了,可以到上面去找。
第五步:仅仅暴漏Hash computed中的某些属性,不把privateSalt 暴漏出去。至此DefaultHashService 的工作就全部完成了。
继续回到DefaultPasswordService:看下一个类HashFormat:
这个就是对Hash进行格式化输出而已,看下接口设计:
HexFormat如下
就是调用Hash本身的toHex方法,同理Hash本身也有String toBase64()方法,所以Base64Format也是同样的道理。
ModularCryptFormat和ParsableHashFormat 如下
他们的实现类Shiro1CryptFormat,来看看是如何format的和如何parse的:
format就是将一些算法信息、hash次数、salt等进行字符串的拼接,parse过程则是根据拼接的信息逆向获取算法信息、hash次数、salt等信息而已。这里就终于明白了,为什么PasswordMatcher 对服务器端存储的密码分成Hash和String来处理了,他们都是存储算法、salt、hash次数等信息的地方,Hash直接是以结构化的类来存储,而String则是以格式化的字符串来存储,需要parse才能获取算法、salt等信息。
HashFormat则也完成了。DefaultPasswordService还剩最后一个HashFormatFactory了,它则是用来生成不同的HashFormat的。
根据String密码(格式化过的)来寻找对应的HashFormat。这里不再详细介绍了,有兴趣的可以自己去研究。
回到我们关注的重点,密码匹配过程:DefaultPasswordService
使用了,DefaultHashService 和Shiro1CryptFormat和DefaultHashFormatFactory。
先来看看是如何匹配加密密码是String的,后面再看看是如何匹配Hash的
分成了两个分支,第一个分支就是能将加密的String密码使用HashFormat解析成Hash,然后调用public boolean passwordsMatch(Object plaintext, Hash saved)即Hash的匹配方式,第二个分支就是,不能解析的情况下,把原始密码封装成HashRequest ,然后使用HashService来讲HashRequest计算出一个Hash,再用HashFormat来格式化它变成String字符串,两个字符串进行equals比较。
对于Hash的匹配方式:
这个过程就是我们之前设想的过程,就是很据已由的Hash saved的算法、salt、hash次数对Object plaintext进行同样的加密过程,然后匹配saved.equals(computed)的信息是否一致。至此我们就走通了PasswordMatcher的整个过程。这是CredentialsMatcher的第二个分支,我们继续看CredentialsMatcher的第三个分支SimpleCredentialsMatcher:
它的实现比较简单,就是直接比较AuthenticationToken的getCredentials() 和AuthenticationInfo 的getCredentials()内容,若为ByteSource则匹配下具体的内容,否则直接匹配引用。
看下它的子类HashedCredentialsMatcher的匹配过程:
其中equals方法仍然是调用父类的方法,即一旦为ByteSource则进行byte匹配,否则进行引用匹配。只是这里的tokenHashedCredentials 和accountCredentials 和父类的方式不一样,如下:
可以看到仍然是使用算法名称和credentials(用户提交的未加密的)、salt、hash次数构建一个SimpleHash(构造时进行加密)。
再看对于已加密的credentials则是也构建一个SimpleHash,但是不再进行加密过程:
对于HashedCredentialsMatcher也就是说AuthenticationToken token, AuthenticationInfo info都去构建一个SimpleHash,前者构建时执行加密过程,后者(已加密)不需要去执行加密过程,然后匹配这两个SimpleHash是否一致。然后就是HashedCredentialsMatcher的子类(全部被标记为已废弃),如Md5CredentialsMatcher:
仅仅是将HashedCredentialsMatcher的算法改为md5,所以Md5CredentialsMatcher 本身就没有存在的价值。HashedCredentialsMatcher其他子类都是同样的道理。
至此CredentialsMatcher的三个分支都完成了。
已经很长了,下一篇文章以具体的案例来使用上述原理。
作者:乒乓狂魔
public interface CredentialsMatcher { boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info); }
根据用户名获取AuthenticationInfo ,然后就需要将用户提交的AuthenticationToken和AuthenticationInfo 进行匹配。
AuthenticatingRealm从第三篇文章知道是用来进行认证流程的,它有一个属性CredentialsMatcher credentialsMatcher,使用如下:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { //在这里进行认证密码匹配 assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; } protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { //not successful - throw an exception to indicate this: String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + "credentials during authentication. If you do not wish for credentials to be examined, you " + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }
以上我们知道了CredentialsMatcher所处的认证的位置及作用,下面就要详细看看具体的匹配过程,还是接口设计图:
对于上图的三个分支,一个一个来说。
对于AllowAllCredentialsMatcher:
public class AllowAllCredentialsMatcher implements CredentialsMatcher { public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { return true; } }
都返回true,这意味着,只要该用户名存在即可,不用去验证密码是否匹配。
对于PasswordMatcher:
public class PasswordMatcher implements CredentialsMatcher { private PasswordService passwordService; public PasswordMatcher() { this.passwordService = new DefaultPasswordService(); } public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //确保有PasswordService,若没有抛异常 PasswordService service = ensurePasswordService(); //获取提交的密码 Object submittedPassword = getSubmittedPassword(token); //获取服务器端存储的密码 Object storedCredentials = getStoredPassword(info); //服务器端存储的密码必须是String或者Hash类型(待会详细介绍什么是Hash),见该方法 assertStoredCredentialsType(storedCredentials); //对服务器端存储的密码分成两类来处理,一类是String,另一类是Hash if (storedCredentials instanceof Hash) { Hash hashedPassword = (Hash)storedCredentials; HashingPasswordService hashingService = assertHashingPasswordService(service); return hashingService.passwordsMatch(submittedPassword, hashedPassword); } //otherwise they are a String (asserted in the 'assertStoredCredentialsType' method call above): String formatted = (String)storedCredentials; return passwordService.passwordsMatch(submittedPassword, formatted); } private void assertStoredCredentialsType(Object credentials) { if (credentials instanceof String || credentials instanceof Hash) { return; } String msg = "Stored account credentials are expected to be either a " + Hash.class.getName() + " instance or a formatted hash String."; throw new IllegalArgumentException(msg); } }
内部使用一个PasswordService 来完成匹配。从上面的匹配过程中,我们了解到了,对于服务器端存储的密码分成String和Hash两种,然后由PasswordService 来分别处理。所以PasswordMatcher 也只是完成了一个流程工作,具体的内容要到PasswordService 来看。
到底什么是Hash呢?
先看下接口图:
看下ByteSource:
public interface ByteSource { byte[] getBytes(); String toHex(); String toBase64(); //略 }
就维护了一个byte[]数组。
看下SimpleByteSource的实现:
public class SimpleByteSource implements ByteSource { private final byte[] bytes; private String cachedHex; private String cachedBase64; public SimpleByteSource(byte[] bytes) { this.bytes = bytes; } public String toHex() { if ( this.cachedHex == null ) { this.cachedHex = Hex.encodeToString(getBytes()); } return this.cachedHex; } public String toBase64() { if ( this.cachedBase64 == null ) { this.cachedBase64 = Base64.encodeToString(getBytes()); } return this.cachedBase64; } //略 }
toHex就是将byte数组准换成16进制形式的字符串。toBase64就是将byte数组进行base64编码。
Hex.encodeToString(getBytes()) 详情如下:
public class Hex { /** * Used to build output as Hex */ private static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; public static String encodeToString(byte[] bytes) { char[] encodedChars = encode(bytes); return new String(encodedChars); } public static char[] encode(byte[] data) { int l = data.length; char[] out = new char[l << 1]; // two characters form the hex value. for (int i = 0, j = 0; i < l; i++) { out[j++] = DIGITS[(0xF0 & data[i]) >>> 4]; out[j++] = DIGITS[0x0F & data[i]]; } return out; } //略 }
对于一个byte[] data数组,byte含有8位,(0xF0 & data[i]) >>> 4 表示取其高四位的值。如
当data[i]=01001111时,0xF0 & data[i]则为01000000,然后右移四位则变成00000100即为值4,所以DIGITS[(0xF0 & data[i])=DIGITS[4]=4,同理data[i]的低四位变成f。最终的结果为一个byte 01001111变成两个char 4f。
Base64.encodeToString(getBytes()):就稍微比较麻烦,这里不再详细说明。原理的话可以到网上搜下,有很多这样的文章。还是回到ByteSource的接口图,该轮到Hash了。
public interface Hash extends ByteSource { String getAlgorithmName(); ByteSource getSalt(); int getIterations(); }
多添加了三个属性,算法名、盐值、hash次数。
继续看Hash的实现者AbstractHash:
public AbstractHash(Object source, Object salt, int hashIterations) throws CodecException { byte[] sourceBytes = toBytes(source); byte[] saltBytes = null; if (salt != null) { saltBytes = toBytes(salt); } byte[] hashedBytes = hash(sourceBytes, saltBytes, hashIterations); setBytes(hashedBytes); }
整个过程就是根据源source和salt和hashIterations(hash次数),算出一个新的byte数组。
再来看下是如何生成新数组的:
protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws UnknownAlgorithmException { MessageDigest digest = getDigest(getAlgorithmName()); if (salt != null) { digest.reset(); digest.update(salt); } byte[] hashed = digest.digest(bytes); int iterations = hashIterations - 1; //already hashed once above //iterate remaining number: for (int i = 0; i < iterations; i++) { digest.reset(); hashed = digest.digest(hashed); } return hashed; }
看到这里就明白了,MessageDigest 是jdk自带的java.security包中的工具,用于对数据进行加密。可以使用不同的加密算法,举个简单的例子,如用md5进行加密。md5是对一个任意的byte数组进行加密变成固定长度的128位,即16个字节。然后这16个字节的展现有多种形式,这就与md5本身没关系了。展现形式如:把加密后的128位即16个字节进行Hex.encodeToString操作,即每个字节转换成两个字符(高四位一个字符,低四位一个字符)。到这个网址http://www.cmd5.com/中去输入字符串"lg",得到的md5("lg",32)的结果为 a608b9c44912c72db6855ad555397470,下面我们就来做出此结果
public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException{ MessageDigest md5=MessageDigest.getInstance("MD5"); String str="lg"; md5.reset(); byte[] ret=md5.digest(str.getBytes("UTF-8")); System.out.println(Hex.encodeToString(ret)); }
md5.reset()表示要清空要加密的源数据。digest(byte[])表示将该数据填充到源数据中,然后加密。
md5算出结果byte[] ret后,我们选择的展现形式是Hex.encodeToString(ret)即转换成16进制字符表示。这里的Hex就是借用shiro的Hex。结果如下:
a608b9c44912c72db6855ad555397470
和上面的结果一样,也就是说该网址对md5加密后的结果也是采用转换成16进制字符的展现形式。该网址的md5(lg,16) = 4912c72db6855ad5 则是取自上述结果的中间字符。
简单介绍完md5后,继续回到AbstractHash的hash方法中,就变得很简单。digest.update(salt)方法就是向源数据中继续添加要加密的数据,digest.digest(hashed)内部调用了update方法即先填充数据,然后执行加密过程。
所以这里的过程为:
第一轮: salt和bytes作为源数据加密得到hashed byte数组
第二轮:如果传递进来的hashIterations hash次数大于1的话,要对上述结果继续进行加密
得到最终的加密结果。
AbstractHash对子类留了一个抽象方法public abstract String getAlgorithmName(),用于获取加密算法名称。然而此类被标记为过时,推荐使用它的子类SimpleHash,不过上述原理仍然没有变,不再详细去说,可以自己去查看,Hash终于解释完了,总结一下,就是根据源字节数组、算法、salt、hash次数得到一个加密的byte数组。
回到CredentialsMatcher的实现类PasswordMatcher中,在该类中,对服务器端存储的密码形式分成了两类,一类是String,另一类就是Hash,Hash中包含了加密采用的算法、salt、hash次数等信息。 PasswordMatcher中的PasswordService 来完成匹配过程。我们就可以试想匹配过程:若服务器端存储的密码为Hash a,则我们就能知道加密过程所采用的算法、salt、hash次数信息,然后对原密码进行这样的加密,算出一个Hash b,然后比较a b的byte数组是否一致,这只是推想,然后来看下实际内容:
PasswordService 接口图如下:
public interface PasswordService { String encryptPassword(Object plaintextPassword) throws IllegalArgumentException; boolean passwordsMatch(Object submittedPlaintext, String encrypted); }
HashingPasswordService:继承了PasswordService ,加入了对Hash处理的功能
public interface HashingPasswordService extends PasswordService { //根据服务器端存储的Hash的采用的算法、salt、hash次数和原始密码得到一个进过相同加密过程的Hash Hash hashPassword(Object plaintext) throws IllegalArgumentException; boolean passwordsMatch(Object plaintext, Hash savedPasswordHash); }
最终的实现类DefaultPasswordService:
public class DefaultPasswordService implements HashingPasswordService { public static final String DEFAULT_HASH_ALGORITHM = "SHA-256"; public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000 private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class); private HashService hashService; private HashFormat hashFormat; private HashFormatFactory hashFormatFactory; private volatile boolean hashFormatWarned; //used to avoid excessive log noise public DefaultPasswordService() { this.hashFormatWarned = false; DefaultHashService hashService = new DefaultHashService(); hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM); hashService.setHashIterations(DEFAULT_HASH_ITERATIONS); hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure this.hashService = hashService; this.hashFormat = new Shiro1CryptFormat(); this.hashFormatFactory = new DefaultHashFormatFactory(); } //略 }
首先还是先了解属性,三个重要属性HashService 、HashFormat、HashFormatFactory 。
HashService接口类图:
public interface HashService { Hash computeHash(HashRequest request); }
将一个HashRequest计算出一个Hash。什么是HashRequest?
public interface HashRequest { ByteSource getSource(); ByteSource getSalt(); int getIterations(); String getAlgorithmName(); //略 }
就是我们上述所说的那几个重要元素。原密码、salt、hash次数、算法名称。这个计算过程也就是上述AbstractHash的过程。
再看HashService 的子类ConfigurableHashService:
public interface ConfigurableHashService extends HashService { void setPrivateSalt(ByteSource privateSalt); void setHashIterations(int iterations); void setHashAlgorithmName(String name); void setRandomNumberGenerator(RandomNumberGenerator rng); }
就是可以对上述几个重要元素进行设置。privateSalt和RandomNumberGenerator接下来再说,再看ConfigurableHashService的实现类DefaultHashService:
public class DefaultHashService implements ConfigurableHashService { //主要是用来生成随机的publicSalt private RandomNumberGenerator rng; private String algorithmName; private ByteSource privateSalt; private int iterations; //标志是否去产生publicSalt private boolean generatePublicSalt; public DefaultHashService() { this.algorithmName = "SHA-512"; this.iterations = 1; this.generatePublicSalt = false; this.rng = new SecureRandomNumberGenerator(); } }
来看下它是怎么实现将HashRequest变成Hash的:
public Hash computeHash(HashRequest request) { if (request == null || request.getSource() == null || request.getSource().isEmpty()) { return null; } //获取算法名字 String algorithmName = getAlgorithmName(request); //获取原密码 ByteSource source = request.getSource(); //获取hash次数 int iterations = getIterations(request); //获取publicSalt ByteSource publicSalt = getPublicSalt(request); //获取privateSalt ByteSource privateSalt = getPrivateSalt(); //结合两者 ByteSource salt = combine(privateSalt, publicSalt); //这就是之前始终强调的原理部分,就是根据算法、原始数据、salt、hash次数进行加密 Hash computed = new SimpleHash(algorithmName, source, salt, iterations); //对于computed 有很多信息,只想对外暴漏某些信息。如publicSalt SimpleHash result = new SimpleHash(algorithmName); result.setBytes(computed.getBytes()); result.setIterations(iterations); //Only expose the public salt - not the real/combined salt that might have been used: result.setSalt(publicSalt); return result; }
第一步:获取算法,先获取request本身的算法,如果没有,则使用DefaultHashService 默认的算法,在DefaultHashService 的构造函数中默认使用SHA-512的加密算法。同理对于hash次数也是同样的逻辑。
第二步:获取publicSalt
protected ByteSource getPublicSalt(HashRequest request) { ByteSource publicSalt = request.getSalt(); if (publicSalt != null && !publicSalt.isEmpty()) { //a public salt was explicitly requested to be used - go ahead and use it: return publicSalt; } publicSalt = null; //check to see if we need to generate one: ByteSource privateSalt = getPrivateSalt(); boolean privateSaltExists = privateSalt != null && !privateSalt.isEmpty(); //If a private salt exists, we must generate a public salt to protect the integrity of the private salt. //Or generate it if the instance is explicitly configured to do so: if (privateSaltExists || isGeneratePublicSalt()) { publicSalt = getRandomNumberGenerator().nextBytes(); } return publicSalt; }
当HashRequest request本身有salt时,则充当publicSalt直接返回。当没有时,则需要去使用RandomNumberGenerator产生一个publicSalt,当DefaultHashService 的privateSalt 存在或者DefaultHashService 的generatePublicSalt标志为true,都会去产生publicSalt。
第三步:结合publicSalt和privateSalt
第四步:Hash computed = new SimpleHash(algorithmName, source, salt, iterations)这就就是上文我们强调的加密核心,不再说明了,可以到上面去找。
第五步:仅仅暴漏Hash computed中的某些属性,不把privateSalt 暴漏出去。至此DefaultHashService 的工作就全部完成了。
继续回到DefaultPasswordService:看下一个类HashFormat:
public interface HashFormat { String format(Hash hash); }
这个就是对Hash进行格式化输出而已,看下接口设计:
HexFormat如下
public class HexFormat implements HashFormat { public String format(Hash hash) { return hash != null ? hash.toHex() : null; } }
就是调用Hash本身的toHex方法,同理Hash本身也有String toBase64()方法,所以Base64Format也是同样的道理。
ModularCryptFormat和ParsableHashFormat 如下
public interface ModularCryptFormat extends HashFormat { public static final String TOKEN_DELIMITER = "$"; String getId(); } public interface ParsableHashFormat extends HashFormat { Hash parse(String formatted); }
他们的实现类Shiro1CryptFormat,来看看是如何format的和如何parse的:
public String format(Hash hash) { if (hash == null) { return null; } String algorithmName = hash.getAlgorithmName(); ByteSource salt = hash.getSalt(); int iterations = hash.getIterations(); StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterations).append(TOKEN_DELIMITER); if (salt != null) { sb.append(salt.toBase64()); } sb.append(TOKEN_DELIMITER); sb.append(hash.toBase64()); return sb.toString(); }
format就是将一些算法信息、hash次数、salt等进行字符串的拼接,parse过程则是根据拼接的信息逆向获取算法信息、hash次数、salt等信息而已。这里就终于明白了,为什么PasswordMatcher 对服务器端存储的密码分成Hash和String来处理了,他们都是存储算法、salt、hash次数等信息的地方,Hash直接是以结构化的类来存储,而String则是以格式化的字符串来存储,需要parse才能获取算法、salt等信息。
HashFormat则也完成了。DefaultPasswordService还剩最后一个HashFormatFactory了,它则是用来生成不同的HashFormat的。
public interface HashFormatFactory { HashFormat getInstance(String token); }
根据String密码(格式化过的)来寻找对应的HashFormat。这里不再详细介绍了,有兴趣的可以自己去研究。
回到我们关注的重点,密码匹配过程:DefaultPasswordService
public DefaultPasswordService() { this.hashFormatWarned = false; DefaultHashService hashService = new DefaultHashService(); hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM); hashService.setHashIterations(DEFAULT_HASH_ITERATIONS); hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure this.hashService = hashService; this.hashFormat = new Shiro1CryptFormat(); this.hashFormatFactory = new DefaultHashFormatFactory(); }
使用了,DefaultHashService 和Shiro1CryptFormat和DefaultHashFormatFactory。
先来看看是如何匹配加密密码是String的,后面再看看是如何匹配Hash的
public boolean passwordsMatch(Object submittedPlaintext, String saved) { ByteSource plaintextBytes = createByteSource(submittedPlaintext); if (saved == null || saved.length() == 0) { return plaintextBytes == null || plaintextBytes.isEmpty(); } else { if (plaintextBytes == null || plaintextBytes.isEmpty()) { return false; } } //First check to see if we can reconstitute the original hash - this allows us to //perform password hash comparisons even for previously saved passwords that don't //match the current HashService configuration values. This is a very nice feature //for password comparisons because it ensures backwards compatibility even after //configuration changes. HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved); if (discoveredFormat != null && discoveredFormat instanceof ParsableHashFormat) { ParsableHashFormat parsableHashFormat = (ParsableHashFormat)discoveredFormat; Hash savedHash = parsableHashFormat.parse(saved); return passwordsMatch(submittedPlaintext, savedHash); } //If we're at this point in the method's execution, We couldn't reconstitute the original hash. //So, we need to hash the submittedPlaintext using current HashService configuration and then //compare the formatted output with the saved string. This will correctly compare passwords, //but does not allow changing the HashService configuration without breaking previously saved //passwords: //The saved text value can't be reconstituted into a Hash instance. We need to format the //submittedPlaintext and then compare this formatted value with the saved value: HashRequest request = createHashRequest(plaintextBytes); Hash computed = this.hashService.computeHash(request); String formatted = this.hashFormat.format(computed); return saved.equals(formatted); }
分成了两个分支,第一个分支就是能将加密的String密码使用HashFormat解析成Hash,然后调用public boolean passwordsMatch(Object plaintext, Hash saved)即Hash的匹配方式,第二个分支就是,不能解析的情况下,把原始密码封装成HashRequest ,然后使用HashService来讲HashRequest计算出一个Hash,再用HashFormat来格式化它变成String字符串,两个字符串进行equals比较。
对于Hash的匹配方式:
public boolean passwordsMatch(Object plaintext, Hash saved) { ByteSource plaintextBytes = createByteSource(plaintext); if (saved == null || saved.isEmpty()) { return plaintextBytes == null || plaintextBytes.isEmpty(); } else { if (plaintextBytes == null || plaintextBytes.isEmpty()) { return false; } } HashRequest request = buildHashRequest(plaintextBytes, saved); Hash computed = this.hashService.computeHash(request); return saved.equals(computed); } protected HashRequest buildHashRequest(ByteSource plaintext, Hash saved) { //keep everything from the saved hash except for the source: return new HashRequest.Builder().setSource(plaintext) //now use the existing saved data: .setAlgorithmName(saved.getAlgorithmName()) .setSalt(saved.getSalt()) .setIterations(saved.getIterations()) .build(); }
这个过程就是我们之前设想的过程,就是很据已由的Hash saved的算法、salt、hash次数对Object plaintext进行同样的加密过程,然后匹配saved.equals(computed)的信息是否一致。至此我们就走通了PasswordMatcher的整个过程。这是CredentialsMatcher的第二个分支,我们继续看CredentialsMatcher的第三个分支SimpleCredentialsMatcher:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenCredentials = getCredentials(token); Object accountCredentials = getCredentials(info); return equals(tokenCredentials, accountCredentials); } protected Object getCredentials(AuthenticationToken token) { return token.getCredentials(); } protected Object getCredentials(AuthenticationInfo info) { return info.getCredentials(); } protected boolean equals(Object tokenCredentials, Object accountCredentials) { if (log.isDebugEnabled()) { log.debug("Performing credentials equality check for tokenCredentials of type [" + tokenCredentials.getClass().getName() + " and accountCredentials of type [" + accountCredentials.getClass().getName() + "]"); } if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) { if (log.isDebugEnabled()) { log.debug("Both credentials arguments can be easily converted to byte arrays. Performing " + "array equals comparison"); } byte[] tokenBytes = toBytes(tokenCredentials); byte[] accountBytes = toBytes(accountCredentials); return Arrays.equals(tokenBytes, accountBytes); } else { return accountCredentials.equals(tokenCredentials); } }
它的实现比较简单,就是直接比较AuthenticationToken的getCredentials() 和AuthenticationInfo 的getCredentials()内容,若为ByteSource则匹配下具体的内容,否则直接匹配引用。
看下它的子类HashedCredentialsMatcher的匹配过程:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
其中equals方法仍然是调用父类的方法,即一旦为ByteSource则进行byte匹配,否则进行引用匹配。只是这里的tokenHashedCredentials 和accountCredentials 和父类的方式不一样,如下:
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) { Object salt = null; if (info instanceof SaltedAuthenticationInfo) { salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt(); } else { //retain 1.0 backwards compatibility: if (isHashSalted()) { salt = getSalt(token); } } return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations()); } protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) { String hashAlgorithmName = assertHashAlgorithmName(); return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); }
可以看到仍然是使用算法名称和credentials(用户提交的未加密的)、salt、hash次数构建一个SimpleHash(构造时进行加密)。
再看对于已加密的credentials则是也构建一个SimpleHash,但是不再进行加密过程:
protected Object getCredentials(AuthenticationInfo info) { Object credentials = info.getCredentials(); byte[] storedBytes = toBytes(credentials); if (credentials instanceof String || credentials instanceof char[]) { //account.credentials were a char[] or String, so //we need to do text decoding first: if (isStoredCredentialsHexEncoded()) { storedBytes = Hex.decode(storedBytes); } else { storedBytes = Base64.decode(storedBytes); } } AbstractHash hash = newHashInstance(); hash.setBytes(storedBytes); return hash; } protected AbstractHash newHashInstance() { String hashAlgorithmName = assertHashAlgorithmName(); return new SimpleHash(hashAlgorithmName); }
对于HashedCredentialsMatcher也就是说AuthenticationToken token, AuthenticationInfo info都去构建一个SimpleHash,前者构建时执行加密过程,后者(已加密)不需要去执行加密过程,然后匹配这两个SimpleHash是否一致。然后就是HashedCredentialsMatcher的子类(全部被标记为已废弃),如Md5CredentialsMatcher:
public class Md5CredentialsMatcher extends HashedCredentialsMatcher { public Md5CredentialsMatcher() { super(); setHashAlgorithmName(Md5Hash.ALGORITHM_NAME); } }
仅仅是将HashedCredentialsMatcher的算法改为md5,所以Md5CredentialsMatcher 本身就没有存在的价值。HashedCredentialsMatcher其他子类都是同样的道理。
至此CredentialsMatcher的三个分支都完成了。
已经很长了,下一篇文章以具体的案例来使用上述原理。
作者:乒乓狂魔
发表评论
-
shiro源码分析(十一)SecurityManager
2015-01-16 05:59 0shiro源码分析(十一)SecurityManager -
shiro源码分析(九)RememberMe分析
2015-01-04 08:20 0shiro源码分析(八)RememberMe分析shiro源码 ... -
shiro源码分析(七)对称式加密解密
2015-01-01 10:29 0shiro源码分析(七)对称式加密解密shiro源码分析(七) ... -
shiro源码分析(八)ini配置文件的解析原理
2014-12-30 06:17 0shiro源码分析(七)ini配置文件的解析shiro源码分析 ... -
shiro源码分析(六)CredentialsMatcher 的案例分析
2015-01-04 07:39 7733有了上一篇文章的原理分析,这一篇文章主要结合原理来进行使用。 ... -
shiro源码分析(四)具体的Realm
2014-12-24 07:28 5257首先还是Realm的接口设计图: 这里只来说明Simple ... -
shiro源码分析(三)授权、认证、缓存的接口设计
2014-12-19 07:46 7196前两篇文章主要说的是认证过程,这一篇来分析下授权的过程。还是开 ... -
Subject Runas模块
2014-12-17 07:05 0Subject Runas模块Subject Runas模块S ... -
shiro源码分析(二)Subject和Session
2014-12-15 08:02 16790继续上一篇文章的案例 ... -
2 MapContext分析
2014-12-11 07:13 02 MapContext分析 -
1 AuthenticationInfo 是如何进行合并的?
2014-12-11 07:12 01 AuthenticationInfo 是如何进行合并的? -
shiro源码分析(一)入门
2014-12-11 07:21 13901最近闲来无事,准备读 ...
相关推荐
在"Shiro源码分析(六)CredentialsMatcher 的案例分析"这篇博文中,作者深入剖析了Shiro中用于验证用户凭证的`CredentialsMatcher`组件。下面我们将详细探讨这一核心机制及其相关知识点。 1. **CredentialsMatcher...
Shiro源码分析将帮助我们理解其工作原理和核心组件。 首先,Shiro的设计理念可以概括为:简单易用且功能强大。Shiro具有三个核心概念:Subject、SecurityManager和Realms。 1. Subject代表了当前与软件交互的用户...
在学习Shiro时,源码分析是一个深入了解其内部工作原理的有效方式。本文将继续上一篇文章的案例,深入分析Shiro的Subject和Session机制,以及SecurityManager在创建Subject时所扮演的角色和过程。 首先,Shiro中的...
在这个"我的shiro源码.zip"压缩包中,包含的是一个整合了Spring(SSM框架的一部分)、Spring MVC和Mybatis的Shiro源代码示例。这个示例项目对于学习如何在实际开发中应用Shiro非常有帮助,因为源代码中有详细的注解...
在“shiro源码分析(四)具体的Realm”这一主题中,我们将深入探讨Shiro的核心组件——Realm,它是Shiro与应用程序安全数据源交互的桥梁。 Realm在Shiro中的地位至关重要,它负责验证用户身份并执行授权操作。 ...
Shiro的认证过程主要由Subject、Realms和CredentialsMatcher组成。Subject是Shiro的中心概念,代表当前用户或系统中的任何实体。Realms是Shiro与应用安全数据源(如数据库、LDAP等)的桥梁,负责验证凭证。...
- **源码分析**:通过阅读Shiro的源码,可以理解其内部的工作机制,包括如何进行身份验证、授权以及会话管理,有助于定制化开发和性能优化。 - **设计模式**:Shiro的源码中大量运用了设计模式,如工厂模式、代理...
Spring Boot学习之Shiro源码【学习狂神说,自己手动书写,可以实现正常所需的功能】 Spring Boot学习之Shiro源码【学习狂神说,自己手动书写,可以实现正常所需的功能】 Spring Boot学习之Shiro源码【学习狂神说,...
**SSH整合Shiro源码详解** 在Web应用开发中,安全性是至关重要的。SSH(Spring、Struts2、Hibernate)和Apache Shiro都是常见的Java安全框架。SSH是用于构建MVC架构的开源框架,而Shiro则专注于身份验证、授权和...
在深入学习 Shiro 源码时,你可以关注以下几个方面: 1. **架构设计**:理解Shiro的MVC设计模式,以及Subject、Realms、Caches等关键概念。 2. **认证流程**:研究`AuthenticationToken`、`AuthenticationInfo`和`...
shiro 视频、源码及课件。 shiro 视频、源码及课件。 shiro 视频、源码及课件。
通过对 SpringMVC 和 Shiro 源码的学习,我们可以深入了解这两个框架的内部机制,提高我们的代码质量和安全性。这不仅有助于优化现有项目,还为未来可能遇到的安全问题提供了解决方案。学习源码可以帮助我们理解框架...
2. **案例分析**:通过具体的场景,演示如何使用Shiro实现登录、权限控制等功能。 3. **最佳实践**:提供在实际项目中使用Shiro时的一些最佳实践和注意事项,帮助避免常见问题。 4. **实战教程**:可能包含一系列...
通过对 Shiro1.2.2 的源码分析,我们可以了解到它如何处理各种安全问题,以及如何构建高效、灵活的安全架构。同时,源码的学习有助于我们理解和解决在实际开发中遇到的安全问题,提升我们的编程技能。
视频讲授过程中通过分析源代码使学员知其然更知其所以然。 【课程内容】 第一章 问候 Shiro 他大爷 1.Shiro 简介 2.ShiroHelloWorld 实现 第二章 身份认证 1.Subject 认证主体 2.身份认证流程 第三章 权限认证...
通过阅读文档和分析源码,我们可以了解Shiro如何与JSP和数据库协作,如何实现用户认证、授权,以及如何定制Shiro以适应特定项目需求。对于Java Web开发者来说,掌握这些知识能提升应用的安全性和用户体验。
"跟我学Shiro源码"这个主题旨在深入理解Shiro的工作原理,通过分析源码来提升我们对安全编程的认知。 1. **Shiro架构** Shiro 的核心组件包括 Realm(认证与授权信息源)、SecurityManager(安全管理器)、Subject...
在深入研究Shiro的源码之前,我们先了解一下它的主要组件和核心概念。 1. **认证**:Shiro 的认证过程是验证用户身份的过程。它通过`Subject`接口实现,`Subject`代表了当前“操作”的主体,可能是用户、系统进程或...
目前无漏洞版本shiro1.9.1源码+jar
通过研究 Shiro 1.13.0 的源码,你可以了解到这些组件的实现细节,例如 Realm 如何连接数据库进行身份验证,Filter 如何拦截请求进行权限检查,以及会话管理如何处理分布式环境下的会话一致性问题。这对于深入理解 ...