自定义的 Realm 使得我们使用安全数据源更加灵活,在自定义的 Realm 中,我们就可以自己控制认证和授权的逻辑了。同时我们还简单介绍了一下在多个 Realm 同时存在的情况下,我们可以配置我们的认证策略来满足我们的需求。

Shiro 学习笔记(3)—— 自定义 Realm

前面两节我们已经介绍过 IniRealm 和 JdbcRealm,这一节我们介绍自定义的 Realm 实现我们自己的安全数据源。

方式一:implements Realm (这种方式不太常用,只是为了说明知识)

这种方式实现的 Realm 仅只能实现认证操作,并不能实现授权操作。

代码:

public class MapRealm implements Realm {

    private static Map<String,String> users;

    static{
        users = new HashMap<>();
        users.put("liwei","123456");
        users.put("zhouguang","666666");
    }


    /**
     * 返回一个唯一的 Realm 名字
     * @return
     */
    public String getName() {
        System.out.println("Map Realm 中设置 Realm 名字的方法");
        return "MyStaticRealm";
    }

    /**
     * 判断此 Realm 是否支持此 Token
     * @param authenticationToken
     * @return
     */
    public boolean supports(AuthenticationToken authenticationToken) {
        System.out.println("Map Realm 中给出支持的 Token 的方法");
        // 表示仅支持 UsernamePasswordToken 类型的 Token
        return authenticationToken instanceof UsernamePasswordToken;
    }

    /**
     * 根据 Token 获取认证信息
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("Map Realm 中返回认证信息的方法");
        String userName = (String)authenticationToken.getPrincipal();
        String password = new String((char[])authenticationToken.getCredentials());
        System.out.println("token 中的用户名:" + userName);
        System.out.println("token 中的密码:" + password);
        if(!users.containsKey(userName)){
            throw new UnknownAccountException("没有这个用户!");
        }else if(!password.equals(users.get(userName))){
            throw new IncorrectCredentialsException("密码错误!");
        }
        return new SimpleAuthenticationInfo(userName,password,getName());
    }
}

接下来我们要在 shiro.ini 文件中声明我们要是用的这个 Realm。

[main]
# 声明了我们自己定义的一个 Realm
myMapRealm=com.liwei.realm.MapRealm
# 将我们自己定义的 Realm 注入到 securityManager 的 realms 属性中去
securityManager.realms=$myMapRealm

方式二:extends AuthorizingRealm(比较常用的一种方式,因为这样做既可以实现认证操作,也可以实现授权操作)

示例代码:

public class MyStaticRealm extends AuthorizingRealm {

    /**
     * 用于授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 暂时忽略,以后介绍
        return null;
    }

    /**
     * 用于认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("Static Realm 中认证的方法");
        String userName = token.getPrincipal().toString();
        String password = new String((char[])token.getCredentials());
        if(!"liwei".equals(userName)){
            throw new UnknownAccountException("无效的用户名");
        }else if(!"123456".equals(password)){
            throw new IncorrectCredentialsException("密码无效");
        }
        return new SimpleAuthenticationInfo("liweipower@gmail","123456",getName());
    }
}

说明:上面的写作也只是为了测试,真正在生产环境中,应该通过查询数据库去完成认证和授权的相关操作。

下面这一行说明的事实是很重要的:

doGetAuthenticationInfo() 方法中须要返回一个正确的 SimpleAuthenticationInfo 对象,这样 Shiro 就会和 Subjectlogin() 方法中传入的 token 信息进行比对,完成认证的操作。

然后我们在 shiro.ini 中也要配置这个自定义的 Realm:

代码:

[main]
myStaticRealm=com.liwei.realm.MyStaticRealm
securityManager.realms=$myStaticRealm

知识点:配置认证策略

这时候,我们会有一个疑问,securityManager 的属性既然是 realms,说明可以设置若干个 Realm,它们认证的顺序是如何的呢。

Shiro 会按照我们声明的顺序,依次验证。在使用了 ini 文件启动 Shiro 的方式中,IniRealm 在 Shiro 中是默认使用的(我个人觉得应该是第一个使用的,即使在我们不声明的情况下)。

那么对于若干个 Realm,Shiro 提供了一种配置方式,让我们来决定在多个 Reaml 同时声明的情况下,采用哪些 Realm 返回的认证信息的方式,这就是我们的认证策略。

认证策略主要有以下三种:

1、FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;

2、AtLeastOneSuccessfulStrategy: (这是默认使用的认证策略,即在不配置情况下 Shiro 所采用的认证策略)只要有一个 Realm 验证成功即可, 和 FirstSuccessfulStrategy 不同,返回所有 Realm 身份验证成功的认证信息;

3、AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。

配置示例:

# 配置认证策略
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

配置好以后,我们可以通过以下的方法来验证我们刚刚配置的 Shiro 的认证策略。

currentSubject.login(token);
PrincipalCollection ps = currentSubject.getPrincipals();
System.out.println(ps.asList());
System.out.println(ps.getRealmNames());
System.out.println(currentSubject.getPrincipals());

自定义 Realm 就介绍到这里了。自定义 Realm 是很重要的,特别是 extends AuthorizingRealm 这种方式。

学习到这里想再强调一下 extends AuthorizingRealm 这种方式覆写的 doGetAuthenticationInfo() 方法,一开始我不是很明白,这个方法到底是做什么的,就上面我们举例,我想再解释一下。

一般地,我们从参数 AuthenticationToken 对象中取出用户填写的用户名和密码信息,这个 token 其实就是 Subject 使用 login() 方法中传入的 UsernamePasswordToken 对象,我们通过这个 UsernamePasswordToken 对象获得用户填写的用户名和密码,然后我们应该通过用户的用户名去数据库查询数据库是否有这个用户名,如果没有,抛出一个用户名不存在异常;如果用户名存在,返回一个用户对象(带密码的),再用数据库返回的密码数据和用户填写的密码数据进行比对,如果错误,就抛出异常,如果正确,就要把正确的用户名和密码信息封装成一个 SimpleAuthenticationInfo 对象返回,这才是一个比较完整并且正确的流程。

另外再补充一下,我在上面的例子中说到“用户填写的用户名和密码”是一种为了方便理解的说法。下面介绍官方正确的说法。

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals, 但只有一个 Primary principals, 一般是用户名/密码/手机号。

credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。

最常见的 principals 和 credentials 组合就是用户名/密码了。