Shiro入门使用

在阅读下文之前,请认准了两个单词,认证 (authentication) 和授权 (authorization),前者是对你的身份进行确认,后者是对你的权限进行确认。例如,前者:你是这个小区的人。后者:你只有 404 房间的权限。

身份认证

流程概括
  1. Shiro 把用户的数据封装成标识 tokentoken 一般封装着用户名,密码等信息。
  2. 使用 Subject 门面获取到封装着用户的数据的标识 token
  3. Subject 把标识token交给 SecurityManager,在 SecurityManager 安全中心中,它将标识 token 委托给认证器 Authenticator 进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些 Realm
  4. 认证器 Authenticator 将传入的标识 token,与数据源 Realm 对比,验证其是否合法
shiro-01authenticator

创建项目,指定 JDK 编译版本,引入 shiro 依赖。

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xn2001</groupId>
  <artifactId>shiro-01authenticator</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>shiro-01authenticator</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-nop</artifactId>
      <version>1.7.2</version>
    </dependency>

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

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
    </dependency>
  </dependencies>
</project>

编写 resoureses/shiro.ini

#声明用户账号和密码
[users]
#账号=密码
#用户名:jay 密码:123
jay=123

编写 HelloShiro 测试类

首先创建 DefaultSecurityManager 实例,其中可以传入两个参数,RealmCollection<Realm>,前面我们提到 Realm 领域,指的是用户账号权限信息。我们可以看一下它的实现类。

IniRealmPropertiesRealm 看起来就是去读取配置信息的,我们当然是选择 IniRealm 来作为参数传入DefaultSecurityManager

//构建默认的安全示例并传入ini数据源(用户权限信息)
DefaultSecurityManager securityManager = new DefaultSecurityManager(new IniRealm("classpath: shiro.ini"));

//使用 SecurityUtils 工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);

接下来我们去获取 Subject 主题对象

//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();

根据该对象我们可以去进行登陆操作,subject.login(AuthenticationToken token),因此我们需要去构造用户信息,也就是创建 AuthenticationToken 示例。

//构建用户,指定用户名密码,进行测试
UsernamePasswordToken user = new UsernamePasswordToken("jay", "123");

登录测试

//测试登录操作
subject.login(user);

System.out.println("是否登录成功:" + subject.isAuthenticated());

完整的代码

public class HelloShiro {
  @Test
  public void shiroLogin() {
    //构建默认的安全示例并传入ini数据源(用户权限信息)
    DefaultSecurityManager securityManager = new DefaultSecurityManager(new IniRealm("classpath: shiro.ini"));

    //使用 SecurityUtils 工具生效安全管理器
    SecurityUtils.setSecurityManager(securityManager);

    //使用SecurityUtils工具获得主体
    Subject subject = SecurityUtils.getSubject();

    //构建用户,指定用户名密码,进行测试
    UsernamePasswordToken user = new UsernamePasswordToken("jay", "123");

    //测试登录操作
    subject.login(user);

    System.out.println("是否登录成功:" + subject.isAuthenticated());
  }
}

打印结果

Realm

image-20210207211622834

Realm 是一个接口,在它的类图中我们也不难猜到,一般在真实的项目中,我们不会直接实现 Realm 接口,而是直接继承 AuthorizingRealm,能够继承到认证与授权功能。它需要强制重写两个方法。

public class DefinitionRealm extends AuthorizingRealm {

  /**
   * 鉴权方法
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    return null;
  }

  /**
   * 认证方法
   * @param token 传递登录 token
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    return null;
  }
}
shiro-02realm

创建项目,指定 JDK 编译版本,引入 shiro 依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xn2001</groupId>
  <artifactId>shiro-02realm</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-nop</artifactId>
      <version>1.7.2</version>
    </dependency>

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

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
    </dependency>
  </dependencies>
</project>

这一次我们去自定义授权认真,模拟访问数据库来获取密码。根据上文,我们当然是先写一个类继承于 AuthorizingRealm,然后重写 doGetAuthenticationInfo 方法。

DefinitionRealm

public class DefinitionRealm extends AuthorizingRealm {
  /**
   * 鉴权方法
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    return null;
  }

  /**
   * 认证方法
   *
   * @param token 传递登录 token
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String username = (String) token.getPrincipal();
    // 实例化数据源访问接口
    SecurityService securityService = new SecurityServiceImpl();
    String password = securityService.findPasswordByLoginName(username);
    if ("".equals(password) || password == null) {
      throw new UnknownAccountException("账户不存在");
    }
    // 传递账号和密码
    return new SimpleAuthenticationInfo(username, password, getName());
  }
}

在代码中我们写到了一个 SecurityService,这其实是我们模拟的一个数据库访问。理论上我们应该调用 Dao,根据用户名去查询密码,但这里为了演示方便直接访问一个字符串了。

public interface SecurityService {

  /**
   * 查询用户密码
   * @param loginName 用户名
   * @return 密码
   */
  String findPasswordByLoginName(String loginName);
}
public class SecurityServiceImpl implements SecurityService {
  @Override
  public String findPasswordByLoginName(String loginName) {
    return "123456";
  }
}

测试方法,注意这里 DefaultSecurityManager 就不用去加载 ini 文件了,直接使用 setRealm(Realm realm) 设置自定义的 DefinitionRealm 即可。

public class HelloShiro {
  @Test
  public void shiroLogin() {
  
    //构建默认的安全示例并传入ini数据源(用户权限信息)
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
  
    //设置Realm
    securityManager.setRealm(new DefinitionRealm());

    //使用 SecurityUtils 工具生效安全管理器
    SecurityUtils.setSecurityManager(securityManager);

    //使用SecurityUtils工具获得主体
    Subject subject = SecurityUtils.getSubject();

    //构建用户,指定用户名密码,进行测试
    UsernamePasswordToken user = new UsernamePasswordToken("jay", "123456");

    //测试登录操作
    subject.login(user);

    System.out.println("是否登录成功:" + subject.isAuthenticated());
  }
}

image-20210208002930882

编码解码

Shiro 提供了 base64 和 16进制字符串编码/解码的API支持,方便一些编码解码操作。

Shiro 内部的一些数据的【存储/表示】都使用了 base64 和 16进制字符串。

对应的分别是 Base64Hex 工具类,使用较为简单,直接调用静态方法即可。

public class EncodesUtil {
    /**
     * @Description HEX-byte[]--String转换
     * @param input 输入数组
     * @return String
     */
    public static String encodeHex(byte[] input){
        return Hex.encodeToString(input);
    }

    /**
     * @Description HEX-String--byte[]转换
     * @param input 输入字符串
     * @return byte数组
     */
    public static byte[] decodeHex(String input){
        return Hex.decode(input);
    }

    /**
     * @Description Base64-byte[]--String转换
     * @param input 输入数组
     * @return String
     */
    public static String encodeBase64(byte[] input){
        return Base64.encodeToString(input);
    }

    /**
     * @Description Base64-String--byte[]转换
     * @param input 输入字符串
     * @return byte数组
     */
    public static byte[] decodeBase64(String input){
        return Base64.decode(input);
    }
}

散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,所以直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如salt(即盐);这样散列的对象是“密码+salt”,这样生成的散列值相对来说更难破解。

Shiro 支持的散列算法:

Md2HashMd5HashSha1HashSha256HashSha384HashSha512Hash

使用也是较为简单,只需要 new SimpleHash() 传入参数即可。

public class DigestsUtil {
  // 加密形式
  private static final String SHA1 = "SHA-1";
  
  // 加密次数
  private static final Integer ITERATIONS = 512;

  /**
   * @param input 需要散列字符串
   * @param salt  盐字符串
   */
  public static String sha1(String input, String salt) {
    /*
     * algorithmName:加密形式
     * source:原始明文密码
     * salt:盐值
     * hashIterations:加密次数
     */
    return new SimpleHash(SHA1, input, salt, ITERATIONS).toString();
  }

  /**
   * 随机获得 salt 盐字符串
   */
  public static String generateSalt() {
    SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
    return randomNumberGenerator.nextBytes().toHex();
  }

  /**
   * 生成密码字符密文和 salt
   */
  public static Map<String, String> encryptPassword(String passwordPlain) {
    Map<String, String> map = new HashMap<>();
    String salt = generateSalt();
    String password = sha1(passwordPlain, salt);
    map.put("salt", salt);
    map.put("password", password);
    return map;
  }
}
public class HelloShiro {
  @Test
  public void DigestsUtilTest() {
    Map<String, String> admin = DigestsUtil.encryptPassword("admin");
    admin.forEach((k, v) -> System.out.println(k + ": " + v));
  }
}

Realm使用散列算法

基于上面第二个 Realm 项目

接下来我们在 realm 中使用上面的密码加密,我们将上面写好的 DigestsUtil 复制到 shiro-02realm 项目,使用它创建出密码为 "123456" 的 password 密文和 salt 密文

在 SecurityServiceImpl 中我们当然是返回使用工具生成后 Map

public class SecurityServiceImpl implements SecurityService {
  @Override
  public Map<String, String> findPasswordByLoginName(String loginName) {
    return DigestsUtil.encryptPassword("123456");
  }
}

调整 DefinitionRealm 类,我们需要在构造方法中指定密码匹配的方式。

public DefinitionRealm() {
  //指定密码匹配方式为sha1
  HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(DigestsUtil.SHA1);
  //指定密码迭代次数
  matcher.setHashIterations(DigestsUtil.ITERATIONS);
  //使用父类方法使匹配方式生效
  setCredentialsMatcher(matcher);
}

然后修改 doGetAuthenticationInfo() 方法,将 Map 中的数据拿出来传入 SimpleAuthenticationInfo 中。

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  String username = (String) token.getPrincipal();
  // 实例化数据源访问接口
  SecurityService securityService = new SecurityServiceImpl();
  Map<String, String> map = securityService.findPasswordByLoginName(username);
  if (map.isEmpty()) {
    throw new UnknownAccountException("账户不存在");
  }
  String password = map.get("password");
  String salt = map.get("salt");

  // 参数1 缓存对象
  // 参数2 明文密码
  // 参数3 字节salt
  // 参数4 当前DefinitionRealm名称
  return new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(salt), getName());
}

运行登陆方法,代码同之前无区别。

@Test
public void shiroLogin() {
  //构建默认的安全示例并传入ini数据源(用户权限信息)
  DefaultSecurityManager securityManager = new DefaultSecurityManager();
  
  //设置Realm
  securityManager.setRealm(new DefinitionRealm());

  //使用 SecurityUtils 工具生效安全管理器
  SecurityUtils.setSecurityManager(securityManager);

  //使用SecurityUtils工具获得主体
  Subject subject = SecurityUtils.getSubject();

  //构建用户,指定用户名密码,进行测试
  UsernamePasswordToken user = new UsernamePasswordToken("jay", "123456");

  //测试登录操作
  subject.login(user);

  System.out.println("是否登录成功:" + subject.isAuthenticated());
}

身份授权

与开头讲到的身份认证同等重要的是身份授权,判断的是权限。

流程概括
  1. 首先调用 Subject.isPermitted/hasRole 接口,委托给 SecurityManager
  2. SecurityManager 接着会委托给内部组件 Authorizer
  3. Authorizer 再将其请求委托给我们的Realm去做;所以 Realm 才是主角。
  4. Realm 将用户请求的参数封装成权限对象。再从我们重写的 doGetAuthorizationInfo 方法中获取从数据库中查询到的权限集合。
  5. Realm 将用户传入的权限对象,与从数据库中查出来的权限对象,进行对比。如果用户传入的权限对象在从数据库中查出来的权限对象中,则返回 true,否则返回 false。

进行授权操作的前提:用户必须通过了认证。

在基于上面的代码,我们继续去学习授权认证。我们是否还记得我们在自定义的 Realm(DefinitionRealm) 中还有一个方法没学习,那就是 doGetAuthorizationInfo()。此方法的传入的参数 PrincipalCollection principals,是一个包装对象,它表示 "用户认证凭证信息"。本质就是 doGetAuthenticationInfo() 方法第一个参数,在我们例子中就是 username。你可以通过这个包装对象的 getPrimaryPrincipal() 方法拿到此值,然后再从数据库中拿到对应的角色和资源,构建 SimpleAuthorizationInfo

/**
 * 鉴权方法
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  //拿到用户认证凭证信息
  String username = (String) principals.getPrimaryPrincipal();
  //从数据库中查询对应的角色和资源
  SecurityService securityService = new SecurityServiceImpl();
  List<String> roles = securityService.findRoleByLoginName(username);
  List<String> permissions = securityService.findPermissionByLoginName(username);
  //构建资源校验
  SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
  authorizationInfo.addRoles(roles);
  authorizationInfo.addStringPermissions(permissions);
  return authorizationInfo;
}

SecurityService 中添加两个新接口

/**
 * @param loginName 登录名称
 * @return
 * @Description 查找角色按用户登录名
 */
List<String> findRoleByLoginName(String loginName);

/**
 * @param loginName 登录名称
 * @return
 * @Description 查找资源按用户登录名
 */
List<String> findPermissionByLoginName(String loginName);

实现方法如下,为了演示方便我们不去查询数据库。

@Override
public List<String> findRoleByLoginName(String loginName) {
  List<String> list = new ArrayList<>();
  list.add("admin");
  list.add("dev");
  return list;
}

@Override
public List<String> findPermissionByLoginName(String loginName) {
  List<String> list = new ArrayList<>();
  list.add("order:add");
  list.add("order:list");
  list.add("order:del");
  return list;
}

完整的一个测试检验

public class HelloShiro {

  public Subject shiroLogin(String username, String password) {
    //构建默认的安全示例并传入ini数据源(用户权限信息)
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    //设置Realm
    securityManager.setRealm(new DefinitionRealm());

    //使用 SecurityUtils 工具生效安全管理器
    SecurityUtils.setSecurityManager(securityManager);

    //使用SecurityUtils工具获得主体
    Subject subject = SecurityUtils.getSubject();

    //构建用户,指定用户名密码,进行测试
    UsernamePasswordToken user = new UsernamePasswordToken(username, password);

    //测试登录操作
    subject.login(user);

    // 返回门面
    return subject;
  }

  @Test
  public void testPermissionRealm() {
    Subject subject = shiroLogin("jay", "123456");

    //判断用户是否已经登录
    System.out.println("是否登录成功:" + subject.isAuthenticated());

    //---------检查当前用户的角色信息------------
    System.out.println("是否有管理员角色:" + subject.hasRole("admin"));

    //---------如果当前用户有此角色,无返回值。若没有此权限,则抛 UnauthorizedException------------
    try {
      subject.checkRole("coder");
      System.out.println("有coder角色");
    } catch (Exception e) {
      System.out.println("没有coder角色");
    }

    //---------检查当前用户的权限信息------------
    System.out.println("是否有查看订单列表资源:" + subject.isPermitted("order:list"));

    //---------如果当前用户有此权限,无返回值。若没有此权限,则抛 UnauthorizedException------------
    try {
      subject.checkPermissions("order:add", "order:del");
      System.out.println("有添加和删除订单资源");
    } catch (Exception e) {
      System.out.println("没有有添加和删除订单资源");
    }
  }
}

我们可以看出来,鉴权需要实现 doGetAuthorizationInfo() ,以门面 subject 中的一系列方法进行鉴权。checkXxx() 等方法会抛出异常,isXxx()hasXxx() 等方法则返回布尔值。

以上就是 Shiro 入门几个重要的概念和基本使用,写的不对的地方请多谅解。

Last modification:March 6, 2021
如果觉得我的文章对你有用,请随意赞赏