加密算法

数据加密的基本过程就是对原来为明文的文件或数据按某种加密算法进行处理,使其成为一段不可读的代码,通常称为“密文”,通过这种途径来达到保护原始数据的目的。通过解密方法或秘钥,经过解密过程,可以将密文还原成可读的原文。

MD5加密

MD5消息摘要算法(Message-Digest Algorithm 5)是一种被广泛使用的密码散列函数算法图示

缺陷

MD5算法仅是摘要,在计算过程中原文的部分信息是丢失了的,所以碰撞是肯定的
通过公开的hash算法和已知的hash值,凑出一串字符串,让这个凑出来的假字符串的hash值与真字符串的hash值相同,称为碰撞攻击

  • 2009年,中国科学院的谢涛和冯登国仅用了2^20.96的碰撞算法复杂度,破解了MD5的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟(How To Find Weak Input Differences For MD5 Collision Attacks)。
  • 2011年,RFC 6151 禁止MD5用作密钥散列消息认证码。

以下两张图片的md5加密结果都是 253dd04e87492e4fc3471de5e776bc3d

加盐

如果只是单纯直接使用md5加密密码,黑客可以使用彩虹表将用户密码复原,但是我们可以加盐(salt)来提高破解的难度。

  • 注册
    • 用户输入用户名 admin 和密码 123456
    • 后台随机生成一段盐 ABCDEFGHIJK,也可以不随机生成,使用hash值或用户名或时间戳都可
    • 拼接到一起得到加盐后的值 123456ABCDEFGHIJK (拼接位置不一定是在后面)
    • MD5(123456ABCDEFGHIJK)(e37c5e2d82bc5fdceca6f1788a5cec8e) 作为密码值,和盐 ABCDEFGHIJK 以及用户名存储到数据库中
  • 登录
    • 用户输入用户名 admin 和密码 123456
    • 从数据库中通过用户名 admin 获取存储的密码值(e37c5e2d82bc5fdceca6f1788a5cec8e)和盐 ABCDEFGHIJK
    • 判断MD5(123456ABCDEFGHIJK)和数据库中是否相等

更暴力的保护密码方法:将MD5迭代次数提高 MD5(MD5(MD5(123456)))
但是,迭代次数是一把双刃剑。迭代次数上一个数量级,延迟也会再上一个数量级。

SHA加密

SHA安全散列算法(Secure Hash Algorithm)是一个密码散列函数家族,由美国国家安全局(NSA)所设计,并由美国国家标准与技术研究院(NIST)发布,是美国的政府标准。

HMAC加密

HMAC散列消息认证码(Hash-based message authentication code)是一种安全的基于加密hash函数和共享密钥的消息认证协议.它可以有效地防止数据在传输过程中被截获和篡改,维护了数据的完整性、可靠性和安全性。

BCrypt加密

Bcrypt验证方式和其它加密方式不同,不是直接解密得到明文,也不是二次加密比较密文,而是把明文和存储的密文一块运算得到另一个密文,如果这两个密文相同则验证成功。

1
2
3
4
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
\__/\/ \____________________/\_____________________________/
Alg Cost Salt Hash
版本 次数 盐 哈希值

Bcrypt 有两个特点

  • 每一次 HASH 出来的值不一样
  • 计算非常缓慢
    因此使用 Bcrypt 的代价是应用自身的性能也会受到影响,不过登录行为频率不高,因此能够忍受。

Java中使用

如果引入了 Spring Security, BCryptPasswordEncoder 提供了相关的方法。

1
2
3
4
5
6
7
8
9
10
String password = "12345";
BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
//加密,每次生成的结果都是不一致的
String e1 = bcrypt.encode(password);
System.out.println(e1); //$2a$10$e5zrk4V1cd1j.bMeOJxyjOebjsbD1UsdK5PVTZldm5LZQNMPXdOou
String e2 = bcrypt.encode(password);
System.out.println(e2); //$2a$10$pklZ/Iks9q8diB38qWk/FubQ9UKUojPowgqngq6svS5Dzqt6mt9JG
//都能够验证成功
System.out.println(bcrypt.matches(password,e1)); //true
System.out.println(bcrypt.matches(password,e2)); //true

顺带一提,Spring Security会直接接管你的站点。如果你只是想测试Spring Security的BCrypt功能,可以使用这个配置类允许匿名访问

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}

JWT

JWT(Json web token), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT官网在线测试

JWT由三部分组成,两个 . 分割

头部 Header 载荷 Payload 签名 Signature
eyJhbGciOiJIUzI1NiJ9 eyJqdGkiOiIxIiwibmFtZSI6ImNvZGVyeGkifQ 0VOiBp2WAyY-YoKbQS12_i_BvMJG77CUbcp0H-WRJEs
Base64后的
{
 "typ": "JWT",
 "alg": "HS256"
}
Base64后的
{
  "jti": "1",
  "name": "coderxi"
}
使用Header中的加密方式, secret 作为盐
再对前两部分拼接成的字符串加密
HS256(
  Base64(header) + "." +
  Base64(payload),
  secret
)
alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT(或不写)

更多常用的头部字段
JWT 规定了7个标准声明字段供选用(不强制,除了官方字段,你还可以在这个部分定义私有字段)。
iss issuer 签发人
exp expiration time 过期时间
sub subject 主题
aud audience 受众
nbf Not Before 生效时间
iat Issued At 签发时间
jti JWT ID 编号
secret 就是用来进行jwt的签发和jwt的验证,也就是服务端的密钥,在任何场景都不应该流露出去。

Java中使用

生成jwt

1
2
3
4
5
6
7
8
9
10
//假设登录获取到了此对象
User user = new User()
.setId(1)
.setUsername("coderxi");
//存入jwt中
JwtBuilder jwtBuilder = Jwts.builder()
.claim("user",user)
.signWith(SignatureAlgorithm.HS256,"my-secret");
//得到token值
String token = jwtBuilder.compact();

解析jwt

1
2
3
4
5
6
7
8
9
10
//其实一般服务端只需要判断是否可以解析成功(parseClaimsJws时不抛异常)即可
Claims claims = Jwts.parser().setSigningKey("my-secret").parseClaimsJws(token).getBody();

//想要获取数据可以使用下面的方式
//实际得到的是一个LinkedHashMap类型的结果
Object userAsMap = claims.get("user");
//转换成原来的实体类
User user2 = new ObjectMapper().convertValue(userAsMap, User.class);
//输出结果
System.out.println(user2);