写在前面

通过前面的学习,我们知道实际进行权限信息验证的是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类,查看一下这个类的源码,如下所示:

1
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {}

可以看到这个AuthorizingRealm(授权)类继承了AuthenticatingRealm(认证)类,也就是说授权必须在认证之后才能进行,这个是正常逻辑,先进行用户身份认证,之后对用户进行权限授权。其实你如果自己研究的话,可以发现这个AuthenticatingRealm(认证)类是继承了CachingRealm(缓存)类:

1
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {}

在前面认证的时候,我们需要使用用户名和密码来构造一个UsernamePasswordToken对象,而这个UsernamePasswordToken其实是AuthenticationToken的子类:

1
2
3
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {}

public interface HostAuthenticationToken extends AuthenticationToken {}

而这个AuthenticationToken则是一个接口,里面有两个方法,其中getPrincipal方法用户获取用户身份(通常为用户名),而getCredentials方法则用户获取用户凭证(通常为用户密码):

1
2
3
4
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对象有多个重载方法,这里主要使用下面这个方法:

1
2
3
4
5
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依赖文件中新增如下依赖:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>

第二步,在项目src/main/resources目录下新建一个shiro.ini文件,然后在里面新增如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#定义用户
[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类,其中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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));
                }
            }
        }
    }
};

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
第五步,运行ShiroIniTest测试类,可以发现运行结果如下所示:

![](https://upload-images.jianshu.io/upload_images/8964398-9849cefef2894fe6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

通过对比之前的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>
1
第二步,在`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() {}

}

1
第三步,在`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;
}

}

1
第四步,在`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");
}

}

1
第五步,运行EnvyShiroJDBCTest测试类,可以发现运行结果如下所示,则表明测试通过:

isAuthenticated:true

```
ok,那么本篇关于自定义Realm的学习就到此为止,后续开始学习其他知识。