Shiro 认证流程详解

1. 认证

  身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。

2. Shiro 中认证的关键对象

  • Subject 主体:访问系统的用户,主体可以是用户、程序等等,进行认证的都称为主体;
  • Principal 身份信息:是主体(Subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)
  • Credential 凭证信息:是只有主体自己知道的安全信息,如密码、证书等。

3. 认证流程

         

4. 认证开发

4.1  模拟数据库中已存储用户信息【创建 shiro.ini 文件】

[users]
wangwu=123
zhangsan=123456
lisi=789

4.2 开发认证代码

public class TestAuthenticator {

    public static void main(String[] args) {
        // 1. 创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2. 给安全管理器设置 realm 【Realm 需要获取用户认证的数据源信息】
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        // 3. SecurityUtils 设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4. 获取关键对象 Subject 主体
        Subject subject = SecurityUtils.getSubject();
        // 5. 创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("wangwu", "123");
        // 用户认证
        try {
            System.out.println("认证状态: " + subject.isAuthenticated());
            subject.login(token);
            System.out.println("认证状态: " + subject.isAuthenticated());
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("认证失败: 用户名不存在~");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("认证失败: 密码错误~");
        }
    }
}
  • DisabledAccountException(帐号被禁用)

  • LockedAccountException(帐号被锁定)

  • ExcessiveAttemptsException(登录失败次数过多)

  • ExpiredCredentialsException(凭证过期)

4.3 自定义 Realm

  上边的程序使用的是 Shiro 自带的 IniRealm,IniRealm 从 shiro.ini 配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 Realm。

1)Shiro 自定义 Realm 

         

2)根据认证源码使用的是 SimpleAccountRealm 【只保留认证和授权这两部分代码】

public class SimpleAccountRealm extends AuthorizingRealm {
        //.......省略
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) {

            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }

        }

        return account;
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = getUsername(principals);
        USERS_LOCK.readLock().lock();
        try {
            return this.users.get(username);
        } finally {
            USERS_LOCK.readLock().unlock();
        }
    }
}

3)自定义 Realm 

 由于 Realm 相当于 Datasource 数据源,SecurityManager 进行安全认证需要通过 Realm 获取用户身份数据,所以可以自定义 Realm 获取用户身份的信息;

public class CustomerRealm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("====================");
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String principal = (String) token.getPrincipal();
        System.out.println("principal = " + principal);
        //根据身份信息使用jdbc mybatis查询相关数据库
        if ("xiaochen".equalsIgnoreCase(principal)) {
            // 参数1:返回数据库中的用户名  参数2:返回数据库中的正确密码  参数3:提供当前的 realm 名字,this.getName()
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("wangwu", "123", this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

4)使用自定义 Realm 进行认证

public class TestCustomerRealm {

    public static void main(String[] args) {
        // 1.创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

        // 2.给安全管理器设置 realm 【数据库中获取用户名和密码】
        defaultSecurityManager.setRealm(new CustomerRealm());

        // 3.SecurityUtils 设置安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        // 4.获取用户登录的主体
        Subject subject = SecurityUtils.getSubject();

        AuthenticationToken token = new UsernamePasswordToken("wangwu", "123");
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            System.out.println("用户名不正确");
            e.printStackTrace();
        } catch (IncorrectCredentialsException e) {
            System.out.println("用户密码不正确");
            e.printStackTrace();
        }
    }

}

5) 使用 MD5 和 Salt 

实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。
  A 自定义 MD5 + salt 的 Realm 
public class CustomerMD5Realm extends AuthorizingRealm {

    /**
     * 一个主题可以有多个身份,参数是集合 principals;但是只有一个主身份;
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取身份信息
        String principal = (String) principals.getPrimaryPrincipal();
        System.out.println("身份信息 = " + principal);
        
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String principal = (String) token.getPrincipal();
        System.out.println("principal = " + principal);
        //根据身份信息使用jdbc mybatis查询相关数据库
        if ("wangwu".equalsIgnoreCase(principal)) {
            // 参数1:返回数据库中的用户名  参数2:返回数据库中的正确密码  参数3:注册时的随机盐  参数4:提供当前的 realm 名字,this.getName()
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,
                    "e4f9bf3e0c58f045e62c23c533fcf633",
                    ByteSource.Util.bytes("X0*7ps"),
                    this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}    
  B 使用 MD5 + Salt 进行认证处理
public class TestCustomerMD5Authenticator {

    public static void main(String[] args) {
        // 1. 创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        // 2. 给安全管理器设置 realm 【Realm 需要获取用户认证的数据源信息】
        CustomerMD5Realm customerMD5Realm = new CustomerMD5Realm();
        // 设置 realm 使用的 hash 凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("md5"); // 设置使用的 Hash 算法
        credentialsMatcher.setHashIterations(1024);     // 需要散列多少次
        customerMD5Realm.setCredentialsMatcher(credentialsMatcher);
        securityManager.setRealm(customerMD5Realm);

        // 3. SecurityUtils 设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);

        // 4. 获取关键对象 Subject 主体
        Subject subject = SecurityUtils.getSubject();

        // 5. 创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("wangwu", "123");

        // 用户认证
        try {
            System.out.println("认证状态: " + subject.isAuthenticated());
            subject.login(token);
            System.out.println("认证状态: " + subject.isAuthenticated());
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("认证失败: 用户名不存在~");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("认证失败: 密码错误~");
        }
    }
}

  注意:深入了解 Shiro 的认证流程,可以 debug 运行程序,查看源代码进行学习深究!

原文地址:https://www.cnblogs.com/blogtech/p/14822112.html