Shiro
一、权限框架介绍
1. 什么是权限管理
权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
1.1 用户身份认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
用户名密码身份认证流程:
1.2 授权流程
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
2. 常见权限框架
2.1 Shiro简介
Apache Shiro是Java的一个安全框架。目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了
2.2 Spring Security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。它是一个轻量级的安全框架,它确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC有很好地集成,并配备了流行的安全算法实现捆绑在一起。安全主要包括两个操作“认证”与“验证”(有时候也会叫做权限控制)。“认证”是为用户建立一个其声明的角色的过程,这个角色可以一个用户、一个设备或者一个系统。“验证”指的是一个用户在你的应用中能够执行某个操作。在到达授权判断之前,角色已经在身份认证过程中建立了。
2.3 Shiro和Spring Security比较
- Shiro比Spring更容易使用,实现和最重要的理解
- Spring Security更加知名的唯一原因是因为品牌名称
- Spring以简单而闻名,但讽刺的是很多人发现安装Spring Security很难
- Spring Security却有更好的社区支持
- Apache Shiro在Spring Security处理密码学方面有一个额外的模块
- Spring-security 对spring 结合较好,如果项目用的springmvc ,使用起来很方便。但是如果项目中没有用到spring,那就不要考虑它了
- Shiro 功能强大、且 简单、灵活。是Apache 下的项目比较可靠,且不跟任何的框架或者容器绑定,可以独立运行
二、Shiro基础介绍
1. Shiro三个核心组件
1.1 Subject
Subject:即“当前操作用发户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
1.2 SecurityManager
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
1.3 Realm
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
1.4 Authenticator
认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;需要自定义认证策略(Authentication Strategy),即什么情况下算用户认证通过了
1.5 Authrizer
授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能
1.6 SessionManager
如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager;而Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB等环境;所以,Shiro 就抽象了一个自己的Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web服务器;接着又上了台EJB 服务器;这时又想把两台服务器的会话数据放到一个地方,我们就可以实现自己的分布式会话(如把数据放到Memcached 服务器)
1.7 SessionDAO
DAO大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
1.8 CacheManager
缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
1.9 Cryptography
密码模块,Shiro提高了一些常见的加密组件用于如密码「加密/解密」的。
2. Shiro相关类介绍
- Authentication 认证 ---- 用户登录
- Authorization 授权 — 用户具有哪些权限
- Cryptography 安全数据加密
- Session Management 会话管理
- Web Integration web系统集成
- Interations 集成其它应用,spring、缓存框架
3. Shiro 特点
- 易于理解的 Java Security API;
- 简单的身份认证(登录),支持多种数据源;
- 对角色的简单的签权(访问控制),支持细粒度的签权;
- 支持一级缓存,以提升应用程序的性能;
- 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
- 异构客户端会话访问;
- 非常简单的加密 API;
- 不跟任何的框架或者容器捆绑,可以独立运行
三、Shiro的内置过滤器
常用的过滤器
认证过滤器:
- anon: 无需认证(登录)可以访问
- authc: 必须认证才可以访问
- user: 如果使用rememberMe的功能可以直接访问
授权过滤器:
- perms: 该资源必须得到资源权限才可以访问
- role: 该资源必须得到角色权限才可以访问
自定义过滤器:
配置类:
package com.smy.Realm;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter; // 角色验证
//import org.apache.shiro.web.filter.authc.AuthenticatingFilter; //权限认证
/**
* @author shi_meng_yong
* @date 2020/6/27 20:45
* 自定义shiro过滤器
*/
public class Authorizatonfilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
Subject subject= getSubject(request, response); // 获得主体
String[] roles = (String[]) mappedValue; //角色数组
if(roles == null || roles.length == 0) {
return true;
}
for(String role:roles) {
if(subject.hasRole(role)) {
//是否有角色
return true;
}
}
return false;
}
}
配置文件:
<!--注入URL拦截规则 -->
<property name="filterChainDefinitions">
<value>
/login.html = anon
/login33 = anon
/login2 = perms["user:update","user:delect"]
/login2 = rolesOr["user","user11"] //使用自定义
/page/base/staff* = perms["staffList"]
</value>
</property >
<property name="filters"> /配置Filters
<util:map>
<entry key="rolesOr" value-ref="rolesOrfilter"></entry>
</util:map>
</property>
</bean>
<bean class="com.springshirodemo.Realm.Authorizatonfilter" id="rolesOrfilter"></bean> //将自定义过滤器注入
四、分析shiro框架登录认证
4.1 登录流程:
Subject 执行 login 方法,传入登录的「用户名」和「密码」,然后 SecurityManager 将这个 login 操作委托给内部的登录模块,登录模块就调用 Realm 去获取安全的「用户名」和「密码」,然后对比,一致则登录,不一致则登录失败
4.1.1 Controller层:
@RequestMapping("/login")
public String login(String name,String password,Model model){
System.out.println("name="+name);
/**
* 使用Shiro编写认证操作
*/
//1.获取Subject -- 获取当前登录用户
Subject subject = SecurityUtils.getSubject();
//2.封装用户数据 创建用户名/密码验证Token(Web 应用中即为前台获取的用户名/密码
UsernamePasswordToken token = new UsernamePasswordToken(name,password);
//3.执行登录方法
try {
subject.login(token);
//登录成功
//跳转到首页
return "redirect:/index";
} catch (UnknownAccountException e) {
//e.printStackTrace();
//登录失败:用户名不存在,UnknownAccountException是Shiro抛出的找不到用户异常
model.addAttribute("msg", "用户名不存在");
return "login";
}catch (IncorrectCredentialsException e) {
//e.printStackTrace();
//登录失败:密码错误,IncorrectCredentialsException是Shiro抛出的密码错误异常
model.addAttribute("msg", "密码错误");
return "login";
}
}
4.2 分析登录流程:
4.2.1 创建token
比如例子中的UsernamePasswordToken,包含登录的用户名和密码以及是否记住我
package org.apache.shiro.authc;
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
private String username;//用户名
private char[] password;//密码
private boolean rememberMe;//是否记住我
private String host;//当前主机
public UsernamePasswordToken() {
this.rememberMe = false;
}
public UsernamePasswordToken(String username, char[] password) {
this(username, (char[])password, false, (String)null);
}
public UsernamePasswordToken(String username, String password) {
this(username, (char[])(password != null ? password.toCharArray() : null), false, (String)null);
}
public UsernamePasswordToken(String username, char[] password, String host) {
this(username, password, false, host);
}
public UsernamePasswordToken(String username, String password, String host) {
this(username, password != null ? password.toCharArray() : null, false, host);
}
public UsernamePasswordToken(String username, char[] password, boolean rememberMe) {
this(username, (char[])password, rememberMe, (String)null);
}
public UsernamePasswordToken(String username, String password, boolean rememberMe) {
this(username, (char[])(password != null ? password.toCharArray() : null), rememberMe, (String)null);
}
public UsernamePasswordToken(String username, char[] password, boolean rememberMe, String host) {
this.rememberMe = false;
this.username = username;
this.password = password;
this.rememberMe = rememberMe;
this.host = host;
}
public UsernamePasswordToken(String username, String password, boolean rememberMe, String host) {
this(username, password != null ? password.toCharArray() : null, rememberMe, host);
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public char[] getPassword() {
return this.password;
}
public void setPassword(char[] password) {
this.password = password;
}
public Object getPrincipal() {
return this.getUsername();
}
public Object getCredentials() {
return this.getPassword();
}
public String getHost() {
return this.host;
}
public void setHost(String host) {
this.host = host;
}
public boolean isRememberMe() {
return this.rememberMe;
}
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}
public void clear() {
this.username = null;
this.host = null;
this.rememberMe = false;
if (this.password != null) {
for(int i = 0; i < this.password.length; ++i) {
this.password[i] = 0;
}
this.password = null;
}
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getName());
sb.append(" - ");
sb.append(this.username);
sb.append(", rememberMe=").append(this.rememberMe);
if (this.host != null) {
sb.append(" (").append(this.host).append(")");
}
return sb.toString();
}
}
4.2.2获取Subject
执行subject.login(token) 方法:
package org.apache.shiro.subject;
public interface Subject {
....(此处省略源码一万字)
void login(AuthenticationToken var1) throws AuthenticationException;
......(此处省略源码一万字)
}
subject.login()方法实现类DelegatingSubject中的login()方法:
public void login(AuthenticationToken token) throws AuthenticationException {
this.clearRunAsIdentitiesInternal();
Subject subject = this.securityManager.login(this, token);//重点
String host = null;
PrincipalCollection principals;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject)subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals != null && !principals.isEmpty()) {
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken)token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = this.decorate(session);
} else {
this.session = null;
}
} else {
String msg = "Principals returned from securityManager.login( token ) returned a null or empty value. This value must be non null and populated with one or more elements.";
//英文翻译:从securityManager.login(token)返回的主体返回空值或空值。 此值必须为非null并填充一个或多个元素
throw new IllegalStateException(msg);
}
}
4.2.3 SecurityManager
可以看到在实现类login()方法中代理给securityManager.login()接口方法,源码如下:
package org.apache.shiro.mgt;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authz.Authorizer;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
//注意SecurityManager实现的接口 Authenticator 认证、Authorizer 授权、SessionManager 会话管理
//登录
Subject login(Subject var1, AuthenticationToken var2) throws AuthenticationException;
//退出
void logout(Subject var1);
Subject createSubject(SubjectContext var1);
}
SecurityManager的login()方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = this.authenticate(token);//重点
} catch (AuthenticationException var7) {
AuthenticationException ae = var7;
try {
this.onFailedLogin(token, ae, subject);
} catch (Exception var6) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6);
}
}
throw var7;
}
Subject loggedIn = this.createSubject(token, info, subject);
this.onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
调用自己的 authenticate 方法执行登录,在 authenticate 方法中代理给 Authenticator 接口类型的属性去真正执行 authenticate(token)
方法
SecurityManager接口继承了Authenticator登录认证,Authenticator接口源代码如下:
package org.apache.shiro.authc;
public interface Authenticator {
AuthenticationInfo authenticate(AuthenticationToken var1