• day55_BOS项目_07


    • 今天内容安排:
      • 1、权限概述(认证、授权)
      • 2、常见的权限控制的方式(URL拦截权限控制、方法注解权限控制)
      • 3、权限模块数据模型(权限表、角色表、用户表、角色权限关系表、用户角色关系表)
      • 4、apache shiro框架入门
      • 5、将shiro应用到bos项目中进行认证和授权
      • 6、shiro提供的权限控制方式(主要使用前三种方式)
        • 6.1、URL拦截权限控制(正常)
        • 6.2、方法注解权限控制(重点)
        • 6.3、页面标签权限控制(shiro标签库)
        • 6.4、代码级别权限控制(了解,很少用)

    1、权限概述(认证、授权)

    • 系统提供了很多功能,并不是所有的用户登录系统都可以操作这些功能。我们需要对用户的访问进行控制。
      • 认证:系统提供的用于识别用户身份的功能(通常是登录功能) --> 让系统知道你是谁?
      • 授权:系统提供的赋予用户访问某个功能的能力 --> 让系统知道你能做什么?

    2、常见的权限控制的方式

    2.1、URL拦截权限控制 --> 基于过滤器或者拦截器

    URL拦截权限控制 图解如下:

    2.2、方法注解权限控制 --> 基于代理技术(给Action添加代理)

    方法注解权限控制 图解如下:


    详解如下:
    我们依旧以StaffAction为例:

    上面的方式类似于:我们在我们的项目中给我们的Action(StaffAction)注入进来的service,不是真正的service实现类(StaffServiceImpl),而是该service的代理。

    对于我们注入进来的service,若是用接口的实现类(StaffServiceImpl)来声明,就会报错,为什么呢?
    因为我们`真正注入给Action的是代理对象`,而这个代理对象跟真正的实现类(StaffServiceImpl)是没有关系的(我们同一级别)。

    对于我们注入进来的service,若是我们用接口(StaffService)来声明,是没有问题的,又为什么呢?
    因为这个代理类实现了这个接口(StaffService),即我们用接口(父类)来引用子类的对象,亦即多态(父类引用子类)。

    说白了,我跟你父亲有关系,跟你没半毛钱关系。

    话说回来,我们现在是为Action创建代理,进行权限检查。
    之前,我(Action)是被动接收者,你们注入你们的代理对象进来给我使用,通过你们的代理进行事务管理等操作。
    现在,我(Action)是主动创造者,我自己创造我自己的代理对象,通过我自己的代理进行权限认证等操作。

    3、权限模块数据模型

    • 一共涉及到5张表
      • 用户表:t_user
      • 角色表:auth_role
      • 权限表:auth_function
      • 角色权限关系表(多对多):role_function
      • 用户角色关系表(多对多):user_role

    权限模块数据模型图如下:

    自关联表:在这个权限表中有一个外键,而这个外键指向的是自己表的主键。同样都是权限却可以有权限大小之分。
    典型的自关联表:部门表,同样都是部门却可以分上下级。

    第一步:我们使用 PowerDesigner 通过 权限控制.pdm文件 生成 建表文件bos_qx.sql,为了避免外键名冲突,需要修改建表文件的外键名称和删除生成的t_user表的语句(因为该表之前已经生成过了)。
    第二步:再将建表文件拖入 Navicat for MySQL 中生成数据库中对应的5张表格。
    第三步:我们再使用MyEclipse中的Hibernate反转引擎生成实体类文件对应的Hibernate映射文件
    第四步:将反转生成的文件拷贝至Eclipse中的项目中去,简单修正一下拷贝的文件(修正2个地方)。新反转生成的User.hbm.xml文件与老的User.hbm.xml文件合并,注意:不要删掉老文件中手动添加的内容。

    4、apache shiro框架入门

    • 官网:https://shiro.apache.org/
    • Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
    • Apache Shiro™ 是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。 借助Shiro易于理解的API,您可以快速轻松地保护任何应用程序 - 从最小的移动应用程序到最大的Web和企业应用程序。
    • 提供的功能:

      详解如下:
    Apache Shiro是一个强大而灵活的开源安全框架,它能够干净利落地处理身份认证,授权,企业会话管理和加密。

    以下是你可以用 Apache Shiro所做的事情: 
        1、验证用户
        2、对用户执行访问控制,如: 
            判断用户是否拥有角色admin。
            判断用户是否拥有访问的权限。
        3、在任何环境下使用 Session API。例如:CS程序。
        4、可以使用多个用户数据源。例如:一个是oracle用户库,另外一个是mysql用户库。
        5、单点登录(SSO)功能。 比如:登录淘宝后,可以直接登录天猫商城。
            详解连接:
            https://blog.csdn.net/cruise_h/article/details/51013597
            https://blog.csdn.net/javaloveiphone/article/details/52439613
        6、“Remember Me”服务,类似购物车的功能,shiro官方建议开启。
    • shiro的程序运行流程图:

      详解如下:
        Application code:应用程序代码,开发人员编写的代码
        Subject:主体,当前用户,Subject 是与程序进行交互的对象,可以是人也可以是服务或者其他,通常就理解为用户!(某个人或者某个应用软件)
        SecurityManager:安全管理器,shiro框架的核心对象,管理各个组件
        Realm:类似于Dao,负责访问安全数据(用户数据、角色数据、权限数据)
    • shiro的程序运行流程详细图:

    5、将shiro应用到bos项目中进行认证和授权

    第一步:第一步:bos项目中导入shiro-all.jar包
    这里只介绍spring配置模式。
    第二步:在web.xml中配置一个spring用于整合shiro的过滤器

        <!-- 配置一个spring用于整合shiro的过滤器 -->
        <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>

    第三步:在spring配置文件applicationContext.xml中配置一个bean,id必须和上面的过滤器名称相同

        <!-- 配置一个工厂bean,用于创建shiro框架用到的过滤器 -->
        <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
            <!-- 注入安全管理器 -->
            <property name="securityManager" ref="securityManager"></property>
            <!-- 注入当前系统的登录页面 -->
            <property name="loginUrl" value="/login.jsp"/>
            <!-- 注入成功页面 -->
            <property name="successUrl" value="/index.jsp"/>
            <!-- 注入权限不足提示页面 -->
            <property name="unauthorizedUrl" value="/404.html"/>
            <!-- 注入URL拦截规则 -->
            <property name="filterChainDefinitions">
                <value>
                    /css/** = anon
                    /images/** = anon
                    /js/** = anon
                    /login.jsp* = anon
                    /validatecode.jsp* = anon
                    /userAction_login.action = anon <!-- 匿名访问 -->
                    /page_base_staff.action = perms["staff"] <!-- 表示当前用户在该页面认证通过,但是还没有授予权限,不能访问 -->
                    /* = authc <!-- 要求当前用户必须要验证通过,才可以访问 -->
                </value>
            </property>
        </bean>

    第四步:在spring配置文件applicationContext.xml中注册安全管理器,为安全管理器注入realm

        <!-- 配置安全管理器bean -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <!-- 为安全管理器注入自定义的realm -->
            <property name="realm" ref="bosRealm"></property>
        </bean>
        <!-- 配置自定义的realm -->
        <bean id="bosRealm" class="com.itheima.bos.shiro.BOSRealm"></bean>

    第五步:realm(接口)可以有两种方式得到:
    方式一:我们可以通过用框架提供的。
    方式二:自定义一个BOSRealm实现类。
    方式一提供的实现类JdbcRealm访问数据库使用的是sql语句,而我们当前的项目使用的是Hibernate框架,使用该框架,我们最好不要再使用sql语句了,所以我们本案例使用方式二。模仿实现类JdbcRealm来写我们自定义的实现类BOSRealm。示例代码如下:

    package com.itheima.bos.shiro;

    import javax.annotation.Resource;

    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.UsernamePasswordToken;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;

    import com.itheima.bos.dao.IUserDao;
    import com.itheima.bos.domain.User;

    public class BOSRealm extends AuthorizingRealm {

        @Resource
        private IUserDao userDao;
        /**
         * 认证方法
         */

        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            System.out.println("执行了我们自定义的认证方法");
            UsernamePasswordToken upToken = (UsernamePasswordToken) token;
            // 从令牌中获取用户名
            String username = upToken.getUsername();
            User user = userDao.findUserByUsername(username);
            if (user == null) {
                // 用户名不存在
                return null;
            } else {
                // 用户名存在
                String password = user.getPassword(); // 获取数据库中存储的密码
                // 把上述密码包装成AuthenticationInfo认证信息对象,让shiro框架帮我们去和我们输入的密码比较
                // AuthenticationInfo认证信息对象是接口,需要使用其实现类 SimpleAuthenticationInfo简单认证信息对象
                // 创建SimpleAuthenticationInfo简单认证信息对象
                /**
                 * 使用有3个参数的构造方法
                 *      参数1:principal 签名,作用:程序可以在任意位置获取当前放入的对象,比如:我们可以把认证通过的用户拿出来放到session中
                 *      参数2:credentials 凭证,从数据库中查询出的密码
                 *      参数3:realmName 当前realm的名称
                 */

                SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, this.getClass().getSimpleName());
                // 返回给安全管理器,由安全管理器负责比对数据库中查询出的密码和页面提交的密码(在token中),若比对成功,不会抛出异常,会修改Subject的状态为“已认证”
                return info;
            }
        }

        /**
         * 授权方法
         */

        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            return null;
        }
    }

    第六步:完善UserAction中的login()方法

        /*
         * 用户登录,使用shiro提供的方式进行认证
         */

        public String login() {
            // 判断用户输入的验证码是否正确
            // 先获取我们自己生成的验证码
            String key = (String) ServletActionContext.getRequest().getSession().getAttribute("key");
            // 判断用户是否有输入验证码和输入的验证码是否和我生成的验证码是否相等
            if (StringUtils.isNotBlank(checkcode) && checkcode.equals(key)) {
                // 说明验证码存在且正确
                // 获得当前用户对象
                Subject subject = SecurityUtils.getSubject(); // 状态为“未认证”
                String password = model.getPassword();
                password = MD5Utils.md5(password);
                // 构造一个用户名密码令牌
                AuthenticationToken token = new UsernamePasswordToken(model.getUsername(), password);
                try {
                    subject.login(token);
                } catch (UnknownAccountException e) {
                    e.printStackTrace(); // 输出异常
                    this.addActionError(this.getText("usernamenotfound")); // 设置错误信息
                    return "login"// 返回登录页面
                } catch (IncorrectCredentialsException e) {
                    e.printStackTrace(); // 输出异常
                    this.addActionError(this.getText("loginError")); // 设置错误信息
                    return "login"// 返回登录页面
                }
                // 获取简单认证信息对象中存储的User对象
                User user = (User) subject.getPrincipal();
                // 将User放入session域中
                ServletActionContext.getRequest().getSession().setAttribute("loginUser", user);
                return "home";
            } else {
                // 说明验证码错误,设置错误提示信息,并跳转至登录页面
                // this.addActionError("验证码错误"); // 在Struts2中,所有的消息提示都是基于国际化的。
                this.addActionError(this.getText("validatecodeError"));
                return "login";
            }
        }

    第七步:在自定义Realm类中编写授权方法

        /**
         * 授权方法
         */

        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) 
    {
            // 创建SimpleAuthenticationInfo简单授权信息对象,使用没有参数的构造方法
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            // 为当前用户授予staff权限(硬编码)--> perms["staff"]
            info.addStringPermission("staff");
            // 为当前用户授予staff角色(硬编码)--> roles["staff"]
            info.addRole("staff");
            // TODO 根据当前登录用户查询数据库,获取其对应的权限数据
            return info;
        }

    注意:上述第七步的授权方法是什么时候被调用的呢?

    答:是我们访问某个功能的时候,而这个功能我们通过spring配置文件applicationContext.xml进行配置的。在没有学习缓存之前,我们每次访问某个功能的时候,该授权方法都会被执行。我们后面的学习中会加入shiro的缓存管理器。

    6、shiro提供的权限控制方式(主要使用前三种方式)

    6.1、URL拦截权限控制(正常)

    通过spring配置文件applicationContext.xml进行配置,如下图所示:


    我们上面的将shiro应用到bos项目中进行认证和授权就是使用shiro提供的URL拦截权限控制。

    6.2、方法注解权限控制(重点)

    第一步:在spring配置文件中开启shiro的注解支持
    问题一:要强制使用cglib为Action创建代理对象,为什么呢?


    答:因为我们的Action都实现了ModelDriven接口,所以Spring框架会默认优先针对接口而给Action创建jdk代理,创建jdk代理是基于实现接口的 而该代理对象里面只有一个方法getModel(),没有我们需要的方法。
    而创建cglib代理是基于继承的,cglib会继承当前的Action类,所以基于cglib创建的代理会继承Action的方法,里面有我们需要的方法。
        <!-- 开启shiro注解支持 -->
        <!-- 自动代理 -->
        <bean id="defaultAdvisorAutoProxyCreator" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
            <!-- 注入强制使用cglib为Action创建代理对象,因为我们的Action都实现了ModelDriven接口,所以Spring框架会默认优先针对接口而给Action创建jdk代理,而该代理对象里面只有一个方法getModel() -->
            <!-- 而cglib是基于继承的,jdk是基于实现接口的 ,cglib会继承当前的Action去创建代理,所以cglib创建的代理会继承Action的方法,里面有我们需要的方法-->
            <property name="proxyTargetClass" value="true"></property>
        </bean>
        <!-- 切面类 -->
        <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"/>

    第二步:在Action中的方法上使用shiro的注解描述执行当前方法需要具有的权限或角色

        /**
         * 批量删除(逻辑删除)
         * @return
         */

        @RequiresPermissions(value="staff") // 执行当前方法需要staff权限,我们要先在BOSRealm中授予当前用户为staff权限才行,否则抛出异常
        @RequiresRoles(value="abc")         // 执行当前方法需要abc角色,我们先在BOSRealm中授予当前用户为abc角色才行,否则抛出异常
        public String delete() {
            staffService.deleteBatch(ids);
            return "list";
        }

    问题二:出现了新的异常,为什么呢?


    出现问题原因如下图所示:

    解决问题方法如下:
    第三步:我们需要修改BaseAction的构造方法,增加判别条件

    第四步:在struts.xml中配置全局异常捕获,统一跳转到权限不足的页面

    使用方法注解权限控制的方式,我们的工作量主要在各种Action上的各种方法上添加注解,工作量还是挺大的!

    6.3、页面标签权限控制(shiro标签库)

    第一步:在jsp页面中引入shiro的标签

    <%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro"%>

    第二步:使用shiro的标签根据当前用户拥有的权限动态展示页面元素,底层是通过java类来实现的。


    使用页面标签权限控制的方式,我们的工作量主要给各个便签添加权限控制,便签很多啊,工作量也很大呀!

    6.4、代码级别权限控制(了解,很少用)


    使用代码级别权限控制的方式,我们需要给每一个Action方法加入这两句代码,这样对代码不好,即代码健壮性差!
  • 相关阅读:
    java网络编程基础——网络基础
    java并发编程基础——线程相关的类
    java并发编程基础——线程池
    java并发编程基础——线程通信
    java并发编程基础——线程同步
    java并发编程基础—生命周期与线程控制
    java并发编程基础——线程的创建
    StreamAPI
    Lambda表达式
    Java Annotation
  • 原文地址:https://www.cnblogs.com/chenmingjun/p/9773479.html
Copyright © 2020-2023  润新知