Shiro 安全框架详解二(概念+权限案例实现)
阅读原文时间:2023年07月09日阅读:2

Shiro 安全框架详解二

总结内容

Shiro 入门以及登录认证案例及实现请看我上篇博客:
Shiro 安全框架详解一(概念+登录案例实现)

1. 概念

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

2. 授权流程图

1. 实现原理图

2. 实现代码

2.1 添加 maven 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>

2.2 编写 ini 配置文件:shiro-authc.ini

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

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

#角色与权限信息
[roles]
hr=employee:list,employee:delete
seller=customer:list,customer:save
  • 权限表达式
    权限表达式的作用主要是用来在权限校验的时候使用,表达式中包含有当前访问资源的相关信息,应该具有唯一性,跟以前的权限表达式一致,shiro中也可以使用*通配符。

2.3 Shiro 常用 API

  • 常用API
    【1】判断用户拥有单个角色:用户对象.hasRole;
    【2】判断同时拥有多个角色:hasRoles;
    【3】判断拥有指定的角色:hasAllRoles ,全部拥有返回 true ,否则返回 false;
    【4】判断用户是否拥有某个权限:isPermitted(),有返回 true,否则返回 false;
    【5】ckeck 开头的是没有返回值的,当没有权限时就会抛出异常;

    @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("admin","123");
    //执行登录操作(将用户的和 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");

    }

1. Shiro 权限验证三种方式

  1. 编程式 通过写 if/else 授权代码块完成

    Subject subject = SecurityUtils.getSubject();
    if(subject.hasRole("hr")) {
    //有权限
    } else {
    //无权限
    }

  2. 注解式 通过在controller的方法上放置相应的注解完成

    @RequiresRoles("admin")
    @RequiresPermissions("user:create")
    public void hello() {
    //有权限
    }

  3. JSP 标签(shiro 自带) 或 freemarker 的标签(第三方) 在页面通过相应的标签完成

    <@shiro.hasPermission name="employee:list">

1. 项目准备

项目准备请看我上一篇博客:
Shiro 安全框架详解一(概念+登录案例实现)

2. 代码实现

2.1 登录的 EmployeeController 控制器

@RequiresPermissions(value = "employee:list")
@RequestMapping("/list")
public String list(Model model){
    model.addAttribute("depts", departmentService.list());
    return "employee/list";
}
  • @RequiresPermissions(“employee:list”)
    权限限定注解,表示当前用户拥有employee:list 权限才可以访问当前请求映射方法
  • @RequiresRoles(“hr”)
    角色限定注解,表示当前用户拥有 hr 角色才可以访问当前请求映射方法

2.2 配置自定义Realm:CarBusinessRealm

package cn.wolfcode.shiro;

import cn.wolfcode.domain.Employee;
import cn.wolfcode.domain.Role;
import cn.wolfcode.service.IEmployeeService;
import cn.wolfcode.service.IPermissionService;
import cn.wolfcode.service.IRoleService;
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.springframework.beans.factory.annotation.Autowired;

import java.util.List;

public class CarBusinessRealm extends AuthorizingRealm {

    @Autowired
    private IEmployeeService employeeService;
    @Autowired
    private IPermissionService permissionService;
    @Autowired
    private IRoleService roleService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 1、获取页面传入的账户:username
        String username = (String) token.getPrincipal();
        // 2、以账户为条件,查询用户对象
        Employee employee = employeeService.selectByUsername(username);
        if (employee == null) {
            return null;
        }
        // 封装成 info 数据
        return new SimpleAuthenticationInfo(employee, employee.getPassword(), this.getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 1、获取当前登录用户对象
        Employee employee = (Employee) principals.getPrimaryPrincipal();
        // 2、查询该用户对象的权限集合
        List<String> permissions = permissionService.selectByEmpId(employee.getId());
        // 3、查询该用户对象的角色集合
        List<String> roles = roleService.queryByEmpId(employee.getId());
        // 4、封装授权 info 对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 是超管获取所有权限
        if (employee.isAdmin()) {
            List<Role> rolesT = roleService.listAll();
            for (Role role : rolesT) {
                info.addRole(role.getSn());
            }
            // 拥有所有权限
            info.addStringPermission("*:*");
            return info;
        }
        info.addRoles(roles);
        info.addStringPermissions(permissions);
        return info;
    }
}

2.3 创建 shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- <aop:config/> 会扫描配置文件中的所有 advisor,并为其创建代理 -->
    <aop:config/>

    <!-- Pointcut advisor 通知器,会匹配所有加了 shiro 权限注解的方法 -->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

    <!-- 自定义 realm -->
    <bean id="carBusinessRealm" class="cn.wolfcode.shiro.CarBusinessRealm"/>

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 设置 realm 值-->
        <property name="realm" ref="carBusinessRealm"/>
        <!-- 注册缓存管理器 -->
        <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>

    <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
                /appointment/save=anon
                /index=anon
                /css/**=anon
                /js/**=anon
                /img/**=anon
                /upload/**=anon
                /static/**=anon
                /userLogout=logout
                /**=authc
            </value>
        </property>
    </bean>
</beans>

2.4 在SpringMVC 中引入 shiro.xml

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

2.5 安全管理器

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

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 这里面有很多配置文件 -->
</bean>

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

2.6 没有权限的异常处理

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

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

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

@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";
}

2.7 标签式权限验证

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

要能够实现上面的控制,需要使用 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类

有了上面的准备工作后,我们就可以在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>

2.8 编程式权限验证

随便找个能执行到的地方,测试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.xml 中的路径规则加入 /logout=logout 即可交给shiro来处理,我们以前写的LoginController中的logout方法可以删掉啦。

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

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

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

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

1. 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 加密,在开发中可以根据实际情况来选择。

2. 实现密码加密

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

2.1 步骤

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

    @Override
    public void save(Employee employee, Long[] ids) {
    //对密码进行加密(把用户名当做盐)
    Md5Hash md5Hash = new Md5Hash(employee.getPassword(), employee.getName());
    employee.setPassword(md5Hash.toString());
    //保存到数据库
    employeeMapper.insert(employee);
    //……
    }

  2. 在登录时, 先对前端传过来的密码进行相同算法的加密,再传给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, "账号不存在");
    } …..
    }

  3. 测试效果之前先把数据库中的密码改为加密后的数据,有了以上操作之后,密码加密的功能就已经实现好了。

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

@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。

1. 集成EhCache

  1. 配置缓存管理器并引用缓存管理器

  2. 添加缓存配置文件

shiro-ehcache.xml

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

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

2. 配置属性说明

maxElementsInMemory: 缓存对象最大个数。

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

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

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

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

缓存策略一般有3种:

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

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

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

总结

以上就是对Shiro 安全框架的总结了,代码仅供参考,欢迎讨论交流。