浅谈Apache Shiro权限模块及数据库设计

时间:2022-07-26
本文章向大家介绍浅谈Apache Shiro权限模块及数据库设计,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

写在最最最~~~前面的:由于Shiro框架在学习过程中假如没有一个实例Demo的参考,理解起来可能较为生涩难懂,所以笔者建议大家参考这个开源的项目:点我下载项目,该项目是我在学习Apache Shiro过程中参考的项目,我在原项目的基础上增加了一些便于理解的注释等,项目采用前后分离的方式开发,原作者:点我查看

简介

Apache Shiro是Java项目中常用的两大安全框架之一,可以完成认证、授权、加密、会话管理等功能。Apache Shiro较Spring家族的Spring Security更为简洁、更易上手的特点。

Shiro三大组件

  • Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者。
  • SubjectManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器。
  • Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。 扩展组件:
  • SessionManager :使用Session可以完成用户登录及单点登录等功能。
  • CacheManager: 对Shiro其他组件提供缓存支持(如使用Redis缓存)。

Apache Shiro使用

1. 实现URL级别的权限控制(某个页面、资源需要什么权限访问)

首先需要在pom文件中引入该框架,配置 web.xml ,使用 shiroFilter 拦截 / * 根据需求完成对页面权限控制

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!-- Shiro核心组件   -->
    <!-- 1 配置 securityManager-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 配置缓存配置 -->
        <property name="cacheManager" ref="cacheManager"/>
        <property name="realm" ref="jdbcRealm"/>
    </bean>

    <!-- 2配置 缓存框架 ehcache-->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <!-- 3 自定义Realm -->
    <bean id="jdbcRealm" class="com.qfjy.web.shiro.ShiroRealm">
            <property name="credentialsMatcher">
                <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                    <!--hashAlgorithm 加密的名称 -->
                    <property name="hashAlgorithmName" value="MD5"></property>
                    <!-- hashIterations 加密的次数-->
                    <property name="hashIterations" value="1902"></property>
                </bean>
            </property>
    </bean>
    <!--
          <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="SHA-256"/>
            </bean>
        </property>
    -->

    <!-- =========================================================
         Shiro Spring特有的整合
         ========================================================= -->
    <!-- 后处理器自动为Spring配置的Shiro对象调用init()和destroy()方法,
    因此您不必1)为每个bean定义指定init-method和destroy-method属性,
    2)甚至知道哪些Shiro对象要求调用这些方法。
     Spring 自动管理Shiro的对象(生命周期管理)
     -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    <!--
      为Spring配置的bean启用Shiro Annotations。仅在lifecycleBeanProcessor运行后运行
          -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>



    <!--  前提条件: bean id 必须要和 web.xml shiroFilter一致 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!--未认证可以访问的页面 -->
        <property name="loginUrl" value="/login.jsp"/>
        <!-- 认证成功后可以访问的页面-->
        <property name="successUrl" value="/main.jsp"/>
        <!-- 无权限页面-->
        <property name="unauthorizedUrl" value="/unauth.jsp"/>

        <!-- <property name="filters">
            <util:map>
                <entry key="aName" value-ref="someFilterPojo"/>
            </util:map>
        </property> -->

 

        <property name="filterChainDefinitions">
            <value>

                /login.jsp = anon
                /users/login=anon
                /css/**=anon
                /images/**=anon
                /js/**=anon
                /login/exit = logout
                /student.jsp=roles[stu]
                /teacher.jsp=roles[tea]
                /list.jsp=roles[stu,tea]
                /** = authc
            </value>
            
        </property>

        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
    </bean>

    <!-- 工厂方法注入 -->
    <!-- 1 构建Bean管理交由Spring ioc容器-->
    <bean id="filterChainDefinitionMapBuilder" class="com.qfjy.web.shiro.FilterChainDefinitionMapBuilder"></bean>
    <!-- 2 工厂方法注入-->
    <bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="builder" />
</beans>

关于配置:

页面权限的优先顺序: 先声明的优先:URL权限采取第一次匹配优先的方式(为准),即从头开始

xxx.html* = anon (未登录可以访问) xxx.html* =authc (必须登录才能访问 ) xxx.html* = perms[权限] (需要特定权限才能访问) xxx.html* = roles[角色] (需要特定角色才能访问 )

2.方法级别细粒度权限控制

方法级别粒度权限控制需要完成对用户的认证功能

认证:
//Realm:SecurityManager要验证用户身份,那么它需要从Realm中获取相应分身进行比较,以确定用户身份是否合法。
//合法:从Realm中获得用户相应的角色/权限。
//ShiroRealm.java

 public class ShiroRealm  extends AuthorizingRealm {
    @Autowired
    private UsersService usersService;


    /**
     * 作用:认证
     * 1、该方法什么情况下会被调用
     *      currentUser.login(token)
     * 2、该方法的入参是什么数据
     *   UsernamePasswordToken
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //从token中获取用户从前端提交的数据
        UsernamePasswordToken upToken= (UsernamePasswordToken) token;
        System.out.println(token);
        //1 得到用户名输入的用户名
        String username=upToken.getUsername();
        //2 判断当前用户在数据库中是否存在
       Users users=usersService.selectByUsername(username);
        //3如果用户名不存在  UnknownAccountException
        if(users==null){
            throw new UnknownAccountException("用户名不存在");
        }
        //4如果用户的状态 锁定  LockedAccountException   status=0  1正常
        if(users.getStatus()==0){
            throw new LockedAccountException("该帐户已被锁定");
        }
        //5 密码的比较(前台的密码=数据库中查询的密码)  Shiro内部来完成的。
        // 方法中的参数:Object principal, Object credentials, String realmName
        /*
        身份验证:一般需要提供如身份 ID 等一些标识信息来表明登录者的身份,如提供 email,用户名/密码来证明。
        在 shiro 中,用户需要提供 principals (身份)和 credentials(证  明)给 shiro,从而应用能验证用户身份:
        •  principals:身份,即主体的标识属性,可以是任何属性,如用户名、  邮箱等,唯一即可。
        一个主体可以有多个 principals,但只有一个  Primary principals,一般是用户名/邮箱/手机号。
        •credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证  书等。
        •  最常见的 principals 和 credentials 组合就是用户名/密码了
         */
        Object principal=username;
        Object credentials=users.getPassword();//数据库查询出的密码,不能给从前端中获取的token中的密码,否则全是对的
        ByteSource credentialsSalt=ByteSource.Util.bytes(username); //加盐主体
        //AuthenticationInfo info=new SimpleAuthenticationInfo(principal,credentials,super.getName());
        AuthenticationInfo info =new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,super.getName());
        return info;
    }

    /**
     * 作用:授权
     * 1、该方法什么情况下会被调用?
     *      1.1当访问需要角色权限时访问
     *      1.2如果找到该角色,就不会再第二次调用。
     * 2、该方法的入参是什么数据?
     *     主体信息(用户名)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
       //1 得到用户名信息
        String username= (String) principals.getPrimaryPrincipal();
        //2 查询数据库,根据用户名称,查询该用户拥有哪些角色
         Set<String> roles=  usersService.selectRnamesByUserName(username);

         //3 管理员拥有所有的角色(特殊权限)
        if(roles.contains("admin")){
            roles=usersService.selectRolesAllRnames();
        }
        AuthorizationInfo info=new SimpleAuthorizationInfo(roles);
        return info;
    }


    /**
     * 注册功能时需要使用的代码
     * @param args
     */
    public static void main(String[] args) {
        //注册时存入的数据
        //对数据库的密码按照相应规则加密
        /**
         * hashAlgorithmName 加密码名称
         * credentials 要加密的密码
         * hashIterations 加密的次数
         */
        Object credentials="123456";
        String hashAlgorithmName ="MD5";
        String username="maoshuai";  //用户名
        Object salt=ByteSource.Util.bytes(username); ;
        int hashIterations=1902;  //加密次数
        Object result=new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }
}

登录:

//UserController.java
@Controller
@RequestMapping("users")

public class UsersController {

    /**
     * 登录功能
     * 如果登录失败,跳到登录页面login.jsp
     * 如果登录成功,跳到主页面 main.jsp
     *
     *@RequestParam注解:
     *将请求的参数赋值到控制器的方法参数上
     *eg:@RequestParam(value="userId",defaultValue="1"
     */
    @RequestMapping("login")  // users/login
    public ModelAndView  login(@RequestParam("username")String username,
                               @RequestParam("password")String password){
        ModelAndView model=new ModelAndView();
        //获取用户信息
        Subject currentUser = SecurityUtils.getSubject();

        if (!currentUser.isAuthenticated()) { //当前Subject是否进行认证(登录)
            //前台用户传入的用户名和密码 (将用户名和密码封装到UsernamePasswordToken对象中)
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            token.setRememberMe(true);//记住我
                try {
                //进行认证(登录)功能
                currentUser.login(token);
            } catch (UnknownAccountException uae) {//未知帐户异常
                    System.out.println(uae.getMessage());
                    model.setViewName("/login.jsp");
                    model.addObject("msg","用户名不存在");
                     return model;
            } catch (IncorrectCredentialsException ice) { //凭证匹配器异常 不正确的凭据异常
                    System.out.println(ice.getMessage());
                    model.setViewName("/login.jsp");
                    model.addObject("msg","密码输入错误");
                    return model;
            } catch (LockedAccountException lae) { //帐户锁定异常 锁定帐户例外  (将来要在业务逻辑中进行判断)
                    System.out.println(lae.getMessage());
                    model.setViewName("/login.jsp");
                    model.addObject("msg","该用户因违规,被锁定");
                    return model;
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) { // 认证异常 身份验证异常

                return null;
            }
        }
        /*
        * 在ShiroRealm中认证成功后
        * */
        model.setViewName("redirect:/main.jsp");
        return model;
    }
    /**
     * 登出功能 (退出登录)
     */
    @RequestMapping("logout") // users/logout
    public String logout(){
        Subject currentUser = SecurityUtils.getSubject();
        //all done - log out!   登出
        currentUser.logout();
        return "/login.jsp";
    }


}

粒度权限控制常用注解:

@RequiresPermissions(权限) 需要特定权限才能访问

@RequiresRoles(角色) 需要特定角色才能访问

@RequiresAuthentication 需要认证才能访问

以下是一个粒度例子:

//UserController.java
//若需要实现这么一个需求:只有特定角色才能实现删除功能
	/**
	 * 删除角色
	 * 传入一个roleId,根据roleId判断用户是否具有删除功能的权限
	 */
	@RequiresPermissions("role:delete")
	@PostMapping("/deleteRole")
	public JSONObject deleteRole(@RequestBody JSONObject requestJson) {
		CommonUtil.hasAllRequired(requestJson, "roleId");
		return userService.deleteRole(requestJson);
	}

基于RBAC的数据库设计

sys_role:用户组/角色(定义系统需要的权限分级,如CEO、HR、员工,表中使用不同的id以区分的角色)
sys_user:用户表(表中放置系统中所有用户的信息,表中的id用于设置主键,由于角色与用户是一对多的关系,role_id用于与角色表中的id进行关联,以实现用户获取其对应的角色)
sys_permission:权限表,用户登录后,获取角色,角色表对应permission中id来获取角色可以使用的操作权限,
sys_role_permission:角色权限表(表中permission_id对应sys_permission表中id,表中有role_id和permission_id,来配置角色中的操作权限)