前言
上一篇博客地址:shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)。源码分析(敲重点)
上个博客主要是了自定义 Realm,里面的用户认证所使用的密码都是明文,这种方式是不可取的往往我们在实战开发中用户的密码都是以密文形势进行存储,并且要求加密算法是不可逆的,著名的加密算法有MD5、SHA1等。所以这节课我们主要介绍 Shiro 安全框架学习它的加密方案。这里我们采用md5的方式加密,然后呢。md5加密又分为加盐,和不加盐。
一、shiro不加盐加密认证
1. 逻辑流程
与上个博客大体一致。
- 构建安全管理器环境
DefaultWebSecurityManager
- 构建数据源,这里使用自定义的数据源
CustomRealmCh07
,模拟的数据代替从数据库获取数据。 - 构建
HashedCredentialsMatcher
,进行设置加密方式和加密次数。这里是对下面第五步中用户输入的密码进行加密。
- 获取主体
- 用户名和密码生成token(这里也就是 用户输入的用户名密码 )
- 进行登入(提交认证)
- 然后进入自定义数据源的
doGetAuthenticationInfo()
进行认证。(源码看上一个博客的最后一节)
认证过程中需要根据用户名获取密码,然后在对密码进行加密,将加密后的密码、用户名、类名称 交给SimpleAuthenticationInfo
类,去认证。
2. 修改部分
Ch07_testMatcher.java 中修改部分
CustomRealmCh07 添加方法
添加方法
/**
* 获得密文密码
* 不加盐
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param currentPassword
* @return java.lang.String
* @throws
*/
private String getPasswordMatcher(String currentPassword){
return new Md5Hash(currentPassword, null,2).toString();
}
doGetAuthenticationInfo()方法中修改
3. 自定义数据源:CustomRealmCh07
编写类 com.feng.shiro.CustomRealmCh07
package com.feng.shiro;
import org.apache.shiro.SecurityUtils;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomRealmCh07 extends AuthorizingRealm {
/**
* 模拟数据库中的用户名和密码
*/
private Map<String, String> userMap =new HashMap<>();
/**
* 使用代码块初始化数据
*/
{
userMap.put("admin", "123456");
userMap.put("test","123456");
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("---------->开始执行 CustomRealm.doGetAuthorizationInfo 方法啦");
// SecurityUtils.getSubject().getPrincipal()用户名: 就是SimpleAuthenticationInfo(username,password,getName()); 第一个参数
String name= (String) SecurityUtils.getSubject().getPrincipal();
System.out.println("---------->(String)SecurityUtils.getSubject().getPrincipal():"+name);
String username = (String) principalCollection.getPrimaryPrincipal();
System.out.println("---------->(String) principalCollection.getPrimaryPrincipal():"+username);
//从数据库或者缓存中获取角色数据
List<String> roles = getRolesByUsername(username);
//从数据库或者缓存中获取权限数据
List<String> permissions = getPerminssionsByUsername(username);
//创建AuthorizationInfo,并设置角色和权限信息
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addStringPermissions(permissions);
authorizationInfo.addRoles(roles);
return authorizationInfo;
}
/**
* 认证
* 逻辑:获取用户输入的用户名。密码不用获取,已经在 shiro 中(密码是 HashedCredentialsMatcher 进行 md5 加密的)
* 根据用户名获取数据库的密码,然后进行MD5加密。
* 然后将 用户名、加密后密码、类名称 交给shiro去认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
/*
* AuthenticationToken 是 UsernamePasswordToken 顶级父类,getPrincipal() 和 getCredentials() 是其接口的两个方法,
* 被 UsernamePasswordToken实现重新,分别是 获取用户名和密码。
* 这里也就是获取用户输入的用户名和密码
* */
System.out.println("---------->开始执行 CustomRealm.doGetAuthenticationInfo 方法啦");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
System.out.println("---------->根据authenticationToken获取到的用户名:"+username);
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
System.out.println("---------->从数据库中获取的密码:"+password);
if (password == null) {
return null;
}
// 不加盐加密
// 获取密文密码,对从数据库获取的密码进行加密(然后与用户输入的加密后的进行比较,这个应该交给shiro去做)
String matcherPwd=getPasswordMatcher(password);
System.out.println("---------->获取到的密文密码:" + matcherPwd);
// 传入用户名、密码、name 。流转到下面去认证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username, matcherPwd, getName());
return authenticationInfo;
}
/**
* 模拟通过数据库获取权限数据
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.util.List<java.lang.String>
* @throws
*/
private List<String> getPerminssionsByUsername(String username) {
List<String> permissions = new ArrayList<>();
/**
* 只有是 admin 用户才有 新增、删除、编辑权限
* 用户只有 遍历 权限
*/
if(username.equals("admin")){
permissions.add("user:delete");
permissions.add("user:add");
permissions.add("user:edit");
}
permissions.add("user:list");
return permissions;
}
/**
* 模拟通过数据库获取用户角色信息
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.util.List<java.lang.String>
* @throws
*/
private List<String> getRolesByUsername(String username) {
List<String> roles = new ArrayList<>();
if(username.equals("admin")){
roles.add("admin");
}
roles.add("test");
return roles;
}
/**
* 通过用户名查询密码,模拟数据库查询
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.lang.String
* @throws
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
/**
* 获得密文密码
* 不加盐
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param currentPassword
* @return java.lang.String
* @throws
*/
private String getPasswordMatcher(String currentPassword){
return new Md5Hash(currentPassword, null,2).toString();
}
}
4. 编写:Ch07_testMatcher
package com.feng;
import com.feng.shiro.CustomRealmCh07;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch07_testMatcher {
/**
* SpringBoot整合 shiro 之 不加盐 加密认证
*
* 不加盐认证:: 设置加密次数 2
* 1、使用 类:HashedCredentialsMatcher,并设置加密算法和加密次数,
* 2、自定义数据源 在设置 此类HashedCredentialsMatcher
*
* 3、然后在
*/
@Test
public void testMatcher() {
// 1. 构建安全管理器环境
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2. 构建数据源
CustomRealmCh07 customRealmCh07 = new CustomRealmCh07();
// 3. 初始化 md5 加密类
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");
//设置加密次数
matcher.setHashIterations(2);
// 对数据源设置加密
customRealmCh07.setCredentialsMatcher(matcher);
// 设置数据源
securityManager.setRealm(customRealmCh07);
// 设置 安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取主体
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
System.out.println("从UsernamePasswordToken 获取的用户名和密码:"+usernamePasswordToken.getUsername()+"/"+usernamePasswordToken.getPassword());
System.out.println("获取用户名和密码的 token:"+usernamePasswordToken.toString());
try {
// 2.主体提交认证
subject.login(usernamePasswordToken);
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
// 检查是否有角色
subject.checkRoles("admin");
System.out.println("---------->有 admin 角色");
// 检查是够有权限
subject.checkPermissions("user:delete", "user:edit", "user:list", "user:add");
System.out.println("---------->有 user:delete, user:edit, user:list, user:add 权限");
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
} catch (UnauthorizedException ae) {
System.out.println("用户没有权限");
}
}
}
5. 测试
- 当用户 admin 登录进来的时候 程序输出
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
程序输出
- 假如改成还是用明文检验
程序输出
二、shiro加盐加密认证
当两个用户的密码相同时,单纯使用不加盐的MD5加密方式,会发现数据库中存在相同结构的密码,这样也是不安全的。我们希望即便是两个人的原始密码一样,加密后的结果也不一样。如何做到呢?其实就好像炒菜一样,两道一样的鱼香肉丝,加的盐不一样,炒出来的味道就不一样。MD5加密也是一样,需要进行盐值加密。
1. 流程逻辑
与上面的案例相同,仅有稍微几处有所改变。
2. 修改部分
- 修改加密方法
getPasswordMatcher()
- 修改
doGetAuthenticationInfo()
方法
3. 自定义数据源:CustomRealmCh08
编写类 com.feng.shiro.CustomRealmCh08
package com.feng.shiro;
import org.apache.shiro.SecurityUtils;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomRealmCh08 extends AuthorizingRealm {
/**
* 模拟数据库中的用户名和密码
*/
private Map<String, String> userMap =new HashMap<>();
/**
* 使用代码块初始化数据
*/
{
userMap.put("admin", "123456");
userMap.put("test","123456");
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("---------->开始执行 CustomRealm.doGetAuthorizationInfo 方法啦");
// SecurityUtils.getSubject().getPrincipal()用户名: 就是SimpleAuthenticationInfo(username,password,getName()); 第一个参数
String name= (String) SecurityUtils.getSubject().getPrincipal();
System.out.println("---------->(String)SecurityUtils.getSubject().getPrincipal():"+name);
String username = (String) principalCollection.getPrimaryPrincipal();
System.out.println("---------->(String) principalCollection.getPrimaryPrincipal():"+username);
//从数据库或者缓存中获取角色数据
List<String> roles = getRolesByUsername(username);
//从数据库或者缓存中获取权限数据
List<String> permissions = getPerminssionsByUsername(username);
//创建AuthorizationInfo,并设置角色和权限信息
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addStringPermissions(permissions);
authorizationInfo.addRoles(roles);
return authorizationInfo;
}
/**
* 认证
* 逻辑:获取用户输入的用户名。密码不用获取,已经在 shiro 中(密码是 HashedCredentialsMatcher 进行 md5 加密的)
* 根据用户名获取数据库的密码,然后进行MD5加密。
* 然后将 用户名、加密后密码、类名称 交给shiro去认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
/*
* AuthenticationToken 是 UsernamePasswordToken 顶级父类,getPrincipal() 和 getCredentials() 是其接口的两个方法,
* 被 UsernamePasswordToken实现重新,分别是 获取用户名和密码。
* 这里也就是获取用户输入的用户名和密码
* */
System.out.println("---------->开始执行 CustomRealm.doGetAuthenticationInfo 方法啦");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
System.out.println("---------->根据authenticationToken获取到的用户名:"+username);
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
System.out.println("---------->从数据库中获取的密码:"+password);
if (password == null) {
return null;
}
// 加盐加密
String passwordMatcher = getPasswordMatcher(password, username);
System.out.println("---------->获取到的密文密码:" + passwordMatcher);
// 传入用户名、密码、name 。流转到下面去认证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username, passwordMatcher,ByteSource.Util.bytes(username), getName());
// authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return authenticationInfo;
}
/**
* 模拟通过数据库获取权限数据
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.util.List<java.lang.String>
* @throws
*/
private List<String> getPerminssionsByUsername(String username) {
List<String> permissions = new ArrayList<>();
/**
* 只有是 admin 用户才有 新增、删除、编辑权限
* 用户只有 遍历 权限
*/
if(username.equals("admin")){
permissions.add("user:delete");
permissions.add("user:add");
permissions.add("user:edit");
}
permissions.add("user:list");
return permissions;
}
/**
* 模拟通过数据库获取用户角色信息
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.util.List<java.lang.String>
* @throws
*/
private List<String> getRolesByUsername(String username) {
List<String> roles = new ArrayList<>();
if(username.equals("admin")){
roles.add("admin");
}
roles.add("test");
return roles;
}
/**
* 通过用户名查询密码,模拟数据库查询
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.lang.String
* @throws
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
/**
* 获得密文密码
* 加盐(用户名)
*
* @param currentPassword
* @param username
* @return
*/
private String getPasswordMatcher(String currentPassword,String username){
return new Md5Hash(currentPassword, username).toString();
}
}
4. 编写:Ch08_testSaltMatcher
package com.feng;
import com.feng.shiro.CustomRealmCh08;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch08_testSaltMatcher {
/**
* SpringBoot整合 shiro 之 盐值加密认证
*
* 加盐认证: 设置加密次数 1
*
* 当两个用户的密码相同时,单纯使用不加盐的MD5加密方式,会发现数据库中存在相同结构的密码,这样也是不安全的。我们希望即
* 便是两个人的原始密码一样,加密后的结果也不一样。如何做到呢?其实就好像炒菜一样,两道一样的鱼香肉丝,加的盐不一样,炒
* 出来的味道就不一样。MD5加密也是一样,需要进行盐值加密。
*/
@Test
public void testSaltMatcher() {
// 1. 构建安全管理器环境
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2. 构建数据源
CustomRealmCh08 customRealmCh08 = new CustomRealmCh08();
// 对数据源进行 md5 加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");
//设置加密次数
matcher.setHashIterations(1);
customRealmCh08.setCredentialsMatcher(matcher);
// 设置数据源
securityManager.setRealm(customRealmCh08);
// 设置 安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
System.out.println("从UsernamePasswordToken 获取的用户名和密码:"+usernamePasswordToken.getUsername()+usernamePasswordToken.getPassword());
System.out.println("获取用户名和密码的 token:"+usernamePasswordToken.toString());
try {
// 2.主体提交认证
subject.login(usernamePasswordToken);
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
// 检查是否有角色
subject.checkRoles("admin");
System.out.println("---------->有 admin 角色");
// 检查是够有权限
subject.checkPermissions("user:delete", "user:edit", "user:list", "user:add");
System.out.println("---------->有 user:delete, user:edit, user:list, user:add 权限");
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
} catch (UnauthorizedException ae) {
System.out.println("用户没有权限");
}
}
}