《Shiro 一》SpringBoot + Shiro 构建Web 工程

Shiro 一款简单易用,功能强大的安全框架,帮助我们安全高效的构建企业级应用。之前几个项目都用到过 Shiro,最近抽空梳理了一下,分享一些经验。

本文demo:https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/springboot-shiro
本文demo选型:thymeleaf, springboot 2, shiro, ehcache
PS:如果我不拖延的话,估计还是会有后续的 :)

目录

Shiro 能做什么
Shiro 常用组件介绍
Shiro 是如何工作的
Shiro 如何集成
关于 thymeleaf-extras-shiro


Shiro 能做什么
  • 认证:登录用户的认证
  • 权限:基于角色和权限的访问权限(url权限),以及颗粒化权限控制(按钮权限)
  • 加密技术:Shiro的crypto包中包含了一系列的易于理解和使用的加密、哈希(aka摘要)辅助类
  • session管理:可在web容器以及 EJB容器中使用 session,可扩展 (例如我们可以通过重写 sessionDao 将 session 存储到数据库中)
  • RememberMe:基于cookie的记住我服务
Shiro 常用组件介绍
image.png
  • Subject:Subject其实代表的就是当前正在执行操作的用户,只不过因为“User”一般指代人,但是一个“Subject”可以是人,也可以是任何的第三方系统,服务账号等任何其他正在和当前系统交互的第三方软件系统。
    所有的Subject实例都被绑定到一个SecurityManager,如果你和一个Subject交互,所有的交互动作都会被转换成Subject与SecurityManager的交互

  • SecurityManager:Shiro的核心,他主要用于协调Shiro内部各种安全组件,不过我们一般不用太关心SecurityManager,对于应用程序开发者来说,主要还是使用Subject的API来处理各种安全验证逻辑

  • Realm:这是用于连接Shiro和客户系统的用户数据的桥梁。一旦Shiro真正需要访问各种安全相关的数据(比如使用用户账户来做用户身份验证以及权限验证)时,他总是通过调用系统配置的各种Realm来读取数据

  • 关于Shiro 的其余核心组件参考 Shiro 官网 或者 Shiro的架构 本文不做过多的阐述

Shiro 是如何工作的

简单来讲的话,在Spring项目中

  1. Shiro 会将他的所有组件注册到 SecurityManager
  2. 再通过将 SecurityManager 注册到 ShiroFilterFactoryBean(这个类实现了Spring 的BeanPostProcessor会预先加载) 中,
  3. 最后以 filter 的形式注册到Spring容器(实现了Spring的FactoryBean,构造一个 filter 注册到 Spring 容器中),实现用户权限的管理。
Shiro 如何集成
  • shiro 所需依赖,完整见demo源码
<!--shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
<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>
<!-- 基于thymeleaf的shiro扩展 -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>
  • ShiroConfig
@Configuration
public class ShiroConfig {

    private static final Logger log = LoggerFactory.getLogger(ShiroConfig.class);

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map<String, String> chainDefinition = new LinkedHashMap<>();
        // 静态资源与登录请求不拦截
        chainDefinition.put("/js/**", "anon");
        chainDefinition.put("/css/**", "anon");
        chainDefinition.put("/img/**", "anon");
        chainDefinition.put("/layui/**", "anon");
        chainDefinition.put("/login", "anon");
        chainDefinition.put("/login.html", "anon");
        // 用户为授权通过认证 && 包含'admin'角色
        chainDefinition.put("/admin/**", "authc, roles[super_admin]");
        // 用户为授权通过认证或者RememberMe && 包含'document:read'权限
        chainDefinition.put("/docs/**", "user, perms[document:read]");
        // 用户访问所有请求 授权通过 || RememberMe
        chainDefinition.put("/**", "user");

        shiroFilter.setFilterChainDefinitionMap(chainDefinition);
        // 当 用户身份失效时重定向到 loginUrl
        shiroFilter.setLoginUrl("/login.html");
        // 用户登录后默认重定向请求
        shiroFilter.setSuccessUrl("/index.html");
        return shiroFilter;
    }

    @Bean
    public Realm realm() {
        ShiroRealm realm = new ShiroRealm();
        realm.setCredentialsMatcher(credentialsMatcher());
        realm.setCacheManager(ehCacheManager());
        return realm;
    }

    @Bean
    public CacheManager ehCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }

    @Bean
    public CredentialsMatcher credentialsMatcher() {
        AuthCredentialsMatcher credentialsMatcher = new AuthCredentialsMatcher(ehCacheManager());
        credentialsMatcher.setHashAlgorithmName(AuthCredentialsMatcher.HASH_ALGORITHM_NAME);
        credentialsMatcher.setHashIterations(AuthCredentialsMatcher.HASH_ITERATIONS);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        log.debug("--------------shiro已经加载----------------");
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setCacheManager(ehCacheManager());
        manager.setRealm(realm());
        manager.setRememberMeManager(rememberMeManager());
        return manager;
    }

    @Bean
    public RememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    @Bean
    public SimpleCookie rememberMeCookie(){
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * Shiro生命周期处理器:
     * 用于在实现了Initializable接口的Shiro bean初始化时调用Initializable接口回调(例如:UserRealm)
     * 在实现了Destroyable接口的Shiro bean销毁时调用 Destroyable接口回调(例如:DefaultSecurityManager)
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 启用shrio授权注解拦截方式,AOP式方法级权限检查
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * thymeleaf的shiro扩展
     *
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

}

以上基本是Spring项目集成 Shiro 的通用配置,下面针对上述的几个Bean 聊一聊
1. ShiroFilterFactoryBean:用于定义 请求的拦截规则, Shiro为我们默认提供了一些选项,常用如下

  • anon: 请求不拦截
  • authc: 要求用户必须认证通过
  • user: 要求用户为记住我状态
  • roles[xxx]: 要求用户必须满足 xxx 角色
  • perms[xxx]: 要求用户必须满足 xxx 权限
    其实上述每一个都对应了一个 Shiro 过滤器
Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
  • 我们也可以自定义 过滤器来实现拦截

2. Realm:上面提到过Realm是用于连接Shiro和客户系统的用户数据的桥梁, 我们通过实现AuthorizingRealm 来提供用户认证和授权两个API

public class ShiroRealm extends AuthorizingRealm {

    private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);

    @Autowired
    @Lazy // 这里lazy 是有必要的, shiro组件会预先加载,导致依赖的bean 没有生成代理对象(AOP失效)
    private UserService userService;

    /**
     * 认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executing doGetAuthenticationInfo", username));
        }

        User user = userService.getUserByUsername(username);

        if (user == null) {
            throw new UnknownAccountException();
        }

        if (Constant.IS_LOCK.equals(user.getIsLock())) {
            throw new LockedAccountException();
        }

        // ShiroUser 作为实际的 principal
        ShiroUser shiroUser = new ShiroUser();
        BeanUtils.copyProperties(user, shiroUser);

        // SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
        // principal 会被封装到 subject 中
        // shiro 默认会把我们的 credentials (也就是password) 和 token 中的作对比,所以我们可以不用做密码校验
        ByteSource salt = ByteSource.Util.bytes(user.getUsername());
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(shiroUser, user.getPassword(), salt, getName());

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executed doGetAuthenticationInfo", username));
        }

        return info;
    }

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        ShiroUser shiroUser = (ShiroUser) principalCollection.getPrimaryPrincipal();

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executing doGetAuthorizationInfo", shiroUser.getUsername()));
        }

        AuthorizationDTO authorizationDTO = userService.getRolesAndPermissions(shiroUser.getId());

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(authorizationDTO.getRoleCodeSet());
        info.addStringPermissions(authorizationDTO.getPermissionCodeSet());

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executed doGetAuthorizationInfo", shiroUser.getUsername()));
        }

        return info;
    }

}
  • doGetAuthenticationInfo : 认证方法,在执行 subject.login(token);后,Shiro认证器会读取 Realm 中的该方法获取 AuthenticationInfo对象(认证信息),包含principal(我们存储在shiro subject中的对象),credentials (密码)。

  • doGetAuthorizationInfo: 授权方法,在需要校验用户访问权限的时候,Shiro授权器会读取 Realm 中的该方法获取 AuthorizationInfo对象(授权信息)读取DB后,可以通过 addRoles(roleCollection)addStringPermissions(permCollection) 设置当前用户的角色和权限。Shiro 在拿到这个权限信息后,会去找缓存管理器,以当前 subject 的 principal 作为key 缓存起来。

3. CredentialsMatcher: 密码匹配器,用于匹配 doGetAuthenticationInfo 方法返回的 credentials 和 subject.login(token);时的 token 中的 password是否一致。常用的实现有 SimpleCredentialsMatcher(默认是该实现)、HashedCredentialsMatcher (该实现可以进行加密匹配)

4. DefaultWebSecurityManager:如上述,用于协调Shiro内部各种安全组件,我们需要将我们扩展的bean 注册到 SecurityManager 中

5. RememberMeManager:开启该组件后使用记住我服务, token 中 rememberMe 为 true 时,登录成功之后会创建RememberMe cookie。

其余参考上文代码注释

关于 thymeleaf-extras-shiro

Shiro 默认支持在 jsp 中使用 shiro标签。但是想在 thymeleaf 中使用 Shiro 标签呢?

使用 thymeleaf-extras-shiro 完美解决 thymeleaf 颗粒化权限控制

你好, <span th:text="${principal}"></span><br>
<p shiro:hasRole="super_admin">当前角色超级管理员</p>
<button shiro:hasPermission="'sys:user:add'">添加</button>
<button shiro:hasPermission="'sys:user:update'">编辑</button>
<button shiro:hasPermission="'sys:user:lock'">冻结</button>
<div shiro:hasAllPermissions="'sys:user:add, sys:user:update, sys:user:lock'">
    <span>满足所有权限时显示</span>
</div>
<div shiro:hasAnyPermissions="'sys:user:add, sys:user:update, sys:user:lock'">
    <span>满足一个权限即可显示</span>
</div>

更多用法参考
Github 文档:https://github.com/theborakompanioni/thymeleaf-extras-shiro

本文demo:https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/springboot-shiro
如果你发现我的文章或者demo中存在问题,请联系我

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

推荐阅读更多精彩内容