写在前面
在一款应用的整个生命周期,我们都会谈及该应用的数据安全问题。用户的合法性与数据的可见性是数据安全中非常重要的一部分。但是,一方面,不同的应用对于数据的合法性和可见性要求的维度与粒度都有所区别;另一方面,以当前微服务、多服务的架构方式,如何共享Session,如何缓存认证和授权数据应对高并发访问都迫切需要我们解决。Shiro的出现让我们可以快速和简单的应对我们应用的数据安全问题
Shiro介绍
Shiro简介
这个官网解释不抽象,所以直接用官网解释:Apache Shiro™是一个强大且易用的 Java 安全框架,可以执行身份验证、授权、加密和会话管理等。基于 Shiro 的易于理解的API,您可以快速、轻松地使任何应用程序变得安全(从最小的移动应用到最大的网络和企业应用)。
谈及安全,多数 Java 开发人员都离不开 Spring 框架的支持,自然也就会先想到 Spring Security,那我们先来看二者的差别
| Shiro | Spring Security |
| 简单、灵活 | 复杂、笨重 |
| 可脱离Spring | 不可脱离Spring |
| 粒度较粗 | 粒度较细 |
虽然 Spring Security 属于名震中外 Spring 家族的一部分,但是了解 Shiro 之后,你不会想 “嫁入豪门”,而是选择追求「诗和远方」冲动。
横看成岭侧成峰,远近高低各不同 (依旧是先了解概念就好)
远看 Shiro 看轮廓
Subject
它是一个主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者
SecurityManager
安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet前端控制器
Realm
域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
近看 Shiro 看细节
看图瞬间懵逼?别慌,会为你拆解来看,结合着图看下面的解释,这不是啥大问题,且看:
Subject
主体,可以看到主体可以是任何可以与应用交互的 “用户”
SecurityManager
相当于 SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理
Authenticator
认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;需要自定义认证策略(Authentication Strategy),即什么情况下算用户认证通过了
Authrizer
授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能
Realm
可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm
SessionManager
如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager;而Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB等环境;所以,Shiro 就抽象了一个自己的Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web服务器;接着又上了台EJB 服务器;这时又想把两台服务器的会话数据放到一个地方,我们就可以实现自己的分布式会话(如把数据放到Memcached 服务器)
SessionDAO
DAO大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
CacheManager
缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography
密码模块,Shiro提高了一些常见的加密组件用于如密码「加密/解密」的
注意上图的结构,我们会根据这张图来逐步拆分讲解,记住这张图也更有助于我们理解 Shiro 的工作原理,所以依旧是打开两个网页一起看就好喽
搭建概览
多数小伙伴都在使用 Spring Boot, Shiro 也很应景的定义了 starter,做了更好的封装,对于我们来说使用起来也就更加方便,来看选型概览
| 序号 | 名称 | 版本 |
| 1 | Springboot | 2.0.4 |
| 2 | JPA | 2.0.4 |
| 3 | Mysql | 8.0.12 |
| 4 | Redis | 2.0.4 |
| 5 | Lombok | 1.16.22 |
| 6 | Guava | 26.0-jre |
| 7 | Shiro | 1.4.0 |
使用 Spring Boot,大多都是通过添加 starter 依赖,会自动解决依赖包版本,所以自己尝试的时候用最新版本不会有什么问题,比如 Shiro 现在的版本是 1.5.0 了,整体问题不大,大家自行尝试就好
添加 Gradle 依赖管理
大体目录结构
application.yml 配置
基本配置
你就让我看这?这只是一个概览,先做到心中有数,我们来看具体配置,逐步完成搭建
其中 shiroFilter bean 部分指定了拦截路径和相应的过滤器,”/user/login”, ”/user”, ”/user/loginout” 可以匿名访问,其他路径都需要授权访问,shiro 提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限(先了解个大概即可):
| 配置缩写 | 对应的过滤器 | 功能 |
| anon | AnonymousFilter | 指定url可以匿名访问 |
| authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
| authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
| Logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
| noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
| perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
| port | PortFilter | 需要指定端口才能访问 |
| rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
| roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
| ssl | SslFilter | 需要https请求才能访问 |
| user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
数据库表设计
数据库表设计请参考 entity package下的 bean,通过@Entity 注解与 JPA 的设置自动生成表结构 (你需要简单的了解一下 JPA 的功能)。
我们要说重点啦~~~
身份认证
身份认证是一个证明 “李雷是李雷,韩梅梅是韩梅梅” 的过程,回看上图,Realm 模块就是用来做这件事的,Shiro 提供了 IniRealm,JdbcReaml,LDAPReam等认证方式,但自定义的 Realm 通常是最适合我们业务需要的,认证通常是校验登录用户是否合法。
新建用户 User
@Data @Entity public class User implements Serializable { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; @Column(unique =true) private String username; private String password; private String salt; }
定义 Repository
@Repository public interface UserRepository extends JpaRepository<User, Long> { public User findUserByUsername(String username); }
编写UserController:
@GetMapping("/login") public void login(String username, String password) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); token.setRememberMe(true); Subject currentUser = SecurityUtils.getSubject(); currentUser.login(token); }
自定义 Realm
自定义 Realm,主要是为了重写 doGetAuthenticationInfo(…)方法
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); User user = userRepository.findUserByUsername(username); SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt())); return simpleAuthenticationInfo; }
这些代码我需要做一个说明,你可能也满肚子疑惑:
- 这段代码怎么应用了 shiro?
- controller 是怎么调用到 custom realm 的?
- 重写的 doGetAuthenticationInfo(…) 方法目的是什么?
认证流程说明
用户访问 /user/login 路径,生成 UsernamePasswordToken, 通过SecurityUtils.getSubject()获取Subject(currentUser),调用 login 方法进行验证,让我们跟踪一下代码,瞧一瞧就知道自定义的CustomRealm怎样起作用的,一起来看源码:
到这里我们要停一停了,请回看 Shiro 近景图,将源码追踪路径与其对比,是完全一致的






