用shiro解决SaaS企业管理系统中存在的权限问题

1,系统中存在的权限问题

  1. 请求需要经过登录验证(登录成功才能访问)

  2. 某些请求需要经过权限验证(只有具备权限才能访问)

  3. 页面资源(按钮,超链接)需要根据权限动态展示

2,shiro的概述

2.1,概述

Apache Shiro是Java的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密的解决方案。

专业术语

  1. 认证(Authentication):认证(用户登录)

  2. 授权(Authorization) :授权(权限校验)

  3. 密码比较和加密

2.2 shiro中的内部结构

在这里插入图片描述

  1. subject:工具类,负责和shiro交互
  2. securityManager:shiro的核心,统一调度shiro框架运行
  3. realm域:和数据库交互
  4. 密码比较器:在认证时,对密码加密比较

2.3 shiro中的过滤器

shiro中所有的权限处理是通过过滤器的形式配置,内置了10个过滤器。只需要关注如下三个即可
在这里插入图片描述
这里的权限过滤器在配置文件中存入多个权限时需同时都满足才可,这时如果需要满足一个权限时就可以访问的话需要自定义过滤器。如:详情见最下面的优化。

3、搭建shiro的运行环境

3.1 引入依赖

<!--shiro-->  
    <!--shiro和spring整合-->  
    <dependency> 
      <groupId>org.apache.shiro</groupId>  
      <artifactId>shiro-spring</artifactId>  
      <version>1.3.2</version> 
    </dependency>  
    <!--shiro核心包-->  
    <dependency> 
      <groupId>org.apache.shiro</groupId>  
      <artifactId>shiro-core</artifactId>  
      <version>1.3.2</version> 
    </dependency>  
    <!--ehcache-->  
    <dependency> 
      <groupId>org.apache.shiro</groupId>  
      <artifactId>shiro-ehcache</artifactId>  
      <version>1.3.2</version> 
    </dependency>  
    <dependency> 
      <groupId>net.sf.ehcache</groupId>  
      <artifactId>ehcache-core</artifactId>  
      <version>2.6.6</version> 
    </dependency>  
    <dependency> 
      <groupId>org.crazycake</groupId>  
      <artifactId>shiro-redis</artifactId>  
      <version>3.2.3</version> 
    </dependency>  

3.2 配置shiro和spring整合

web项目的resource/spring文件夹下创建applicationContext-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">

    <description>Shiro与Spring整合</description>

    <!--自定义的过滤器-->
    <bean id="MyPermissionsFilter" class="cn.itcast.web.shiro.MyPermissionsFilter"></bean>

    <!--redis配置-->
    <bean id="redisManager" class="org.crazycake.shiro.RedisManager">
        <property name="host" value="127.0.0.1:6379"></property>
    </bean>

    <!--定义redis的缓存管理器-->
    <bean id="cacheManager" class="org.crazycake.shiro.RedisCacheManager">
        <property name="redisManager" ref="redisManager"></property>
    </bean>

    <!-- 1、自定义Realm域的编写 -->
    <bean id="authRealm" class="cn.itcast.web.shiro.AuthRealm">
        <!-- 注入自定义的密码比较器 -->
        <property name="credentialsMatcher" ref="customerCredentialsMatcher" ></property>
    </bean>

    <!--2、自定义的密码比较器 -->
    <bean id="customerCredentialsMatcher" class="cn.itcast.web.shiro.CustomCredentialsMatcher"></bean>


    <!--3、安全管理器-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 引用自定义的realm -->
        <property name="realm" ref="authRealm"/>
        <!--缓存管理器-->
        <property name="cacheManager" ref="cacheManager"></property>
    </bean>


    <!--4、 filter-name这个名字的值来自于web.xml中filter的名字
        代理过滤器:代理shrio中的所有过滤器
           一对10:在web.xml中只需要配置一个代理过滤器,自动的将请求绑定到多个shiro内部过滤器上
           业主 和 中介
    -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!--指定过滤器的简称-->
        <property name="filters">
            <map>
                <entry key="myPerms" value-ref="MyPermissionsFilter"></entry>
            </map>
        </property>

        <property name="securityManager" ref="securityManager"/>
        <!--登录页面  -->
        <property name="loginUrl" value="/login.jsp"></property>
        <!-- 鉴权失败后 -->
        <property name="unauthorizedUrl" value="/unauthorized.jsp"></property>

        <property name="filterChainDefinitions">
            <!-- /**代表下面的多级目录也过滤 -->
            <value>
                /system/module/list.do = perms["模块管理"]
                /index.jsp = anon
                /login.jsp = anon
                /login* = anon
                /css/** = anon
                /img/** = anon
                /plugins/** = anon
                /make/** = anon
                /** = authc
            </value>
        </property>
    </bean>

    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 安全管理器 -->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

    <aop:aspectj-autoproxy proxy-target-class="true"/>

</beans>

3.3 在web.xml中配置代理过滤器

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

3.4 自定义Realm域

package cn.itcast.web.shiro;

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

/**
 * 自定义realm域
 *  1、继承父类 AuthorizingRealm
 *  2、实现2个抽象方法(认证,授权)
 */
public class AuthRealm extends AuthorizingRealm {

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

	/**
	 * 认证
	 */
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		return null;
	}
}

3.5 自定义密码比较器

package cn.itcast.web.shiro;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

/**
 * 自定义密码比较器
 *  1、继承父类 SimpleCredentialsMatcher
 *  2、重写doCredentialsMatch方法(密码加密比较)
 */
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
	//密码比较加密
	public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
		return super.doCredentialsMatch(token, info);
	}
}

4、基于shiro的用户认证

4.1 传统登录的比较

在这里插入图片描述

改造LoginController

通过shiro方式发起shiro调用

@RequestMapping(value = "/login",name = "用户登录")
    public String login(String email,String password) {
        //判断邮箱和密码是否为空
        if (StringUtils.isEmpty(email) || StringUtils.isEmpty(password)){
            request.setAttribute("error","用户名或密码不能为空");
            return "forward:login.jsp";
        }
        try{
            //获取工具类subject
            Subject subject = SecurityUtils.getSubject();
            // 在shiro中,将登录的用户名密码封装成功token对象
            UsernamePasswordToken token = new UsernamePasswordToken(email,password);
            //调用subject的login方法,进入shiro内部完成登录
            subject.login(token);
            //4.正常执行,登录成功(数据存入session,跳转主页)
            //通过shiro框架获取当前登录的用户对象
            User user = (User)subject.getPrincipal();//获取安全数据(用户对象)
            session.setAttribute("loginUser",user);
            List<Module> modules = moduleService.findByUser(user);
            session.setAttribute("modules",modules);
            return "home/main";
        }catch (Exception e){
            e.printStackTrace();
            //抛出异常,登录失败
            request.setAttribute("error","用户名或密码错误");
            return "forward:login.jsp";
        }
    }

4.3 在Realm完成认证方法

找到自定义的AuthRealm域对象,补充认证方法
注意:这里需要调用UserService根据邮箱查询用户,需要注入UserService。

/**
     * 认证: 用户登录
     *  * 提供数据(提供用户对象),调用service查询数据
     *  参数:AuthenticationToken (用户登录输入的邮箱和密码)
     *  返回值:AuthenticationInfo(认证数据)
     *      * 用户对象
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户输入的登录信息
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String email = upToken.getUsername();
        //用户输入的密码用char数组存的,将char数组转化为string字符串
        String password = new String(upToken.getPassword());
        //根据邮箱查询用户
        User user = userService.findByEmail(email);
        //如果用户存在,构造返回值AuthenticationInfo
        if (user != null){
            /**
             * AuthenticationInfo:认证数据
             *  1.安全数据(用户对象)
             *  2.密码:为了方便操作,存入数据库密码
             *  3.realm域名称:约定俗称(当前类名)
             */
            return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
        }
        //4.如果用户不存在,返回null(抛出异常)
        return null;
    }

4.4 在密码比较器中比较密码

找到密码比较器CustomCredentialsMatcher,改造密码比较方法doCredentialsMatch

/**
	 * 密码比较方法
	 *  参数:
	 *      token : 用户登录输入的用户名(邮箱)和密码
	 *      info:realm域认证的返回值(用户对象,数据库密码,域名称)
	 *  返回值boolean:
	 *      true:登录输入密码和数据库密码一致
	 *      flase:不一致,自动的抛出异常
	 */
	public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
		//1.获取输入的用户名(邮箱)密码
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		String email = upToken.getUsername();
		String password = new String(upToken.getPassword());
		//2.获取数据库中的密码
		String dbPassword = (String)info.getCredentials();
		//3.对用户登录输入的密码加密
		password = Encrypt.md5(password,email);
		//4.比较
		return dbPassword.equals(password);
	}

4.5 退出清空登录数据

修改Logincontroller中ogout方法

//退出
    @RequestMapping(value = "/logout",name="用户登出")
    public String logout(){
	    Subject subject = SecurityUtils.getSubject();
	    subject.logout(); //退出(清空登录数据)
	    return "forward:login.jsp";
    }

4.6 修改web.xml的servlet配置

让tomcat启动的时候,创建spring容器,创建对象

<servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 指定加载的配置文件 ,通过参数contextConfigLocation加载-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/springmvc.xml;classpath*:spring/applicationContext-*.xml</param-value>
        </init-param>
        <!--当tomcat启动,初始化servlet,创建spring容器-->
        <load-on-startup>1</load-on-startup>
    </servlet>

4.7 登录校验的过程分析

在这里插入图片描述

  1. 用户在浏览器输入或者点击某个URL
  2. 通过web.xml中代理过滤器的设置找到spring和shiro整合的配置
  3. 找符合此链接的过滤器配置(/**=authc)
  4. authc:登录成功才能访问,内部会自动的判断当前请求是否已经登录
  5. 如果没有登录:跳转到执行的登录页面(loginUrl)配置

5、基于shiro的用户授权

5.1 完成Realm域的授权方法

授权方法:realm的授权方法主要目的是获取当前登录人员的可操作性权限数组(权限名称数组)
当realm域中准备好了权限数据,shiro框架会自动的根据权限配置(xml,注解),进行权限校验

修改了AuthRealm域中的授权方法,提供登录用户的权限数据

/**
	 * 授权方法:
	 *  本质:提供当前登录用户的权限数组(权限名称集合)
	 *  参数:PrincipalCollection(安全数据集合),只获取唯一的一个安全数据(用户对象)
	 *  返回值:
	 *      AuthorizationInfo:授权数据(权限名称集合)
	 *
	 */
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		//1.获取当前登录用户(安全数据)
		User user = (User)principals.getPrimaryPrincipal();
		//2.根据当前登录的用户,查询此用户的模块权限  List<Module>
		List<Module> list = moduleService.findByUser(user);
		//3.提取所有的模块名称:set集合
		Set<String> modulsNames = new HashSet<>();
		for (Module module : list) {
			modulsNames.add(module.getName());
		}
		//4.构造返回值
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		info.setStringPermissions(modulsNames);
		return info;
	}

5.2 基于XML的权限配置

在 3.2 配置shiro和spring整合中的配置文件中已经配置,如图所示:
在这里插入图片描述
在这里插入图片描述

5.3 基于注解的权限配置

  1. 注解配置到请求URL对应的控制器方法上
  2. 通过注解的形式替代xml的配置
  3. @RequiresPermissions注解,注解的value属性中执行所需权限
  4. 使用注解形式配置权限,如果权限不足抛出异常
    需求:当抛出UnauthorizedException异常的时候,表示权限不足。跳转到一个权限不足页面

5.3.1 自定义异常处理器

package cn.itcast.web.controller.exception;

import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义的异常处理器
 *   1、实现HandlerExceptionResolver
 *   2、交给容器管理
 */
@Component
public class ExceptionHandler implements HandlerExceptionResolver {
    /**
     *  异常处理器方法
     *      返回值:ModelAndView(跳转页面,携带错误信息)
     *      参数: Exception 当前抛出的异常
     */
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        //当出现的异常时权限不足异常时:UnauthorizedException跳转到权限不足页面
        ModelAndView mv = new ModelAndView();
        if(ex instanceof UnauthorizedException){
            ex.printStackTrace();
            mv.addObject("errorMsg","你没有访问这个的权限");
            mv.setViewName("forward:/unauthorized.jsp");
        }else{
            ex.printStackTrace();
            mv.addObject("errorMsg", "王牌程序猿请求出战");
            mv.setViewName("error");
        }
        return mv;
    }
}

5.4 页面标签库控制页面资源

  1. 在需要控制的资源(按钮,超链接)页面上引入shiro标签库
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
  1. 通过shiro:hasPermission控制显示对应的按钮
<shiro:hasPermission name="删除部门">
     <button type="button" ><i class="fa fa-trash-o"></i> 删除</button>
</shiro:hasPermission>

6、shiro优化

6.1 缓存优化

  1. 用户的权限数据一旦确定很少修改,为了缓解数据库压力。可以将用的数据(权限数据)保存到redis当中。
  2. 在shiro中redis缓存不需要编写代码,值需要配置即可
  3. 借助shiro中内置缓存管理器实现

在 3.2 配置shiro和spring整合中的配置文件中已经配置,如下这段代码:

<!--redis配置-->
<bean id="redisManager" class="org.crazycake.shiro.RedisManager">
    <property name="host" value="127.0.0.1:6379"></property>
</bean>

<!--定义redis的缓存管理器-->
<bean id="cacheManager" class="org.crazycake.shiro.RedisCacheManager">
    <property name="redisManager" ref="redisManager"></property>
</bean>

<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- 引用自定义的realm -->
    <property name="realm" ref="authRealm"/>
    <!--缓存管理器-->
    <property name="cacheManager" ref="cacheManager"></property>
</bean>

6.2 自定义过滤器

说明:shiro中内置了10个过滤器。往往不能满足我们的要求。

需求:针对/company/list.do链接,只要满足[“企业管理”,“部门管理”]其中的一项就可以访问
自定义过滤器。

package cn.itcast.web.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 * 自定义shiro过滤器。继承父类 AuthorizationFilter
 */
public class MyPermissionsFilter extends AuthorizationFilter {
    /**
     * 判断是否具有某种权限
     *      /company/** = perms["企业管理","部门管理"]
     *      mappedValue : 配置过滤器中,需要匹配的权限数组
     * 返回值:
     *  true:具有权限
     *  false:没有权限
     * 需求:对传入的权限数据,只需要满足其中一个权限,返回true即可
     */
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        //获取配置的filter的参数,数据转型获取数组
        String[] perms = (String[])mappedValue;
        //获取subject
        Subject subject = getSubject(request, response);
        //循环判断权限,满足其一即可返回true
        if (perms != null && perms.length>0){
            for (String perm : perms) {
                if (subject.isPermitted(perm)){
                    return true;
                }
            }
            return false;
        }else {
            return true;
        }
    }
}

再在web项目的resource/spring文件夹下的applicationContext-shiro.xml配置文件中配置

<!--自定义过滤器交给spring容器管理-->
    <bean id="myPermissionsFilter" class="cn.itcast.web.shiro.MyPermissionsFilter"></bean>




    <!-- filter-name这个名字的值来自于web.xml中filter的名字
        代理过滤器:代理shrio中的所有过滤器
           一对10:在web.xml中只需要配置一个代理过滤器,自动的将请求绑定到多个shiro内部过滤器上
           业主 和 中介
    -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">

        <!--指定过滤器的简称-->
        <property name="filters">
            <map>
                <entry key="myPerms" value-ref="myPermissionsFilter"></entry>
            </map>
        </property>

        <property name="securityManager" ref="securityManager"/>
        <!--登录页面  -->
        <property name="loginUrl" value="/login.jsp"></property>
        <!-- 没有操作权限的时候,跳转的页面 -->
        <property name="unauthorizedUrl" value="/unauthorized.jsp"></property>

        <property name="filterChainDefinitions">
            <!-- /**代表下面的多级目录也过滤 -->
            <value>
                <!--URL = perms["所需要权限的名称"]
                    与的关系:多个权限同时满足才能访问
                -->
              <!-- /company/** = perms["企业管理","部门管理"]-->
                <!--配置过滤器-->
                <!--/company/** = myPerms["企业管理","部门管理"]-->
                /index.jsp = anon
                /login.jsp = anon
                /login* = anon
                /css/** = anon
                /img/** = anon
                /plugins/** = anon
                /make/** = anon
                /** = authc
            </value>
        </property>
    </bean>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值