简介
- Apache Shiro 是 Java 的一个安全(权限)框架。目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
- Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境
- Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存等。
- 功能
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
- Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
- Web Support:Web 支持,可以非常容易的集成到Web 环境
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
- Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去
- Testing:提供测试支持
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
Shiro 架构
外部
- 我们从外部来看 Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作。如下图
- 可以看到:应用代码直接交互的对象是 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 让开发人员自己注入。Realm 读取数据库,将数据提供给SecutiryManager
内部
- 从 Shiro 内部来看下 Shiro 的架构,主要组件包括:Subject,SecurityManager,Authenticator,Authorizer,SessionManager,CacheManager,Cryptography,Realms。如下图所示:
- Subject:1.访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;Subject一词是一个专业术语,其基本意思是“当前的操作用户”。它是一个抽象的概念,可以是人,也可以是第三方进程或其他类似事物,如爬虫,机器人等。 在程序任意位置:Subject currentUser = SecurityUtils.getSubject(); 类似 Employee user = UserContext.getUser()一旦获得Subject,你就可以立即获得你希望用Shiro为当前用户做的90%的事情,如登录、登出、访问会话、执行授权检查等
- SecurityManager:安全管理器,是 Shiro 的心脏,相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher,所有具体的交互都通过 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 提高了一些常见的加密组件用于如密码加密 / 解密的
shiro组件
身份验证
- 在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明
- 在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:
- principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号
- credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等
- 最常见的 principals 和 credentials 组合就是用户名 / 密码了。接下来先进行一个基本的身份认证
登录/退出
- maven依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- log4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.2</version>
</dependency>
<!-- shiro相关依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies>
- 编写ini配置文件:shiro.ini
#用户的身份、凭据
[user]
zhang=555
wang=123
- 使用Shiro相关的API完成身份认证
@Test
public void testLoginLoginout(){
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2、得到SecurityManager实例 并绑定给SecurityUtils
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
//4、登录,即身份验证
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("用户名错误");
//5、身份验证失败
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
System.out.println("登录后的认证状态:"+subject.isAuthenticated());//true
//6、退出
subject.logout();
}
- 步骤
- 加载shiro.ini配置文件,得到配置中的用户信息(账号+密码)
- 创建Shiro的安全管理器,它在整个认证过程中担任控制器的角色
- 将创建的安全管理器添加到运行环境中
- 获取登录的用户主体对象,其中包含验证时需要的账号和密码
- 创建登录用户的身份凭证,携带登录用户的账号和密码
- 登录认证,将用户的和ini配置中的账号密码做匹配
- 如果输入的身份和凭证和ini文件中配置的能够匹配,那么登录成功,返回登录状态为true,反之登录状态为false。
- 登录失败存在两种情况:
- 账号错误:org.apache.shiro.authc.UnknownAccountException
- 密码错误:org.apache.shiro.authc.IncorrectCredentialsException
自定义Realm
- Realm 实现需要实现四个方法
public class MyRealm extends AuthorizingRealm {
/**
* 获取角色与权限
*doGetAuthorizationInfo执行时机有三个,如下:
* 1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
* 2、@RequiresRoles("admin") :在方法上加注解的时候;
* 3、@shiro.hasPermission name = "admin"/@shiro.hasPermission:"dustin:test"在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 登录信息验证
* 1.doGetAuthenticationInfo执行时机如下
* 当调用Subject currentUser = SecurityUtils.getSubject();
* currentUser.login(token);
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
String password = new String((char[])token.getCredentials());
if(!"zhang".equals(username)) {
throw new UnknownAccountException(); //如果用户名错误
}
if(!"123".equals(password)) {
throw new IncorrectCredentialsException(); //如果密码错误
}
return new SimpleAuthenticationInfo(username, password, getName());
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
- ini 配置文件指定自定义 Realm 实现 (shiro-realm.ini)
#声明一个realm
myRealm=net.wanho.realm.MyRealm
#指定securityManager的realms实现
securityManager.realms=$myRealm
#声明多个realm
myRealm1=net.wanho.realm.MyRealm1
myRealm2=net.wanho.realm.MyRealm2
#指定securityManager的realms实现
securityManager.realms=$myRealm1,$myRealm2
- 测试:只需要把之前的 shiro.ini 配置文件改成 shiro-realm.ini 即可
- 注:securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,如果删除 “securityManager.realms=myRealm1,myRealm2”,那么securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现),当我们显示指定 realm 后,其他没有指定 realm 将被忽略,如 “securityManager.realms=$myRealm1”,那么 myRealm2 不会被自动设置进去。
JDBC Realm 使用
- 数据库及依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.11</version>
</dependency>
- 到数据库 shiro 下建三张表:users(用户名 / 密码)、user_roles(用户 / 角色)、roles_permissions(角色 / 权限),并添加一个用户记录,用户名 / 密码为 zhang/123;
- ini 配置(shiro-jdbc-realm.ini)
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro
dataSource.username=root
dataSource.password=11111
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm
# 变量名 = 全限定类名会自动创建一个类实例 全限定类名会自动创建一个类实例
# 变量名. 属性 = 值 自动调用相应的 setter 方法进行赋值
# $ 变量名 引用之前的一个对象实例
授权
- 授权即访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
- 主体:即访问应用的用户,在 Shiro 中使用 Subject 代表该用户,用户只有授权后才允许访问相应的资源
- 资源:在应用中用户可以访问的任何东西用户只要授权后才能访问
- 权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如: 访问用户列表页面,Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的)
- 角色:代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便
- 隐式角色:即直接通过角色来验证用户有没有操作权限
- 显示角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合
授权方式
- Shiro 支持三种方式的授权
- 编程式:通过写 if/else 授权代码块完成
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
};
- 注解式:通过在执行的 Java 方法上放置相应的注解完成
@RequiresRoles("admin")
public void hello() {
//有权限
};
- SP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成
<shiro:hasRole name="admin">
<!— 有权限 —>
</shiro:hasRole>;
基于角色的访问控制(隐式角色)
- 在 ini 配置文件配置用户拥有的角色(shiro-role.ini)
[users]
zhang=123,role1,role2
wang=123,role1
# 规则即:“用户名=密码,角色1,角色2”,如果需要在应用中判断用户是否有相应角色,就需要在相应的 Realm 中返回角色信息,也就是说 Shiro 不负责维护用户-角色信息,需要应用提供,Shiro 只是提供相应的接口方便验证。
- 测试
private Subject login(String configFile,String userName,String password) {
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory =
new IniSecurityManagerFactory(configFile);
//2、得到SecurityManager实例 并绑定给SecurityUtils
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
subject.login(token);
return subject;
}
- Shiro提供了hasRole/hasRole用于判断用户是否拥有某个角色/某些权限
@Test
public void testHasRole() {
Subject subject = login("classpath:shiro-role.ini", "zhang", "123");
//判断拥有角色:role1
System.out.println(subject.hasRole("role1"));
//判断拥有角色:role1 and role2
System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2")));
//判断拥有角色:role1 and role2 and !role3
boolean[] result = subject.hasRoles(Arrays.asList("role1", "role2", "role3"));
System.out.println(result[0]);
System.out.println(result[1]);
System.out.println(result[2]);
}
- Shiro提供的checkRole/checkRoles和hasRole/hasAllRoles不同的地方是它在判断为假的情况下会抛出UnauthorizedException异常
@Test(expected = UnauthorizedException.class)
public void testCheckRole() {
Subject subject = login("classpath:shiro-role.ini", "zhang", "123");
//判断是否拥有角色:role1
subject.checkRole("role1");
//判断是否拥有角色:role1 and role3 失败抛出异常
subject.checkRoles("role1", "role3");
}
基于角色的访问控制(显式角色)
- 在ini配置文件配置用户拥有的角色及角色-权限关系(shiro-permission.ini)
[users]
zhang=123,role1,role2
wang=123,role1
[roles]
role1=user:create,user:update
role2=user:create,user:delete
# 规则:“用户名=密码,角色1,角色2”“角色=权限1,权限2”,即首先根据用户名找到角色,然后根据角色再找到权限;即角色是权限集合;Shiro同样不进行权限的维护,需要我们通过Realm返回相应的权限信息。只需要维护“用户——角色”之间的关系即可
- 测试
- Shiro提供了isPermitted和isPermittedAll用于判断用户是否拥有某个权限或所有权限
@Test
public void testIsPermitted() {
Subject subject = login("classpath:shiro-permission.ini", "zhang", "123");
//判断拥有权限:user:create
System.out.println(subject.isPermitted("user:create"));
//判断拥有权限:user:update and user:delete
System.out.println(subject.isPermittedAll("user:update", "user:delete"));
//判断没有权限:user:view
System.out.println(subject.isPermitted("user:view"));
}
@Test(expected = UnauthorizedException.class)
public void testCheckPermission () {
Subject subject = login("classpath:shiro-permission.ini", "zhang", "123");
//断言拥有权限:user:create
subject.checkPermission("user:create");
//断言拥有权限:user:delete and user:update
subject.checkPermissions("user:delete", "user:update");
//断言拥有权限:user:view 失败抛出异常
subject.checkPermissions("user:view");
}
与Web 集成
- Shiro 提供了与 Web 集成的支持,其通过一个ShiroFilter 入口来拦截需要安全控制的URL,然后进行相应的控制
- ShiroFilter 类似于如 Strut2/SpringMVC 这种web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件),然后判断URL 是否需要登录/权限等工作
- 在访问的时候,需要做一系列的预处理操作,我们的最佳选择就是使用过滤器来实现了
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy </filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- shiro过滤器,DelegatingFilterProx会从spring容器中找shiroFilter,所以过滤器的生命周期还是交给Spring进行管理的
- shiro中定义了多个过滤器来完成不同的预处理操作
过滤器的名称 | Java类 | 解释 |
anon | AnonymousFilter | 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon” |
authc | FormAuthenticationFilter | 表示需要认证(登录)才能使用;示例“/**=authc” |
authcBasic | BasicHttpAuthenticationFilter | HTTP身份验证拦截器 |
roles | RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有资源角色;示例“/admin/=roles[admin]” |
perms | PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有资源权限,示例“/employee/input=perms[“user:update”]” |
user | UserFilter | 用户拦截器,用户已经身份验证/记住我登录的都可;示例“/index=user” |
logout | LogoutFilter | 注销拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout” |
port | PortFilter | 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 |
rest | HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构建权限字符串(GET=read,POST=create,PUT=update,DELETE=delete, HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串; 示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll) |
ssl | SslFilter | SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样 |