四、Realm解析
4.1 Realm概述
Shiro 从 Realm 获取安全数据(如用户、角色、权限),即 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
4.1.1 两个概念
principal:主体的标识,可以有多个,但是需要具有唯一性,常见的有用户名,手机号,邮箱等。
credential:凭证,一般就是密码。
4.1.2 继承结构
Realm提供待验证数据的比对值,即安全数据源,可以理解为数据的源头,可以是数据库,文件等。Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
Realm接口:
public interface Realm {
String getName();//返回一个唯一的Realm名字
boolean supports(AuthenticationToken var1);//判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;//根据Token获取认证信息
}
CachingRealm抽象类:
CachingRealm提供了可缓存的Realm。
AuthenticatingRealm抽象类:
AuthenticatingRealm实现了Realm的全部3个接口方法,其中getAuthenticationInfo方法的实现如下:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 先获取缓存信息,通过CachingRealm中设置的CacheManager来实现的
AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
if (info == null) {
// 如果缓存信息为空,则执行本类中的doGetAuthenticationInfo方法,该方法是抽象方法,由实现类实现
info = this.doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
this.cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
通过该源码可知,AuthenticatingRealm实现了shiro的认证流程,也就是调用login方法后如果有缓存则返回缓存认证信息,如果没有缓存则调用doGetAuthenticationInfo方法,我们自定义Realm就需要实现该方法。
AuthorizingRealm抽象类:
AuthorizingRealm 其实现了Authorizer接口,Authorizer接口中定义了has*、check等权限认证的方法,AuthorizingRealm中实现了这些权限认证的方法,每个has,check*方法执行时都会执行如下getAuthorizationInfo方法。
4.2 IniRealm
主要是将数据存放到相应的 xxx.ini
即文件系统中,从文件中查找相应的数据是否存在。
4.2.1 ini文件
[users]
# 配置用户信息与角色
admin=123,root,user
tom=111,user
[roles]
# 配置角色和权限信息
root=emp:find,emp:add,emp:edit,emp:remove,dept:*
user=emp:find,dept:find
配置权限时可使用通配符 *
*
表示所有权限。dept:*
表示dept模块下的所有权限。
4.2.2 测试用例
public class IniRealmTest {
@Test
public void run(){
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//创建IniRealm对象,加载ini文件
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123");
subject.login(usernamePasswordToken);
System.out.println("身份认证结果:" + subject.isAuthenticated());
System.out.println("是否拥有root角色:" + subject.hasRole("root"));
System.out.println("是否拥有user角色:" + subject.hasRole("user"));
// 权限
System.out.println("是否拥有emp:find权限:" + subject.isPermitted("emp:find"));
System.out.println("是否拥有emp:add权限:" + subject.isPermitted("emp:add"));
System.out.println("是否拥有emp:edit权限:" + subject.isPermitted("emp:edit"));
System.out.println("是否拥有emp:remove权限:" + subject.isPermitted("emp:remove"));
System.out.println("是否拥有dept:find权限:" + subject.isPermitted("dept:find"));
System.out.println("是否拥有dept:add权限:" + subject.isPermitted("dept:add"));
System.out.println("是否拥有dept:edit权限:" + subject.isPermitted("dept:edit"));
System.out.println("是否拥有dept:remove权限:" + subject.isPermitted("dept:remove"));
subject.logout();
System.out.println("身份认证结果:" + subject.isAuthenticated());
}
}
4.3 JdbcRealm
JdbcRealm的方式访问数据库,通过与数据库的连接,验证相应的登录用户与授权。
4.3.1 默认的数据库SQL语句
users表:
CREATE TABLE `users` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`username` varchar(100),
`password` varchar(100),
`password_salt` varchar(100)
);
INSERT INTO `users` VALUES (1, 'admin', '123456', NULL);
INSERT INTO `users` VALUES (2, 'tom', '111111', NULL);
user_roles表:
CREATE TABLE `user_roles` (
`username` varchar(100),
`role_name` varchar(100),
PRIMARY KEY (`username`, `role_name`)
);
INSERT INTO `user_roles` VALUES ('admin', 'root');
INSERT INTO `user_roles` VALUES ('admin', 'user');
INSERT INTO `user_roles` VALUES ('tom', 'user');
roles_permissions表:
CREATE TABLE `roles_permissions` (
`role_name` varchar(100),
`permission` varchar(100),
PRIMARY KEY (`role_name`, `permission`)
);
INSERT INTO `roles_permissions` VALUES ('root', 'news:add');
INSERT INTO `roles_permissions` VALUES ('root', 'news:del');
INSERT INTO `roles_permissions` VALUES ('root', 'news:edit');
INSERT INTO `roles_permissions` VALUES ('root', 'news:find');
INSERT INTO `roles_permissions` VALUES ('user', 'news:find');
4.3.2 JdbcRealm中定义的SQL语句
/*--------------------------------------------
| C O N S T A N T S |
============================================*/
/**
* The default query used to retrieve account data for the user.
*/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
/**
* The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
*/
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
/**
* The default query used to retrieve the roles that apply to a user.
*/
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
/**
* The default query used to retrieve permissions that apply to a particular role.
*/
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
4.3.3 测试用例
public class JdbcRealmTest {
@Test
public void run(){
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//创建JdbcRealm对象
JdbcRealm jdbcRealm = new JdbcRealm();
//设置数据源
DruidDataSource dataSource=new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
dataSource.setUsername("root");
dataSource.setPassword("root");
jdbcRealm.setDataSource(dataSource);
//默认为false,必须设置为true才能进行角色的授权【重要】
jdbcRealm.setPermissionsLookupEnabled(true);
securityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
subject.login(usernamePasswordToken);
System.out.println("身份认证结果:" + subject.isAuthenticated());
System.out.println("是否拥有root角色:" + subject.hasRole("root"));
System.out.println("是否拥有user角色:" + subject.hasRole("user"));
// 权限鉴定
System.out.println("是否拥有news:find权限:" + subject.isPermitted("news:find"));
System.out.println("是否拥有news:add权限:" + subject.isPermitted("news:add"));
System.out.println("是否拥有news:edit权限:" + subject.isPermitted("news:edit"));
System.out.println("是否拥有news:del权限:" + subject.isPermitted("news:del"));
subject.logout();
System.out.println("身份认证结果:" + subject.isAuthenticated());
}
}
4.4 自定义Realm
自定义Realm继承 AuthorizingRealm 重写 doGetAuthorizationInfo 方法做鉴权和 doGetAuthenticationInfo 方法做认证。
4.4.1 步骤
- 创建一个类 ,继承AuthorizingRealm -> AuthenticatingRealm -> CachingRealm -> Realm。
- 重写鉴权方法 doGetAuthorizationInfo。
- 重写认证方法 doGetAuthenticationInfo。
4.4.2 方法
- 用户登陆的时会调用 doGetAuthenticationInfo。
- 进行权限校验的时会调用 doGetAuthorizationInfo。
4.4.3 对象介绍
-
UsernamePasswordToken: 对应就是shiro的token中有Principal标识和Credential凭证。
UsernamePasswordToken -> HostAuthenticationToken -> AuthenticationToken。
-
SimpleAuthorizationInfo: 代表用户角色权限信息。
-
SimpleAuthenticationInfo: 代表该用户的认证信息。
4.4.4 自定义Realm类
public class MyRealm extends AuthorizingRealm {
/*
* doGetAuthorizationInfo 实现权限鉴定的方法
* @param principalCollection 用户标识集合
* @return org.apache.shiro.authz.AuthorizationInfo 权限信息对象
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("权限鉴定doGetAuthorizationInfo...");
//用户标识集合中获取当前用户的主标识,主标识和身份认证的信息对象有关
String username = (String) principalCollection.getPrimaryPrincipal();
//根据用户标识查询当前用户的角色编码列表以及权限编码列表
//角色名称列表
Set<String> roleSet = this.findRoleByUsername(username);
Set<String> permissionSet = this.findPermissionByUsername(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//在SimpleAuthorizationInfo中设置当前用户的角色名称集合和权限名称集合
simpleAuthorizationInfo.setRoles(roleSet);
simpleAuthorizationInfo.setStringPermissions(permissionSet);
// simpleAuthorizationInfo.addRole(单个角色名称);
// simpleAuthorizationInfo.addRoles(角色名称列表或集合);
// simpleAuthorizationInfo.addStringPermission(单个权限名称);
// simpleAuthorizationInfo.addStringPermissions(权限名称列表或集合);
return simpleAuthorizationInfo;
}
/*
* doGetAuthenticationInfo 实现身份认证的方法
* @param authenticationToken 身份认证令牌对象
* @return org.apache.shiro.authc.AuthenticationInfo 身份认证信息对象
* @throws AuthenticationException 当身份认证出错,抛出此异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("身份认证doGetAuthenticationInfo...");
//从Token中获取用户名/用户标识
String username = (String) authenticationToken.getPrincipal();
//从数据库中查询到的密码
String password = this.findUserByUsername(username);
//用户的初步验证
if(password == null || "".equals(password)){
//表示身份认证失败
return null;
}
//将数据库中查询到的密码交给shiro来进行密码的比对
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, this.getName());
return simpleAuthenticationInfo;
}
/**
* 1.根据用户名查询用户对象
*/
private String findUserByUsername(String username){
return userMap.get(username);
}
/**
* 2.根据用户名查询该用户所拥有的所有角色
*/
private Set<String> findRoleByUsername(String username){
return roleMap.get(username);
}
/**
* 3.根据用户名查询该用户所拥有的所有权限
*/
private Set<String> findPermissionByUsername(String username){
return permissionMap.get(username);
}
//模拟数据
private static final Map<String, String> userMap = new HashMap<>();
private static final Map<String, Set<String>> roleMap = new HashMap<>();
private static final Map<String, Set<String>> permissionMap = new HashMap<>();
static {
//用户
userMap.put("admin", "123456");
userMap.put("steven", "111111");
//角色
Set<String> roleSet1 = new HashSet<>();
roleSet1.add("root");
roleSet1.add("user");
Set<String> roleSet2 = new HashSet<>();
roleSet2.add("user");
roleMap.put("admin", roleSet1);
roleMap.put("steven", roleSet2);
//权限
Set<String> permissionSet1 = new HashSet<>();
permissionSet1.add("stu:find");
permissionSet1.add("stu:add");
permissionSet1.add("stu:edit");
permissionSet1.add("stu:remove");
Set<String> permissionSet2 = new HashSet<>();
permissionSet2.add("stu:find");
permissionMap.put("admin", permissionSet1);
permissionMap.put("steven", permissionSet2);
}
}
4.4.5 测试用例
public class MyRealmTest {
@Test
public void run(){
DefaultSecurityManager securityManager = new DefaultSecurityManager();
MyRealm myRealm = new MyRealm();
securityManager.setRealm(myRealm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
//在执行login方法时,会自动调用realm中的身份认证的方法
subject.login(usernamePasswordToken);
System.out.println("身份认证结果:" + subject.isAuthenticated());
//在执行判断角色和权限的方法时,会自动调用realm中的权限鉴定的方法
System.out.println("是否拥有root角色:" + subject.hasRole("root"));
System.out.println("是否拥有user角色:" + subject.hasRole("user"));
System.out.println("是否拥有stu:find权限:" + subject.isPermitted("stu:find"));
System.out.println("是否拥有stu:add权限:" + subject.isPermitted("stu:add"));
System.out.println("是否拥有stu:edit权限:" + subject.isPermitted("stu:edit"));
System.out.println("是否拥有stu:remove权限:" + subject.isPermitted("stu:remove"));
}
}
4.5 Shiro认证和授权流程的源码解读
4.5.1 认证流程解读
当我们执行 subject.login(usernamePasswordToken); 的时候,会触发认证流程:
1、DelegatingSubject类中的login()方法执行。
2、DefaultSecurityManager类中的login()方法执行。
3、AuthenticatingSecurityManager类中的authenticate()方法执行。
4、AbstractAuthenticator类中的authenticate()方法执行。
5、ModularRealmAuthenticator类中的doAuthenticate()方法执行。
6、ModularRealmAuthenticator类中的doSingleRealmAuthentication()方法执行。
7、AuthenticatingRealm类中的getAuthenticationInfo()方法执行。
8、自定义Realm类中的doGetAuthenticationInfo()方法执行。
9、密码验证方法: AuthenticatingRealm类中的assertCredentialsMatch()方法执行。
4.5.2 授权流程解读
当我们执行 subject.hasRole(“admin”); 的时候,会触发授权流程:
1、DelegatingSubject类中的hasRole()方法执行。
2、AuthorizingSecurityManager类中的hasRole()方法执行。
3、ModularRealmAuthorizer类中的hasRole()方法执行。
4、AuthorizingRealm类中的getAuthorizationInfo()方法执行。
5、自定义Realm类中的doGetAuthorizationInfo()方法执行。
6、AuthorizingRealm类中的hasRole()方法执行。