写在前面
通过前面的学习,我们知道实际进行权限信息验证的是Realm,而Shiro框架内部提供了两种实现,一种是查询.ini文件的IniRealm;另一种是查询数据库的JdbcRealm。那么本篇就来分别研究这两个实现。
本篇主要学习以下内容:(1)Realm;(2)Shiro功能;(3)Shiro优点:(4)Shiro架构:(5)Shiro概念:(6)Shiro认证流程:(7)Shiro授权流程。
Realm
Shiro从Realm中获取安全数据(用户、角色、权限),也就是说SecurityManager想要验证用户身份,那么它必须从Realm中获取安全数据进而来确定用户的身份是否合法,之后判断用户是否具有某个角色,某个权限。
自定义Realm
一般来说,自定义Realm都需要继承org.apache.shiro.realm.AuthorizingRealm类,查看一下这个类的源码,如下所示:
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {}
可以看到这个AuthorizingRealm(授权)类继承了AuthenticatingRealm(认证)类,也就是说授权必须在认证之后才能进行,这个是正常逻辑,先进行用户身份认证,之后对用户进行权限授权。其实你如果自己研究的话,可以发现这个AuthenticatingRealm(认证)类是继承了CachingRealm(缓存)类:
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {}
在前面认证的时候,我们需要使用用户名和密码来构造一个UsernamePasswordToken对象,而这个UsernamePasswordToken其实是AuthenticationToken的子类:
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {}
public interface HostAuthenticationToken extends AuthenticationToken {}
而这个AuthenticationToken则是一个接口,里面有两个方法,其中getPrincipal方法用户获取用户身份(通常为用户名),而getCredentials方法则用户获取用户凭证(通常为用户密码):
public interface AuthenticationToken extends Serializable {
Object getPrincipal();
Object getCredentials();
}
接下来看一下此时的用户认证流程: (1)将AuthenticationToken转为UsernamePasswordToken; (2)从UsernamePasswordToken中获取username; (3)调用数据库中的方法,从数据库中查询username所对应的记录; (4)判断(3)中得到的记录是否存在,不存在则抛出UnkownAccountException异常; (5)根据用户信息来决定其抛出的其他异常; (6)根据用户需要来重写AuthorizingRealm类中的doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法。在该方法内,构造一个AuthenticationInfo对象,通常使用SimpleAuthenticationInfo对象,主要这个SimpleAuthenticationInfo对象有多个重载方法,这里主要使用下面这个方法:
public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = hashedCredentials;
this.credentialsSalt = credentialsSalt;
}
可以看到它有四个参数,第一个是principal,表示认证的主体信息,可以是username,也可以是数据库对应的用户的实体类对象;第二个是hashedCredentials,表示用户凭证(通常为用户密码);第三个是credentialsSalt,表示盐值;第四个是realmName,即当前自定义Realm对象的name,通常直接调用父类的getName()方法即可。
IniRealm
接下来通过一个实例来学习IniRealm如何通过查询.ini文件中的信息来进行权限信息验证:
第一步,新建项目。使用Maven新建一个名为envy-shiroini的文件,然后在其pom.xml依赖文件中新增如下依赖:
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
第二步,在项目src/main/resources目录下新建一个shiro.ini文件,然后在里面新增如下信息:
#定义用户
[users]
#用户名envy,密码1234, 角色admin
envy = 1234,admin
#用户名book,密码4321, 角色producter
book = 4321,developer
#定义角色
[roles]
#管理员拥有所有权限
admin = *
#产品维护人只能维护产品相关信息
producter = addProduct,deleteProduct,editProduct,updateProduct,listProduct
#订单维护人只能维护订单相关信息
orderer = addOrder,deleteOrder,editOrder,updateOrder,listOrder
第三步,在src/main/java目录下新建shiroini目录,并在shiroini目录中新建一个User类,其中的代码如下:
public class User {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public User(String name, String password) {
this.name = name;
this.password = password;
}
public User() {}
}
第四步,在src/main/java/shiroini目录下新建一个ShiroIniTest类,其中的代码如下:
public class ShiroIniTest {
/**
* 返回一个Subject对象
*/
private static Subject getSubject() {
//1、创建一个SecurityManager对象
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
//2、创建一个IniRealm对象
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
//3、设置Realm信息
defaultSecurityManager.setRealm(iniRealm);
//4、将安全管理者放入全局对象
SecurityUtils.setSecurityManager(defaultSecurityManager);
//5、全局对象通过安全管理者生成Subject对象
Subject subject = SecurityUtils.getSubject();
return subject;
}
/**
* 判断用户是否登录
*/
public static boolean login(User user) {
Subject subject = getSubject();
if (subject.isAuthenticated()) {
//如果用户已经登录,那么就退出
subject.logout();
}
//用户未登录,需要取出用户信息并进行验证
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword());
try {
//将用户信息token传递到IniRealm进行验证
subject.login(token);
} catch (AuthenticationException e) {
//验证失败
return false;
}
return subject.isAuthenticated();
}
/**
* 判断用户是否有对应角色
*/
public static boolean hasRole(String role) {
Subject subject = getSubject();
return subject.hasRole(role);
}
/**
* 判断用户是否有对应权限
*/
public static boolean isPermitted(String permission) {
Subject subject = getSubject();
return subject.isPermitted(permission);
}
public static void main(String[] args){
//模拟几个用户,其中envy和book在之前的shiro.ini文件中,movie用户不存在
List<User> userList = Arrays.asList(
new User("envy","1234"),
new User("book","4321"),
new User("movie","1234")
);
//定义角色信息
List<String> rolesList = Arrays.asList("admin","producter");
//定义权限信息
List<String> permitsList = Arrays.asList("listProduct","listOrder");
System.out.println("*********用户登录*********");
//用户登录
for(User user:userList){
if(login(user)){
System.out.println(String.format("%s 登录成功,登录密码为:%s",user.getName(),user.getPassword()));
}else{
System.out.println(String.format("%s 登录失败,使用密码为:%s",user.getName(),user.getPassword()));
}
}
System.out.println("*********角色判断*********");
//判断用户是否属于某个角色
for(User user:userList){
for(String role:rolesList){
if(login(user)){
if(hasRole(role)){
System.out.println(String.format("%s 属于%s角色",user.getName(),role));
}else{
System.out.println(String.format("%s 不属于%s角色",user.getName(),role));
}
}
}
}
System.out.println("*********权限判断*********");
//判断用户是否具有某个权限
for(User user:userList){
for(String permit:permitsList){
if (login(user)){
if(isPermitted(permit)){
System.out.println(String.format("%s 具有%s权限",user.getName(),permit));
}else{
System.out.println(String.format("%s 不具有%s权限",user.getName(),permit));
}
}
}
}
};
}
第五步,运行ShiroIniTest测试类,可以发现运行结果如下所示:
通过对比之前的shiro.ini文件,可以发现控制台的输出信息是正确的。
JdbcRealm
使用数据库
IniRealm是通过查询.ini文件中的信息来进行权限信息验证,但是在实际工作中开发者通常都会将权限相关信息存在数据库中,因此IniRealm不太适合这种场景,应当使用另一种权限信息验证的Realm---查询数据库的JdbcRealm。
RBAC解释
在权限控制方面,RBAC是权限系统的设计基础,它有两种解释:一种是Role-Base Access Control(基于角色的访问控制),举个例子当用户想对订单进行编辑和修改,那么用户必须具备订单维护人这一角色;而另一种则是Resource-Base Access Control(基于资源的访问控制),举个例子当用户想对商品进行查询和删除,那么用户必须具备可以查询和删除商品的这些权限。
因此如果使用RBAC理论,那么必须存在三张基础表:用户表、角色表和权限表。当然除此之外,一般还有两个表:一个是用户与角色的多对多关系表,另一个是角色与权限的多对多关系表。请注意,你可能觉得还应该有用户与权限之间的多对多关系表,其实不需要的,因为用户和权限之间可以通过角色来间接建立。
用户与角色的多对多关系是指,一个用户可以有多个角色,一个角色可以对应多个用户;角色与权限的多对多关系表是指,一个角色可以有多个权限,一个权限可以对应多个角色。
JdbcRealm实例
接下来通过一个伪实例来学习Shiro如何通过JdbcRealm来查询数据库,并实现权限信息验证。所谓伪实例是指通过模拟数据库的操作,来实现数据库相类似的操作。
第一步,新建项目。使用Maven新建一个名为envy-shirojdbc的文件,然后在其pom.xml依赖文件中新增如下依赖:
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
第二步,在src/main/java目录下新建shirojdbc目录,并在shirojdbc目录中新建一个User类,其中的代码如下:
public class User {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public User(String name, String password) {
this.name = name;
this.password = password;
}
public User() {}
}
第三步,在src/main/java/shirojdbc目录下新建一个MyUserRealm类,注意这个类需要继承Shiro的AuthorizingRealm类,并重写其中用于认证的doGetAuthenticationInfo和授权的doGetAuthorizationInfo方法,其中的代码如下:
public class MyUserRealm extends AuthorizingRealm {
/**
* 模拟数据库用户
* */
Map<String,String> userMap = new HashMap<String, String>();
{
userMap.put("envy","1234");
//设置自定义Realm的名称
super.setName("MyUserRealm");
}
/**
* 模拟数据库通过用户名来查询用户密码
* */
public String getPasswordByUserName(String userName){
return userMap.get(userName);
}
/**
* 模拟数据库通过用户名来查询用户角色名称
* */
public Set<String> getRolesByUserName(String userName){
Set<String> roleSet = new HashSet<String>();
roleSet.add("admin");
roleSet.add("user");
return roleSet;
}
/**
* 模拟数据库通过角色名来查询角色权限
* */
public Set<String> getPermissionsByUserName(String roleName){
Set<String> permissionSet = new HashSet<String>();
permissionSet.add("user:add");
permissionSet.add("user:update");
permissionSet.add("user:delete");
return permissionSet;
}
/**
* 认证
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1、从主体传过来的认证信息中获取用户名
String userName = (String) authenticationToken.getPrincipal();
//2、通过用户名去“数据库”中获取用户密码
String password = getPasswordByUserName(userName);
if(password==null){
return null;
}
//3、新建一个认证对象
//验证通过,认证信息中存放账号密码, getName()用于获取当前Realm的继承方法,通常返回当前类名MyDatabaseReal
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName,password,getName());
return simpleAuthenticationInfo;
}
/**
*授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//认证通过后,接下来开始授权操作
//1、获取用户名
String userName = (String) principalCollection.getPrimaryPrincipal();
//2、从数据库中查询当前username对象所具有的角色和权限
Set<String> roleSet = getRolesByUserName(userName);
Set<String> permissionSet = getPermissionsByUserName(userName);
//3、新建一个授权对象
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(roleSet);
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
}
第四步,在src/main/java/shirojdbc目录下新建一个EnvyShiroJDBCTest类,其中的代码如下:
public class EnvyShiroJDBCTest {
@Test
public void test(){
//1、实例化自定义Realm对象
MyUserRealm myUserRealm = new MyUserRealm();
//2、创建一个SecurityManager对象
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//3、设置Realm信息
defaultSecurityManager.setRealm(myUserRealm);
//4、将安全管理者放入全局对象
SecurityUtils.setSecurityManager(defaultSecurityManager);
//5、全局对象通过安全管理者生成Subject对象
Subject subject = SecurityUtils.getSubject();
//6、实例化一个对象,并进行登录验证
UsernamePasswordToken token = new UsernamePasswordToken("envy","1234");
subject.login(token);
//判断用户是否认证成功,注意返回值是boolean类型
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
// 判断subject即当前登录用户是否具有admin和user两个角色权限,如果没有则会报错
subject.checkRoles("admin", "user");
//subject.checkRole("xxx"); // 报错
// 判断subject即当前登录用户是否具有user:add权限
subject.checkPermission("user:add");
}
}
第五步,运行EnvyShiroJDBCTest测试类,可以发现运行结果如下所示,则表明测试通过:
isAuthenticated:true
ok,那么本篇关于自定义Realm的学习就到此为止,后续开始学习其他知识。