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

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

本文demo:戳这里
本文demo选型:thymeleaf, springboot 2, shiro, ehcache
PS:如果我不拖延的话,估计还是会有后续的 :)

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源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!--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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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 颗粒化权限控制

1
2
3
4
5
6
7
8
9
10
11
你好, <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中存在问题,请联系我