Shiro

权限管理概述

权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源,不多不少。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。 很多人常将“用户身份认证”、“密码加密”、“系统管理”等概念与权限管理概念混淆。

在权限管理中使用最多的还是功能权限管理中的基于角色访问控制(RBAC,Role Based Access Control)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZF2TY5ZS-1622731059992)(image\image-20200707151958178.png)]

当项目中需要使用权限管理的时候,我们可以选择自己去实现(前面的课程中所实现的 RBAC 系统),也可以选择使用第三方实现好的框架去实现,他们孰优孰劣这就需要看大家在项目中具体的需求了。

这里我们介绍两种常用的权限管理框架:

1. Apache Shiro

Apache Shiro 是一个强大且易用的 Java 安全框架,使用 Apache Shiro 的人越来越多,它可实现身份验证、授权、密码和会话管理等功能。

2. Spring Security

Spring Security 也是目前较为流行的一个安全权限管理框架,它与 Spring 紧密结合在一起。

ShiroSpring Security 比较

Shiro 比 Spring Security更容易上手使用和理解,Shiro 可以不跟任何的框架或者容器绑定,可独立运行,而Spring Security 则必须要有Spring环境, Shiro 可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。

Shiro 概述

Shiro 能做什么事情

Shiro 可以帮助我们完成:认证授权、加密、会话管理、与 Web 集成、缓存等。

Shiro 架构

Shiro 主要组件包括:Subject,SecurityManager,Authenticator,Authorizer,SessionManager,CacheManager,Cryptography,Realms。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXVSU3n3-1622731059995)(image\image-20200707152041577.png)]

  • Subject(用户): 访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体; Subject 一词是一个专业术语,其基本意思是“当前的操作用户”。

    在程序任意位置可使用:Subject currentUser = SecurityUtils.getSubject() 获取到subject主体对象,类似 Employee user = UserContext.getUser()

  • SecurityManager(安全管理器):它是 shiro 功能实现的核心,负责与后边介绍的其他组件(认证器/授权器/缓存控制器)进行交互,实现 subject 委托的各种功能。有点类似于SpringMVC 中的 DispatcherServlet 前端控制器,负责进行分发调度。

  • Realms(数据源): Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。;可以把Realm 看成 DataSource,即安全数据源。执行认证(登录)和授权(访问控制)时,Shiro 会从应用配置的 Realm 中查找相关的比对数据。以确认用户是否合法,操作是否合理。

  • Authenticator(认证器): 用于认证,从 Realm 数据源取得数据之后进行执行认证流程处理。

  • Authorizer(授权器):用户访问控制授权,决定用户是否拥有执行指定操作的权限。

  • SessionManager (会话管理器):Shiro 与生俱来就支持会话管理,这在安全类框架中都是独一无二的功能。即便不存在 web 容器环境,shiro 都可以使用自己的会话管理机制,提供相同的会话 API。

  • CacheManager (缓存管理器):用于缓存认证授权信息等。

  • Cryptography(加密组件): Shiro 提供了一个用于加密解密的工具包。

Shiro 认证

认证的过程即为用户的身份确认过程,所实现的功能就是我们所熟悉的登录验证,用户输入账号和密码提交到后台,后台通过访问数据库执行账号密码的正确性校验。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmKLb9aO-1622731059996)(image\image-20200707152052684.png)]

前面我们介绍过,Shiro 不仅在 web 环境中可以使用,在 JavaSE 中一样可以完美的实现相关的功能,下面我们先来看看在 JavaSES 环境中它是如何实现认证功能的。

基于 ini 的认证

准备工作:

  1. 导入基本的 jar 包
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.2</version>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.22</version>
    <scope>provided</scope>
</dependency>
  1. 编写 ini 配置文件:shiro-authc.ini

    shiro默认支持的是ini配置的方式,这里只是为了方便,项目中还是会选择xml

#用户的身份、凭据
[users]
zhangsan=555 
dafei=666
  1. 使用 Shiro 相关的 API 完成身份认证
@Test
public void testLogin(){
    //创建Shiro的安全管理器,是shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
    IniRealm iniRealm = new IniRealm("classpath:shiro-authc.ini");
    securityManager.setRealm(iniRealm);
    //把安全管理器注入到当前的环境中
    SecurityUtils.setSecurityManager(securityManager);
    //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
    Subject subject = SecurityUtils.getSubject();
    System.out.println("认证状态:"+subject.isAuthenticated());
    //创建令牌(携带登录用户的账号和密码)
    UsernamePasswordToken token = new UsernamePasswordToken("dafei","666");
    //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());
    //登出
    //System.out.println("认证状态:"+subject.isAuthenticated());
}

如果输入的身份和凭证和 ini 文件中配置的能够匹配,那么登录成功,登录状态为true,反之登录状态为false。

登录失败一般存在两种情况:

  1. 账号错误 org.apache.shiro.authc.UnknownAccountException
  2. 密码错误 org.apache.shiro.authc.IncorrectCredentialsException

Shiro 认证流程源码分析

详细流程可自行debug,或参考源码分析图

1、调用subject.login方法进行登录,其会自动委托给securityManager.login方法进行登录;
2、securityManager通过Authenticator(认证器)进行认证;
3、Authenticator的实现类ModularRealmAuthenticator调用realm从ini配置文件取用户真实的账号和密码,这里使用的是IniRealm(shiro自带,相当于数据源);
4、IniRealm先根据token中的账号去ini中找该账号,如果找不到则给ModularRealmAuthenticator返回null,如果找到则匹配密码,匹配密码成功则认证通过。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GWVEOUvS-1622731059997)(image\image-20200707152208225.png)]

自定义 Realm

自定义 Realm 在实际开发中使用非常多,应该我们需要使用的账户信息通常来自程序或者数据库中, 而不是前面使用到的 ini 配置文件。

在 AuthenticatingRealm 中调用doGetAuthenticationInfo方法来获取,如果返回的 info不等于空,说明账号存在,才会进行密码校验,如果不存在则直接抛出UnknownAccountException异常。

所以,如果我们要自定义 Realm,应该覆写 doGetAuthenticationInfo()方法,然后在该方法中实现账号的校验,并返回 AuthenticationInfo 对象给上层调用者 AuthenticatingRealm 做进一步的校验。

Realm 的继承体系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BlCw9kuW-1622731059999)(image\image-20200707163237123.png)]

一共有三个Realm接口的实现类
CachingRealm: 缓存 一个功能;
AuthenticatingRealm: 缓存,认证 二个功能;
AuthorizingRealm: 缓存,认证,授权 三个功能;
  1. 自定义 Realm

在继承体系中的每个类所能够实现的功能不一样,在后面的开发中,我们通常需要使用到缓存,认证和授权所有的功能,所以选择继承 AuthorizingRealm。

// 自定义数据源
public class EmployeeRealm extends AuthorizingRealm {
    //提供认证信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
        throws AuthenticationException {
        // 获取页面传入的账号
        String name = (String) token.getPrincipal();
        // 暂且使用假数据来模拟数据库中真实的账号和密码
        Employee employee = new Employee();
        employee.setName("admin");
        employee.setPassword("1");
        //获取token中需要登录的账号名
        Object username = token.getPrincipal();
        //如果账号存在,则返回一个 AuthenticationInfo 对象
        if(username.equals(employee.getName())){
            return new SimpleAuthenticationInfo(
                employee,//身份对象,与主体subject绑定在一起的对象,暂时无用但后续要用 
                employee.getPassword(),//该账号真正的密码,传给shiro做密码校验的
                this.getName()//当前 Realm 的名称,暂时无用,不需纠结
            );
        }
        return null;
    }
	//提供授权信息
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
}
  1. 在注册自定义的Realm,并设置到SecurityManager对象中

修改之前创建的测试方法

@Test
public void testLogin(){
    //创建Shiro的安全管理器,是shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
    //IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
	//此处用自定义数据源
    EmployeeRealm realm = new EmployeeRealm();
    securityManager.setRealm(realm);
    //把安全管理器注入到当前的环境中
    SecurityUtils.setSecurityManager(securityManager);
    //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
    Subject subject = SecurityUtils.getSubject();
    System.out.println("认证状态:"+subject.isAuthenticated());

    //创建令牌(携带登录用户的账号和密码)
    UsernamePasswordToken token = new UsernamePasswordToken("admin","1");
    //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());

    //登出
    //subject.logout();
    //System.out.println("认证状态:"+subject.isAuthenticated());
}

运行之前的测试代码,现在使用的是我们自定义的 Realm 完成账号的校验。

在实际开发中,上面账户信息的假数据应该是从数据库中查询得到,下面我们将 Shiro 认证应用到CRM 项目中

项目一中集成 Shiro 认证

添加依赖

<properties>
	<shiro.version>1.5.2</shiro.version>
</properties>
<!--shiro 核心-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 的 Web 模块-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 和 Spring 集成-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 底层使用的 ehcache 缓存-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 依赖的日志包-->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<!--Freemarker 的 shiro 标签库-->
<dependency>
    <groupId>net.mingsoft</groupId>
    <artifactId>shiro-freemarker-tags</artifactId>
    <version>1.0.1</version>
</dependency>

首先思考下面几个问题

  1. 访问系统资源的时候,需要先判断用户是否有认证 , 那么shiro是如何拦截请求进行处理的呢?

  2. 在 SE 环境中,我们使用的是 DefaultSecurityManager 来实现认证的控制,那么现在再 EE 环境中应该使用哪一个 SecurityManager 来实现认证控制呢?

  3. 前面我们在测试方法中指定自定义的 Realm,但是如果需要查询数据库,意味着要结合我们的mapper或service来查询,那该如何把Realm交给spring管理呢?

以上几个问题就是我们继承 Shiro 认证功能时需要解决的问题,来吧,一个个搞定他!


配置代理过滤器

在访问的时候,需要做一系列的预处理操作,Shiro是选择使用filter过滤器来进行拦截的,因为Shiro不依赖Spring容器,所以当没有springmvc时意味着不能用拦截器,但过滤器则不同,只要是web项目都可以使用。

web.xml

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

这里使用了一个代理过滤器DelegatingFilterProxy,因为真正的shiroFilter需要注入很多复杂的对象,而web.xml中只能配置字符串或数字的参数,是不能满足的,因此我们会把shiroFilter交给 Spring 进行管理,通过spring的xml文件来配置。 使用DelegatingFilterProxy代理过滤器后,但浏览器发送请求过来,被代理过滤器拦截到后,代理过滤器会自动从 spring 容器中找filter-name所配置相同名称的bean,来实现真正的业务。

创建shiro.xml

首先,为了单独对 Shiro 相关的配置进行管理,我们分离出一个 shiro.xml 配置文件,并在 mvc.xml 中引入

mvc.xml

<import resource="classpath:shiro.xml"/>

配置shiro过滤器

注意此处的id必须要与web.xml的代理过滤器的filter-name值一致

shiro.xml

<bean id="shiroFilter"
      class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> 
    <!--引用指定的安全管理器-->
    <property name="securityManager" ref="securityManager"/>
    <!--shiro默认的登录地址是/login.jsp 现在要指定我们自己的登录页面地址-->
    <property name="loginUrl" value="/login.html"/>
    <!--路径对应的规则-->
    <property name="filterChainDefinitions">
        <value>
            /userLogin=anon
            /css/**=anon
            /js/**=anon
            /img/**=anon
            /upload/**=anon
            /**=authc
        </value>
    </property>
</bean>

配置安全管理器

在 JavaEE 环境中,我们需要使用的安全管理器是:DefaultWebSecurityManager

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"></bean>

有了上面的配置,当我们的访问到达具体资源之前,会先进过指定的过滤器做预处理,在允许通过之后才能继续访问。

Shiro 中定义了多个过滤器来完成不同的预处理操作:

过滤器的名称Java
anonorg.apache.shiro.web. lter.authc.AnonymousFilter
authcorg.apache.shiro.web. lter.authc.FormAuthenticationFilter
authcBasicorg.apache.shiro.web. lter.authc.BasicHttpAuthenticationFilter
rolesorg.apache.shiro.web. lter.authz.RolesAuthorizationFilter
permsorg.apache.shiro.web. lter.authz.PermissionsAuthorizationFilter
userorg.apache.shiro.web. lter.authc.UserFilter
logoutorg.apache.shiro.web. lter.authc.LogoutFilter
portorg.apache.shiro.web. lter.authz.PortFilter
restorg.apache.shiro.web. lter.authz.HttpMethodPermissionFilter
sslorg.apache.shiro.web. lter.authz.SslFilter

anon: 匿名处理过滤器,即不需要登录即可访问;一般用于静态资源过滤;

示例 /static/**=anon

authc: 表示需要认证(登录)才能使用;

示例 /**=authc

***roles:***角色授权过滤器,验证用户是否拥有资源角色;

示例 /admin/*=roles[admin]

***perms:***权限授权过滤器,验证用户是否拥有资源权限;

示例 /employee/input=perms[“user:update”]

***logout:***注销过滤器

示例 /logout=logout

修改登录功能

LoginController

@RequestMapping("/userLogin")
@ResponseBody
public JsonResult login(String username, String password) {
    try {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 通过shiro的安全工具获取主体再调用登录方法进行登录验证
        SecurityUtils.getSubject().login(token);
        return new JsonResult();
    } catch (UnknownAccountException e) {
        return new JsonResult(false, "账号不存在");
    } catch (IncorrectCredentialsException e) {
        return new JsonResult(false, "密码错误");
    } catch (Exception e) { 
        e.printStackTrace();
        return new JsonResult(false, "登录异常,请联系管理员");
    }
}

配置自定义Realm-CarBusinessRealm

/**
 * 自定义Realm查询mysql
 */
public class CarBusinessRealm extends AuthorizingRealm {
    @Autowired
    private IEmployeeService employeeService;
    //提供认证信息
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //通过token获取用户名(用户登录的时候填的)
        String username = (String)token.getPrincipal();
        //判断是否存在数据库(根据名字查询员工)
        Employee employee = employeeService.selectByUsername(username);
        if(employee!=null){
            //身份对象,真正的密码,当前realm的名字
            return new SimpleAuthenticationInfo(employee,
                                                employee.getPassword(),
                                                this.getName());
        }
        //返回值就是查询出来的数据,如果查到这个账号,就应该返回该账号正确的数据,如果查不到,就返回null
        return null;
    }
    //提供授权信息
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
}

在安全管理器中指定我们自定义的 Realm,并且需要保证已经将 Realm 交给了 Spring 容器

<bean id="employeeRealm" class="cn.wolfcode.shiro.CarBusinessRealm">
</bean>

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="carBusinessRealm"/>  
</bean>

有了这一步,Shiro 就会通过自定义的 Realm 进行账号校验了,到此,Shiro 的认证功能已经成功的集成到了 CRM 项目中。目前可以很方便的完成登录验证,登录拦截,注销等功能。

注销功能

shiro.xml 中的路径规则加入 /logout=logout 即可交给shiro来处理,我们以前写的LoginController中的logout方法可以删掉啦。

Shiro 授权

系统中的授权功能就是为用户分配相关的权限,只有当用户拥有相应的权限后,才能访问对应的资源。

如果系统中无法管理用户的权限,那么将会出现客户信息泄露,数据被恶意篡改等问题,所以在绝大多数的应用中,我们都会有权限管理模块。

前面介绍过我们的权限管理系统是基于角色的权限管理,所以在系统中应该需要下面三个子模块:

  1. 用户管理 2. 角色管理 3. 权限管理

这三个模块我们已经完成,同时可以很好的完成他们之间的关系管理。

那么目前我们所需要的就是将用户拥有的权限告知 Shiro,供其在权限校验的时候使用。

基于ini的认证

1:编写 ini 配置文件:shiro-author.ini

#用户的身份、凭据、角色
[users]
zhangsan=555,hr,seller
dafei=666,seller

#角色与权限信息
[roles]
hr=employee:list,employee:delete
seller=customer:list,customer:save

权限表达式:

权限表达式的作用主要是用来在权限校验的时候使用,表达式中包含有当前访问资源的相关信息,应该具有唯一性,跟以前的权限表达式一致,shiro中也可以使用*通配符。

主要是用来在权限校验的时候使用,表达式中包含有当前访问资源的相关信息,应该具有唯一性。

权限表达式 在 ini 文件中用户、角色、权限的配置规则是:

用户名=密码,角色 1,角色 2…

角色=权限 1,权限 2…

首先根据用户名找角色,再根据角色找权限,角色是权限集合。

权限字符串也可以使用*通配符

2:使用 Shiro 相关的 API 完成权限校验

@Test
public void testAuthor(){
    //创建Shiro的安全管理器,是shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
    IniRealm iniRealm = new IniRealm("classpath:shiro-author.ini");
    securityManager.setRealm(iniRealm);
    //把安全管理器注入到当前的环境中
    SecurityUtils.setSecurityManager(securityManager);
    //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
    Subject subject = SecurityUtils.getSubject();
    System.out.println("认证状态:"+subject.isAuthenticated());
    //创建令牌(携带登录用户的账号和密码)
    UsernamePasswordToken token = new UsernamePasswordToken("dafei","666");
    //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());
    //登出
    //subject.logout();
    //System.out.println("认证状态:"+subject.isAuthenticated());
    
    //判断用户是否有某个角色
    System.out.println("role1:"+subject.hasRole("role1"));
    System.out.println("role2:"+subject.hasRole("role2"));

    //是否同时拥有多个角色
    System.out.println("是否同时拥有role1和role2:"+subject.hasAllRoles(Arrays.asList("role1", "role2")));

    //check开头的是没有返回值的,当没有权限时就会抛出异常
    subject.checkRole("hr");

    //判断用户是否有某个权限
    System.out.println("user:delete:"+subject.isPermitted("user:delete"));
    subject.checkPermission("user:delete");
}

自定义 Realm

在继承体系中的每个类所能够实现的功能不一样,在后面的开发中,我们通常需要使用到缓存,认证和授权所有的功能,所以选择继承 AuthorizingRealm。

public class EmployeeRealm extends AuthorizingRealm {
    //提供认证信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
        throws AuthenticationException {
        //暂且使用假数据来模拟数据库中真实的账号和密码
        Employee employee = new Employee();
        employee.setName("admin");
        employee.setPassword("1");
        //获取token中需要登录的账号名
        Object username = token.getPrincipal();
        //如果账号存在,则返回一个 AuthenticationInfo 对象
        if(username.equals(employee.getName())){
            return new SimpleAuthenticationInfo(
                employee,//身份对象,与主体subject绑定在一起的对象,暂时无用但后续要用 
                employee.getPassword(),//该账号真正的密码,传给shiro做密码校验的
                this.getName()//当前 Realm 的名称,暂时无用,不需纠结
            );
        }
        return null;
    }
	//提供授权信息
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //登录成功用户对象
        Employee employee = (Employee)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 根据登录用户的id查询到其拥有的所有角色的编码(模拟从数据库查出)
        List<String> roleSns = Arrays.asList("hr","manager","seller");
        // 将用户拥有的角色添加到授权信息对象中,供 Shiro 权限校验时使用
        info.addRoles(roleSns);
        // 根据登录用户的 id 查询到其拥有的所有权限表达式(模拟从数据库查出)
        List<String> expressions = Arrays.asList("employee:list","employee:save");
        // 将用户拥有的权限添加到授权信息对象中,供 Shiro 权限校验时使用
        info.addStringPermissions(expressions);
        return info;
    }
}
@Test
public void testLogin(){
    //创建Shiro的安全管理器,是shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
    EmployeeRealm realm = new EmployeeRealm();
    securityManager.setRealm(realm);
    //把安全管理器注入到当前的环境中
    SecurityUtils.setSecurityManager(securityManager);
    //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
    Subject subject = SecurityUtils.getSubject();
    System.out.println("认证状态:"+subject.isAuthenticated());
    //创建令牌(携带登录用户的账号和密码)
    UsernamePasswordToken token = new UsernamePasswordToken("dafei","666");
    //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());
    //登出
    //subject.logout();
    //System.out.println("认证状态:"+subject.isAuthenticated());
    
    //判断用户是否有某个角色
    System.out.println("role1:"+subject.hasRole("role1"));
    System.out.println("role2:"+subject.hasRole("role2"));

    //是否同时拥有多个角色
    System.out.println("是否同时拥有role1和role2:"+subject.hasAllRoles(Arrays.asList("role1", "role2")));

    //check开头的是没有返回值的,当没有权限时就会抛出异常
    subject.checkRole("hr");

    //判断用户是否有某个权限
    System.out.println("user:delete:"+subject.isPermitted("user:delete"));
    subject.checkPermission("user:delete");
}

CRM 中集成 Shiro 授权

Shiro 权限验证三种方式

  1. 编程式 通过写 if/else 授权代码块完成
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("hr")) {
	//有权限
} else {
	//无权限
}
  1. 注解式 通过在controller的方法上放置相应的注解完成
@RequiresRoles("admin") // 角色验证
@RequiresPermissions("user:create") // 权限验证
public void hello() {
    //有权限
}
  1. JSP 标签(shiro 自带) 或 freemarker 的标签(第三方) 在页面通过相应的标签完成
<@shiro.hasPermission name="employee:list">
    <!-- 有权限 -->
</@shiro.hasRole>
注解式权限验证

在RBAC中,我们采用自定义权限注解贴在需要的方法上,然后再扫描对应类的方法,获取对应的注解生成权限表达式。其中的注解是我们自定义的,很明显,Shiro 权限框架并不认识这个注解,自然也无法完成权限的校验功能,所以我们需要使用 Shiro 自身提供的一套注解来完成

步骤:

  1. 在 Controller 的方法上贴上 Shiro 提供的权限注解(@RequiresPermissions,@RequiresRoles)
@RequiresPermissions(value = "employee:list") 
@RequestMapping("/list")
public String list(Model model, @ModelAttribute("qo") EmployeeQueryObject qo){
    model.addAttribute("result", employeeService.query(qo)); 
    model.addAttribute("depts", departmentService.list()); 
    return "employee/list";
}

@RequiresPermissions(“employee:list”): 权限限定注解,表示当前用户拥有employee:list 权限才可以访问当前请求映射方法

@RequiresRoles(“hr”):角色限定注解,表示当前用户拥有 hr 角色才可以访问当前请求映射方法

  1. 开启 Shiro 注解扫描器

    当扫描到 Controller 中有使用 @RequiresPermissions 注解时,会使用cglib动态代理为当前 Controller 生成代理对象,增强对应方法,进行权限校验。

<!-- <aop:config/> 会扫描配置文件中的所有advisor,并为其创建代理 -->
<aop:config />
<!-- Pointcut advisor通知器, 会匹配所有加了shiro权限注解的方法 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>
查询数据库真实数据
EmployeeRealm 中实现授权的方法,查询当前登录用户的角色和权限信息
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //获取当前登录主体对象 
    //Subject subject = SecurityUtils.getSubject();
    //获取主体的身份对象(这里获取的对象与认证方法doGetAuthenticationInfo返回的SimpleAuthenticationInfo对象的第一个参数是同一个对象)
    Employee employee = (Employee)subject.getPrincipal();
    //判断是否是超级管理员
    if(employee.isAdmin()){
        info.addRole("admin"); //方便后续可以直接用subject来判断是否管理员
        info.addStringPermission("*:*"); //权限通配符
    }else{
        //根据用户的id查询该用户拥有的角色编码
        List<Role> roles = roleMapper.selectByEmpId(employee.getId());
        ArrayList<String> roleSnList = new ArrayList<>();
        for (Role role : roles) {
            info.addRole(role.getSn()); 
        }
        //根据用户的id查询该用户拥有的权限表达式
        List<String> permissions = permissionMapper.selectByEmpId(employee.getId());
        info.addStringPermissions(permissions);
    }
    return info; //info对象中有哪些角色和权限,就代表当前登录用户拥有哪些角色和权限
}

注意: *😗 通配符表示所有权限,即不做任何限制,可以访问系统中的任意资源(超级管理员)

没有权限的异常处理

如果用户不是超级管理员,只能访问分配给他的相关资源,如果访问了没有权限的资源,会抛出下面的异常。

org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [department:list]

因为之前使用过 SpringMVC 的统一异常处理,此时若没有权限,会跳转到错误页面,但是用户体验不好,直接提示出错了用户不知道到底怎么回事,所以我们应该根据异常类型区分开来,如果是没有权限的异常,则应该跳转到没有权限的提示页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bVXlEzj1-1622731060000)(image\image-20200707180614892.png)]

我们只需要想办法捕获到该异常,然后进行处理即可。

@ExceptionHandler(AuthorizationException.class)
public String exceptionHandler(AuthorizationException e, HandlerMethod method, HttpServletResponse response){
    e.printStackTrace(); //方便开发的时候找bug
    //如果原本控制器的方法是返回jsonresult数据,现在出异常也应该返回jsonresult
    //获取当前出现异常的方法,判断是否有ResponseBody注解,有就代表需要返回jsonresult
    if(method.hasMethodAnnotation(ResponseBody.class)){
        try {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().print(JSON.toJSONString(new JsonResult("没有权限操作!")));
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        return null;
    }
    //如果原本控制器的方法是返回视图页面,现在也应该返回视图页面
    return "common/nopermission";
}
修改原本的权限加载业务

​ 单纯使用@RequiresPermissions(value = “employee:list”) 进行标记请求映射方法,相比之前我们自定义权限注解来看,少了权限的名称,此时怎么办?这时需要用上@RequiresPermissions 的logical 属性

@RequiresPermissions(value={"employee:list", "员工列表"}, logical = Logical.OR)

​ value属性: 这个属性是一个数组,也就是说一个请求映射方法可以运行配置多个权限。

​ logical 属性:该属性根据配置属性值对当前用户是否有权限访问请求映射方法进行限制

​ Logical.AND: 必须同时拥有value配置所有权限才允许访问

​ Logical.OR:只需要拥有value配置所有权限中一个即可允许访问

  • 根据上面这个特性,约定@RequiresPermissions 注解中的value属性值(数组)中第一位为权限表达式, 第二位位权限名称
public void reload() {
 	//...略....
    // 通过 handlerMapping 容器 获取所有贴了@RequestMapping注解的处理方法
    Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
    for (HandlerMethod method : handlerMethods.values()) {
        // 获取方法的注解
        RequiresPermissions annotation = method.getMethodAnnotation(RequiresPermissions.class);
        // 方法上有贴我们的自定义权限注解
        if (annotation != null) {
            // 获取方法的注解 name 和 expression 值
            String name = annotation.value()[1];
            String expression = annotation.value()[0];
            // 存入到数据库之前要判断一下, 若数据库中不存在就存入
            if(!expressions.contains(expression)) {
                Permission permission = new Permission();
                permission.setName(name);
                permission.setExpression(expression);
                permissionMapper.insert(permission);
            }
        }
    }
}
标签式权限验证

在前端页面上,我们通常可以根据用户拥有的权限来显示具体的页面,如:用户拥有删除员工的权限,页面上就把删除按钮显示出来,否则就不显示删除按钮,通过这种方式来细化权限控制。

要能够实现上面的控制,需要使用 Shiro 中提供的相关标签,标签的使用步骤如下:

  1. 拓展freemarker标签

前端页面我们选择的是freemarker,而默认 freemarker 是不支持 shiro 标签的,所以需要对其功能做拓展,可以理解为注册 shiro 的标签,达到在freemarker 页面中使用的目的

public class ShiroFreeMarkerConfig extends FreeMarkerConfigurer {
    @Override
    public void afterPropertiesSet() throws IOException, TemplateException {
        //继承之前的属性配置,这不能省
        super.afterPropertiesSet();
        Configuration cfg = this.getConfiguration();
        cfg.setSharedVariable("shiro", new ShiroTags());//注册shiro 标签
    }
}
  1. 在mvc.xml 中把以前的FreeMarkerConfigurer修改成我们自定义的MyFreeMarkerConfig类
<bean class="cn.wolfcode.crm.util.ShiroFreeMarkerConfig"> 
    <!-- 配置 freemarker 的文件编码 -->
    <!-- ..略... -->
</bean>

有了上面的准备工作后,我们就可以在freemarker 页面中使用 shiro 相关的标签来对页面显示做控制了。

  1. 使用shiro标签

常用标签:

authenticated 标签:已认证通过的用户。

<@shiro.authenticated> </@shiro.authenticated>

notAuthenticated 标签:未认证通过的用户。与 authenticated 标签相对。

<@shiro.notAuthenticated></@shiro.notAuthenticated>

principal 标签:输出当前用户信息,通常为登录帐号信息

后台是直接将整个员工对象作为身份信息的,所以这里可以直接访问他的 name 属性得到员工的姓名

<@shiro.principal property="name" />

对应realm中返回的SimpleAuthenticationInfo对象的第一个参数

new SimpleAuthenticationInfo(employee,employee.getPassword(),this.getName());

hasRole 标签:验证当前用户是否拥有该角色

<@shiro.hasRole name="admin">Hello admin!</@shiro.hasRole>

hasAnyRoles 标签:验证当前用户是否拥有这些角色中的任何一个,角色之间逗号分隔

<@shiro.hasAnyRoles name="admin,user,operator">Hello admin</@shiro.hasAnyRoles>

hasPermission 标签:验证当前用户是否拥有该权限

<@shiro.hasPermission name="department:delete">删除</@shiro.hasPermission>
编程式权限验证

随便找个能执行到的地方,测试shiro提供的权限api是否能结合realm完成权限判断功能

@RequestMapping("/list")
public String list(Model model, QueryObject qo){
    System.out.println("当前用户是否有admin角色:"
                       + SecurityUtils.getSubject().hasRole("admin"));
    System.out.println("当前登录用户是否有employee:delete权限:"
                       + SecurityUtils.getSubject().isPermitted("employee:delete"));
    model.addAttribute("pageInfo", departmentService.query(qo));
    return "department/list"; 
}

Shiro加密

加密的目的是从系统数据的安全考虑,如,用户的密码,如果我们不对其加密,那么所有用户的密码在数据库中都是明文,只要有权限查看数据库的都能够得知用户的密码,这是非常不安全的。所以,只要密码被写入磁盘,任何时候都不允许是明文, 以及对用户来说非常机密的数据,我们都应该想到使用加密技术,这里我们采用的是 MD5 加密。

如何实现项目中密码加密的功能:

  1. 添加用户的时候,对用户的密码进行加密

  2. 登录时,按照相同的算法对表单提交的密码进行加密然后再和数据库中的加密过的数据进行匹配

Shiro加密工具

在 Shiro 中实现了 MD5 的算法,所以可以直接使用它来对密码进行加密。

@Test
public void testMD5() throws Exception{ 
    Md5Hash hash = new Md5Hash("1");
	System.out.println(hash);//c4ca4238a0b923820dcc509a6f75849b
}

MD5 加密的数据如果一样,那么无论在什么时候加密的结果都是一样的,所以,相对来说还是不够安全,但是我们可以对数据加“盐”。同样的数据加不同的“盐”之后就是千变万化的,因为我们不同的人加的“盐”都不一样。这样得到的结果相同率也就变低了。

盐一般要求是固定长度的字符串,且每个用户的盐不同。

可以选择用户的唯一的数据来作为盐(账号名,身份证等等),注意使用这些数据作为盐要求是不能改变的,假如登录账号名改变了,则再次加密时结果就对应不上了。

@Test
public void testMD5() throws Exception{ 
    Md5Hash hash = new Md5Hash("1","admin");
	System.out.println(hash);//e00cf25ad42683b3df678c61f42c6bda
}

或者是在数据库中多记录一个数据 , 如下所述:

username | password                         |salt  
---------|---------—------------------------|----------
zp1996   |2636fd878959548z2abf3423833901f6e  |63UrCwJhTH   
zpy      |659ec972c3ed72d04fac7a2147b5827b  |84GljVnhDT

Md5Hash() 构造方法中的第二个参数就是对加密数据添加的“盐”,加密之后的结果也和之前不一样了。

如果还觉得不够安全,我们还可以通过加密次数来增加 MD5 加密的安全性。

@Test
public void testMD5() throws Exception{ 
    Md5Hash hash = new Md5Hash("1","admin",3);
	System.out.println(hash);//f3559efea469bd6de83d27d4284b4a7a
}

上面指定对密码进行 3 次 MD5 加密,在开发中可以根据实际情况来选择。

实现密码加密

在知道 Shiro 如何使用MD5 加密之后,接下来我们来看看如何将其使用到我们的 CRM 项目中来

步骤:

  1. 在添加用户的时候,需要对用户的密码进行加密
@Override
public void save(Employee employee, Long[] ids) {
    //对密码进行加密(把用户名当做盐)
    Md5Hash md5Hash = new Md5Hash(employee.getPassword(), employee.getName());
    employee.setPassword(md5Hash.toString());
    //保存到数据库
    employeeMapper.insert(employee);
	//......
}
  1. 在登录时, 先对前端传过来的密码进行相同算法的加密,再传给shiro进行认证处理
@RequestMapping("/login")
@ResponseBody
public JsonResult login(String username, String password) {
    try {
        //对密码进行加密(把用户名当做盐)
        Md5Hash md5Hash = new Md5Hash(password, username);
        UsernamePasswordToken token = new UsernamePasswordToken(username, md5Hash.toString()); 			
        SecurityUtils.getSubject().login(token);
        return new JsonResult();
    } catch (UnknownAccountException e) {
        return new JsonResult(false, "账号不存在");
    } .....
}
  1. 测试效果之前先把数据库中的密码改为加密后的数据

有了以上操作之后,密码加密的功能就已经实现好了

Shiro缓存

在请求中一旦需要进行权限的控制,如:

@RequiresPermissions("employee:view") //注解
<shiro:hasPermission name="employee:input"> //标签
subject.hasRole("admin") //注解

都会去调用 Realm 中的 doGetAuthorizationInfo 方法获取用户的权限信息,这个授权信息是要从数据库中查询的, 如果每次授权都要去查询数据库就太频繁了,性能不好, 而且用户登陆后,授权信息一般很少变动,所以我们可以在第一次授权后就把这些授权信息存到缓存中,下一次就直接从缓存中获取,避免频繁访问数据库。

Shiro 中没有实现自己的缓存机制,只提供了一个可以支持具体缓存实现(如:Hazelcast, Ehcache, OSCache, Terracotta, Coherence, GigaSpaces, JBossCache 等)的抽象 API 接口,这样就允许 Shiro 用户根据自己的需求灵活地选择具体的 CacheManager。这里我们选择使用 EhCache。

集成EhCache

  1. 配置缓存管理器并引用缓存管理器
<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!--注册自定义数据源-->
    <property name="realm" ref="employeeRealm"/>
    <!--注册缓存管理器-->
    <property name="cacheManager" ref="cacheManager"/>
</bean>

<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> 
    <!-- 设置配置文件 -->
    <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
  1. 添加缓存配置文件

shiro-ehcache.xml

<ehcache>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="600"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

配置结束,登录之后,检查多次访问需要权限控制的代码时,是否不再反复查询权限数据(是否有多次进入Realm的doGetAuthorizationInfo 方法),如果只进入一次,则代表缓存已经生效。

配置属性说明

maxElementsInMemory: 缓存对象最大个数。

**eternal **:对象是否永久有效,一但设置了,timeout 将不起作用。

timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。

timeToLiveSeconds:对象存活时间,指对象从创建到失效所需要的时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,默认是 0,也就是对象存活时间无穷大。

memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。

缓存策略一般有3种:

默认LRU(最近最少使用,距离现在最久没有使用的元素将被清出缓存)。

FIFO(先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉)。

LFU(较少使用,意思是一直以来最少被使用的,缓存的元素有一个hit 属性(命中率),hit 值最小的将会被清出缓存)。

总结:Shiro 为我们做了哪些事情?

  1. 过滤器拦截

  2. 登录认证

  3. 系统注销

  4. 权限校验

  5. 密码加密

设置配置文件 -->


2. 添加缓存配置文件 

shiro-ehcache.xml

```xml
<ehcache>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="600"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

配置结束,登录之后,检查多次访问需要权限控制的代码时,是否不再反复查询权限数据(是否有多次进入Realm的doGetAuthorizationInfo 方法),如果只进入一次,则代表缓存已经生效。

配置属性说明

maxElementsInMemory: 缓存对象最大个数。

**eternal **:对象是否永久有效,一但设置了,timeout 将不起作用。

timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。

timeToLiveSeconds:对象存活时间,指对象从创建到失效所需要的时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,默认是 0,也就是对象存活时间无穷大。

memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。

缓存策略一般有3种:

默认LRU(最近最少使用,距离现在最久没有使用的元素将被清出缓存)。

FIFO(先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉)。

LFU(较少使用,意思是一直以来最少被使用的,缓存的元素有一个hit 属性(命中率),hit 值最小的将会被清出缓存)。

总结:Shiro 为我们做了哪些事情?

  1. 过滤器拦截

  2. 登录认证

  3. 系统注销

  4. 权限校验

  5. 密码加密

  6. 数据缓存

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值