Shiro原理
Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能,是目前用得比较多的登陆框架。
基本框架
- Subject:主体,代表了当前“用户”。这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等。所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager。我们可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者。
- SecurityManager:安全管理器。即所有与安全有关的操作都会与 SecurityManager 交互,且它管理着所有 Subject。可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,我们可以把它看成 DispatcherServlet 前端控制器。
- Realm:域。Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作。我们可以把 Realm 看成 DataSource,即安全数据源。
运行原理
- Subject:主体,可以看到主体可以是任何与应用交互的“用户”。
- SecurityManager:相当于 SpringMVC 中的 DispatcherServlet。它是 Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制。它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,我们可以自定义实现。其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了。
- Authrizer:授权器,或者访问控制器。它用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能。
- Realm:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的。它可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等。
- SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager。而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境。
- SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD。我们可以自定义 SessionDAO 的实现,控制 session 存储的位置。如通过 JDBC 写到数据库或通过 jedis 写入 redis 中。另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能。
- CacheManager:缓存管理器。它来管理如用户、角色、权限等的缓存的。因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
- Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。
认证过程
1、应用程序构建了一个终端用户认证信息的AuthenticationToken 实例后,调用Subject.login方法。
2、Sbuject的实例通常是DelegatingSubject类(或子类)的实例对象,在认证开始时,会委托应用程序设置的securityManager实例调用securityManager.login(token)方法。
3、SecurityManager接受到token(令牌)信息后会委托内置的Authenticator的实例(通常都是ModularRealmAuthenticator类的实例)调用authenticator.authenticate(token). ModularRealmAuthenticator在认证过程中会对设置的一个或多个Realm实例进行适配,它实际上为Shiro提供了一个可拔插的认证机制。
4、如果在应用程序中配置了多个Realm,ModularRealmAuthenticator会根据配置的AuthenticationStrategy(认证策略)来进行多Realm的认证过程。在Realm被调用后,AuthenticationStrategy将对每一个Realm的结果作出响应。
注:如果应用程序中仅配置了一个Realm,Realm将被直接调用而无需再配置认证策略。
5、判断每一个Realm是否支持提交的token,如果支持,Realm将调用getAuthenticationInfo(token); getAuthenticationInfo 方法就是实际认证处理,我们通过覆盖Realm的doGetAuthenticationInfo方法来编写我们自定义的认证处理。
@PostMapping("/login")
publicStringlogin(Stringusername, Stringpassword, Modelmodel){
Subjectsubject=SecurityUtils.getSubject();
UsernamePasswordTokentoken=newUsernamePasswordToken(username,password);
//捕获异常
try {
subject.login(token);//将token交给shiro管理,进入自定义的realm中的验证方法
Accountaccount= (Account) subject.getPrincipal();//登录成功之后,取出当前用户
subject.getSession().setAttribute("account", account);//将用户存入session
return"redirect:index";
} catch (UnknownAccountExceptione) {//账户的异常
e.printStackTrace();
model.addAttribute("msg", "用户名错误");
return"loginPage";
}catch (IncorrectCredentialsExceptione){//密码的异常
e.printStackTrace();
model.addAttribute("msg", "密码错误");
return"loginPage";
}
}
Shiro 核心组件
author:作者
authorization:权限授予(授权,认可;批准,委任)
authentic:真正的,真实的;可信的
authentication:身份验证(证明;鉴定;证实)
用户、角色、权限
会给角色赋予权限,给用户赋予角色
UsernamePasswordToken
用来封装用户登录信息,使用用户的信息来创建令牌 Token
创建UsernamePasswordToken对象,参数为username和password
UsernamePasswordTokentoken=newUsernamePasswordToken(username,password);
Subject
subject:主题;起因;科目;主词;(绘画、摄影等的)题材;实验对象;主语;国民;主旋律;主体;中心实体
Shiro中认证授权组件Subject,为我们提供了当前用户、角色和授权的相关信息,可以进行登录,退出,权限验证,获取用户信息,session。
获取subject
Subjectsubject=SecurityUtils.getSubject();
0. 登录
所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager。我们可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者
subject.login(token);//subject会委托securityManager实例调用securityManager.login(token)方法,最终传到realm方法中
1. 获得Session对象
Sessionsession=subject.getSession();
session.setAttribute( "key", "aValue" );
这里的Session并不是HttpSession,而是shiro为我们提供的,它的操作与HttpSession一样,他们最大的区别就是shiro session不需要依赖http服务器。
2. 得到当前登录的用户名
StringcurrentUser=subject.getPrincipal().toString();
System.out.println("当前登录的用户是:"+currentUser);
3. 校验当前用户的权限
//判断用户是否是拥有某种角色
booleanisRole=subject.hasRole( "admin" );
//是否拥有某种功能
booleanisPer=subject.isPermitted("xiaoming:run");
4. 退出登录
//退出登录
subject.logout();
SecurityManager
外观模式(facade):为子系统中一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
SecurityManager是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
SecurityManager接受到token(令牌)信息后会委托内置的Authenticator的实例,如果应用程序中仅配置了一个Realm,Realm将被直接调用而无需再配置认证策略
Realm
realm:领域,范围;王国
Realm提供待验证数据的比对值,即安全数据源,可以理解为数据的源头,可以是数据库,文件等。
Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro 。当配置 Shiro时,你必须至少指定一个 Realm ,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。
Spring Boot 整合 Shiro
1. 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
2. 编写Realm
realm域中存放着真实数据
packagecn.upeveryday.realm;
publicclassAccountRealmextendsAuthorizingRealm {
@Autowired
privateAccountServiceaccountService;
/**
* 获取授权信息:获取当前用户的角色和权限
* @param principalCollection
* @return
*/
@Override
protectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipalCollection) {
//获取当前用户主体
Subjectsubject=SecurityUtils.getSubject();
Accountaccount= (Account) subject.getPrincipal();
//获取当前用户角色
HashSet<String>roles=newHashSet();
roles.add(account.getRole());
//SimpleAuthorizationInfo是AuthorizationInfo的子类
SimpleAuthorizationInfoinfo=newSimpleAuthorizationInfo(roles);
//获取当前用户权限
info.addStringPermission(account.getPerms());
//返回当前用户的授权信息(包括角色与权限)
returninfo;
}
/**
* 获取身份验证信息:将前端传来的用户信息与安全数据进行比较
* @param authenticationToken:就是controller中传来的token,subject.login(token);
* @return
* @throws AuthenticationException
*/
@Override
protectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken) throwsAuthenticationException {
UsernamePasswordTokentoken= (UsernamePasswordToken) authenticationToken;
//用户名验证手写,拿到account后,密码验证就不用手写了,直接:new SimpleAuthenticationInfo(对象本身,对象密码,对象名字),会自动将对象密码与token中的密码进行验证
Accountaccount=accountService.findByUsername(token.getUsername());
if (account!=null){
returnnewSimpleAuthenticationInfo(account,account.getPassword(),getName());
}
returnnull;
}
}
3. 编写ShiroConfig
- 将自定义的
realm
对象装配进IOC容器 - 将安全管理器对象
DefaultWebSecurityManager
装配进IOC容器,并将realm插入管理器中 - 将过滤器工厂对象
ShiroFilterFactoryBean
装配进IOC容器,并将DefaultWebSecurityManager
装配进工厂
在过滤器工厂中配置并创建过滤器
认证过滤器
anon:无需认证。
authc:必须认证(必须登录状态)
authcBasic:需要通过 HTTPBasic 认证。
user:不一定通过认证,只要曾经被 Shiro 记录即可,比如:
记住我
的操作授权过滤器
perms:必须拥有某个权限才能访问。
role:必须拥有某个角色才能访问。
port:请求的端口必须是指定值才可以。
rest:请求必须基于 RESTful,POST、PUT、GET、DELETE。
ssl:必须是安全的 URL 请求,协议 HTTPS。
package cn.upeveryday.config;
import cn.upeveryday.realm.AccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
@Bean
public AccountRealm accountRealm(){
return new AccountRealm();
}
//自定义的 Realm 需要插入 DefaultWebSecurityManager 进行管理才能生效
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("accountRealm") AccountRealm accountRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(accountRealm);
return manager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(defaultWebSecurityManager);
//配置过滤器
HashMap<String, String> map = new HashMap<>();
map.put("/main","authc");//请求/main时,必须是登录状态
map.put("/manage", "perms[manage]");//必须有manage权限才可以访问
map.put("/administrator", "roles[administrator]");//必须是administrator才可以访问
//创建过滤器链
factoryBean.setFilterChainDefinitionMap(map);
//设置登录界面:当未登录状态访问其他页面时,会自动跳转到登录界面
factoryBean.setLoginUrl("/login");
//设置未授权401显示界面,提升界面友好度
factoryBean.setUnauthorizedUrl("/unauthorized");
return factoryBean;
}
}
4. Shiro 整合 Thymeleaf
1、pom.xml 引入依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2、配置类添加 ShiroDialect(配置方言)
//shiro整合thymeleaf
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
3、在HTML中使用
- 引入命名空间
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
- 当前用户符合权限或角色才显示当前标签,否则不显示
<div shiro:hasPermission="manage">
<a href="manage">manage</a> <br/>
</div>
<div shiro:hasRole="administrator">
<a href="/administrator">administrator</a>
</div>
4、实例
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#"/>
</head>
<body>
<h1>index</h1>
<div th:if="${session.account != null}">
<span th:text="${session.account.username}+'欢迎回来!'"></span><a href="/logout">退出</a>
</div>
<a href="/main">main</a> <br/>
<div shiro:hasPermission="manage">
<a href="manage">manage</a> <br/>
</div>
<div shiro:hasRole="administrator">
<a href="/administrator">administrator</a>
</div>
</body>
</html>
controller
package cn.upeveryday.controller;
@Controller
public class AccountController {
@GetMapping("/{url}")
public String redirect(@PathParam("url")String url){
return url;
}
@PostMapping("/login")
public String login(String username, String password, Model model){
Subject subject= SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
//捕获异常
try {
subject.login(token);//subject会委托应用程序设置的securityManager实例调用securityManager.login(token)方法
Account account= (Account) subject.getPrincipal();//登录成功之后,取出当前用户
subject.getSession().setAttribute("account", account);//将用户存入session
return "redirect:index";
} catch (UnknownAccountException e) {//账户的异常
e.printStackTrace();
model.addAttribute("msg", "用户名错误");
return "loginPage";
}catch (IncorrectCredentialsException e){//密码的异常
e.printStackTrace();
model.addAttribute("msg", "密码错误");
return "loginPage";
}
}
@GetMapping("/unauthorized")
@ResponseBody
public String unauthorized(){
return "无权限访问";
}
@GetMapping("/logout")
public String logout(){
//销毁session
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/loginPage";
}
}
账户实体
package cn.upeveryday.entity;
@Data
public class Account {
private Integer id;
private String username;
private String password;
private String perms;
private String role;
}