Skip to content

Shiro-安全管理框架学习和实战

仅仅为了学习使用,无商用价值

文章转载链接

权限管理和实现方案

权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户访问而且只能访问授权访问的资源。

权限管理主要分为两个部分,一是身份认证(authentication),二是授权(authorization)。

目前主流的解决方案spring security+JWT或者Shiro+JWT方案。当然也有相关团队(公司)是自己编写过滤器进行访问控制。但这并不值得推荐,除非你设计的权限管理方案能经得起生产环境的考验。

权限管理模型的话,主要有这么几种:

  • 自主访问控制(DAC: Discretionary Access Control)。一个典型的例子就是windows操作系统的权限管理。DAC 最大的缺陷就是对权限控制比较分散,不便于管理,比如说简单地对一组文件设置统一的权限并授予指定的一组用户。

  • 强制访问控制模型(MAC: Mandatory Access Control)MAC为了弥补DAC而生,MAC给用户和资源分别数字化标记其权限等级。当用户访问某一资源时,只有它的权限等级高于或等于资源的权限等级时,才能访问,否则拒绝访问。比如存在某一资源404.MP4,资源等级为1024.存在用户Ferrayman,其权限等级为256,存在用户boss,其权限等级为2048.那么,boss就能正常访问资源404.MP4,而Ferrayman则无权访问。

  • 基于角色的访问控制模型(RBAC: Role-based Access Control),即给用户分配角色,角色下对应一定的资源,用户对其角色下的资源具有访问权限。RBAC细分为RBAC0、RBAC1、RBAC2、RBAC3几个版本。

我们主要采用的是RBAC()基于角色的访问控制模型

Shiro是什么

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。使用起来相对于spring security简单。由于Shiro自己实现了一套Session方案,使得Shiro不仅支持Web应用也支持非Web应用,无缝集成。

Shiro组成

Shiro三大组件:Subject, SecurityManagerRealms

Subject

即“当前主体”。在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。 它主要由身份信息Principal和凭证Principals组成。Principal可以理解为主体在系统中的账号,且是具有唯一性的。Principals可以理解为主体在当前系统账户所对应的密码、证书。

SecurityManager

它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务,同时也是由SecurityManager来管理Subject

Realm

Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。

Realm可以理解成是安全数据源

Shiro的简单Demo

创建一个空的maven项目,并删除src目录,创建一个模块 shiro-demo maven工程

引入如下依赖:

pom.xml

xml
 <dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-core</artifactId>
     <version>1.5.3</version>
 </dependency>
 <!-- Junit -->
 <dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.12</version>
 </dependency>
 <dependency>
     <groupId>org.slf4j</groupId>
     <artifactId>slf4j-nop</artifactId>
     <version>1.7.2</version>
     <type>jar</type>
 </dependency>

shiro的配置文件是以.ini结尾的文件。之所以使用.ini格式,是因为该文件类型支持比较复杂的数据格式。主要用来存储shiro的一些权限数据。这个主要是拿来学习shiro用的,平时项目中,权限数据存储于数据库中。

配置主体的身份信息和凭证,其中 = 前面的是账号,后面的是对应的密码,这里账号zhangsan的密码就是123

src/main/resources/shiro.ini

ini
[users]
zhangsan=123
lisi=456

创建认证器 com.kuang.Authenticator

java
package com.kuang;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

/**
 * 认证器
 */
public class Authenticator {

    private DefaultSecurityManager securityManager;

    public Authenticator(){
        // 1、创建安全管理器
        this.securityManager = new DefaultSecurityManager();

        // 2、给安全管理器设置数据源
        this.securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

        // 3、注入安全管理器,并使用SecurityUtils完成认证
        SecurityUtils.setSecurityManager(securityManager);
    }


    /**
     * 认证
     * @param username
     * @param password
     */
    public void authenticate(String username,String password){
        // 4、获取当前主题
        Subject subject = SecurityUtils.getSubject();

        // 5、根据当前登陆对象身份凭证创建登陆令牌
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        // 6、认证
        //如果认证通过,则不抛出异常,否则抛出AuthenticationExceptixon异常子类
        //正式项目建议直接抛出,统一异常处理
        try{
            subject.login(token);
        }catch (IncorrectCredentialsException | UnknownAccountException e){
            System.out.println("用户名或密码不正确");
            e.printStackTrace();
        }catch (ConcurrentAccessException e){
            e.printStackTrace();
        }catch (ExcessiveAttemptsException e){
            e.printStackTrace();
        }catch (ExpiredCredentialsException e){
            e.printStackTrace();
        }catch (LockedAccountException e){
            e.printStackTrace();
        }

    }
}

com.kuang.AuthenticatorTest

java
package com.kuang;

import org.junit.Test;

public class AuthenticatorTest {

    @Test
    public void authenticate() {
        Authenticator authenticator = new Authenticator();

        authenticator.authenticate("lisi","456");
        System.out.println("登陆成功");
    }
}

当我们输入正确的账号时则提示登陆成功,当用户名或者密码错误时则提示用户或密码不正确

使用自定义Reaml完成授权和认证

上一个示例中,我们是使用shiro.ini中提供的数据源完成认证

java
 this.securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

但在实际的开发过程中,我们一般是读取数据库中的账号密码进行比对认证,这就需要我们自定义Realm进行认证

自定义Realm需要我们集成AuthorizingRealm类,并实现两个方法,认证和授权

这里的认证正常流程是从数据库中查询对应的账号比对,这里逻辑相同,故只是模拟

java
    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;

        User user = userService.queryUserByName(userToken.getUsername());

        if(user == null){ // 没有这个人
            return null; // UnknowAccountException
        }

        // 密码认证是shiro做
        return new SimpleAuthenticationInfo("",user.getPwd(),"");
    }

自定义Realm(安全数据源) com.kuang.MySqlRealm

java
package com.kuang;

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 MySqlRealm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 授权后续添加
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1、从token中获取用户名
        String name = (String) authenticationToken.getPrincipal();

        // 2、根据用户名去数据库查询相关信息,这里只是模拟
        //  User user =userService.queryNyName(name)
        //  if(user == null){
        //      return null; // 账号不存在
        //  }

        // 这里检验账号是否存在,这里是模拟假设数据库里只有张三这个账号
        if (name == "zhangsan") {
            // 如果传入的账号是zhansgan则正常放行认证
            AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("zhangsan", "123", this.getName());
            return authenticationInfo;
        }

        // 返回空默认返回UnknowAccountException
        return null;
    }
}

在认证器中指定使用自定义的Realm

com.kuang.CurrentSystemAuthenticator

java
package com.kuang;

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完成认证
 * 配合MySqlRealm完成认证
 */
public class CurrentSystemAuthenticator {

    private DefaultSecurityManager securityManager;

    public CurrentSystemAuthenticator() {
        // 创建安全管理器
        securityManager = new DefaultSecurityManager();

        // 设置自定义的Realm,安全数据源
        securityManager.setRealm(new MySqlRealm());

        // 将安全管理器设置到安全工具类中
        SecurityUtils.setSecurityManager(securityManager);
    }

    // 封装认证器对应的认证函数
    public void authenticate(String username,String password){
        // 获取当前主题,即需要验证的人或事物
        Subject subject = SecurityUtils.getSubject();

        // 根据username和password生成token
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        // 进行认证,调用Subject.login方法
        try {
            subject.login(token);
        } catch (UnknownAccountException | IncorrectCredentialsException e) {
            System.out.println("账号或者密码错误");
            e.printStackTrace();
        }

        // 打印认证状态
        if(subject.isAuthenticated()){
            System.out.println(token.getPrincipal()+"认证通过!");
        }else{
            System.out.println(token.getPrincipal()+"认证未通过!");
        }

    }
}

添加对应的测试方法

java
    /**
     * 测试通过自定义Realm安全数据源完成认证
     */
    @Test
    public void testCustomRealm(){
        CurrentSystemAuthenticator authenticator = new CurrentSystemAuthenticator();

        authenticator.authenticate("zhangsan","123"); // 输出 zhangsan认证通过!
        // authenticator.authenticate("lisi","123"); // 输出 账号或者密码错误  lisi认证未通过!
    }

使用MD5+salt对Realm进行加密

我们学会了使用自定义Realm实现shiro数据源的切换,我们可以切换成从关系数据库如MySQL中读取用户认证信息进行认证,亦可从非关系型数据库例如mongodb中读取用户认证信息进行认证。

那么,请大家思考一个问题,我们的应用程序真的安全了吗?

我们看一下这里的认证方法:

java
/**
 * 认证
 */
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    // 1. 从token中获取用户名
    String principal = (String) authenticationToken.getPrincipal();

    //2. 根据用户名查询数据库并封装成authenticationinfo对象返回(模拟)
    if (principal == "xiangbei") {
        AuthenticationInfo authInfo = new SimpleAuthenticationInfo("xiangbei","123",this.getName());
        return authInfo;
    }

    return null;
}

我们模拟从数据库当中查询出了用户的注册信息,包括账户和密码,并且这里的密码是明文的。这意味着如果我们的用户密码被泄露了(这里用户原因导致的泄露除外),那么一些不友好的朋友将可以随意的进出我们的系统。这不但让我们的应用程序变得不安全,而且还会让我们面临法律风险。

所以,我们需要对用户信息进行加密保护。对于账户密码信息,我们应该采取不可逆的加密方式。也就是说,我们对密码进行加密存储后,哪怕其获取了我们的密文,他也不能得到我们的密码明文。这样就对我们的用户信息起到了一个很好的保护作用。

MD5加密算法

MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。

特点

  • 不可逆,也就是说其本身上不能由密文推出明文,但是,如果明文比较简单常见,还是存在泄露风险,例如先生成好简单明文的密文,然后使用穷举法进行破解;

  • 对于同一个明文,无论加密多少次其密文都是一样的;

  • 生成的结果始终是一个16进制的32位字符串。

作用

  • 数字签名,例如对于一份文件,为了保证网络传输当中不发生改变,我提前对其用md5加密算法进行加密,得到一段密文。我将这份文件和密文分别发给你。你在收到文件后也对其使用md5加密一次,得到一个密文。这时,你就可以比较两个密文是否一致,如果一致,则文件没有被篡改,反之,文件已经被篡改。

  • 加密

  • 垃圾邮件筛选

salt 盐值加密策略

在上面的介绍md5加密算法时我们讲到,虽然MD5算法本身不可逆,但是如果用户采用简单的字符串作为密码的话,仍然有被暴力破解的风险。因此,为了解决这个问题,我们需要在对密码加密之前使其变得复杂化。 而加盐就是其中的一种方式。所谓的加盐就是在原密码的基础上,加上一段随机字符串。然后再加密。 当然,如果盐值随着密码一起被泄露出去,也是存在着密码被破解的风险的,我们只能做到相对安全。 为了增加破解难度,可以在加盐时采取一定的策略,例如哈希加盐、加密后多次哈希。 当然,这要在安全跟性能直接做个平衡。

shiro使用MD5+salt加密

我们可以分析下常规的使用业务流程

  • 1、用户注册或系统分配账户时,服务层在接收到账号和凭证信息后,先对凭证信息采用md5+salt进行加密处理,然后将账号、加密后的密码还有盐值存入数据库;

  • 2、用户登录请求接收后,先根据请求中的账号查询数据库: 2.1 如果没有查到,直接返回“用户名或密码错误”的类似提示 2.2 如果查到了账户信息,就执行步骤3;

  • 3、将账号和加盐后凭证封装成AuthenticationInfo对象返回给shiro,shiro执行步骤4

  • 4、对请求中的凭证进行加盐处理并执行步骤5

  • 5、对加盐后的凭证进行md5加密,并将密文跟数据库当中的存储的密文进行比对: 5.1 如果匹配成功,则认证通过 5.2 如果匹配失败,则返回“用户名或密码错误”的类似提示

实现

由于我们这里是从模拟数据库数据,所以先获取一下账户对应加密后的密码和盐值

java
    @Test
    public void testMd5(){
        String salt = UUID.randomUUID().toString();
        Md5Hash md5Hash = new Md5Hash("1234",salt,1024);
        System.out.println("密文:"+md5Hash.toHex()); // 26cd6531d2446b0541eca4f70dcdcfca
        System.out.println("盐值:"+salt); // 3b709090-9eac-4459-bf5d-62822089205a
    }

接下来需要改造自定义的Realm,并且切换掉默认的凭证匹配器

com.kuang.EncryptionRealm

java
package com.kuang;

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.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

/**
 * 通过md5以及salt加密后的Realm
 */
public class EncryptionRealm extends AuthorizingRealm {

    public EncryptionRealm() {
        // 设置凭证匹配器,修改为hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();

        // 设置算法
        credentialsMatcher.setHashAlgorithmName("md5");

        // 散列次数
        credentialsMatcher.setHashIterations(1024);

        this.setCredentialsMatcher(credentialsMatcher);
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1、从token中获取用户名
        String name = (String) authenticationToken.getPrincipal();

        // 2、这里模拟从数据库查询执行用户
        if(name == "zhangsan"){
            AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("zhangsan","26cd6531d2446b0541eca4f70dcdcfca", ByteSource.Util.bytes("3b709090-9eac-4459-bf5d-62822089205a"),this.getName());
            return authenticationInfo;
        }

        return null;
    }

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        return null;
    }
}

封装认证器,使用修改好的自定义Realm com.kuang.EncryptionAuthenticator

java
package com.kuang;

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;

/**
 * 使用Md5 加 salt加密的Realm完成认证
 */
public class EncryptionAuthenticator {

    private DefaultSecurityManager securityManager;

    public EncryptionAuthenticator() {
        // 创建安全管理器
        securityManager = new DefaultSecurityManager();

        // 设置自定义的Realm,安全数据源
        // MysqlRealm 是普通的安全数据源
        securityManager.setRealm(new EncryptionRealm());

        // 将安全管理器设置到安全工具类中
        SecurityUtils.setSecurityManager(securityManager);
    }

    // 封装认证器对应的认证函数
    public void authenticate(String username,String password){
        // 获取当前主题,即需要验证的人或事物
        Subject subject = SecurityUtils.getSubject();

        // 根据username和password生成token
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        // 进行认证,调用Subject.login方法
        try {
            subject.login(token);
        } catch (UnknownAccountException | IncorrectCredentialsException e) {
            System.out.println("账号或者密码错误");
            e.printStackTrace();
        }

        // 打印认证状态
        if(subject.isAuthenticated()){
            System.out.println(token.getPrincipal()+"认证通过!");
        }else{
            System.out.println(token.getPrincipal()+"认证未通过!");
        }

    }
}

进行测试

java
    @Test
    public void testEncryAuthenRealm(){
        EncryptionAuthenticator encryptionAuthenticator = new EncryptionAuthenticator();

        encryptionAuthenticator.authenticate("zhangsan","1234"); // zhangsan认证通过!
        encryptionAuthenticator.authenticate("zhangsan","123"); // 账号或者密码错误 zhangsan认证未通过!
    }

授权

这里的授权指的是授予某一系统的某一用户访问受保护资源的权限,分为查询、修改、插入和删除几类。没有相关权限的用户将无法访问受保护资源,具有权限的用户只能在自己权限范围内操作受保护资源。

关键对象

主体(Subject)

即指定的某一用户,这里的用户可以是浏览器、APP和第三方应用程序等。

资源(Resource)

这里的资源包括资源本身和对资源的操作。资源本身具备资源类型和资源实例两个属性。用户信息就是一个资源类型,向南的具体信息就是用户信息的实例。资源操作主要由查询、修改、删除、添加等组成。

权限

权限即是主体操作资源所需要的许可。权限本身不存在,只是某一系统的受保护资源的访问标识。脱离了资源谈权限就没有了意义。

授权流程(访问控制流程)

​前提:访问主体已经通过认证,登录到系统中。

  • 1、用户请求访问某一受保护资源

  • 2、判断用户是否具备访问权限:

    2.1 有,则执行步骤3

    2.2 没有,拒绝用户访问,并返回相应的提示

  • 3、用户访问资源

授权模型

目前面向民用系统主流的授权模式主要有基于资源的的访问控制和基于角色的访问控制

1、基于角色的访问控制RBAC (Role-Based Access Control)

其基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。

2、基于资源的访问控制RBAC(Resource-Based Access Control )

在基于角色的访问控制当中,一但用户的角色确定了,那么,其权限也就被固定下来了。也就是说具有相同角色的两个主体,他们的权限是一样的。没有办法做到动态地改变主体的权限。基于资源的访问控制就是为了解决这个问题。

权限字符串

权限字符串由资源标识符、操作符、资源实例标识符和分隔符组成,格式:资源标识符:操作符:资源实例标识符 。其含义是:对指定的一个或多个资源实例具有指定的操作权限,当资源实例为任意个时,资源实例标识符用表示。分隔符:也可以换成/等形式。操作符一般分为create、find、update、delete四类,当具备该资源的所有操作时,操作符也可以用表示。当资源标识符、操作符、资源实例标识符都为*时,代表该用户具备该系统的所有资源访问权限。

对用户01的查询权限表示为:user:find:01

*对用户具有查询权限表示为:user:find:或者user:find

对用户01具有所有权限表示为:user:*:01

shiro中的授权(访问控制)实现方式

编程式

java
Subject currentSubject = SecurityUtils.getSubject();
if (currentSubject.hasRole("admin")){
    //有权限的操作
}else{
    //无权限
}

注解式(常用)

java
@RequiresRoles("admin")
public boolean createUser(User user){
    //有权限才能执行方法
}

标签式

java
//在jsp中
<shiro:hasRole name="admin">
	//有权限才展示
</shiro:hasRole>

基于角色的访问控制(编程实现)

改造自定义Realm获取角色信息

com.kuang.EncryptionRealm

java
package com.kuang;

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.authc.credential.HashedCredentialsMatcher;
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;

/**
 * 通过md5以及salt加密后的Realm
 * 使用普通的角色授权示例
 */
public class EncryptionRealm extends AuthorizingRealm {

    public EncryptionRealm() {
        // 设置凭证匹配器,修改为hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();

        // 设置算法
        credentialsMatcher.setHashAlgorithmName("md5");

        // 散列次数
        credentialsMatcher.setHashIterations(1024);

        this.setCredentialsMatcher(credentialsMatcher);
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1、从token中获取用户名
        String name = (String) authenticationToken.getPrincipal();

        // 2、这里模拟从数据库查询执行用户
        if(name == "zhangsan"){
            AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("zhangsan","26cd6531d2446b0541eca4f70dcdcfca", ByteSource.Util.bytes("3b709090-9eac-4459-bf5d-62822089205a"),this.getName());
            return authenticationInfo;
        }

        return null;
    }


    /**
     * 授权
     * 对于授权方法,每次判断主题是否具备对应权限时都会调用
     * 因此,这里应该做缓存
     * 缓存会在后面整合SpringBoot时讲
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1、获取当前主题的身份信息,即用户名
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();

        // 2、根据主题信息查询数据库,获取主体权限(模拟),这里以账号zhangsan为例,为账号:zhangsan授予管理员权限
        SimpleAuthorizationInfo authenticationInfo = null;
        if("zhangsan".equals(primaryPrincipal)){
            authenticationInfo = new SimpleAuthorizationInfo();
            // 角色授权
            authenticationInfo.addRole("admin");
        }
        return authenticationInfo;
    }
}

封装对应的认证器 com.kuang.EncryptionAuthenticator

java
package com.kuang;

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;

/**
 * 使用Md5 加 salt加密的Realm完成认证
 */
public class EncryptionAuthenticator {

    private DefaultSecurityManager securityManager;

    public EncryptionAuthenticator() {
        // 创建安全管理器
        securityManager = new DefaultSecurityManager();

        // 设置自定义的Realm,安全数据源
        // MysqlRealm 是普通的安全数据源
        securityManager.setRealm(new EncryptionRealm());

        // 将安全管理器设置到安全工具类中
        SecurityUtils.setSecurityManager(securityManager);
    }

    // 封装认证器对应的认证函数
    public void authenticate(String username,String password){
        // 获取当前主题,即需要验证的人或事物
        Subject subject = SecurityUtils.getSubject();

        // 根据username和password生成token
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        // 进行认证,调用Subject.login方法
        try {
            subject.login(token);
        } catch (UnknownAccountException | IncorrectCredentialsException e) {
            System.out.println("账号或者密码错误");
            e.printStackTrace();
        }

        // 打印认证状态
        if(subject.isAuthenticated()){
            System.out.println(token.getPrincipal()+"认证通过!");
        }else{
            System.out.println(token.getPrincipal()+"认证未通过!");
        }

    }
}

进行测试 com.kuang.AuthenticatorTest

java
    // ====================== 测试角色授权 ========================
    /**
     * 测试主体是否包含某一角色
     */
    @Test
    public void testHasRole(){
        // 进行认证和授权
        EncryptionAuthenticator encryptionAuthenticator = new EncryptionAuthenticator();
        encryptionAuthenticator.authenticate("zhangsan","1234");


        // 测试是否授权正常
        Subject subject = SecurityUtils.getSubject();
        // 主体具备某一角色时即可访问
        if(subject.hasRole("admin")){
            System.out.println(subject.getPrincipal() + "具有 admin 角色");
        }

        // 输出 zhangsan具有 admin 角色
    }

    /**
     * 适用于只要具有其中一个角色即可即可的情况
     */
    @Test
    public void testHasRoles(){
        // 进行认证和授权
        EncryptionAuthenticator encryptionAuthenticator = new EncryptionAuthenticator();
        encryptionAuthenticator.authenticate("zhangsan","1234");

        // 测试是否授权正常
        Subject subject = SecurityUtils.getSubject();
        boolean[] booleans = subject.hasRoles(Arrays.asList("admin","user"));
        System.out.println("booleans:"+booleans.toString());
        for(boolean b : booleans){
            if(b){
                System.out.println(subject.getPrincipal() + "具有访问权限");
                break;
            }
        }
        // 输出 zhangsan具有访问权限
    }

    /**
     * 适用于只有具备所有角色才可访问
     */
    @Test
    public void testHasAllRoles(){
        // 进行认证和授权
        EncryptionAuthenticator encryptionAuthenticator = new EncryptionAuthenticator();
        encryptionAuthenticator.authenticate("zhangsan","1234");

        // 测试是否授权正常
        Subject subject = SecurityUtils.getSubject();
        boolean b = subject.hasAllRoles(Arrays.asList("admin","user"));
        System.out.println("b:"+b);
        if(b){
            System.out.println(subject.getPrincipal()+"具备访问权限");
        }else{
            System.out.println(subject.getPrincipal()+"没有访问权限");
        }

        // 输出 zhangsan具备访问权限
        // 如果再为当前用户授权User角色后 输出:具备访问权限
    }

基于资源的访问控制(编程实现)

改造自定义Realm com.kuang.ResourceRealm

java
package com.kuang;

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.authc.credential.HashedCredentialsMatcher;
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;

/**
 * 通过md5以及salt加密后的Realm
 * 使用资源授权示例
 */
public class ResourceRealm extends AuthorizingRealm {

    public ResourceRealm() {
        // 设置凭证匹配器,修改为hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();

        // 设置算法
        credentialsMatcher.setHashAlgorithmName("md5");

        // 散列次数
        credentialsMatcher.setHashIterations(1024);

        this.setCredentialsMatcher(credentialsMatcher);
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1、从token中获取用户名
        String name = (String) authenticationToken.getPrincipal();

        // 2、这里模拟从数据库查询执行用户
        if(name == "zhangsan"){
            AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("zhangsan","26cd6531d2446b0541eca4f70dcdcfca", ByteSource.Util.bytes("3b709090-9eac-4459-bf5d-62822089205a"),this.getName());
            return authenticationInfo;
        }

        return null;
    }


    /**
     * 授权
     * 对于授权方法,每次判断主题是否具备对应权限时都会调用
     * 因此,这里应该做缓存
     * 缓存会在后面整合SpringBoot时讲
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1、获取当前主题的身份信息,即用户名
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();

        // 2、根据主题信息查询数据库,获取主体权限(模拟),这里以账号zhangsan为例,为账号:zhangsan授予具备的权限
        SimpleAuthorizationInfo authenticationInfo = null;

        if("zhangsan".equals(primaryPrincipal)){
            authenticationInfo = new SimpleAuthorizationInfo();
            // 资源授权

            // 具备user的所有权限
            authenticationInfo.addStringPermission("user:*");

            // 具备product的创建权限
            authenticationInfo.addStringPermission("product:create");
        }
        return authenticationInfo;
    }
}

改造自定义认证器 com.kuang.ResourceAuthenticator

java
package com.kuang;

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;

/**
 * 使用Md5 加 salt加密的Realm完成认证
 */
public class ResourceAuthenticator {

    private DefaultSecurityManager securityManager;

    public ResourceAuthenticator() {
        // 创建安全管理器
        securityManager = new DefaultSecurityManager();

        // 设置自定义的Realm,安全数据源
        // MysqlRealm 是普通的安全数据源
        // 使用资源授权demo realm
        securityManager.setRealm(new ResourceRealm());

        // 将安全管理器设置到安全工具类中
        SecurityUtils.setSecurityManager(securityManager);
    }

    // 封装认证器对应的认证函数
    public void authenticate(String username,String password){
        // 获取当前主题,即需要验证的人或事物
        Subject subject = SecurityUtils.getSubject();

        // 根据username和password生成token
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        // 进行认证,调用Subject.login方法
        try {
            subject.login(token);
        } catch (UnknownAccountException | IncorrectCredentialsException e) {
            System.out.println("账号或者密码错误");
            e.printStackTrace();
        }

        // 打印认证状态
        if(subject.isAuthenticated()){
            System.out.println(token.getPrincipal()+"认证通过!");
        }else{
            System.out.println(token.getPrincipal()+"认证未通过!");
        }

    }
}

com.kuang.AuthenticatorTest

java
// ====================== 测试资源授权 ========================

    /**
     * 测试当前用户是否具备具体资源权限是否可以访问
     */
    @Test
    public void testHasPermission(){
        // 进行认证和授权
        ResourceAuthenticator authenticator = new ResourceAuthenticator();
        authenticator.authenticate("zhangsan","1234");

        // 测试是否授权正常
        // 当前用户具备用户模块的全部权限
        Subject subject = SecurityUtils.getSubject();
        if(subject.isPermitted("user:create")){
            System.out.println(subject.getPrincipal() + "具有 创建用户 权限");
        }

        if(subject.isPermitted("product:create")){
            System.out.println(subject.getPrincipal() + "具有 创建产品 权限");
        }

        if(subject.isPermitted("product:delete")){
            System.out.println(subject.getPrincipal() + "具有 删除产品 权限");
        }

        // 输出:
        // zhangsan具有 创建用户 权限
        // zhangsan具有 创建产品 权限
    }

    /**
     * 测试当前用户是否具备其中某一个资源权限
     */
    @Test
    public void testIsPermitted(){
        // 进行认证和授权
        ResourceAuthenticator authenticator = new ResourceAuthenticator();
        authenticator.authenticate("zhangsan","1234");

        // 测试是否授权正常
        // 当前用户是否具备用户的查询和创建权限
        Subject subject = SecurityUtils.getSubject();
        boolean[] permitted = subject.isPermitted("user:create","user:find");
        for(boolean b: permitted){
            if(b){
                System.out.println(subject.getPrincipal() + "具有访问权限");
                break;
            }
        }

        // 输出:zhangsan具有访问权限
    }

    /**
     * 测试当前用户需要具备列出的所有资源权限才能访问
     */
    @Test
    public void testIsPermittedAll(){
        // 进行认证和授权
        ResourceAuthenticator authenticator = new ResourceAuthenticator();
        authenticator.authenticate("zhangsan","1234");

        // 测试是否授权正常
        Subject subject = SecurityUtils.getSubject();
        boolean b = subject.isPermittedAll("product:*");
        if(b){
            System.out.println(subject.getPrincipal() + "具备 product 模块所有权限");
        }else{
            System.out.println(subject.getPrincipal() + "没有 product 模块权限");
        }

        // 输出:zhangsan没有 product 模块权限
    }

SpringBoot整合Shiro实现动态权限管理

前面我们已经学到了shiro的认证、授权、加盐,接下来我们将整合SpringbootMybatisShiro,同时Shiro会采用Md5Salt的加密方式进行加密。

常规业务鉴权流程分析

  • 1、用户通过客户端(浏览器、手机App、小程序)发起请求

  • 2、ShiroFilter拦截请求并判断请求访问的资源是否为受保护资源:

  • 2.1、是,则执行步骤3

  • 2.2、不是,则直接放行

  • 3、判断用户是否已通过认证:

  • 3.1 是 ,则执行步骤4

  • 3.2 否,将用户请求重定向到认证页面,让用户先认证

  • 4、获取用户权限信息和访问资源所需要的权限信息进行比对:

  • 4.1 用户具备访问权限,则放行

  • 4.2 用户不具备权限,返回403的相应提示

数据库表分析设计

  • 用户表(user) 记录用户信息
  • 角色表(role) 记录角色信息
  • 用户角色关联表(user_role) 记录用户关联的角色,多对多的关系
  • 资源表(resource) 资源表,记录资源信息,例如前端按钮资源,菜单资源,接口访问资源信息
  • 角色资源表(role_resource) 角色资源表,记录角色信息关联的资源信息,多对多的关系

资源表说明

我们将系统当中的菜单、按钮、后端接口都抽象成系统的资源数据,例如前端后台管理系统产品模块的数据查询按钮我们可以设置资源resource_uri/product/get/list,是否具有产品模块列表查询接口权限我们可以设置resource_permission_tagproduct:get,方便细粒度的对用户权限进行控制。

环境搭建

pom.xml

xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.kuang</groupId>
    <artifactId>sprinboot-shiro-auth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sprinboot-shiro-auth</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--    mybatis相关依赖    -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <!--      spring-web相关依赖      -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <!--     mysql驱动       -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <!--     lombox简化开发       -->
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <!--     spring-boot-test 测试依赖       -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <!--    shiro相关依赖    -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.6.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

添加依赖以后,我们需要分别针对UserRoleResource模块完成CURD代码,也就是SpringBoot整合Mybatis,并完成针对三张表的增删改查模块。

整合mybatis首先需要修改application

yml
server:
  port: 3001
  servlet:
    context-path: /shiro

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/shiro_demo1?serverTimezone=GMT%2B8

mybatis:
  configuration:
    map-underscore-to-camel-case: true #用于将java中的驼峰格式转化为数据库表中的下划线格式

SpringBoot启动类上添加MapperScan扫描注解 com.kuang.SprinbootShiroAuthApplication

java
package com.kuang;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;


@SpringBootApplication
@MapperScan(basePackages = {"com.kuang.**.dao"})
public class SprinbootShiroAuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(SprinbootShiroAuthApplication.class, args);
    }

}

完成用户、角色和资源模块业务代码

整个动态权限过程需要用到与数据库交互,我们添加必要的服务

用户模块

com.kuang.entity.User

java
package com.kuang.entity;


import java.io.Serializable;

/**
 * 用户表(User)实体类
 *
 * @since 2020-10-06 15:39:02
 */

public class User implements Serializable {
    private static final long serialVersionUID = -80775848048368632L;


    private Long userId;

    private String username;

    private String password;

    private String salt;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getSalt() {
        return salt;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }
}

com.kuang.server.user.dao.IUserDao

java
package com.kuang.server.user.dao;

import com.kuang.entity.User;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

@Repository
public interface IUserDao {
    @Select("select * from user where username = #{username}")
    User findByUsername(String username);
}

com.kuang.server.user.service.IUserService

java
package com.kuang.server.user.service;

import com.kuang.entity.User;

public interface IUserService {
    User findByUsername(String principal);
}

com.kuang.server.user.service.impl.IUserServiceImpl

java
package com.kuang.server.user.service.impl;

import com.kuang.entity.User;
import com.kuang.server.user.dao.IUserDao;
import com.kuang.server.user.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class IUserServiceImpl implements IUserService {

    @Autowired
    private IUserDao userDao;

    @Override
    public User findByUsername(String principal) {
        if(principal == null || "".equals(principal)){
            return null;
        }

        return userDao.findByUsername(principal);
    }
}

角色模块

com.kuang.entity.Role

java
package com.kuang.entity;


import java.io.Serializable;

/**
 * 角色表(Role)实体类
 *
 * @author 赖柄沣 bingfengdev@aliyun.com
 * @since 2020-10-06 15:37:16
 */

public class Role implements Serializable {
    private static final long serialVersionUID = 534384732226656028L;


    private Long roleId;


    private String roleName;


    private String roleDesc;


    public Long getRoleId() {
        return roleId;
    }

    public void setRoleId(Long roleId) {
        this.roleId = roleId;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getRoleDesc() {
        return roleDesc;
    }

    public void setRoleDesc(String roleDesc) {
        this.roleDesc = roleDesc;
    }

    @Override
    public String toString() {
        return "Role{" +
                "roleId=" + roleId +
                ", roleName='" + roleName + '\'' +
                ", roleDesc='" + roleDesc + '\'' +
                '}';
    }
}

com.kuang.server.role.dao.IRoleDao

java
package com.kuang.server.role.dao;

import com.kuang.entity.Role;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface IRoleDao {

    @Select("select role_id from user_role  where username=#{username}")
    List<Long> findByUsername(String username);

    @Select("select * from role where role_id in (1)")
    List<Role> findByRoleIds(@Param("roleIdList") List<Long> roleIdList);

    @Select("select * from role where role_id in (" +
            " select role_id from user_role where username=#{username})")
    List<Role> findAllRoleByUsername(String username);

}

com.kuang.server.role.service.IRoleService

java
package com.kuang.server.role.service;


import com.kuang.entity.Role;

import java.util.List;

/**
 * @version 1.0
 * @date 2020/10/6 15:24
 */
public interface IRoleService {
    List<Role> findByUsername(String username);
}

com.kuang.server.role.service.impl.RoleServiceImpl

java
package com.kuang.server.role.service.impl;

import com.kuang.entity.Role;
import com.kuang.server.role.dao.IRoleDao;
import com.kuang.server.role.service.IRoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @version 1.0
 * @date 2020/10/6 15:25
 */
@Service
public class RoleServiceImpl implements IRoleService {

    @Autowired
    private IRoleDao roleDao;

    @Override
    public List<Role> findByUsername(String username) {
        if (username == null) {
            return null;
        }

        List<Role> roleList = roleDao.findAllRoleByUsername(username);

        return roleList;
    }
}

资源模块

com.kuang.entity.Resource

java
package com.kuang.entity;


import java.io.Serializable;

/**
 * 权限菜单表(资源表)(Resource)实体类
 *
 * @since 2020-10-07 00:23:46
 */

public class Resource implements Serializable {
    private static final long serialVersionUID = -80603043571024051L;
    /**
     * 主键
     */

    private Long resourceId;
    /**
     * 资源名称
     */

    private String resourceName;
    /**
     * 资源uri
     */

    private String resourceUri;
    /**
     * 父资源id
     */

    private Long resourceFartherId;
    /**
     * 资源图标路径
     */

    private String resourceIcoUrl;
    /**
     * 描述
     */

    private String resourceDesc;
    /**
     * 权限标识符
     */

    private String resourcePermissionTag;

    public Long getResourceId() {
        return resourceId;
    }

    public void setResourceId(Long resourceId) {
        this.resourceId = resourceId;
    }

    public String getResourceName() {
        return resourceName;
    }

    public void setResourceName(String resourceName) {
        this.resourceName = resourceName;
    }

    public String getResourceUri() {
        return resourceUri;
    }

    public void setResourceUri(String resourceUri) {
        this.resourceUri = resourceUri;
    }

    public Long getResourceFartherId() {
        return resourceFartherId;
    }

    public void setResourceFartherId(Long resourceFartherId) {
        this.resourceFartherId = resourceFartherId;
    }

    public String getResourceIcoUrl() {
        return resourceIcoUrl;
    }

    public void setResourceIcoUrl(String resourceIcoUrl) {
        this.resourceIcoUrl = resourceIcoUrl;
    }

    public String getResourceDesc() {
        return resourceDesc;
    }

    public void setResourceDesc(String resourceDesc) {
        this.resourceDesc = resourceDesc;
    }

    public String getResourcePermissionTag() {
        return resourcePermissionTag;
    }

    public void setResourcePermissionTag(String resourcePermissionTag) {
        this.resourcePermissionTag = resourcePermissionTag;
    }

    @Override
    public String toString() {
        return "Resource{" +
                "resourceId=" + resourceId +
                ", resourceName='" + resourceName + '\'' +
                ", resourceUri='" + resourceUri + '\'' +
                ", resourceFartherId=" + resourceFartherId +
                ", resourceIcoUrl='" + resourceIcoUrl + '\'' +
                ", resourceDesc='" + resourceDesc + '\'' +
                ", resourcePermissionTag='" + resourcePermissionTag + '\'' +
                '}';
    }
}

com.kuang.server.resource.dao.IResourceDao

java
package com.kuang.server.resource.dao;

import com.kuang.entity.Resource;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @version 1.0
 * @date 2020/10/6 15:28
 */

@Repository
public interface IResourceDao {

    @Select("<script>" +
            "select resource_id from role_resource where role_id in" +
            "<foreach item='item' index='index' collection='roleIdList' open='(' separator=',' close=')'> " +
            "#{item}" +
            "</foreach>" +
            "</script>")
    List<Long> findResourceIdsByRoleIds(@Param("roleIdList") List<Long> roleIdList);

    @Select({"<script>",
            "select * from resource where resource_id in ",
            "<foreach item='item' index='index' collection='resourceIds' open='(' separator=',' close=')'> ",
            "#{item}",
            "</foreach>",
            "</script>"})
    List<Resource> findAllById(@Param("resourceIds") List<Long> resourceIds);
}

com.kuang.server.resource.service.IResourceService

java
package com.kuang.server.resource.service;


import com.kuang.entity.Resource;

import java.util.List;

/**
 * @version 1.0
 * @date 2020/10/6 15:26
 */
public interface IResourceService {
    List<Resource> findByRoleIds(List<Long> roleIdList);
}

com.kuang.server.resource.service.impl.ResourceServiceImpl

java
package com.kuang.server.resource.service.impl;


import com.kuang.entity.Resource;
import com.kuang.server.resource.dao.IResourceDao;
import com.kuang.server.resource.service.IResourceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @version 1.0
 * @date 2020/10/6 15:27
 */
@Service
public class ResourceServiceImpl implements IResourceService {

    @Autowired
    private IResourceDao resourceDao;

    @Override
    public List<Resource> findByRoleIds(List<Long> roleIdList) {
        if (roleIdList == null || roleIdList.isEmpty()) {
            return null;
        }

        List<Long> resourceIds = resourceDao.findResourceIdsByRoleIds(roleIdList);
        if (resourceIds.size() == 0) {
            return null;
        }

        return resourceDao.findAllById(resourceIds);

    }
}

前置工作已经完成,接下来需要完成关于Shiro的配置

Shiro配置

自定义Realm

自定义Realm安全数据源,Realm的主要功能是完成授权和认证,通过读取数据库完成认证和授权

com.kuang.realm.MyRealm

java
package com.kuang.realm;

import com.kuang.entity.Resource;
import com.kuang.entity.Role;
import com.kuang.entity.User;
import com.kuang.server.resource.service.IResourceService;
import com.kuang.server.role.service.IRoleService;
import com.kuang.server.user.service.IUserService;
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;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private IUserService userService;

    @Autowired
    private IRoleService roleService;

    @Autowired
    private IResourceService resourceService;

    /**
     * 授权
     * 这个方法每次访问shiro里面配置的受保护的资源时就会调用
     * 因此,需要做缓存
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取当前用户名
        String username = (String) principalCollection.getPrimaryPrincipal();
        // 根据用户名称获取当前用户关联的角色
        List<Role> roleList = roleService.findByUsername(username);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        // ===========角色授权============
        // 循环为当前认证通过后的用户添加角色
        for(Role role : roleList){
            authorizationInfo.addRole(role.getRoleName());
        }


        // ===========访问资源授权============
        // 保存当前用户关联的角色id
        List<Long> roleIdList = new ArrayList<>();
        for(Role role : roleList){
            roleIdList.add(role.getRoleId());
        }
        // 根据roleList 获取当前用户有权访问的资源权限
        List<Resource> resourceList = resourceService.findByRoleIds(roleIdList);
        for(Resource resource : resourceList){
            authorizationInfo.addStringPermission(resource.getResourcePermissionTag());
        }

        return authorizationInfo;
    }

    /**
     * 认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        if(token == null){
            return null;
        }

        String principal = (String) token.getPrincipal();
        // 通过用户名获取用户信息
        User user = userService.findByUsername(principal);

        // 将盐值,密码等信息交由shiro去认证
        SimpleAuthenticationInfo simpleAuthorizationInfo = new SimpleAuthenticationInfo(
                user.getUsername(),
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()),
                getName());

        return simpleAuthorizationInfo;
    }
}

配置ShiroCofnig

我们添加shiro配置文件ShiroConfigShiroConfig配置文件主要功能是创建SecurityManager、安全数据源Realm、以及进行创建Filter拦截器,对需要访问的资源添加权限等级认证。

com.kuang.config.ShiroConfig

java
package com.kuang.config;

import com.kuang.realm.MyRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;

@Configuration
public class ShiroConfig {

    /**
     * 创建shiro web 应用下的安全管理器
     * @param realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager getSecurityManager(Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(realm);

        SecurityUtils.setSecurityManager(securityManager);

        return securityManager;
    }

    /**
     * 创建安全数据源
     * @return
     */
    @Bean
    public Realm getRealm(){
        // 设置凭证匹配器,修改为hash凭证匹配器
        HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher();

        // 设置算法
        myCredentialsMatcher.setHashAlgorithmName("md5");

        // 散列次数
        myCredentialsMatcher.setHashIterations(1024);
        MyRealm realm = new MyRealm();
        realm.setCredentialsMatcher(myCredentialsMatcher);

        return realm;
    }

    /**1
     * 创建ShiroFilter拦截器
     * @param securityManager
     * @return
     */
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 配置不拦截路径和拦截路径,顺序不能反
        HashMap<String,String> map = new HashMap<>(5);

        // anno 指定url可以匿名访问
        // authc authc 需要登陆后才能访问
        map.put("/authc/**","anon");
        map.put("/login.html","anon");
        map.put("/js/**","anon");
        map.put("/css/**","anon");

        // 其他资源都需要登陆后才可以访问
        map.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        // 覆盖默认的登录url,当还未进行认证而访问认证资源时会调用这个地址的服务
        shiroFilterFactoryBean.setLoginUrl("/authc/unauthc");

        return shiroFilterFactoryBean;
    }
}

封装认证模块

我们已经完成了ShiroFilter以及配置了自定义Realm,接下来我们可以封装认证模块,提供给外部服务调用

VO层 com.kuang.web.authc.pojo.vo.LoginVO

java
package com.kuang.web.authc.pojo.vo;

import lombok.Data;

import java.io.Serializable;

/**
 * 认证请求参数
 */
@Data
public class LoginVO implements Serializable {

    private String username;

    private String password;
}

servicecom.kuang.server.authc.AuthcService

java
package com.kuang.server.authc;

import com.kuang.web.authc.pojo.vo.LoginVO;

import javax.naming.AuthenticationException;

public interface AuthcService {
    boolean login(LoginVO loginVO) throws AuthenticationException;
}

com.kuang.server.authc.impl.AuthcServiceImpl

java
package com.kuang.server.authc.impl;

import com.kuang.server.authc.AuthcService;
import com.kuang.web.authc.pojo.vo.LoginVO;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;

import javax.naming.AuthenticationException;

/**
 * 用户授权登录,调用Subject.login 进行登录
 */
@Service
public class AuthcServiceImpl implements AuthcService {
    @Override
    public boolean login(LoginVO loginVO) throws AuthenticationException {
        if(loginVO == null){
            return false;
        }

        if(loginVO.getUsername() == null || "".equals(loginVO.getUsername())){
            return false;
        }

        if(loginVO.getPassword() == null || "".equals(loginVO.getPassword())) {
            return false;
        }

        // 创建主题
        Subject subject= SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(loginVO.getUsername(),loginVO.getPassword());

        subject.login(token);

        return true;
    }
}

controllercom.kuang.web.authc.controller.AuthcController

java
package com.kuang.web.authc.controller;

import com.kuang.server.authc.AuthcService;
import com.kuang.web.authc.pojo.vo.LoginVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.naming.AuthenticationException;

@RestController
@RequestMapping("/authc")
public class AuthcController {

    @Autowired
    private AuthcService authcService;

    @PostMapping("/login")
    public boolean login(@RequestBody LoginVO loginVO) throws AuthenticationException {
        return  authcService.login(loginVO);
    }

    @GetMapping("/unauthc")
    public String unauthc(){
        return "请先登录";
    }

}

测试

现在整个模块已经完成,我们接下来可以进行简单的测试

我们添加一个产品模块,并提供几个接口进行测试

com.kuang.web.product.ProductController

java
package com.kuang.web.product;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/product")
public class ProductController {

    @RequiresPermissions("product:get")
    @GetMapping("/get/list")
    public String getProductList(){
        return "productList";
    }

    @RequiresPermissions("product:delete")
    @GetMapping("/delete")
    public String deleteProduct(){
        return "删除产品数据";
    }

    @RequiresRoles("user")
    @GetMapping("/add")
    public String testAdmin(){
        return "管理员添加用户";
    }

    @RequiresRoles("admin")
    @GetMapping("/update")
    public String testUpdateAdmin(){
        return "管理员修改用户";
    }
}

对于注解实现访问控制,shiro主要有两个注解:RequiresPermissionsRequiresRoles。均可以用在类和方法上。具体用在哪可以根据自己的系统权限划分粒度决定。

对于这两个注解,有两个参数

value :分别对应permission的权限字符串值和role的角色名称;

logical:逻辑运算符。这是一个枚举类型,有ANDOR两个值。当使用AND时表示需要满足所有传入的value值,OR表示仅需满足一个value 即可。默认为AND

未登录的情况

进行登录

上次更新于: