欢迎光顾我的博客
开端
好的,起因是我把最近做好的项目给一部分人进行了测试,发现大部分朋友都提出了同一个问题,你的系统权限管理是如何实现的。我只能尴尬的说一句,不好意思这部分还没开发。然而我也知道,其实对于一个项目来说,权限可以说是最主要的一部分。后端除了对权限进行处理,其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去,顺便把我的学习笔记分享给大家。
而对于权限控制的框架呢,听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的,不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架,可以用在各种场合,甚至非 Web 项目中,由于它的会话独立于容器,后期学习分布式和微服务的时候也比较方便使用。~~最重要的是 Spring 官网用的也是 Shiro 的框架。~~所以还是打算学习 Shiro。
由于这部分内容比较多,我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服,所以我打算把这个内容分成几个部分:初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于 Shiro 该如何配置的,面向 Spring Boot,讲一些比较基础的内容。
深入Shiro
之前我们了解了一下 Shiro 的基本功能和架构,现在我们来看看它的工作流程。
工作流程
![截屏2020-08-12 下午9.00.15](https://gitee.com/Robot_Kevin/TypeChoImg/raw/master/image/截屏2020-08-12%20下午9.00.15.png)
拦截
Shiro 特别重要的一块就是拦截,它通过过滤器拦截所有请求,根据配置决定哪些可以认证通过,哪些不用被拦截,还有哪些没有通过认证强制跳转。我们看一段配置代码,如何通过过滤器决定谁去谁留。
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/user/login");
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login","anon");
filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
return shiroFilterFactoryBean;
}
代码非常简单,首先使用 @Bean 注释将这个方法交给 Spring 来管理,名字必须是 shiroFilter,这样才能和默认配置中的名字匹配,否则 Spring 会找不到这个过滤器。在这个方法中首先 new 一个过滤器工厂对象,将获取到的 securityManager 注入。之后设置一个登陆 url,告诉 Shiro,这个页面/接口是我的登陆页面,用户角色权限的获取也都在这里。
接着就是刚刚图中的 filterChainDefinition,我们定义一个 map 来存储不被拦截的页面。每一个键值对中 key 存储 url,value 存储拦截器的名字,拦截器主要是以下几个:
拦截器名称 | 拦截器功能 |
---|---|
anno | 不需要授权和登录,可以匿名访问 |
authc | 需要登录授权才能访问 |
logout | 退出成功之后可以自定义重定向 |
user | 需要登录授权或者登录后开启了记住我(remember me)就能访问 |
ssl | 通过https协议才能通过 |
roles | 角色拦截 |
rest | rest风格拦截 |
port | 端口拦截 |
perms | 权限拦截 |
noSessionCreation | 不创建会话连接器 |
authcBasic | 基本http身份验证拦截 |
比较常用的四个拦截器我使用加粗表示出来了,anno 一般是最先使用的,为什么说最先使用,因为拦截器拦截的顺序是按照你 map 中加入的键值对的顺序来的。按照第一次匹配优先原则,如果一开始就被拦截,那么之后如果符合条件也不会放行。有一些公开资源,尤其是登录接口,肯定是要设置 anno 匿名的。假如说连入口的登录功能都拦截,那用户还这么使用这个系统的其他功能呢?
authc 是指需要登录或授权才能访问,这个值的 key 一般都是 “/**” ,而且写在最后。表示除了上述设置的拦截规则,对于其他所有 url 全部拦截并且需要授权才能访问。而我这里没有这么写,而是自定义了一个 corsAuthenticationFilter。从后面的代码也可以看到我又定义了一个 filterMap ,使用我自定义的一个过滤方式拦截其他所有请求。本来我也是采用 authc 的方式,但是由于前后端跨域的需要,必须要加上跨域请求头的一些参数,所以直接另写了一个过滤器类,并且在其中处理了验证通过和验证失败的处理方式。具体可见下一篇,SSM 整合 Shiro,这里不多赘述。
认证
那么拦截之后我们怎么样判断用户是什么角色,拥有哪些权限呢?这块其实我在上一篇中有所讲到,已经把源码拉出来解析过了。有需要的朋友可以再去回顾一下,我这里只讲它整个认证的流程。
- 获取当前的 Subject,调用 SecurityUtils.getSubject( )
- 测试当前的用户是否已经被认证,即是否已登陆,调用 Subject 的 isAuthenticated( )
- 若没有被认证,则把用户名和密码封装为 UsernamePasswordToken 对象
- 执行登陆:调用 Subject 的 login(AuthenticationToken) 方法
- 自定义 Realm 方法,从数据库中获取相应的记录,返回给 Shiro
- 由 Shiro 完成对密码的比对
回顾第一篇我们讲到的,第一步是从数据库中找到所有的用户。第三步是通过我们前端获取到的用户名和密码生成 token。第四步是利用生成的 token 参数传给 login 方法进行登录。登录时对用户角色和权限的验证采用了自定义 Reaml 中重写的两个方法 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( ),还记得吧,挺容易搞混的。
机制
在 Shiro 中有许多我们曾经经常使用到的内容,比如缓存,还有一些我们不太了解的东西,比如盐值加密。我这里简要讲一讲这些机制。
MD5盐值加密
盐值加密是我以前没有听说过的知识,所以在我第一次整合 Shiro 的时候也没有加入这一部分的内容。
众所周知,用户名和密码是被保存在数据库中。可是一旦数据库发生了泄露,用户名和密码就都遭到了泄露。攻击者可以轻松的获取用户名和密码,进行操作。更大的危害是,由于现在需要注册的网站、app越来越多。用户名和密码很多时候都是相同的。一旦某处发生了泄露,则后果会慢慢的扩散。这些危害大家可以查询下近些年发生的一些安全事故,如Sony数据库泄露、网易数据库泄露、CSDN数据库泄露等,因此我们要对密码进行加密。
为何取名为盐,此盐非彼颜。盐是许许多多微小的颗粒组成的,他就像一个随机数,你猜不到有多少颗。当你给密码加了盐,再结合 MD5,就不那么容易被破解了。
盐值加密主要就是如何生成盐,我们一般使用唯一的数据来产生。比如说我的数据库中用户名是不可重复的,那么用户名就是唯一的(昵称不唯一),所以可以使用用户名生成盐。
ByteSource.Util.bytes(userName)
ByteSource 是 Shiro 提供的一个工具类,可以转换进制。其中还有一个静态子类 Util,里面的 bytes( ) 方法返回了一个 SimpleByteSource 对象,而这个对象又使用 CodecSupport 中的 toBytes( ) 方法对接收到的不同参数采取不同的加密方式。以下为源码:
protected byte[] toBytes(Object o) {
if (o == null) {
String msg = "Argument for byte conversion cannot be null.";
throw new IllegalArgumentException(msg);
} else if (o instanceof byte[]) {
return (byte[])((byte[]