Shiro概述
1 简介
Apache Shiro是一个强大且易用的Java安全框架!可以完成身份验证、授权、密码和会话管理!
Shiro不仅可以用在 JavaSE 环境中,也可以用在 JavaEE 环境中!
官网: http://shiro.apache.org/
2 功能
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
3 从外部看
应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:
Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个Shiro应用:
应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入!
4 外部架构
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是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提高了一些常见的加密组件用于如密码加密/解密的!
5 认证流程
用户 提交 身份信息、凭证信息 封装成 令牌 交由 安全管理器 认证!
SpringBoot 集成 Shiro
1 编写配置文件
1、IDEA新建一个 springboot 模块;
2、导入 SpringBoot 和 Shiro 整合包的依赖;
Apache Shiro 在 1.8.0 之前,当将 Apache Shiro 与 Spring Boot 配合使用时,特制的 HTTP 请求可能会导致身份验证绕过。用户应该更新到Apache Shiro 1.8.0。
<!--SpringBoot 和 Shiro 整合包-->
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
下面是编写配置文件Shiro 三大要素
- subject -> ShiroFilterFactoryBean
- securityManager -> DefaultWebSecurityManager
- realm
实际操作中对象创建的顺序 : realm -> securityManager -> subject
3、编写自定义的 realm ,需要继承 AuthorizingRealm
//自定义的 Realm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//打印一个提示
System.out.println("执行了授权方法");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//打印一个提示
System.out.println("执行了认证方法");
return null;
}
}
4、新建一个 ShiroConfig
配置文件(按照上面说的创建过程来写);
创建 Realm
:
// 1、创建一个Realm类,需要自定义
@Bean(name = "userRealm")
public UserRealm userRealm() {
return new UserRealm();
}
创建 securityManager
:可以看到这里需要的参数为一个Realm类,可以与上面所创建的Realm类对应起来!
// 2、DefaultWebSecurityManager
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联Realm
securityManager.setRealm(userRealm);
return securityManager;
}
创建 subject
:
//1、 subject -> ShiroFilterFactoryBean
// @Qualifier("securityManager") 指定 Bean 的名字为 securityManager
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean subject = new ShiroFilterFactoryBean();
//设置安全管理器
//需要关联 securityManager ,通过参数把 securityManager 对象传递过来
subject.setSecurityManager(securityManager);
return subject;
}
2 搭建简单测试环境
- 新建一个登录页面;
- 新建一个首页;
- 首页上有三个链接,一个登录,一个增加用户,一个删除用户;
- 新建一个增加用户页面;
- 新建一个删除用户页面;
- 编写对应的 Controller;
3 使用Shiro
1、登录拦截
在上面的 getShiroFilterFactoryBean
方法中加上需要拦截的登录请求;
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加 Shiro 的内置过滤器=======================
/*
anon : 无需认证,就可以访问
authc : 必须认证,才能访问
user : 必须拥有 “记住我”功能才能用
perms : 拥有对某个资源的权限才能访问
role : 拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedMap();
// filterMap.put("/add", "authc");
// filterMap.put("/update", "authc");
// 支持通配符
filterMap.put("/user/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
// 设置登录的请求
shiroFilterFactoryBean.setLoginUrl("/toLogin");
// 设置未授权的请求
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
return shiroFilterFactoryBean;
}
测试:点击 addUser 链接,不会跳到 addUser 页面,而是跳到登录页,拦截成功!
2.、用户认证
1、在 Controller 中写一个登录的控制器;
@RequestMapping("login")
public String login(String username, String password, Model model) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token); // 执行登录方法
return "redirect:/index";
} catch (UnknownAccountException e) {
model.addAttribute("error", "用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("error", "密码不存在");
return "login";
}
}
2、重启,测试,可以发现当用户名错误之后正确弹出“用户名不存在”的信息,并且在弹出信息之前,还弹出了“执行了认证方法”的信息,可以看出,是先执行了 自定义的 UserRealm
中的 AuthenticationInfo
方法,也就是认证方法,再执行了登录的相关操作。
3、修改 UserRealm
中的 AuthenticationInfo
以获取用户信息;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//打印一个提示
System.out.println("执行了认证方法");
// 用户名密码(暂时先自定义一个做测试)
String name = "root";
String password = "1234";
//通过参数获取登录的控制器中生成的 令牌
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//用户名认证
if (!token.getUsername().equals(name)){
// return null 就表示控制器中抛出的相关异常
return null;
}
//密码认证, Shiro 自己做,为了避免和密码的接触
//最后返回一个 AuthenticationInfo 接口的实现类,这里选择 SimpleAuthenticationInfo
// 三个参数:获取当前用户的认证 ; 密码 ; 认证名
return new SimpleAuthenticationInfo(principal, user.getPwd(), this.getName());
}
测试:
-
输入错误的密码后,可以发现登录被拦截了,而且抛出了对应的异常(虽然乍一眼看上去,Controller类中编写的登录验证和自定义Realm类中认证方法并无多大关联,但是它们俩确确实实联系在一起了!)
-
输入正确的密码登录成功,且可以访问add和update两个页面!
3、退出登录
在控制器中添加一个退出登录的方法;
//退出登录
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
4、授权实现
1、授权的拦截主要在ShiroConfig配置类中进行配置,也就是需要在内置过滤器中进行配置:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加 Shiro 的内置过滤器=======================
/*
anon : 无需认证,就可以访问
authc : 必须认证,才能访问
user : 必须拥有 “记住我”功能才能用
perms : 拥有对某个资源的权限才能访问
role : 拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedMap();
// filterMap.put("/add", "authc");
// filterMap.put("/update", "authc");
// 授权(正常的情况之下,没有授权会跳转到未授权页面)
// 当用户访问/user/add页面时,需要为user用户且拥有add权限时才能访问
filterMap.put("/user/add", "perms[user:add]");
filterMap.put("/user/update", "perms[user:update]");
filterMap.put("/user/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
shiroFilterFactoryBean.setLoginUrl("/toLogin");
// 设置未授权的请求(当用户没有权限访问时跳转到未授权页面)
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
return shiroFilterFactoryBean;
}
2、编写未授权页面的跳转Controller(主要用于测试):
@RequestMapping("/unauthorized")
@ResponseBody
public String unauthorized() {
return "未经过授权的用户暂时无法访问该页面!";
}
3、测试:当用户没有权限时,访问页面将会返回信息“未经过授权的用户暂时无法访问该页面”!而通过该测试也可以发现,**当用户进行页面跳转时,会进入到自定义Realm中AuthorizationInfo方法,也就是授权方法!**我们可以通过在授权方法中对所有用户进行授权:
info.addStringPermission("user:add");
以上代码给所有访问页面的用户都基于了访问权限(add页面的访问权限)!当然,真实情况下是不可能给所有用户都一视同仁的,这个权限信息按照逻辑,应该在数据库里拿,所以这边对实体类进行修改!
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private String pwd;
// 保存用户授权信息
private String perms;
}
在AuthenticationInfo的认证方法中,将用户信息返回:
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo==>认证");
// 这里需要认证用户名以及密码(链接真实数据库)
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 用户的信息
User user = userService.queryUserByName(token.getUsername());
if (user == null) {
return null; // UnknownAccountException
}
// 返回用户信息
return new SimpleAuthenticationInfo(user, user.getPwd(), "");
}
在授权方法中,用SecurityUtils获得Subject对象,拿到用户信息:
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("doGetAuthorizationInfo==>授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 在认证方法中返回的用户信息对象通过subject拿到
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();
info.addStringPermission(currentUser.getPerms());
// info.addStringPermission("user:add");
return info;
}
数据库中的数据如下:
测试:当用户ayin访问add页面时,可以顺利访问,但是因为未拥有update的访问权限,所以访问update页面时会被拦截!
4、Shiro整合Thymeleaf
在首页中,会显示一个登录按钮,点进按钮之后会跳转至登录页,输入用户名和密码之后,跳转至已登录状态的首页,但这里存在一个问题:登录按钮理应在未登录状态下显示,而此时登录按钮无论是在登录状态或者是未登录状态下都会显示!
使用Shiro整合Thymeleaf可以解决这类问题,当然,在进行操作之前,也是需要导入所需的依赖:
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
导入依赖之后,在所需要修改的主页导入命名空间,导入命名空间的原因是:
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro
关于shiro标签的使用方法:
<shiro:authenticated> 登录之后
<shiro:notAuthenticated> 不在登录状态时
<shiro:guest> 用户在没有RememberMe时
<shiro:user> 用户在RememberMe时
<shiro:hasAnyRoles name="abc,123" > 在有abc或者123角色时
<shiro:hasRole name="abc"> 拥有角色abc
<shiro:lacksRole name="abc"> 没有角色abc
<shiro:hasPermission name="abc"> 拥有权限资源abc
<shiro:lacksPermission name="abc"> 没有abc权限资源
<shiro:principal> 显示用户身份名称
<shiro:principal property="username"/> 显示用户身份中的属性值
如上所示,我们这里所使用的标签为shiro:hasPermission以及shiro:notAuthenticated权限,不过这里有一些问题:在shiro:hasPermission中直接写上user:add,这里的冒号会报错,不过并不意向测试结果,而在双引号再加上单引号,主要的目的是为了避免报错,因为即使这样,测试结果也并不会出错,单纯为了讨好强迫症而已!
<h1 th:text="${msg}"></h1>
<div shiro:notAuthenticated>
<p>
<a th:href="@{/toLogin}">登录</a>
</p>
</div>
<div shiro:hasPermission="'user:add'">
<h2><a th:href="@{/user/add}">add</a></h2>
</div>
||
<div shiro:hasPermission="'user:update'">
<h2><a th:href="@{/user/update}">update</a></h2>
</div>
<h1><a th:href="@{/logout}">注销</a></h1>
号会报错,不过并不意向测试结果,而在双引号再加上单引号,主要的目的是为了避免报错,因为即使这样,测试结果也并不会出错,单纯为了讨好强迫症而已!
<h1 th:text="${msg}"></h1>
<div shiro:notAuthenticated>
<p>
<a th:href="@{/toLogin}">登录</a>
</p>
</div>
<div shiro:hasPermission="'user:add'">
<h2><a th:href="@{/user/add}">add</a></h2>
</div>
||
<div shiro:hasPermission="'user:update'">
<h2><a th:href="@{/user/update}">update</a></h2>
</div>
<h1><a th:href="@{/logout}">注销</a></h1>