zoukankan      html  css  js  c++  java
  • Shiro的认证和权限控制

    权限控制的方式

    从类别上分,有两大类: 
    - 认证:你是谁?–识别用户身份。 
    - 授权:你能做什么?–限制用户使用的功能。

    权限的控制级别

    从控制级别(模型)上分: 
    - URL级别-粗粒度 
    - 方法级别-细粒度 
    - 页面级别-自定义标签(显示) 
    - 数据级别-最细化的(数据)

    URL级别的权限控制-粗粒度

    在web.xml中配置一个过滤器filter,在过滤器中,对请求的地址进行解析,字符串截取: 
    url.substring()…把上下文前面的路径都截取掉,剩下user_login.action。 
    过滤器代码: 
    以通过查询数据库,来判断,当前登录用户,是否可以访问user_login.action。 
    url级别控制,每次请求过程中只控制一次 ,相比方法级别权限控制 是粗粒度的 !URL级别权限控制,基于Filter实现。

    方法级别的权限控制-细粒度

    aop面向切面的编程,在方法执行之前,进行权限判断,如果没有权限,抛出异常,终止方法的继续运行。 
    自定义注解 在需要权限控制方法上, 添加需要的权限信息 
    代理 (Spring AOP ),在目标方法运行时 进行增强 ,通过反射技术获取目标方法上注解中权限 , 查询数据库获取当前登陆用户具有权限,进行比较。 
    相比URL级别权限控制, 可以控制到服务器端执行的每个方法,一次请求中可以控制多次。

    页面(显示)级别的权限控制-自定义标签

    页面显示的权限控制,通常是通过 自定义标签来实现

    数据级别的权限控制

    在每条数据上增加一个字段,该字段记录了权限的值。数据和权限绑定。 
    代码,你在查询数据的时候,需要去权限和用户对应表中,通过当前登录用户的条件,查询出你的数据权限。然后再将数据权限作为一个条件,放到业务表中进行查询。从而限制了数据的访问。

    权限系统的数据表设计

    • 资源:用户要访问的目标,通常是服务中的程序或文件
    • 权限:用户具有访问某资源的能力
    • 角色:权限的集合,为了方便给用户授权。
    • 用户:访问系统的’人’。

    表对象实体: 
    - 用户(User)表:访问系统的用户,比如用户登录要用 
    - 权限(Function)表:系统某个功能允许访问而对应的权限 
    - 角色(Role)表:角色是权限的集合(权限组),方便用户授权。

    表对象之间的关系: 
    - 用户和角色关系表:一个用户对应N个角色,一个角色可以授予N个用户—》多对多关系 
    - 角色和权限关系表:一个角色包含N个权限,一个权限可以属于N个角色—》多对多关系

    完整的权限相关表: 
    URL级别权限控制包含:资源表、权限表、角色表、用户表,以及相关关系(都是多对多),共7张表。 
    方法级别的权限控制包含:功能权限、角色、用户,以及相关关系(都是多对多),共5张表。

    但Apache Shiro框架支持的URL级别权限控制,是将资源和资源权限对应关系配置到了配置文件中,不需要表的支撑,只需要5张表了。

    Apache Shiro权限控制

    Apache Shiro 可以不依赖任何技术使用, 可以直接和web整合,通常在企业中和Spring 结合使用。

    Authentication: 认证 — 用户登录 
    Authorization : 授权 —- 功能权限管理

    通过引入Maven坐标导入shiro

    官方建议:不推荐直接引入shiro-all,依赖比较多,原因怕有jar冲突。官方推荐根据需要单独导入jar。

    Shiro基本原理

    Shiro的框架的体系结构: 

    Shiro权限控制流程的原理: 

    • 应用代码 —- 调用Subject (shiro的Subject 就代表当前登陆用户) 控制权限 —- Subject 在shiro框架内部 调用 Shiro SecurityManager 安全管理器 —– 安全管理器调用 Realm (程序和安全数据连接器 )。
    • Subject要进行任何操作,都必须要调用安全管理器(对我们来说是自动的)。 
      而安全管理器会调用指定的Realms对象,来连接安全数据。
    • Realms用来编写安全代码逻辑和访问安全数据,是连接程序和安全数据的桥梁。

    URL级别的权限控制

    配置整合和url级别认证

    配置过滤器web.xml:放在struts的前端控制器之前配置,但放在openEntitymanage之后。

        <!-- shiro权限过滤器 -->
            <filter>
                <!-- 这里的 filter-name 要和 spring 的 applicationContext-shiro.xml 里的 org.apache.shiro.spring.web.ShiroFilterFactoryBean 
                    的 bean name 相同 -->
                <filter-name>shiroSecurityFilter</filter-name>
                <!-- spring的代理过滤器类:以前的过滤器 -->
                <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
                <init-param>
                    <!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->    
                    <param-name>targetFilterLifecycle</param-name>
                    <param-value>true</param-value>
                </init-param>
            </filter>
            <filter-mapping>
                <filter-name>shiroSecurityFilter</filter-name>
                <url-pattern>/*</url-pattern>
            </filter-mapping>
    
    配置ApplicationContext.xml:(shiro权限控制过滤器+ shiro安全管理器)
    
            <!-- shiro权限控制过滤器bean -->
            <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
                <!-- shiro 的核心安全接口 -->
                <property name="securityManager" ref="securityManager" />
                <!-- 要求登录时的链接 -->
                <property name="loginUrl" value="/login.jsp" />
                <!-- 登陆成功后要跳转的连接 -->
                <property name="successUrl" value="/index.jsp" />
                <!-- 未授权时要跳转的连接,权限不足的跳转路径 -->
                <property name="unauthorizedUrl" value="/unauthorized.jsp" />
                <!-- shiro 连接约束配置(URL级别的权限控制),即URL和filter的关系,URL控制规则:路径=规则名 -->
                <property name="filterChainDefinitions">
                    <value>
                        <!--按需求配置-->
                        /login.jsp = anon
                        /validatecode.jsp = anon
                        /js/** = anon
                        /css/** = anon
                        /images/** = anon
                        /user_login.action* = anon
                        /page_base_staff.action = anon
                        /page_base_region.action = perms["user"]
                        /page_base_subarea.action = roles["operator"]
                        /** = authc
                    </value>
                </property>
            </bean>
            <!-- shiro安全管理器 -->
            <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
                <!-- 注入 Realm连接安全数据-->
            </bean>

    配置shiroFilter 其实是一个过滤器链,含有10个Filter(校验功能)。

    常用: 
    认证 
    - anon不用认证(登录)就能访问(单词注意大小写) 
    - authc: 需要认证(登录)才能使用,例如/admins/user/**=authc,没有参数。 
    授权: 
    - perms:需要拥有某权限才能使用,如具体允许的权限:/page_base_region.action =perms[“user”],如果要访问该action,当前登录用户必须拥有user名字的权限。 
    - roles:需要拥有某角色才能使用,如具体允许的角色:/page_base_subarea.action = roles[“operator”]如果要访问该action,当前用户必须拥有operator权限。

    用户认证(登录)—自定义Realm

    Shiro实现登录逻辑 
    用户输入用户名和密码 —- 应用程序调用Subject的login方法 —- Subject 调用SecurityManager的方法 —- SecurityManager 调用Realm的认证方法 —- 认证方法根据登录用户名查询密码 ,返回用户的密码 —- SecurityManager 比较用户输入的密码和真实密码是否一致 。

    编写Shiro的认证登录逻辑

            @Action(value="user_login",results={@Result(name=SUCCESS,type="redirect",location="/index.jsp"),@Result(name=LOGIN,location="/login.jsp")})
            @InputConfig(resultName="login")
            public String login() throws Exception {        
                //shrio:登陆逻辑
                //获取认证对象的包装对象
                Subject subject = SecurityUtils.getSubject();
    
                //获取一个认证的令牌:
                //直接获取页面的用户和密码进行校验
                AuthenticationToken authenticationToken = new UsernamePasswordToken(model.getUsername(),MD5Utils.md5(model.getPassword()));
                //认证过程
                try {
                    // 如果成功,就不抛出异常,会自动将用户放入session的一个属性
                    subject.login(authenticationToken);
                    //成功,返回首页
                    return SUCCESS;
                }catch(UnknownAccountException e){
                    //用户名错误
                    addActionError(getText("UserAction.usernamenotfound"));
                    //返回登陆页面
                    return LOGIN;
                }catch (IncorrectCredentialsException e) {
                    //密码错误
                    addActionError(getText("UserAction.passwordinvalid"));
                    //返回登陆页面
                    return LOGIN;
                }
                catch (AuthenticationException e) {
                    //认证失败
                    e.printStackTrace();
                    //页面上进行提示
                    addActionError(getText("UserAction.loginfail"));
                    //返回登陆页面
                    return LOGIN;
                }
            }

    编写Realm,给SecurityManager提供

    JdbcRealm和jndiLdapRealm,直接连接jdbc或jndi或ldap。 
    相当于dao和reaml整合了,能直接读取数据库,逻辑代码都实现好了。

            /**
             *  实现认证和授权功能
             *自定义的realm,作用从数据库查询数据,并返回数据库认证的信息
             */
            @Component("bosRealm")
            public class BosRealm extends AuthorizingRealm{
    
                //注入ehcache的缓存区域
                @Value("BosShiroCache")//注入缓存具体对象的名字,该名字在ehcache.xml中配置的
                public void setSuperAuthenticationCacheName(String authenticationCacheName){
                    super.setAuthenticationCacheName(authenticationCacheName);
                }
    
                //注入service
                @Autowired
                private UserService userService;
    
                //注入角色dao
                @Autowired
                private RoleDao roleDao;
    
                //注入功能的dao
                @Autowired
                private FunctionDao functionDao;
    
                //授权方法:获取用户的权限信息
                //授权:回调方法
                //如果返回null,说明没有权限,shiro会自动跳到<property name="unauthorizedUrl" value="/unauthorized.jsp" />
                //如果不返回null,根据配置/page_base_subarea.action = roles["weihu"],去自动匹配
                //给授权提供数据的
                @Override
                protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
                    //给当前用户授权的权限(功能权限、角色)
                    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
                    //两种方式:
                    //方式1:工具类来获取(首长-)
            //      User user=(User)SecurityUtils.getSubject().getPrincipal();
                    //方式2:通过参数获取首长(推荐)
                    User user = (User) principals.getPrimaryPrincipal();
    
                    //实际:需要根据当前用户的角色和功能权限来构建一个授权信息对象,交给安全管理器
    
                    if (user.getUsername().equals("admin")) {
                        //如果是超级管理员
                        //查询出所有的角色,给认证信息对象
                        List<Role> roleList = roleDao.findAll();
                        for (Role role : roleList) {
                            authorizationInfo.addRole(role.getCode());
                        }
                        //查询出所有的功能权限,给认证对象
                        List<Function> functionList = functionDao.findAll();
                        for (Function function : functionList) {
                            authorizationInfo.addStringPermission(function.getCode());
                        }
                    } else {
                        //如果是普通用户
                        List<Role> roleList = roleDao.findByUsers(user);
                        for (Role role : roleList) {
                            authorizationInfo.addRole(role.getCode());
                            //导航查询,获取某角色的拥有的功能权限
                            Set<Function> functions = role.getFunctions();
                            for (Function function : functions) {
                                authorizationInfo.addStringPermission(function.getCode());
                            }
                        }
                    }
    
                    return authorizationInfo;//将授权信息交给安全管理器接口。
                }
    
                //认证:回调,认证管理器会将认证令牌放到这里(action层的令牌AuthenticationToken)
                //发现如果返回null,抛出用户不存在的异常UnknownAccountException
                @Override
                protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
                    //用户名密码令牌(action传过来)
                    UsernamePasswordToken upToken = (UsernamePasswordToken) token;
                    //调用业务层来查询(根据用户名来查询用户,无需密码)
                    User user = userService.findByUsername(upToken.getUsername());
                    //判断用户是否存在
                    if (user == null) {
                        //用户不存在
                        return null;//抛出异常
                    } else {
                        //用户名存在
                        //参数1:用户对象,将来要放入session,数据库查询出来的用户
                        //参数2:凭证(密码):密码校验:校验的动作交给shiro
                        //参数3:当前使用的Realm在Spring容器中的名字(bean的名字,自动在spring容器中寻找)
                        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), super.getName());
                        return authenticationInfo;//密码校验失败,会自动抛出IncorrectCredentialsException
                    }
                }
    
            }

    ApplicatonContext.xml:

        <!-- service需要spring扫描 -->
        <context:component-scan base-package="cn.aric.bos.service,cn.aric.bos.web,cn.aric.bos.auth.realm" />
    
        <!-- shiro安全管理器 -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <!-- 注入 Realm连接安全数据-->
            <property name="realm" ref="bosRealm"></property>
            <!-- 注入shiro的缓存管理器 -->
            <property name="cacheManager" ref="shiroCacheManager"/>
        </bean>

    用户认证(退出)以及修改密码

            /**
             * 用户退出登录
             * @return
             * @throws Exception
             */
            @Action(value="user_logout",results={@Result(name=LOGIN,type="redirect",location="/login.jsp")})
            public String logout() throws Exception {       
                //shiro退出
                Subject subject = SecurityUtils.getSubject();
                subject.logout();
                //跳转登陆页面
                return LOGIN;
            }
    
            /**
             * 用户修改密码
             * @return
             * @throws Exception
             */
        //  @Action(value="user_editPassword",results={@Result(name=JSON,type=JSON)})
            @Action("user_editPassword")
            public String editPassword() throws Exception {
                //获取Principal就是获取当前用户
                User loginUser = (User) SecurityUtils.getSubject().getPrincipal();
                model.setId(loginUser.getId());
    
                //页面结果
                HashMap<String,Object> resultMap = new HashMap<String,Object>();
                try {
                    //调用service进行修改密码
                    userService.updateUserPassword(model);
                    //修改成功
                    resultMap.put("result", true);
                } catch (Exception e) {
                    e.printStackTrace();
                    //修改失败
                    resultMap.put("result", false);
                }
                //将结果压入栈顶
                ActionContext.getContext().getValueStack().push(resultMap);
                //转换为json
                return JSON;
            }

    用户授权(授权)—自定义Ream

    数据库数据添加,applicationContext.xml配置

            <property name="filterChainDefinitions">
                <value>
                    /login.jsp = anon
                    /validatecode.jsp = anon
                    /js/** = anon
                    /css/** = anon
                    /images/** = anon
                    /user_login.action = anon
                    /page_base_staff.action = anon
                    /page_base_region.action = perms["region"]
                    /page_base_subarea.action = roles["weihu"]
                    /page_qupai_noticebill_add.action = perms["noticebill"]
                    /page_qupai_quickworkorder.action = roles["kefu"]
                    /** = authc
                </value>
            </property>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    代码在上面的BosRealm的中,protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) 
    RoleDao省略。

    方法级别的权限控制

    启用Shiro注解

    需要 Shiro 的 Spring AOP 集成来扫描合适的注解类以及执行必要的安全逻辑。 
    ApplicationContext.xml

            <!-- 开启权限控制的注解功能并且配置aop -->
            <!-- 后处理器:通过动态代理在某bean实例化的前增强。:自己去找权限注解 -->
            <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
            <!-- 切面自动代理:相当于以前的AOP标签配置 
            advisor:切面 advice:通知
            -->
            <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" 
            depends-on="lifecycleBeanPostProcessor">
    
            </bean>
    
            <!-- Advisor切面配置:授权属性的切面 -->
            <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
                <!-- 注入安全管理器 -->
                <property name="securityManager" ref="securityManager"/>
            </bean>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在需要权限控制的目标方法上面使用shiro的注解: 
    @RequiresAuthentication 需要用户登录 
    subject.isAuthenticated() 必须返回true 
    @ RequiresUser 
    subject.isAuthenticated() 返回true 或者subject.isRemembered() 返回true 
    “Remember Me”服务: 
    认证机制 基于 session 
    被记忆机制 基于 cookie (subject.isAuthenticated() 返回 false )

    @ RequiresGuest 与 @RequiresUser 相反,不能认证也不能被记忆。 
    @ RequiresRoles 需要角色 
    @RequiresPermissions 需要权限

    异常

    动态代理异常

    解决方案: 
    配置ApplicationContext.xml,设置代理为cglib代理(对目标类代理)

            <!-- 切面自动代理:相当于以前的AOP标签配置 -->
            <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" 
            depends-on="lifecycleBeanPostProcessor" >
                <!-- 设置aop的代理使用CGLIB代理 -->
                <property name="proxyTargetClass" value="true"/>
            </bean>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    方案二:

    <aop:config proxy-target-class="true" />
    • 1

    类型转换异常

    解决方案:递归向上寻找泛型的类型。

        //递归向上 查找
            Class actionClass =this.getClass();
            //向父类递归寻找泛型
            while(true){
                //得到带有泛型的类型,如BaseAction<Userinfo>
                Type type = actionClass.getGenericSuperclass();
    
                if(type instanceof ParameterizedType){
                    //转换为参数化类型
                    ParameterizedType parameterizedType = (ParameterizedType) type;
                    //获取泛型的第一个参数的类型类,如Userinfo
                    Class<T> modelClass = (Class<T>) parameterizedType.getActualTypeArguments()[0];
                    //实例化模型对象
                    try {
                        model=modelClass.newInstance();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    break;
                }
                //寻找父类
                actionClass=actionClass.getSuperclass();
    
            }

    空指针异常

    解决方案1:使用public 的Setter方法上的注解直接注入Service。 

    SubareaAction:

            //注入service
            private SubareaService subareaService;
            @Autowired
            public void setSubareaService(SubareaService subareaService) {
                this.subareaService = subareaService;
            }

    解决方案2: 
    @Autowire还放到私有声明上, 
    在struts.xml中覆盖常量(开启自动装配策略): 
    值默认是false,struts2默认注入采用的是构造器注入(从spring中寻找的bean) 
    改成true,struts2会采用setter方法注入

    页面标签(实现页面内容定制显示)

            <!-- 引入Shiro标签 -->
            <%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro"%>

    页面拿Session中的user对象: 代表user对象。

    程序中拿Session中的user对象:SecurityUtils.getSubject().getPrincipal()

    资源通配符和权限通配符可便捷开发。

    代码级别

    使用代码编程的方式,直接在程序中使用Subject对象,调用内部的一些API。(有代码侵入)

        //代码级别的权限控制(授权):功能权限和角色权限:两套机制:boolean判断,异常判断
            //授权的权限控制
            //====布尔值判断
            //功能权限
            if(subject.isPermitted("staff")){
                //必须拥有staff功能权限才能执行代码
                System.out.println("我是一段代码。。。。。");
            }
            //角色权限
            if(subject.hasRole("weihu")){
                //必须拥有staff功能权限才能执行代码
                System.out.println("我是一段代码。。。。。");
            }
            //====异常判断
            //功能权限
            try {
                subject.checkPermission("staff");
                //有权限
            } catch (AuthorizationException e) {
                // 没权限
                e.printStackTrace();
            }
            //角色权限
            try {
                subject.checkRole("weihu");
                //有权限
            } catch (AuthorizationException e) {
                // 没权限
                e.printStackTrace();
            }
  • 相关阅读:
    Java script基础 回顾
    Application、 session、iewstate,以及repeater 的commang用法
    Response、Request、QueryString,repeater添加,修改,删除数据
    Repeater 使用方法
    web form 复合控件
    weborm 简单控件
    WebForm开发基础
    Asp.Net 基础理论
    winform 进程,线程
    repeater使用
  • 原文地址:https://www.cnblogs.com/SUNSHINEC/p/9558799.html
Copyright © 2011-2022 走看看