一、认证的概念

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

二、shiro认证

2.1 shrio中认证的关键对象

(1) Subject 主体

访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;

(2) Principal:身份信息

是主体(subject)进行身份认证的标识,标识必须具有**唯一性**,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。

(3) credential:凭证信息

  1. ** 是只有主体自己知道的安全信息,如密码、证书等**。

2.2 shiro中的认证流程

shiro中的认证(二) - 图2

2.3 简单demo了解登录相关API

(1). 选择maven快速开启骨架创建项目

(2). 引入shrio依赖

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.5.3</version>
</dependency>

#这里可以去maven远程仓库下载最新的地址  如:1.7.1

(3). 创建resources资源包与shiro.ini文件

关于.ini文件,这个是shiro给我们学习人员进行初始学习api快速使用而扩展的,文件名称可以随意,只要是.ini结尾的放在resources文件下就ok
注意:
在实际的项目开发中不会使用这种方式,这种方式可以用来初学练手,省去了与数据库的交互,大大节省了我们学习的成本

[users]
zhangsan=123456
lisi=456789

shiro中的认证(二) - 图3

(4). 相关代码实现

package com.zara;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

/**
 * @author 王振宇
 * @Description <h1>认证</h1>
 * @create 2021-06-30 23:52
 */
public class TestAuthenticator {
    public static void main(String[] args) {
        //1. 创建安全管理器 DefaultSecurityManager是SecurityManager的实现类
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //2. 给安全管理器设置数据支持 也就是realm
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //3.SecurityUtils全局安全工具类  给全局安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        //4.创建主体
        Subject subject = SecurityUtils.getSubject();
        //5.创建令牌
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("xiaowang","123");
        //6.进行认证
        try {
          /* securityManager.login(subject,usernamePasswordToken);*/
            System.out.println("认证状态:"+ subject.isAuthenticated());  //查看认证状态
            //调用主体的login方法传入认证令牌进行身份认证
            subject.login(usernamePasswordToken);
            System.out.println("认证状态:"+ subject.isAuthenticated());
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("认证失败,用户名不存在");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败,密码错误");
        }


    }
 }

#UnknownAccountException异常为shiro认证异常 , 父类属于运行时异常 , 表示用户名不存在
#IncorrectCredentialsException异常为shiro认证异常 , 父类属于运行时异常 , 表示密码不正确

(5). 常见的异常类型

  • DisabledAccountException(帐号被禁用)
  • LockedAccountException(帐号被锁定)
  • ExcessiveAttemptsException(登录失败次数过多)
  • ExpiredCredentialsException(凭证过期)等

    2.4 自定义Realm

    通过分析源码可得:
    认证:
  1. 最终执行用户名比较是 在SimpleAccountRealm类 的 doGetAuthenticationInfo 方法中完成用户名校验
  2. 最终密码校验是在 AuthenticatingRealm类 的 assertCredentialsMatch方法 中

总结:
AuthenticatingRealm 认证realm doGetAuthenticationInf
AuthorizingRealm 授权realm doGetAuthorizationInfo

自定义Realm的作用:放弃使用.ini文件,使用数据库查询

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

2.4.1 shiro提供的Realm

image.png

2.4.2 根据认证源码->认证使用的是SimpleAccountRealm

image.png
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();
        }
    }
}

2.4.3 自定义Realm

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * 自定义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 {
        //在token中获取 用户名
        String principal = (String) token.getPrincipal();
        System.out.println(principal);

        //实际开发中应当 根据身份信息使用jdbc mybatis查询相关数据库
        //在这里只做简单的演示
        //假设username,password是从数据库获得的信息
        String username="zhangsan";
        String password="123456";
        if(username.equals(principal)){
            //参数1:返回数据库中正确的用户名
            //参数2:返回数据库中正确密码
            //参数3:提供当前realm的名字 this.getName();
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,password,this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

2.5 使用自定义Realm认证

import com.lut.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

/**
 * 测试自定义的Realm
 */
public class TestAuthenticatorCusttomerRealm {

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

        //2.给安全管理器设置realm(设置为自定义realm获取认证数据)
        defaultSecurityManager.setRealm(new CustomerRealm());


        //3.给安装工具类中设置默认安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);

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

        //5.创建token令牌
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
        try {
            subject.login(token);//用户登录
            System.out.println("登录成功~~");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!!");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误!!!");
        }

    }
}

2.6 使用MD5+Salt+Hash

补充:MD5算法
作用:一般用来加密或者签名(检验和)
特点:MD5算法不可逆,内容相同无论执行多少次md5生成结果始终是一致
网路上提供的MD5在线解密一般是用穷举的方法
生成结果:始终是一个16进制32位长度字符串

shrio中已经给我们提供了一个类【Md5Hash】专门来进行密码加盐以及对MD5操作

import org.apache.shiro.crypto.hash.Md5Hash;

public class TestShiroMD5 {

    public static void main(String[] args) {

        //使用md5
        Md5Hash md5Hash = new Md5Hash("123");
        System.out.println(md5Hash.toHex());

        //使用MD5 + salt处理
        Md5Hash md5Hash1 = new Md5Hash("123", "X0*7ps");
        System.out.println(md5Hash1.toHex());

        //使用md5 + salt + hash散列(参数代表要散列多少次,一般是 1024或2048)
        Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps", 1024);
        System.out.println(md5Hash2.toHex());

    }
}

执行结果:
    202cb962ac59075b964b07152d234b70
    bad42e603db5b50a78d600917c2b9821
    7268f6d32ec8d6f4c305ae92395b00e8

实际应用:将 盐和散列 后的值存在数据库中,realm在认证时候,把数据库存储的用户信息(其中包括用户名和加密后的密码,还有盐),
与 用户输入的用户名密码进行 密码校验

在 自定义 realm 中进行代码实现,完成md5 + salt +hash散列 密码校验

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

/**
 * 使用自定义realm 加入md5 + salt +hash
 */
public class CustomerMd5Realm extends AuthorizingRealm {

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取 token中的 用户名
        String principal = (String) token.getPrincipal();

        //假设这是从数据库查询到的信息
        String username="zhangsan";
        String password="7268f6d32ec8d6f4c305ae92395b00e8";//加密后

        //根据用户名查询数据库
        if (username.equals(principal)) {
            //参数1:数据库用户名
            //参数2:数据库md5+salt之后的密码
            //参数3:注册时的随机盐
            //参数4:realm的名字
            return new SimpleAuthenticationInfo(
                    principal,
                    password,
                    ByteSource.Util.bytes("@#$*&QU7O0!"),
                    this.getName());
        }
        return null;
    }
}

注意 SimpleAuthenticationInfo这一块 构造参数的变化 
    ByteSource类的使用方式

SecurityManager 这里的变动:

package com.lut.test;

import com.lut.realm.CustomerMd5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

import java.util.Arrays;

public class TestCustomerMd5RealmAuthenicator {

    public static void main(String[] args) {

        //1.创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

        //2.注入realm
        CustomerMd5Realm realm = new CustomerMd5Realm();

        //3.设置realm使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //声明:使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        //声明:散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);

        //4.将安全管理器注入安全工具
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        //5.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();

        //6.认证
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");

        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }



    }
}
注意: 这里再给SecurityManager 注入 realm 时候,需要先给 我们自定义的 Realm 添加一个hash 凭证匹配器,
    要不我们在进行密码校验的时候默认使用的是 equals() 的 方式。

2.7 总结

通过这个认证例子,我们简单的了解了 shrio 中 每一个对象的含义在哪里。我们创建 Shiro 的核心对象 SecurityManager 当然我们使用的是该接口的实现类 DefaultSecurityManager ,通过给SecurityManager 注入 我们自定义的 Realm,然后再把 注入过 realm 的 SecurityManager 交给我们的 SecurityUtils 安全管理工具,通过 SecurityUtils 安全管理工具 创建出我们的主角(也就是主体),进行longin 和 longout 方法的调用 完成我们的 认证操作。

我们弄到这里,其实就是给shiro 搭建了个环境,以及认证的触发,真正的认证数据校验是在 realm 中实现的。其实我们了解源码以后,我发现 shiro 的程序设计是真的不错,当我们要 自定义 realm 的时候 我们要实现 AuthorizingRealm 这个类,因为这个类又继承了 AuthonticatingRealm 这个类,所以我们 一下就 把授权和 认证都做到了,然后重新实现其中 doGetAuthenticationInfo 和 doGetAutorizationInfo 两个方法就可以了。从他们的传递过来的参数中 。获取身份信息,拿着身份信息进行数据库的数据查询,完成用户认证以及授权。