Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全有时可能非常复杂,甚至是痛苦的,但并非必须如此。框架应尽可能掩盖复杂性,并提供简洁直观的API,以简化开发人员确保其应用程序安全的工作。其基本功能点如下图所示:
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro的架构有三个主要概念:和Subject
,SecurityManager
和Realms
。下图是这些组件如何交互的详细概述如下图:
可以看到:应用代码直接交互的对象是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,即安全数据源。
从这个意义上讲,Realm本质上是一个特定于安全性的DAO:它封装了数据源的连接细节,并根据需要使相关数据可用于Shiro。配置Shiro时,必须至少指定一个Realm用于身份验证和/或授权。所述SecurityManager
可与多个境界被配置,但至少有一个是必需的。
Shiro提供了开箱即用的领域,可以连接到许多安全数据源(也称为目录),如LDAP,关系数据库(JDBC),文本配置源(如INI和属性文件等)。如果默认域不符合您的需要,您可以插入自己的Realm实现来表示自定义数据源。
与其他内部组件一样,Shiro SecurityManager
管理如何使用Realms获取要表示为Subject
实例的安全性和身份数据。
详细的架构图如下:
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的核心;所有具体的交互都通过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提供了一些常见的加密组件用于如密码加密/解密的。
应用程序SecurityManager
执行安全操作并管理所有应用程序用户的状态。在Shiro的默认SecurityManager
实现中,这包括:
-
Authentication
-
Authorization
-
Session Management
-
Cache Management
-
Realm coordination
-
Event propagation
-
“Remember Me” Services
-
Subject creation
-
Logout and more.
为了简化配置并支持灵活的配置/可插拔性,Shiro的实现在设计上都是高度模块化的——实际上模块化到SecurityManager实现(及其类层次结构)根本不做什么。相反,SecurityManager实现主要充当轻量级的“容器”组件,将几乎所有行为委托给嵌套/封装的组件。这种“包装器”设计反映在上面详细的架构图中。
当组件实际执行逻辑时,SecurityManager
实现知道如何以及何时协调组件以获得正确的行为。
Shiro认证:
认证流程:
创建SecurityManager -->Subject提交认证 -->SecurityMaager认证 -->Authenticator认证-->Realm验证
代码演示环境:
java JDK 1.8
spring boot 2.0.4
IDEA
MySql 5.6
maven 3.5.4
添加maven依赖:
<!-- shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
创建一个测试类AUthTest:
public class AUthTest {
SimpleAccountRealm simpleAccountRealm=new SimpleAccountRealm();
@Before
public void add()
{
simpleAccountRealm.addAccount("zhangsan","123456");//在认证的时候从realm取数据
}
@Test
public void TestAuth()
{
//1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);//将simpleAccountRealm设置到我们的环境当中来
//2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
subject.login(usernamePasswordToken);
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
subject.logout();
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
}
}
shiro授权
授权流程:
创建SecurityManager -->Subject授权 -->SecurityManger授权 -->Authorize授权-->Realm获取角色权限数据
认证的过程和授权的过程是类似的,我们依然用AuthTest这个类来测试:
public class AUthTest {
SimpleAccountRealm simpleAccountRealm=new SimpleAccountRealm();
@Before
public void add()
{
// simpleAccountRealm.addAccount("zhangsan","123456","admin");//再添加用户的时候我们给用户赋予一个adminq权限
simpleAccountRealm.addAccount("zhangsan","123456","admin","user");//再添加用户的时候我们给用户赋予一个adminq权限
}
@Test
public void TestAuth()
{
//1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);//将simpleAccountRealm设置到我们的环境当中来
//2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
subject.checkRole("admin");
subject.login(usernamePasswordToken);
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
//需要在认证完成之后检查权限
subject.checkRole("admin");
subject.checkRoles("admin","user");
subject.logout();
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
}
}
Shiro内置Realm:
IniRealm 通过ini文件来设置用户和权限
JdbcRealm 需要连接数据库来取得角色和权限数据
IniRealm:
在Resources目录下面创建 user.ini文件
[users]
zhangsan=123456,admin
[roles]
admin=user:delete,user:update
我们依然是创建一个测试类IniRealmTest:
public class IniRealmTest
{
@Test
public void TestAuth()
{
IniRealm iniRealm =new IniRealm("classpath:user.ini");
//1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
defaultSecurityManager.setRealm(iniRealm);
//2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
subject.login(usernamePasswordToken);
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
subject.checkRole("admin");
subject.checkPermissions("user:update","user:dele2te");//Subject does not have permission [user:dele2te]
// subject.checkPermission("user:dele2te");//Subject does not have permission [user:dele2te]
subject.logout();
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
}
}
JdbcRealm:
需要连接数据库,我们先在maven中引入:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.0</version>
</dependency>
创建一个测试类JdbcRealmTest:
public class JdbcRealmTest
{
DruidDataSource dataSource=new DruidDataSource();
{
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test1");
dataSource.setUsername("root");
dataSource.setPassword("123456");
}
@Test
public void TestAuth()
{
//jdbcrealm需要访问数据库
JdbcRealm jdbcRealm=new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
//1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
defaultSecurityManager.setRealm(jdbcRealm);
//2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
subject.login(usernamePasswordToken);
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
// subject.checkPermission("user:dele2te");//Subject does not have permission [user:dele2te]
subject.logout();
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
}
}
运行这个测试方法,我们需要在test1数据库中创建users表。大家可能会疑惑,我们这里没有写任何sql 那么realm是怎么工作的,点击JdbcRealm对象我们会看到,它会有默认的sql 这里截取了部分JdbcRealm类的内容
public class JdbcRealm extends AuthorizingRealm {
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
...
通常情况下我们数据库的表和JdbcRealm默认访问的表不一致,这个时候我们可以使用
jdbcRealm.setAuthenticationQuery("my sql"); 传入我们自己的sql语句
public class JdbcRealmTest
{
DruidDataSource dataSource=new DruidDataSource();
{
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test1");
dataSource.setUsername("root");
dataSource.setPassword("123456");
}
@Test
public void TestAuth()
{
//jdbcrealm需要访问数据库
JdbcRealm jdbcRealm=new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
jdbcRealm.setPermissionsLookupEnabled(true);//注意默认情况下是不会去查询我们的权限数据的,这里需要设置为true
jdbcRealm.setAuthenticationQuery("select * from user where name=? limit 1");//提供我们自己的sql
//1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
defaultSecurityManager.setRealm(jdbcRealm);
//2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","1");
subject.login(usernamePasswordToken);
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
// subject.checkPermission("user:dele2te");//Subject does not have permission [user:dele2te]
subject.logout();
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
}
}
同样我们可以自定义我们的角色和权限的查询sql
jdbcRealm.setUserRolesQuery("select role_name from user_roles where username = ?");
jdbcRealm.setPermissionsQuery("select permission from roles_permissions where role_name = ?");
自定义Realm:
仿照JdbcRealm对象,要实现自定义的Realm我们需要继承AuthorizingRealm
新建一个类CustomRealm
public class CustomRealm extends AuthorizingRealm {
Map<String,String> userMap=new HashMap<>(16) ;
{
userMap.put("zhangsan","123456");
super.setName("CustomRealm");//设置我们访问的realm名称(这里取什么名称无所谓)
}
//这个是用来做授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username=(String)principals.getPrimaryPrincipal();//从我们的认证信息中获得用户名
System.out.println(username);
//从数据库和缓存中获取角色数据
Set<String> roles=GetRolesByUserName(username);
//通过用户名来获得权限数据
Set<String> premission=GetPremissionsByUserName(username);
//将我们去到的角色数据和权限数据返回
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(premission);
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
//这个是用来做认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.从主体传过来的认证信息中获得用户名
String userName = (String) token.getPrincipal();
//2.通过用户名去到数据库中获取凭证
String password=GetPasswordByUserName(userName);
if(password==null) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo("zhangsan","123456",getName());//最后一个参数是Realm
return simpleAuthenticationInfo;
}
/**
* 这里是模拟数据库访问
* @param userName
* @return
*/
private String GetPasswordByUserName(String userName)
{
return userMap.get(userName);
}
/**
* 获得角色数据
* @param username
* @return
*/
private Set<String> GetRolesByUserName(String username) {
Set<String> sets=new HashSet<>();
sets.add("admin");
sets.add("user");
return sets;
}
/**
* 模拟数据库
* @param username
* @return
*/
private Set<String> GetPremissionsByUserName(String username)
{
Set<String> sets=new HashSet<>();
sets.add("user:delete");
sets.add("user:update");
return sets;
}
}
新建一个测试类CustomRealmTest
public class CustomRealmTest
{
@Test
public void TestAuth()
{
CustomRealm customRealm=new CustomRealm();
//1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
defaultSecurityManager.setRealm(customRealm);
//2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
subject.login(usernamePasswordToken);
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
subject.checkPermission("user:delete");//Subject does not have permission [user:dele2te]
subject.checkRole("admin");
subject.logout();
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
}
}
Shiro加密
我们可以在自定义Realm基础上使用特定的加密算法(如MD5),以及加盐操作来保证我们密码的安全
我们依然用CustomRealm 与 CustomRealmTest做演示
public class CustomRealm extends AuthorizingRealm {
Map<String,String> userMap=new HashMap<>(16) ;
{
userMap.put("zhangsan","e8b6ba86bafab35cfdf900a5402ba046");
super.setName("CustomRealm");//设置我们访问的realm名称(这里取什么名称无所谓)
}
//这个是用来做授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username=(String)principals.getPrimaryPrincipal();//从我们的认证信息中获得用户名
System.out.println(username);
//从数据库和缓存中获取角色数据
Set<String> roles=GetRolesByUserName(username);
//通过用户名来获得权限数据
Set<String> premission=GetPremissionsByUserName(username);
//将我们去到的角色数据和权限数据返回
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(premission);
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
//这个是用来做认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.从主体传过来的认证信息中获得用户名
String userName = (String) token.getPrincipal();
//2.通过用户名去到数据库中获取凭证
String password=GetPasswordByUserName(userName);
if(password==null) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo("zhangsan",password,getName());//最后一个参数是Realm
//加盐
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("zhang"));
return simpleAuthenticationInfo;
}
/**
* 这里是模拟数据库访问
* @param userName
* @return
*/
private String GetPasswordByUserName(String userName)
{
return userMap.get(userName);
}
/**
* 获得角色数据
* @param username
* @return
*/
private Set<String> GetRolesByUserName(String username) {
Set<String> sets=new HashSet<>();
sets.add("admin");
sets.add("user");
return sets;
}
/**
* 模拟数据库
* @param username
* @return
*/
private Set<String> GetPremissionsByUserName(String username)
{
Set<String> sets=new HashSet<>();
sets.add("user:delete");
sets.add("user:update");
return sets;
}
public static void main(String[] args)
{
Md5Hash md5Hash=new Md5Hash("123456","zhang",2);
System.out.println(md5Hash.toString());
}
}
public class CustomRealmTest
{
@Test
public void TestAuth()
{
CustomRealm customRealm=new CustomRealm();
//1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
defaultSecurityManager.setRealm(customRealm);
//2.主体提交认证请求
HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//传一个加密名称
hashedCredentialsMatcher.setHashIterations(2);//设置加密次数,这边的值得和加密的值一致
customRealm.setCredentialsMatcher(hashedCredentialsMatcher);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
subject.login(usernamePasswordToken);
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
subject.checkPermission("user:delete");//Subject does not have permission [user:dele2te]
subject.checkRole("admin");
subject.logout();
System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
}
}
Shiro官方文档:http://shiro.apache.org/reference.html