一、Shiro 登陆架构
下面是 Shiro 的用户登陆架构图,我们根据箭头来看一下流程。
1、Token:使用用户的登录信息创建令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password, true); |
我们要先通过用户名和密码,生成一个 token,token 是一个用户令牌,用于在登陆的时候,Shiro 来验证用户是否有合法的身份。
2、Subject:执行登陆动作 (login)
Subject subject = SecurityUtils.getSubject(); // 获取 Subject 单例对象 |
再通过 Subject 来执行登陆操作,将 token 发送给 Security Manager,让他来验证这个 token。Subject 中文翻译是主题。你可以理解为它是一个用户,是 User 的抽象概念。
3、Realm:自定义代码实现登陆身份验证和访问权限控制
先来看看 Realm,你从上图可以看出,Realm 在 Shiro 方框的外面。图片很形象,因为这一部分恰恰是需要我们自己去实现的。需要我们来设计如何验证登录用户的身份 (role),和这个用户是否具有访问某个 URL 的权限 (permission)。前者使用 AuthenticationInfo(验证) 实现,后者使用 AuthorizationInfo(授权) 实现。
4、Security Manager:Shiro 架构的核心
Security Manager,是 Shiro 架构的核心,简单来说,它根据我们自定义的 Realm,去完成验证和授权工作
如果这部分没有看懂,建议先根据下面的 “与 SpringBoot2 集成” 部分,搭建一个 demo,在项目中直观体验一下了,再回来看。
二、与 SpringBoot2 集成
注:以下内容是根据 “纯洁的微笑” 大神的《springboot 整合 shiro - 登录认证和权限管理》一文,将其中 SpringBoot1.5 升级到 2.1,针对 2.1 做了相应修改,同时针对一些知识点延伸学习。博文地址:http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html
建议你手动搭建一个 demo,这样能更深入了解。
pom 依赖
<dependencies> |
为使用热部署,配置 <build></build>
<build> |
配置文件
spring: |
数据库设计
使用基于角色的访问控制 (Role-Based Access Control)—RBAC 来实现数据库设计,用户依赖角色,角色依赖权限。这样设计结构清晰,管理方便。建立三张表:user_info,sys_role,sys_permission。使用 sys_user_role 关联用户和角色,使用 sys_role_permission 关联角色和权限,不使用外键。
使用 jpa 技术,运行实体代码自动生成数据表。
用户信息实体。@Getter、@Setter 注解用于提供读写属性。因为有 getCredentialsSalt(),所以不使用 @Data 注解。
|
角色实体。使用 @Data 注解,为类提供读写属性,此外还提供了 equals()、hashCode()、toString() 方法。上面用户信息实体和角色实体会根据 @JoinTable 注解生成 sys_user_role 表。
|
权限实体。同理,角色实体和权限实体,通过 @JoinTable 注解生成 sys_role_permission 表
|
数据库数据:
INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', ' 管理员 ', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0); |
三、配置 Shiro
Apache Shiro 核心通过 Filter 来实现,就好像 SpringMvc 通过 DispachServlet 去实现。
Filter 和 Interceptor 的区别:
Filter 是过滤器,Interceptor 是拦截器。前者基于回调函数实现,必须依靠容器支持。因为需要容器装配好整条 FilterChain 并逐个调用。后者基于代理实现,属于 AOP 的范畴。
在 Shrio 中实现登陆身份验证和访问权限控制有三种方式:
- 完全使用注解来实现登陆身份验证和访问权限控制
- 完全使用 URL 配置来实现登陆身份验证和访问权限控制
- 使用 URL 配置来实现登陆身份验证、使用注解来实现访问权限控制
第 3 种方式最灵活,所以用第三种。
1、使用 URL 配置来实现登陆身份验证
要实现当用户在浏览器地址访问项目 URL 时,Shiro 会拦截所有的请求,再根据配置的 ShrioFilter 过滤器来进行下一步操作。原理:Spring 容器会将所有的 Filter 交给 ShiroFilter 管理。
|
authc 更深层次含义:指定 url 需要 form 表单登录,默认会从请求中获取 username、password 等参数并尝试登录,如果登录不了就会跳转到 loginUrl 配置的路径
在 Realm 中实现 AuthenticationInfo(登陆身份验证)
doGetAuthenticationInfo():用于验证 token 的 User 是否具有合法的身份,即检验账号密码是否正确,每次用户登录的时候都会调用。
public class MyShiroRealm extends AuthorizingRealm { |
验证密码原理
先了解两个算法,散列算法与加密算法。
两者都是将一个 Object 变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。
在这里,我们将用户的密码使用散列算法 (MD5) 加密后保存到数据库。加密的时候就使用了 salt,salt 中文翻译是盐,你可以将他看成一个钥匙。
因为散列算法加密是单项的,不能还原。那我们如何来验证密码呢,这时候也需要使用 salt。我们将 token 中的明文密码,采用生成密文密码时一样的方式,通过 salt 再加密一次,对比两个加密后的密码。最后的验证是 SimpleAuthenticationInfo 去实现的。
如何散列加密
//newPassword(密文密码):d3c59d25033dbf980d29554025c23a75 |
如何验证密码
配置 hashedCredentialsMatcher(凭证匹配器),让 SimpleAuthorizationInfo 知道如何验证密码:
|
2、使用注解来实现访问权限控制
|
在 Relm 中实现 AuthorizationInfo(访问权限控制)
如果项目只需要 Apache Shiro 用于登陆验证,那么就不用使用 AuthorizationInfo,只需要返回一个 null。
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { |
doGetAuthorizationInfo():当用户访问带有 @RequiresPermissions
注解的 URL 时,会调用此方法验证是否有权限访问。
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { |
代码开启注解
使用 Shrio 注解,需要在 ShrioConfig 中使用 AuthorizationAttributeSourceAdvisor 开启
|
自定义异常处理
当没有访问权限时,会抛出异常,需要自定义异常处理,将没有权限的异常重定向到 403 页面
|
同理,可以使用 @RequiresRoles("admin")
注解来验证角色 (身份)
在前端页面使用 Shiro 标签时也会触发权限控制,请看:https://www.jianshu.com/p/6786ddf54582
其他两种验证和授权请看:https://juejin.im/entry/5ad95ef26fb9a07a9f01185a
也可直接编代码测试:
Boolean isPermitted = SecurityUtils.getSubject().isPermitted("***");// 是否有什么权限 |
登陆实现
前端登陆页面:
|
登陆接口:
|
你可能发现了,登陆没有用文章开头的 Subject 执行登陆动作,而是直接使用 action=”” 的表单登录。
为什么 action=”” 呢,这是因为设置了 "/**", "authc"
,当用户没有登陆时,所有 url(/**) 都会被重定向到 /login,而 action=""
或者 "/login"
将不被拦截,doGetAuthenticationInfo() 验证表单中的账号密码。
使用 Subject 执行登陆动作
那如何使用 Subject 执行登陆动作呢,需要使用 user
过滤器。当没有登录时,user 跟 authc 一样,会拦截所有的 url。
//user: 需要已登录或 “记住我” 的用户才能访问;filterChainDefinitionMap.put("/**", "user"); |
当使用 Post 请求的 /login 登陆时,将使用 Subject 执行登陆动作,doGetAuthenticationInfo() 方法验证
<form action="/login" method="post"> |
项目源码
其余未贴出代码请查看项目源码:springboot-shiro
总结
Shiro 的作用是拦截所有请求,判断用户是否登录,如果登录,给与「放行」,若没有登录,重定向到登录页面。
Shiro 判断登陆的方法:第一次登录,查 token 的密码是否跟数据库一致。登录后,服务端返回 cookie,以后每次登录利用 cookie 判断(可以通过登录后测试发现)。Shiro 内部维护了一个 SessionId 与 Session 的框架。