shiro学习笔记

权限管理

为了实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问且只能访问自己被授权的资源。

用户认证

为了验证用户访问系统的合法性。

用户授权

在用户认证通过后,只能访问被系统授权的资源,授权过程可以理解为who对what(which)进行how操作

关键对象

  • subject:主体
    访问系统资源的对象,权限管理需要对主体进行身份认证
  • principal:身份信息
    身份信息通常是唯一的,一个主体可能会有多个身份信息,都有一个主身份信息(primary principal)
  • credential:凭证信息
    密码、证书等,主体在进行身份认证时需要身份信息和凭证信息
  • resource 资源
    必须具备相应权限才可以访问的对象
  • permission 权限/许可
    主体需要相应权限才能访问、操作相应资源

权限模型

  • 主体:账户、密码
  • 角色:角色名称
  • 权限:权限名称、资源名称、资源访问地址
  • 主体与角色的关系
  • 角色与权限的关系

权限控制

基于角色的访问控制

RBAC(Role Based Access Control),基于角色的访问控制

基于资源的访问控制

RBAC(Resource Based Access Control),基于资源的访问控制

权限粒度

  • 粗粒度权限管理:对资源类型的权限管理
  • 细粒度权限管理:对资源实例的权限管理

shiro架构

shiro架构
  • Subject:主体
  • SecurityManager:安全管理器,进行主体的认证和授权
  • Authenticator:用户认证管理器
  • Authorizer:权限管理器
  • SessionManager:web应用中一般是用web容器对session管理,shiro也提供一套管理session的方式
  • SessionDao:对Session进行CRUD操作(可与redis集成管理session数据)
  • CacheManager:缓存管理器,主要对session和授权数据进行缓存
  • Cryptography:加密方式
  • Realm:存取认证、授权相关数据(逻辑)
shiro缓存

当需要访问受限资源时,会实时去查询权限数据,这样的查询是频繁的,而权限信息又不是经常变化的,所以需要配置缓存来提高性能。
缓存带来的问题:当用户不退出系统(正常退出、非正常退出),是不会清空缓存的,如果权限发生变更,不能及时改变用户所拥有的权限。

shiro会话

shiro支持通过SessionManager取代web容器来管理会话,可以通过配置SessionDao(对Session的CRUD)集成Reis集群来对session进行共享、更新、删除。

使用Spring集成Shiro

数据库设计
DROP TABLE IF EXISTS users;
CREATE TABLE users (
  id       INT          NOT NULL AUTO_INCREMENT
  COMMENT '用户编号',
  name     VARCHAR(255) NOT NULL
  COMMENT '用户名称',
  username VARCHAR(255) NOT NULL
  COMMENT '账号',
  password VARCHAR(255) NOT NULL
  COMMENT '密码',
  salt     VARCHAR(255) NOT NULL
  COMMENT '盐',
  status   TINYINT      NOT NULL DEFAULT 1
  COMMENT '用户状态 0-无效,1-有效',
  PRIMARY KEY (id),
  UNIQUE KEY (username)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '用户';

DROP TABLE IF EXISTS roles;
CREATE TABLE roles (
  id        INT          NOT NULL AUTO_INCREMENT
  COMMENT '角色编号',
  role_name VARCHAR(255) NOT NULL
  COMMENT '角色名称',
  PRIMARY KEY (id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '角色';

DROP TABLE IF EXISTS permission;
CREATE TABLE permission (
  id       INT          NOT NULL AUTO_INCREMENT
  COMMENT '权限编号',
  url      VARCHAR(255) NOT NULL
  COMMENT 'url地址',
  url_name VARCHAR(255) NOT NULL
  COMMENT 'url描述',
  perm     VARCHAR(255) NOT NULL
  COMMENT '权限标识符',
  PRIMARY KEY (id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '权限';

DROP TABLE IF EXISTS user_roles;
CREATE TABLE user_roles (
  user_id INT NOT NULL
  COMMENT '用户编号',
  role_id INT NOT NULL
  COMMENT '角色编号',
  PRIMARY KEY (user_id, role_id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '用户-角色';

DROP TABLE IF EXISTS role_permissions;
CREATE TABLE role_permissions (
  role_id       INT NOT NULL
  COMMENT '角色编号',
  permission_id INT NOT NULL
  COMMENT '权限编号',
  PRIMARY KEY (role_id, permission_id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '角色-权限';
依赖

除了基本的Spring依赖,还需要shiro-spring、shiro-cache、aspectj。

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.11</version>
        </dependency>
spring-shiro配置文件
<?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.xsd">

    <!-- shiroFilter -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!-- 登录地址(登录页面地址,不拦截,登录失败跳回该页) -->
        <property name="loginUrl" value="/"/>
        <!-- 成功登录跳转地址 -->
        <property name="successUrl" value="/home"/>
        <!-- 自定义表单验证filter配置 -->
        <property name="filters">
            <map>
                <entry key="authc" value-ref="authFormFilter" />
            </map>
        </property>
        <!-- 过滤器链定义,由上往下顺序执行 -->
        <property name="filterChainDefinitions">
            <value>
                <!-- 设置静态资源匿名访问 -->
                /resources/** = anon
                <!-- ajax登录url,不拦截 -->
                /login = anon
                <!-- 配置登出url -->
                /logout = logout
                <!-- 此处可以配置权限,也可在类或方法上标注
                /home = authc
                /query = perms[/query]
                /add = perms[/add]
                /update = perms[/update]
                /delete = perms[/delete]
                -->
            </value>
        </property>
    </bean>

    <!-- securityManager -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="authRealm"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>

    <!-- 配置realm,用于认证、授权 -->
    <bean id="systemRealm" class="com.wch.ssm.shiro.realm.SystemRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
    </bean>

    <!-- 配置凭证匹配器,加密方式和hash次数 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="1"/>
    </bean>

    <!-- cacheManager -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:config/shiro-ehcache.xml"/>
    </bean>

    <!-- sessionManager -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- 设置session的失效时长 -->
        <property name="globalSessionTimeout" value="600000"/>
        <!-- 删除失效的session -->
        <property name="deleteInvalidSessions" value="true"/>
    </bean>

    <!-- 配置自定义表单验证过滤器 -->
    <bean id="authFormFilter" class="com.wch.ssm.shiro.AuthFormFilter"/>

</beans>
自定义Realm
/**
 * 自定义Realm,用于认证和授权
 */
public class AuthRealm extends AuthorizingRealm {

    @Resource
    private SecurityService securityService;

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthRealm.class);

    /**
     * 认证
     *
     * @param token token
     * @return AuthenticationInfo
     * @throws AuthenticationException AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        User user = securityService.getPasswordAndSalt(username);

        if (null == user) {
            throw new UnknownAccountException("不存在该账户!");
        }

        String name = user.getName();
        String password = user.getPassword();
        String salt = user.getSalt();
        if (null == name || null == password || null == salt) {
            throw new AccountException("账户异常!");
        }

        // 身份信息,密码(数据库中加密后的密码),salt,realmName
        return new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(salt), this.getName());
    }

    /**
     * 授权
     *
     * @param principals principals
     * @return AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = null;
        try {
            // 获取身份信息
            User user = (User) principals.getPrimaryPrincipal();
            // 查询权限信息
            Set<String> permissions = securityService.getStringPermissions(user.getId());
            info = new SimpleAuthorizationInfo();
            info.addStringPermissions(permissions);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        return info;
    }

    /**
     * 用户权限发生变动,调用此方法清除缓存
     */
    public void clearCache() {
        PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }
}
控制器
    /**
     * 验证登录
     *
     * @return json data
     * @throws ShiroException ShiroException
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public @ResponseBody
    Result login(String username, String password) {
        try {
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // 登录失败:包括账户不存在、密码错误等,都会抛出ShiroException
            SecurityUtils.getSubject().login(token);
            return Result.response(ResultEnum.SUCCESS);
        } catch (ShiroException e) {
            LOGGER.error("登录失败,{},{}", e.getClass().getName(), e.getMessage());
            return Result.response(ResultEnum.FAIL);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            return Result.response(ResultEnum.FAIL);
        }
    }

    /**
     * successUrl
     * 使用注解 @RequiresAuthentication 来标注该访问该url需要认证
     *
     * @param model model
     * @return Page
     */
    @RequestMapping("/home")
    @RequiresAuthentication
    public String home(Model model) {
        // 获取在身份认证时放入的身份信息
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("name", user.getName());
        return "home";
    }

    /**
     * unauthorizedUrl,未授权时跳转该url
     *
     * @return json
     */
    @ExceptionHandler(UnauthorizedException.class)
    @RequiresAuthentication
    public @ResponseBody
    String forbidden() {
        return "403";
    }

    /**
     * 使用 @RequiresPermissions 注解来标注访问该url需要 "user:query" 权限
     *
     * @return json
     */
    @RequestMapping("/query")
    @RequiresPermissions("user:query")
    public @ResponseBody
    String query() {
        return "permit query.";
    }
登录交互
<script type="text/javascript">
    $('#submit').click(function () {
        $.ajax({
            url: 'login',
            type: 'POST',
            data: {
                username: $('#username').val().trim(),
                password: $('#password').val().trim()
            },
            success: function (res) {
                if (res.code === 200) {
                    window.location.href = 'home'
                } else {
                    alert("Login Failed!");
                }
            }
        });
    });
</script>

使用SpringBoot集成shiro

配置ShiroConfig

对于需要配置权限的url,每个都配置注解是很不方便的,可以通过应用启动时查询持久化到数据库中的权限配置来生成拦截器链。
ShiroConfig加载到容器中时,查询权限的Service可能还未注入,导致空指针异常。因此在ShiroConfig中应使用手动注入的方式来获取查询权限Service。

获取ApplicationContext

为了获取ApplicationContext,ShiroConfig需要实现ApplicationContextAware接口,实现setApplicationContext()方法。

    private ApplicationContext context;

    /**
     * 获取ApplicationContext
     *
     * @param applicationContext applicationContext
     * @throws BeansException BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
适配权限标识

在自定义Realm中重写的doGetAuthorizationInfo()方法,返回类型SimpleAuthorizationInfo,添加权限的方式是通过ddStringPermissions(Collection<String> permissions)添加权限的字符串形式,例如sys:add,但是在拦截器链中配置权限的要求是perms[sys:add]的形式,因此需要对权限标识进行适配。

     /**
     * 适配拦截器权限标识符
     *
     * @param perm perm
     * @return perms[]
     */
    private String adaptPerms(String perm) {
        StringBuilder sb = new StringBuilder();
        sb.append("perms[").append(perm).append("]");
        return sb.toString();
    }
配置拦截器链
    // 拦截器链,由上到下顺序执行
    Map<String, String> filterChain = new LinkedHashMap<>();

    // 动态添加权限
    SecurityService securityService = null;
    while (securityService == null) {
        securityService = (SecurityService) context.getBean("securityServiceImpl");
    }
    List<Permission> permissions = securityService.getPermissions();
    for (Permission permission : permissions) {
      filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
    }
完整配置
@Configuration
public class ShiroConfig implements ApplicationContextAware {

    private ApplicationContext context;

    /**
     * 配置realm,用于认证、授权
     *
     * @return Realm
     */
    @Bean
    public Realm authRealm() {
        // 凭证匹配器,配置加密方式和hash次数
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(CommonConstants.HASH_CREDENTIAL_NAME);
        credentialsMatcher.setHashIterations(CommonConstants.HASH_ITERATIONS);

        AuthRealm authRealm = new AuthRealm();
        authRealm.setCredentialsMatcher(credentialsMatcher);
        return authRealm;
    }

    /**
     * 配置EhCache缓存管理器,用于授权信息缓存
     *
     * @return CacheManager
     */
    private CacheManager getEhCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:config/shiro-ehcache.xml");
        return cacheManager;
    }

    /**
     * 配置SecurityManager
     *
     * @return SecurityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm());
        securityManager.setCacheManager(getEhCacheManager());
        return securityManager;
    }

    /**
     * 设置由servlet容器管理filter生命周期
     *
     * @return LifecycleBeanPostProcessor
     */
    @Bean
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启aop,对类代理
     *
     * @return Proxy
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启shiro注解支持
     *
     * @return Advisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager());
        return advisor;
    }

    /**
     * 配置shiroFilter,beanName必须为shiroFilter
     *
     * @return ShiroFilter
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        // 配置SecurityManager
        filter.setSecurityManager(securityManager());
        // 配置登录页
        filter.setLoginUrl("/");
        // 登录成功跳转链接
        filter.setSuccessUrl("/sys");
        // 未授权界面
        filter.setUnauthorizedUrl("/403");
        // 拦截器链,由上到下顺序执行
        Map<String, String> filterChain = new LinkedHashMap<>();
        // 配置ajax登录url匿名访问
        filterChain.put("/login", "anon");
        // 配置登出路径
        filterChain.put("/logout", "logout");
        // 静态资源处理
        filterChain.put("/js/**", "anon");
        filterChain.put("/css/**", "anon");
        filterChain.put("/img/**", "anon");

        // 动态添加权限
        SecurityService securityService = null;
        while (securityService == null) {
            securityService = (SecurityService) context.getBean("securityServiceImpl");
        }
        List<Permission> permissions = securityService.getPermissions();
        for (Permission permission : permissions) {
            filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
        }

        // 认证后访问
        filterChain.put("/**", "authc");
        filter.setFilterChainDefinitionMap(filterChain);
        return filter;
    }

    /**
     * 适配拦截器权限标识符
     *
     * @param perm perm
     * @return perms[]
     */
    private String adaptPerms(String perm) {
        StringBuilder sb = new StringBuilder();
        sb.append("perms[").append(perm).append("]");
        return sb.toString();
    }

    /**
     * 获取ApplicationContext
     *
     * @param applicationContext applicationContext
     * @throws BeansException BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 构建一个互联网应用,权限校验管理是很重要的安全措施,这其中主要包含: 认证 - 用户身份识别,即登录 授权 - 访...
    zhuke阅读 3,475评论 0 30
  • 1.简介 Apache Shiro是Java的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供...
    H_Man阅读 3,145评论 4 48
  • 前言 Spring boot 是什么,网上的很多介绍,这里博客就不多介绍了。如果不明白Spring boot是什么...
    xuezhijian阅读 17,880评论 13 39
  • 柳哲 我研究家谱二十余年,收藏家谱不计其数。和圣柳下惠后裔世代传承2700多年的《展氏族谱》,尤为罕见,极其珍贵。...
    柳志儒阅读 240评论 0 0
  • 捆绑你的是爱吗? 我一直觉得每个人都是欣欣向荣的,只要是内心渴望光明,就不会被黑暗罩住,心里的渴望越大,眼神就会越...
    笑不二阅读 467评论 3 1