how2j.cn

下载区
文件名 文件大小
shiro.rar 1m
步骤 1 : md5 加密   
步骤 2 : 盐   
步骤 3 : 数据库调整   
步骤 4 : 先运行,看到效果,再学习   
步骤 5 : 模仿和排错   
步骤 6 : DAO   
步骤 7 : DatabaseRealm   
步骤 8 : TestShiro   
步骤 9 : 表中数据   
步骤 10 : 另一个做法的DatabaseRealm   
步骤 11 : shiro.ini   
步骤 12 : TestShiro   

在前面的例子里,用户密码是明文的,这样是有巨大风险的,一旦泄露,就不好了。
所以,通常都会采用非对称加密,什么是非对称呢?就是不可逆的,而 md5 就是这样一个算法.
如代码所示 123 用 md5 加密后,得到字符串: 202CB962AC59075B964B07152D234B70
这个字符串,却无法通过计算,反过来得到源密码是 123.
这个加密后的字符串就存在数据库里了,下次用户再登陆,输入密码 123, 同样用md5 加密后,再和这个字符串一比较,就知道密码是否正确了。
如此这样,既能保证用户密码校验的功能,又能保证不暴露密码。
md5 加密
package com.how2java; import org.apache.shiro.crypto.hash.Md5Hash; public class TestEncryption { public static void main(String[] args) { String password = "123"; String encodedPassword = new Md5Hash(password).toString(); System.out.println(encodedPassword); } }
package com.how2java;

import org.apache.shiro.crypto.hash.Md5Hash;

public class TestEncryption {

	public static void main(String[] args) {
		String password = "123";
		String encodedPassword = new Md5Hash(password).toString();
		
		System.out.println(encodedPassword);
	}
}
上面讲了md5加密,但是md5加密又有一些缺陷:
1. 如果我的密码是 123,你的也是 123, 那么md5的值是一样的,那么通过比较加密后的字符串,我就可以反推过来,原来你的密码也是123.
2. 与上述相同,虽然 md5 不可逆,但是我可以穷举法呀,我把特别常用的100万或者更多个密码的 md5 值记录下来,比如12345的,abcde的。 相当一部分人用的密码也是这些,那么只要到数据库里一找,也很快就可以知道原密码是多少了。这样看上去也就破解了,至少一部分没有想象中那么安全吧。
为了解决这个问题,引入了盐的概念。 盐是什么意思呢? 比如炒菜,直接使用md5,就是对食材(源密码)进行炒菜,因为食材是一样的,所以炒出来的味道都一样,可是如果加了不同分量的盐,那么即便食材一样,炒出来的味道也就不一样了。

所以,虽然每次 123 md5 之后都是202CB962AC59075B964B07152D234B70,但是 我加上盐,即 123+随机数,那么md5值不就不一样了吗? 这个随机数,就是盐,而这个随机数也会在数据库里保存下来,每个不同的用户,随机数也是不一样的。
再就是加密次数,加密一次是202CB962AC59075B964B07152D234B70,我可以加密两次呀,就是另一个数了。 而黑客即便是拿到了加密后的密码,如果不知道到底加密了多少次,也是很难办的。

在代码里就演示了,如何用Shiro自带的工具类,做生成盐,两次md5的做法,如图所示得到一串机密都很高的密文。
盐
package com.how2java; import org.apache.shiro.crypto.SecureRandomNumberGenerator; import org.apache.shiro.crypto.hash.SimpleHash; public class TestEncryption { public static void main(String[] args) { String password = "123"; String salt = new SecureRandomNumberGenerator().nextBytes().toString(); int times = 2; String algorithmName = "md5"; String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString(); System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodedPassword); } }
package com.how2java;

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;

public class TestEncryption {

	public static void main(String[] args) {
		String password = "123";
		String salt = new SecureRandomNumberGenerator().nextBytes().toString();
		int times = 2;
		String algorithmName = "md5";
		
		String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString();
		
		System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodedPassword);
		
	}
}
有了以上基础,那么就可以开始在原来的教程里加入对加密的支持了。 在开始之前,要修改一下user表,加上盐 字段: salt。
因盐是随机数,得保留下来,如果不知道盐巴是多少,我们也就没法判断密码是否正确了
alter table user add (salt varchar(100) )
alter table user add (salt varchar(100) )
步骤 4 :

先运行,看到效果,再学习

edit
老规矩,先下载右上角的可运行项目,配置运行起来,确认可用之后,再学习做了哪些步骤以达到这样的效果。
运行 TestShiro 进行账号密码验证测试。 注意,测试之前要先释放注释,以执行一次注册用户行为,如果不注册,后面肯定是不能验证通过的,因为数据库里没有嘛。

new DAO().createUser("tom", "123");
先运行,看到效果,再学习
//这里要释放注释,先注册一个用户 // new DAO().createUser("tom", "123"); User user = new User(); user.setName("tom"); user.setPassword("123"); if(login(user)) System.out.println("登录成功"); else System.out.println("登录失败");
    	//这里要释放注释,先注册一个用户
//    	new DAO().createUser("tom", "123");
        
        User user = new User();
        user.setName("tom");
        user.setPassword("123");

        if(login(user)) 
        	System.out.println("登录成功");
        else
        	System.out.println("登录失败");
在确保可运行项目能够正确无误地运行之后,再严格照着教程的步骤,对代码模仿一遍。
模仿过程难免代码有出入,导致无法得到期望的运行结果,此时此刻通过比较正确答案 ( 可运行项目 ) 和自己的代码,来定位问题所在。
采用这种方式,学习有效果,排错有效率,可以较为明显地提升学习速度,跨过学习路上的各个槛。

推荐使用diffmerge软件,进行文件夹比较。把你自己做的项目文件夹,和我的可运行项目文件夹进行比较。
这个软件很牛逼的,可以知道文件夹里哪两个文件不对,并且很明显地标记出来
这里提供了绿色安装和使用教程:diffmerge 下载和使用教程
增加两个方法 createUser,getUser
createUser 用于注册,并且在注册的时候,将用户提交的密码加密
getUser 用于取出用户信息,其中不仅仅包括加密后的密码,还包括盐
package com.how2java; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashSet; import java.util.Set; import org.apache.shiro.crypto.SecureRandomNumberGenerator; import org.apache.shiro.crypto.hash.SimpleHash; public class DAO { public DAO() { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public Connection getConnection() throws SQLException { return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root", "admin"); } public String createUser(String name, String password) { String sql = "insert into user values(null,?,?,?)"; String salt = new SecureRandomNumberGenerator().nextBytes().toString(); //盐量随机 String encodedPassword= new SimpleHash("md5",password,salt,2).toString(); try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, name); ps.setString(2, encodedPassword); ps.setString(3, salt); ps.execute(); } catch (SQLException e) { e.printStackTrace(); } return null; } public String getPassword(String userName) { String sql = "select password from user where name = ?"; try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); if (rs.next()) return rs.getString("password"); } catch (SQLException e) { e.printStackTrace(); } return null; } public User getUser(String userName) { User user = null; String sql = "select * from user where name = ?"; try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); if (rs.next()) { user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); user.setSalt(rs.getString("salt")); } } catch (SQLException e) { e.printStackTrace(); } return user; } public Set<String> listRoles(String userName) { Set<String> roles = new HashSet<>(); String sql = "select r.name from user u " + "left join user_role ur on u.id = ur.uid " + "left join Role r on r.id = ur.rid " + "where u.name = ?"; try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); while (rs.next()) { roles.add(rs.getString(1)); } } catch (SQLException e) { e.printStackTrace(); } return roles; } public Set<String> listPermissions(String userName) { Set<String> permissions = new HashSet<>(); String sql = "select p.name from user u "+ "left join user_role ru on u.id = ru.uid "+ "left join role r on r.id = ru.rid "+ "left join role_permission rp on r.id = rp.rid "+ "left join permission p on p.id = rp.pid "+ "where u.name =?"; try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); while (rs.next()) { permissions.add(rs.getString(1)); } } catch (SQLException e) { e.printStackTrace(); } return permissions; } }
修改 DatabaseRealm,把用户通过 UsernamePasswordToken 传进来的密码,以及数据库里取出来的 salt 进行加密,加密之后再与数据库里的密文进行比较,判断用户是否能够通过验证。
package com.how2java; import java.util.Set; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; public class DatabaseRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //能进入到这里,表示账号已经通过验证了 String userName =(String) principalCollection.getPrimaryPrincipal(); //通过DAO获取角色和权限 Set<String> permissions = new DAO().listPermissions(userName); Set<String> roles = new DAO().listRoles(userName); //授权对象 SimpleAuthorizationInfo s = new SimpleAuthorizationInfo(); //把通过DAO获取到的角色和权限放进去 s.setStringPermissions(permissions); s.setRoles(roles); return s; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取账号密码 UsernamePasswordToken t = (UsernamePasswordToken) token; String userName= token.getPrincipal().toString(); String password =new String(t.getPassword()); //获取数据库中的密码 User user = new DAO().getUser(userName); String passwordInDB = user.getPassword(); String salt = user.getSalt(); String passwordEncoded = new SimpleHash("md5",password,salt,2).toString(); if(null==user || !passwordEncoded.equals(passwordInDB)) throw new AuthenticationException(); //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName()); return a; } }
进行账号密码验证测试。 注意,测试之前要先释放注释,以执行一次注册用户行为,如果不注册,后面肯定是不能验证通过的,因为数据库里没有嘛。

new DAO().createUser("tom", "123");
TestShiro
package com.how2java; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; public class TestShiro { public static void main(String[] args) { //这里要释放注释,先注册一个用户 // new DAO().createUser("tom", "123"); User user = new User(); user.setName("tom"); user.setPassword("123"); if(login(user)) System.out.println("登录成功"); else System.out.println("登录失败"); } private static boolean hasRole(User user, String role) { Subject subject = getSubject(user); return subject.hasRole(role); } private static boolean isPermitted(User user, String permit) { Subject subject = getSubject(user); return subject.isPermitted(permit); } private static Subject getSubject(User user) { //加载配置文件,并获取工厂 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //获取安全管理者实例 SecurityManager sm = factory.getInstance(); //将安全管理者放入全局对象 SecurityUtils.setSecurityManager(sm); //全局对象通过安全管理者生成Subject对象 Subject subject = SecurityUtils.getSubject(); return subject; } private static boolean login(User user) { Subject subject= getSubject(user); //如果已经登录过了,退出 if(subject.isAuthenticated()) subject.logout(); //封装用户的数据 UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword()); try { //将用户的数据token 最终传递到Realm中进行对比 subject.login(token); } catch (AuthenticationException e) { //验证错误 return false; } return subject.isAuthenticated(); } }
观察表中的数据,可以发现新增加的用户 他的盐值是 KjJR57qg08XWwcqBLHzOgQ==, 而加密后的密文是 dba57276a413ac826bf473e08eb1c650。

你们运行的效果,和我肯定是不一样的,因为盐值是随机数,我们的盐是不一样的。
表中数据
步骤 10 :

另一个做法的DatabaseRealm

edit
DatabaseRealm 中的做法是自己计算加密后的秘文,再自己比较。 另一个做法是使用Shiro提供的 HashedCredentialsMatcher 帮我们做。
在创建 SimpleAuthenticationInfo 的时候,把数据库中取出来的密文以及盐作为参数传递进去。

SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,passwordInDB,ByteSource.Util.bytes(salt),getName());

对比 DatabaseRealm 中的做法,传递的是加密之前的密码,也没有传盐

SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName());

这是第一步,下一步就是 修改 shiro.ini
package com.how2java; import java.util.Set; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; public class DatabaseRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //能进入到这里,表示账号已经通过验证了 String userName =(String) principalCollection.getPrimaryPrincipal(); //通过DAO获取角色和权限 Set<String> permissions = new DAO().listPermissions(userName); Set<String> roles = new DAO().listRoles(userName); //授权对象 SimpleAuthorizationInfo s = new SimpleAuthorizationInfo(); //把通过DAO获取到的角色和权限放进去 s.setStringPermissions(permissions); s.setRoles(roles); return s; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println(this.getCredentialsMatcher()); //获取账号密码 UsernamePasswordToken t = (UsernamePasswordToken) token; String userName= token.getPrincipal().toString(); //获取数据库中的密码 User user = new DAO().getUser(userName); String passwordInDB = user.getPassword(); String salt = user.getSalt(); //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm //盐也放进去 //这样通过shiro.ini里配置的 HashedCredentialsMatcher 进行自动校验 SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,passwordInDB,ByteSource.Util.bytes(salt),getName()); return a; } }
另一个做法的DatabaseRealm 中只是提供了密文和盐,那么具体算法怎么指定呢? 那么就
修改shiro.ini:
为DatabaseRealm 指定credentialsMatcher,其中就指定了算法是 md5, 次数为2, storedCredentialsHexEncoded 这个表示计算之后以密文为16进制。

这样Shiro就拿着在subject.login() 时传入的UsernamePasswordToken 中的源密码, 数据库里的密文和盐巴,以及配置文件里指定的算法参数,自己去进行相关匹配了。
[main] credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher credentialsMatcher.hashAlgorithmName=md5 credentialsMatcher.hashIterations=2 credentialsMatcher.storedCredentialsHexEncoded=true databaseRealm=com.how2java.DatabaseRealm databaseRealm.credentialsMatcher=$credentialsMatcher securityManager.realms=$databaseRealm
[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true

databaseRealm=com.how2java.DatabaseRealm
databaseRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$databaseRealm
TestShiro 没有任何变化,一样运行,效果也是一样的
运行 TestShiro 进行账号密码验证测试。 注意,测试之前要先释放注释,以执行一次注册用户行为,如果不注册,后面肯定是不能验证通过的,因为数据库里没有嘛。

new DAO().createUser("tom", "123");
TestShiro
//这里要释放注释,先注册一个用户 // new DAO().createUser("tom", "123"); User user = new User(); user.setName("tom"); user.setPassword("123"); if(login(user)) System.out.println("登录成功"); else System.out.println("登录失败");
        //这里要释放注释,先注册一个用户
//      new DAO().createUser("tom", "123");
         
        User user = new User();
        user.setName("tom");
        user.setPassword("123");
 
        if(login(user))
            System.out.println("登录成功");
        else
            System.out.println("登录失败");


HOW2J公众号,关注后实时获知最新的教程和优惠活动,谢谢。


问答区域    
2021-08-20 登陆失败踩坑
白芷233




最新版的大家都知道getSubject那边有废弃类,所以换了获取方法,于是就登录失败了。
	private static Subject getSubject(User user) {
		// 加载配置文件
		DefaultSecurityManager securityManager = new DefaultSecurityManager();
		IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
		securityManager.setRealm(iniRealm);

		// 将安全管理者放入全局对象
		SecurityUtils.setSecurityManager(securityManager);
		// 全局对象通过安全管理者生成Subject对象
		return SecurityUtils.getSubject();
	}

用这个就不行,用旧代码就可以

							


1 个答案

otakuhuang
答案时间:2021-10-12



回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到





2021-04-20 非对称加密!!!
摸爬滚打

非对称加密 !!! 不是不可逆,而是加密和解密用的 密钥 不同,不可逆的不叫加密,MD5:Message-Digest Algorithm - 信息摘要算法,是一种散列算法




2 个答案

FancyXie
答案时间:2022-03-14
你是对的,站长这里搞错了

javalx
答案时间:2021-10-26
迷惑发言



回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到




2020-12-24 盐应该放代码里面的,不应该放数据库里面
2020-09-08 登录失败可能是shiro.ini文件的问题,把页面上shiro.ini的内容复制粘贴到自己项目上的shiro.ini,然后保存关掉shiro.ini,再删掉数据库的数据,重新运行就可以了
2020-06-14 我想问注册用户怎么做啊


提问太多,页面渲染太慢,为了加快渲染速度,本页最多只显示几条提问。还有 15 条以前的提问,请 点击查看

提问之前请登陆
提问已经提交成功,正在审核。 请于 我的提问 处查看提问记录,谢谢
关于 工具和中间件-Shiro-加密 的提问

尽量提供截图代码异常信息,有助于分析和解决问题。 也可进本站QQ群交流: 578362961
提问尽量提供完整的代码,环境描述,越是有利于问题的重现,您的问题越能更快得到解答。
对教程中代码有疑问,请提供是哪个步骤,哪一行有疑问,这样便于快速定位问题,提高问题得到解答的速度
在已经存在的几千个提问里,有相当大的比例,是因为使用了和站长不同版本的开发环境导致的,比如 jdk, eclpise, idea, mysql,tomcat 等等软件的版本不一致。
请使用和站长一样的版本,可以节约自己大量的学习时间。 站长把教学中用的软件版本整理了,都统一放在了这里, 方便大家下载: https://how2j.cn/k/helloworld/helloworld-version/1718.html

上传截图