md5 加密
在前面的例子里,用戶密碼是明文的,這樣是有巨大風險的,一旦泄露,就不好了。
所以,通常都會采用非對稱加密,什么是非對稱呢?就是不可逆的,而 md5 就是這樣一個算法.
如代碼所示 123 用 md5 加密后,得到字符串: 202CB962AC59075B964B07152D234B70
這個字符串,卻無法通過計算,反過來得到源密碼是 123.
這個加密后的字符串就存在數據庫里了,下次用戶再登陸,輸入密碼 123, 同樣用md5 加密后,再和這個字符串一比較,就知道密碼是否正確了。
如此這樣,既能保證用戶密碼校驗的功能,又能保證不暴露密碼。
1 package com.how2java; 2 3 import org.apache.shiro.crypto.hash.Md5Hash; 4 5 public class TestEncryption { 6 7 public static void main(String[] args) { 8 String password = "123"; 9 String encodedPassword = new Md5Hash(password).toString(); 10 11 System.out.println(encodedPassword); 12 } 13 }
這個大家都用過,沒啥好說的。
鹽
面講了md5加密,但是md5加密又有一些缺陷:
1. 如果我的密碼是 123,你的也是 123, 那么md5的值是一樣的,那么通過比較加密后的字符串,我就可以反推過來,原來你的密碼也是123.
2. 與上述相同,雖然 md5 不可逆,但是我可以窮舉法呀,我把特別常用的100萬或者更多個密碼的 md5 值記錄下來,比如12345的,abcde的。 相當一部分人用的密碼也是這些,那么只要到數據庫里一找,也很快就可以知道原密碼是多少了。這樣看上去也就破解了,至少一部分沒有想象中那么安全吧。
為了解決這個問題,引入了鹽的概念。 鹽是什么意思呢? 比如炒菜,直接使用md5,就是對食材(源密碼)進行炒菜,因為食材是一樣的,所以炒出來的味道都一樣,可是如果加了不同分量的鹽,那么即便食材一樣,炒出來的味道也就不一樣了。
所以,雖然每次 123 md5 之后都是202CB962AC59075B964B07152D234B70,但是 我加上鹽,即 123+隨機數,那么md5值不就不一樣了嗎? 這個隨機數,就是鹽,而這個隨機數也會在數據庫里保存下來,每個不同的用戶,隨機數也是不一樣的。
再就是加密次數,加密一次是202CB962AC59075B964B07152D234B70,我可以加密兩次呀,就是另一個數了。 而黑客即便是拿到了加密后的密碼,如果不知道到底加密了多少次,也是很難辦的。
在代碼里就演示了,如何用Shiro自帶的工具類,做生成鹽,兩次md5的做法,如圖所示得到一串機密都很高的密文。
1 package com.how2java; 2 3 import org.apache.shiro.crypto.SecureRandomNumberGenerator; 4 import org.apache.shiro.crypto.hash.SimpleHash; 5 6 public class TestEncryption { 7 8 public static void main(String[] args) { 9 String password = "123"; 10 String salt = new SecureRandomNumberGenerator().nextBytes().toString(); 11 int times = 2; 12 String algorithmName = "md5"; 13 14 String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString(); 15 16 System.out.printf("原始密碼是 %s , 鹽是: %s, 運算次數是: %d, 運算出來的密文是:%s ",password,salt,times,encodedPassword); 17 18 } 19 }
數據庫調整
因鹽是隨機數,得保留下來,如果不知道鹽巴是多少,我們也就沒法判斷密碼是否正確了。
1 alter table user add (salt varchar(100) )
User

1 package com.how2java; 2 3 public class User { 4 5 private int id; 6 private String name; 7 private String salt; 8 private String password; 9 public String getName() { 10 return name; 11 } 12 public void setName(String name) { 13 this.name = name; 14 } 15 public String getPassword() { 16 return password; 17 } 18 public void setPassword(String password) { 19 this.password = password; 20 } 21 public int getId() { 22 return id; 23 } 24 public void setId(int id) { 25 this.id = id; 26 } 27 public String getSalt() { 28 return salt; 29 } 30 public void setSalt(String salt) { 31 this.salt = salt; 32 } 33 34 35 }
DAO
增加兩個方法 createUser,getUser
createUser 用於注冊,並且在注冊的時候,將用戶提交的密碼加密
getUser 用於取出用戶信息,其中不僅僅包括加密后的密碼,還包括鹽

1 package com.how2java; 2 3 import java.sql.Connection; 4 import java.sql.DriverManager; 5 import java.sql.PreparedStatement; 6 import java.sql.ResultSet; 7 import java.sql.SQLException; 8 import java.util.HashSet; 9 import java.util.Set; 10 11 import org.apache.shiro.crypto.SecureRandomNumberGenerator; 12 import org.apache.shiro.crypto.hash.SimpleHash; 13 14 public class DAO { 15 public DAO() { 16 try { 17 Class.forName("com.mysql.jdbc.Driver"); 18 } catch (ClassNotFoundException e) { 19 e.printStackTrace(); 20 } 21 } 22 23 public Connection getConnection() throws SQLException { 24 return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root", 25 "admin"); 26 } 27 28 public String createUser(String name, String password) { 29 30 String sql = "insert into user values(null,?,?,?)"; 31 32 String salt = new SecureRandomNumberGenerator().nextBytes().toString(); //鹽量隨機 33 String encodedPassword= new SimpleHash("md5",password,salt,2).toString(); 34 35 try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { 36 37 ps.setString(1, name); 38 ps.setString(2, encodedPassword); 39 ps.setString(3, salt); 40 ps.execute(); 41 } catch (SQLException e) { 42 43 e.printStackTrace(); 44 } 45 return null; 46 47 } 48 49 public String getPassword(String userName) { 50 String sql = "select password from user where name = ?"; 51 try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { 52 53 ps.setString(1, userName); 54 55 ResultSet rs = ps.executeQuery(); 56 57 if (rs.next()) 58 return rs.getString("password"); 59 60 } catch (SQLException e) { 61 62 e.printStackTrace(); 63 } 64 return null; 65 } 66 public User getUser(String userName) { 67 User user = null; 68 String sql = "select * from user where name = ?"; 69 try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { 70 71 ps.setString(1, userName); 72 73 ResultSet rs = ps.executeQuery(); 74 75 if (rs.next()) { 76 user = new User(); 77 user.setId(rs.getInt("id")); 78 user.setName(rs.getString("name")); 79 user.setPassword(rs.getString("password")); 80 user.setSalt(rs.getString("salt")); 81 } 82 83 } catch (SQLException e) { 84 85 e.printStackTrace(); 86 } 87 return user; 88 } 89 90 public Set<String> listRoles(String userName) { 91 92 Set<String> roles = new HashSet<>(); 93 String sql = "select r.name from user u " 94 + "left join user_role ur on u.id = ur.uid " 95 + "left join Role r on r.id = ur.rid " 96 + "where u.name = ?"; 97 try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { 98 ps.setString(1, userName); 99 ResultSet rs = ps.executeQuery(); 100 101 while (rs.next()) { 102 roles.add(rs.getString(1)); 103 } 104 105 } catch (SQLException e) { 106 107 e.printStackTrace(); 108 } 109 return roles; 110 } 111 public Set<String> listPermissions(String userName) { 112 Set<String> permissions = new HashSet<>(); 113 String sql = 114 "select p.name from user u "+ 115 "left join user_role ru on u.id = ru.uid "+ 116 "left join role r on r.id = ru.rid "+ 117 "left join role_permission rp on r.id = rp.rid "+ 118 "left join permission p on p.id = rp.pid "+ 119 "where u.name =?"; 120 121 try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { 122 123 ps.setString(1, userName); 124 125 ResultSet rs = ps.executeQuery(); 126 127 while (rs.next()) { 128 permissions.add(rs.getString(1)); 129 } 130 131 } catch (SQLException e) { 132 133 e.printStackTrace(); 134 } 135 return permissions; 136 } 137 }
DatabaseRealm
修改 DatabaseRealm,把用戶通過 UsernamePasswordToken 傳進來的密碼,以及數據庫里取出來的 salt 進行加密,加密之后再與數據庫里的密文進行比較,判斷用戶是否能夠通過驗證。

1 package com.how2java; 2 3 import java.util.Set; 4 5 import org.apache.shiro.authc.AuthenticationException; 6 import org.apache.shiro.authc.AuthenticationInfo; 7 import org.apache.shiro.authc.AuthenticationToken; 8 import org.apache.shiro.authc.SimpleAuthenticationInfo; 9 import org.apache.shiro.authc.UsernamePasswordToken; 10 import org.apache.shiro.authz.AuthorizationInfo; 11 import org.apache.shiro.authz.SimpleAuthorizationInfo; 12 import org.apache.shiro.crypto.hash.SimpleHash; 13 import org.apache.shiro.realm.AuthorizingRealm; 14 import org.apache.shiro.subject.PrincipalCollection; 15 import org.apache.shiro.util.ByteSource; 16 17 public class DatabaseRealm extends AuthorizingRealm { 18 19 @Override 20 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 21 22 //能進入到這里,表示賬號已經通過驗證了 23 String userName =(String) principalCollection.getPrimaryPrincipal(); 24 //通過DAO獲取角色和權限 25 Set<String> permissions = new DAO().listPermissions(userName); 26 Set<String> roles = new DAO().listRoles(userName); 27 28 //授權對象 29 SimpleAuthorizationInfo s = new SimpleAuthorizationInfo(); 30 //把通過DAO獲取到的角色和權限放進去 31 s.setStringPermissions(permissions); 32 s.setRoles(roles); 33 return s; 34 } 35 36 @Override 37 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 38 //獲取賬號密碼 39 UsernamePasswordToken t = (UsernamePasswordToken) token; 40 String userName= token.getPrincipal().toString(); 41 String password =new String(t.getPassword()); 42 //獲取數據庫中的密碼 43 44 User user = new DAO().getUser(userName); 45 String passwordInDB = user.getPassword(); 46 String salt = user.getSalt(); 47 String passwordEncoded = new SimpleHash("md5",password,salt,2).toString(); 48 49 if(null==user || !passwordEncoded.equals(passwordInDB)) 50 throw new AuthenticationException(); 51 52 //認證信息里存放賬號密碼, getName() 是當前Realm的繼承方法,通常返回當前類名 :databaseRealm 53 SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName()); 54 return a; 55 } 56 57 }
shiro.ini
1 [main] 2 credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher 3 credentialsMatcher.hashAlgorithmName=md5 4 credentialsMatcher.hashIterations=2 5 credentialsMatcher.storedCredentialsHexEncoded=true 6 7 databaseRealm=com.how2java.DatabaseRealm 8 databaseRealm.credentialsMatcher=$credentialsMatcher 9 securityManager.realms=$databaseRealm
TestShiro
進行賬號密碼驗證測試。 注意,測試之前要先釋放注釋,以執行一次注冊用戶行為,如果不注冊,后面肯定是不能驗證通過的,因為數據庫里沒有嘛。
new DAO().createUser("tom", "123");

1 package com.how2java; 2 3 import org.apache.shiro.SecurityUtils; 4 import org.apache.shiro.authc.AuthenticationException; 5 import org.apache.shiro.authc.UsernamePasswordToken; 6 import org.apache.shiro.config.IniSecurityManagerFactory; 7 import org.apache.shiro.mgt.SecurityManager; 8 import org.apache.shiro.subject.Subject; 9 import org.apache.shiro.util.Factory; 10 11 public class TestShiro { 12 public static void main(String[] args) { 13 //這里要釋放注釋,先注冊一個用戶 14 // new DAO().createUser("tom", "123"); 15 16 User user = new User(); 17 user.setName("tom"); 18 user.setPassword("123"); 19 20 if(login(user)) 21 System.out.println("登錄成功"); 22 else 23 System.out.println("登錄失敗"); 24 25 } 26 27 private static boolean hasRole(User user, String role) { 28 Subject subject = getSubject(user); 29 return subject.hasRole(role); 30 } 31 32 private static boolean isPermitted(User user, String permit) { 33 Subject subject = getSubject(user); 34 return subject.isPermitted(permit); 35 } 36 37 private static Subject getSubject(User user) { 38 //加載配置文件,並獲取工廠 39 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); 40 //獲取安全管理者實例 41 SecurityManager sm = factory.getInstance(); 42 //將安全管理者放入全局對象 43 SecurityUtils.setSecurityManager(sm); 44 //全局對象通過安全管理者生成Subject對象 45 Subject subject = SecurityUtils.getSubject(); 46 47 return subject; 48 } 49 50 private static boolean login(User user) { 51 Subject subject= getSubject(user); 52 //如果已經登錄過了,退出 53 if(subject.isAuthenticated()) 54 subject.logout(); 55 56 //封裝用戶的數據 57 UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword()); 58 try { 59 //將用戶的數據token 最終傳遞到Realm中進行對比 60 subject.login(token); 61 } catch (AuthenticationException e) { 62 //驗證錯誤 63 return false; 64 } 65 66 return subject.isAuthenticated(); 67 } 68 69 }
最后
代碼下載地址:https://gitee.com/fengyuduke/my_open_resources/blob/master/shiro-md5.rar