原文来自于http://jinnianshilongnian.iteye.com/blog/2018398
Apache Shiro是Java的一个安全框架。可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。
其基本功能点如下图所示:
四大核心(Primary Concerns)
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
支持的功能(Supporting Features):
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro的架构
应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject
Shiro3大核心组件:
Subject(the current “user”):主体,代表了当前“用户”;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager(managers all Subject):安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm(access your security data):域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
就是说,一个最简单的应用是:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
从Shiro内部来看下Shiro的架构
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,用户可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:Session需要有人去管理它的生命周期,这个组件就是SessionManager;Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;
SessionDAO:用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
身份验证,即在应用中证明是本人。一般提供一些标识信息来表明,如提供身份证,用户名/密码来证明。
在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份:
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
所需依赖,在pom.xml文件中添加
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.9version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.2.2version>
dependency>
准备一些用户身份/凭据(shiro.ini),在resources文件夹下
[users]
zhang=123
wang=123
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Assert;
import org.junit.Test;
public class LoginLogoutTest {
@Test
public void testHelloWorld(){
//1.获取SecurityManager工厂,这里使用Ini配置文件初始化SercurityManager
Factory factory=new IniSecurityManagerFactory("classpath:shiro.ini");
//2.获得SecurityManager实例,并绑定给SecurityUtils
SecurityManager securityManager=factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3.得到Subject及创建用户名/密码验证Token(即用户身份/凭证)
Subject subject=SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken("wang","1234");
try{
//4.登录,即身份验证
subject.login(token);
//内部调用SecurityManager.login
}catch (AuthenticationException e){
//5.身份验证失败
System.out.println("error");
}
Assert.assertEquals(true,subject.isAuthenticated()); //断言用户已经登录
//6.退出
subject.logout();
}
}
1、首先通过new IniSecurityManagerFactory并指定一个ini配置文件来创建一个SecurityManagerFactory;
2、接着获取SecurityManager并绑定到SecurityUtils,这是一个全局设置,设置一次即可;
3、通过SecurityUtils得到Subject,其会自动绑定到当前线程;如果在web环境在请求结束时需要解除绑定;然后获取身份验证的Token,如用户名/密码;
4、调用subject.login方法进行登录,其会自动委托给SecurityManager.login方法进行登录;
5、如果身份验证失败请捕获AuthenticationException或其子类,常见的如: DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;
6、最后可以调用subject.logout退出,其会自动委托给SecurityManager.logout方法退出。
可以看出身份验证的步骤:
1、收集用户身份/凭证,即如用户名/密码;
2、调用Subject.login进行登录,如果失败将得到相应的AuthenticationException异常,根据异常提示用户错误信息;否则登录成功;
3、最后调用Subject.logout进行退出操作。
1、首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;
2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator(是SecurityManager的一部分)进行身份验证;
3、Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
4、Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;
5、Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
如上测试的几个问题:
1、用户名/密码硬编码在ini配置文件,以后需要改成如数据库存储,且密码需要加密存储;
2、用户身份Token可能不仅仅是用户名/密码,也可能还有其他的,如登录时允许用户名/邮箱/手机号同时登录。
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。如我们之前的ini配置方式将使用org.apache.shiro.realm.text.IniRealm。
org.apache.shiro.realm.Realm接口有下面的方法
String getName(); //返回一个唯一的Realm名字
boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException; //根据Token获取认证信息
单Realm配置
public class MyRealm1 implements Realm {
@Override
public String getName() {
return "myrealm1";
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken; //仅支持UsernamePasswordToken类型的Token
}
@Override
public AuthenticationInfo getAuthenticationInfo(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(); //如果密码错误
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}
shiro-multi-realm.ini
#声明一个realm
myRealm1=chapter2.MyRealm1
#指定securityManager的realms实现,通过$name来引入之前的realm定义
securityManager.realms=$myRealm1
1、变量名=全限定类名会自动创建一个类实例
2、变量名.属性=值 自动调用相应的setter方法进行赋值
3、$变量名 引用之前的一个对象实例
将testHelloWorld的ini文件替换了即可
Subject.login(token)进行登录,其会自动委托给Security Manager,我们将securityManager.realms设置为myRealm1(结合上图去理解),像前面的例子将shiro.ini中的信息包装到realm中
多Realm配置
ini配置文件,shiro-multi-realm.ini
#声明realm
myRealm1=chapter2.MyRealm1
myRealm2=chapter2.MyRealm2
#指定securityMananger的realms实现
securityManager.realms=$myRealm1,$myRealm2
把classpath改为shiro-multi-realm.ini即可
显示指定realm后,其他没有指定realm将被忽略,如果没有设置realms属性,其会自动发现声明的realm
JDBC Realm使用
引入依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.25version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>0.2.23version>
dependency>
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=123456
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm
数据库shiro下建三张表:users(用户名/密码)、user_roles(用户/角色)、roles_permissions(角色/权限),具体请参照shiro-example-chapter2/sql/shiro.sql;并添加一个用户记录,用户名/密码为zhang/123;
classpath改为该ini文件(realm会自动从多个表中搜索?)
Authenticator的职责是验证用户帐号,是Shiro API中身份验证核心的入口点
SecurityManager接口继承了Authenticator,另外还有一个ModularRealmAuthenticator实现(默认使用AtLeastOneSuccessfulStrategy策略),其委托给多个Realm进行验证,验证规则通过AuthenticationStrategy接口指定,默认提供的实现:
FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息;
AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了
假设我们有三个realm:
myRealm1: 用户名/密码为zhang/123时成功,且返回身份/凭据为zhang/123;
myRealm2: 用户名/密码为wang/123时成功,且返回身份/凭据为wang/123;
myRealm3: 用户名/密码为zhang/123时成功,且返回身份/凭据为[email protected]/123,和myRealm1不同的是返回时的身份变了;
public class MyRealm3 implements Realm {
@Override
public String getName() {
return "myrealm3";
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken; //仅支持UsernamePasswordToken类型的Token
}
@Override
public AuthenticationInfo getAuthenticationInfo(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(); //如果密码错误
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现,身份改为@163.com;
return new SimpleAuthenticationInfo(username + "@163.com", password, getName());
}
}
配置文件ini为shiro-authenticator-all-success.ini,策略为AllSuccessfulStrategy
#指定securityManager的authenticator实现(委托给多个Realm进行验证)
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#指定securityManager.authenticator的authenticationStrategy(所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息)
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
myRealm1=chapter2.MyRealm1
myRealm2=chapter2.MyRealm2
myRealm3=chapter2.MyRealm3
securityManager.realms=$myRealm1,$myRealm3
测试类,要所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,
public class AuthenticatorTest {
//首先实现登录逻辑
private void login(String configFile){
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory 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("zhang", "123");
subject.login(token);
}
//测试AllSuccessfulStrategy成功
@Test
public void testAllSuccessfulStartegyWithSuccess(){
login("classpath:shiro-authenticator-all-success.ini");
Subject subject=SecurityUtils.getSubject();
//得到一个身份集合,其包含了Realm验证成功的身份信息
PrincipalCollection principalCollection=subject.getPrincipals();
//得到两个验证成功的身份信息
Assert.assertEquals(2,principalCollection.asList().size());
}
}
如果两个reaml没有都验证成功就失败,如securityManager.realms= myRealm1, myRealm2;
测试AtLeastOneSuccessfulStrategy策略
配置文件shiro-authenticator-atLeastOne-success.ini
[main]
#指定securityManager的authenticator实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#指定securityManager.authenticator的authenticationStrategy
SuccessfulStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$SuccessfulStrategy
myRealm1=chapter2.MyRealm1
myRealm2=chapter2.MyRealm2
myRealm3=chapter2.MyRealm3
securityManager.realms=$myRealm1,$myRealm2,$myRealm3
@Test
public void testAtLeastOneSuccessfulStrategyWithSuccess() {
login("classpath:shiro-authenticator-atLeastOne-success.ini");
Subject subject = SecurityUtils.getSubject();
//得到一个身份集合,其包含了Realm验证成功的身份信息
PrincipalCollection principalCollection = subject.getPrincipals();
Assert.assertEquals(2, principalCollection.asList().size());
}
测试FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略
[main]
#指定securityManager的authenticator实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#指定securityManager.authenticator的authenticationStrategy
SuccessfulStrategy=org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$SuccessfulStrategy
myRealm1=chapter2.MyRealm1
myRealm2=chapter2.MyRealm2
myRealm3=chapter2.MyRealm3
securityManager.realms=$myRealm1,$myRealm2,$myRealm3
自定义AuthenticationStrategy实现,首先看其API:
//在所有Realm验证之前调用
AuthenticationInfo beforeAllAttempts(
Collection extends Realm> realms, AuthenticationToken token)
throws AuthenticationException;
//在每个Realm之前调用
AuthenticationInfo beforeAttempt(
Realm realm, AuthenticationToken token, AuthenticationInfo aggregate)
throws AuthenticationException;
//在每个Realm之后调用
AuthenticationInfo afterAttempt(
Realm realm, AuthenticationToken token,
AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)
throws AuthenticationException;
//在所有Realm之后调用
AuthenticationInfo afterAllAttempts(
AuthenticationToken token, AuthenticationInfo aggregate)
throws AuthenticationException;
自定义实现时一般继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy即可
这里我们自定义AtLeastTwoAuthenticatorStrategy:必须要有两个Realm验证成功,返回所有验证成功的身份信息
public class AtLeastTwoAuthenticatorStrategy extends AbstractAuthenticationStrategy{
//在所有Realm验证之前调用
@Override
public AuthenticationInfo beforeAllAttempts(Collection extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo();//返回一个权限的认证信息
}
//在每个Realm之前调用
@Override
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
return aggregate;//返回之前合并的
}
//在所有Realm验证之后调用
@Override
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
AuthenticationInfo info;
if (singleRealmInfo == null) {
info = aggregateInfo;
} else {
if (aggregateInfo == null) {
info = singleRealmInfo;
} else {
info = merge(singleRealmInfo, aggregateInfo);
}
}
return info;
}
//在所有Realm之后调用
@Override
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
//在验证了所有Reaml后,如果验证数目为空或大于2则抛出异常
if (aggregate == null || CollectionUtils.isEmpty(aggregate.getPrincipals()) || aggregate.getPrincipals().getRealmNames().size() < 2) {
throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " +
"could not be authenticated by any configured realms. Please ensure that at least two realm can " +
"authenticate these tokens.");
}
return aggregate;
}
}
[main]
#指定securityManager的authenticator实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#指定securityManager.authenticator的authenticationStrategy
SuccessfulStrategy=chapter2.AtLeastTwoAuthenticatorStrategy
securityManager.authenticator.authenticationStrategy=$SuccessfulStrategy
myRealm1=chapter2.MyRealm1
myRealm2=chapter2.MyRealm2
myRealm3=chapter2.MyRealm3
myRealm4=chapter2.MyRealm4
securityManager.realms=$myRealm1,$myRealm2,$myRealm4
myRealm4和1一样
@Test
public void testAtLeastTwoStrategyWithSuccess() {
login("classpath:shiro-authenticator-atLeastTwo-success.ini");
Subject subject = SecurityUtils.getSubject();
//得到一个身份集合,因为myRealm1和myRealm4返回的身份一样所以输出时只返回一个
PrincipalCollection principalCollection = subject.getPrincipals();
Assert.assertEquals(1, principalCollection.asList().size());
}
只能有一个验证成功OnlyOneAuthenticatorStrategy
public class OnlyOneAuthenticatorStrategy extends AbstractAuthenticationStrategy {
//在所有Reaml之前使用
@Override
public AuthenticationInfo beforeAllAttempts(Collection extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo();//返回一个权限的认证信息
}
//在每个Reaml前使用
@Override
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
return aggregate;//返回之前合并的
}
//在每个Reaml后使用
@Override
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
AuthenticationInfo info;
if (singleRealmInfo == null) {
info = aggregateInfo;
} else {
if (aggregateInfo == null) {
info = singleRealmInfo;
} else {
info = merge(singleRealmInfo, aggregateInfo);
//如果验证成功返回的身份验证数目大于一抛出异常
if(info.getPrincipals().getRealmNames().size() > 1) {
System.out.println(info.getPrincipals().getRealmNames());
throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " +
"could not be authenticated by any configured realms. Please ensure that only one realm can " +
"authenticate these tokens.");
}
}
}
return info;
}
//在所有Reaml后使用
@Override
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
return aggregate;
}
}