Shiro简介
Apache Shiro 是一个强大易用的 Java 安全(权限)框架,提供了认证、授权、加密、会话管理、与Web集成和缓存等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务,相较于 Spring Security,Shiro 更加简单易用。
shiro基本功能点如下:
Authentication:身份认证/登录;
Authenrization:授权,即权限验证,验证某个已认证的用户是否拥有某个角色或对某个资源具有某个权限;
Session Manager:会话管理,即用户登录后到退出前就是一次会话,会话可以保存所有信息;
Cryptograpy:加密,保护数据的安全性;
Web Support:Web支持,可以很容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、角色/权限不必每次查询;
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次自动登录。
Shiro架构
一. 从外部来看shiro,即从应用程序角度来观察如何使用shiro完成工作:
可以看出:Application Code(应用代码)直接调用的对象是Subject,shiro对外的API核心就是Subject。
Subject:主体,代表了当前“用户”,所有的Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager,具体看源码实现;
SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
Realm:域,shiro从Realm获取安全数据(如用户、角色、权限),可以将Realm看成DataSource,即安全数据源。
一个简单的Shiro应用:
1.应用代码通过Subject来进行认证和授权,Subject内部将委托给SecurityManager来处理;
2.给shiro的SecurityManager注入Realm,SecurityManger将获取到数据与用户数据比较进行判断。
二. 从shiro内部来看shiro的架构,如下图:
Subject:主体,应用代码直接交互的对象,代表当前交互的“用户”;
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证,可以自定义实现;
Authrizer:授权器,控制用户的访问和操作权限;
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等,需要用户自己实现获取数据;
SessionManager:Shiro 抽象了一个自己的 Session 来管理主体与应用之间交互的数据,可以在JavaSE、Web、EJB等环境使用;
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的。
Shiro简单应用
下载shiro jar包,下载地址https://shiro.apache.org/download.html,同时需要下载日志jar包:
log4j-1.2.17.jar
slf4j-api.1.6.1.jar
slf4j-log4j12-1.6.1.jar
maven配置如下:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
创建一个简单的java工程,在src目录下创建一个配置文件shiro.ini。
规则:[users]用户=密码,角色1,角色2,……..
[roles] 角色=资源:权限…(:实例),资源:权限,…(:实例),……..
roles中可以使用通配符。具体配置如下:
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
[roles]
# 所有资源的所有权限
admin = *
# 资源lightsaber的所有权限
schwartz = lightsaber:*
# 资源winnebago的实例eagle5拥有drive权限
goodguy = winnebago:drive:eagle5
创建java类
public class SimpleShiro {
private static final transient Logger log = LoggerFactory.getLogger(SimpleShiro.class);
public static void main(String[] args) {
//读取配置文件,创建SecurityManager工厂,通过工厂创建SecurityManager对象
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
//将SecurityManager绑定到SecurityUtils,一个简单的shiro环境就建好了
SecurityUtils.setSecurityManager(securityManager);
//获取主体
Subject currentUser = SecurityUtils.getSubject();
// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) { //判断用户是否已认证
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); //将用户名和密码绑定到UsernamePasswordToken对象中
token.setRememberMe(true); //记住密码
try {
currentUser.login(token); //登录认证,委托给SecurityManager处理
} catch (UnknownAccountException uae) { //该用户不存在
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) { //该用户密码错误
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) { //该用户被锁定
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
}
//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //获取用户名
//test a role:
if (currentUser.hasRole("schwartz")) { //判断用户是否有schwartz角色
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:weild")) { //判断用户是否有lightsaber:weild权限
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) { //判断用户是否有操作eagle5的winnebago:driven权限
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//all done - log out!
currentUser.logout(); //登出
System.exit(0);
}
}
- 首先通过 new IniSecurityManagerFactory 并指定一个 ini 配置文件来创建一个SecurityManager 工厂;接着获取 SecurityManager 并绑定到 SecurityUtils,这是一个全局设置,设置一次即可;
- 通过 SecurityUtils 得到 Subject,其会自动绑定到当前线程(TreadContext);如果在 web环境在请求结束时需要解除绑定;然后获取身份验证的 Token,如用户名 / 密码;
- 调用 subject.login 方法进行登录,其会自动委托给 SecurityManager.login方法进行登录;
- 如果身份验证失败请捕获 AuthenticationException 或其子类,常见的如:
DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException(错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如“用户名 / 密码错误” 而不是 “用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库; - 最后可以调用 subject.logout 退出,其会自动委托给 SecurityManager.logout 方法退出。