一、了解Shiro
Apache Shiro 是Java 的一个安全框架。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与Web 集成、缓存等。
1.为什么学习Shiro:
既然shiro将安全认证相关的功能抽取出来组成一个框架,使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户开始使用shiro。
java领域中spring security(原名Acegi)也是一个开源的权限管理框架,但是spring security依赖spring运行,而shiro就相对独立,最主要是因为shiro使用简单、灵活,所以现在越来越多的用户选择shiro。
2.基本功能:
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web 支持,可以非常容易的集成到Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro 不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
3.shiro架构:
Subject:Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
SecurityManager:SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
Authenticator:Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer:Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
realm:Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
sessionManager:sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO:SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
CacheManager:CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
Cryptography:Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
二、认证
1.身份验证是:在应用中谁能证明他就是他本人。一般提供如他们的身份ID 一些标识信息来表明他就是他本人,如提供身份证,用户名/密码来证明。在 shiro 中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份(其实就是用户名和密码)
2.principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。最常见的principals和credentials组合就是用户名/密码了。
3.认证流程:
4.认证入门代码示例:
1)加入相关jar包
commons-beanutils-1.9.2.jar commons-logging-1.2.jar junit-4.10.jar shiro-all-1.2.3.jar slf4j-api-1.7.7.jar log4j-1.2.17.jar slf4j-log4j12-1.7.5.jar |
2)log4j.properties日志配置文件
log4j.rootLogger=debug, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n |
3)配置shiro环境文件shiro.ini:通过Shiro.ini配置文件初始化SecurityManager环境。
[users] zhangsan=1111 lisi=1111 |
4)代码实现
//用户登录和退出
@Test
public void testAuthenticator(){
// 构建SecurityManager工厂,IniSecurityManagerFactory可以从ini文件中初始化SecurityManager环境
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//通过工厂获得SecurityManager实例
SecurityManager securityManager = factory.getInstance();
//将securityManager设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
//获取subject实例
Subject subject = SecurityUtils.getSubject();
//创建用户名,密码身份验证Token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "1111");
try {
//登录,即身份验证
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
//身份认证失败
}
//断言用户已经登录
Assert.assertEquals(true, subject.isAuthenticated());
//退出
subject.logout();
}
5)常见的异常信息及处理:
ConcurrentAccessException:用户正在登陆,线程异常。
DisabledAccountException:账号失效异常
UnknownAccountException:账号不正确异常
ExpiredCredentialsException:凭证过期异常
IncorrectCredentialsException:凭证错误异常
无论异常多么详细,给用户的提示必须是模糊的!(为系统安全着想)
6)执行流程(底层剖析):
a)通过shiro相关API,创建SecurityManager及获取Subject实例
b)封装token信息(UsernamePasswordToken对象)
c)通过subject.login(token)进行用户认证
subject接收token,通过其实现类 DelegatingSubject 将 token 委托给 SecurityManager 来完成认证。SecurityManager 接口通过其实现类 DefaultSecurityManager 中的 login 来完成认证相关的功能。在login方法中调用了 authenticate()来完成认证。该方法是SecuityManager的子类,DefaultSecurityManager的父类--> AuthenticatingSecurityManager 来完成的。在该类的 authenticating()中通过调用 authenticator(认证器) 来完成认证操作。 authenticator 是由其默认实现类 ModularRealmAuthenticator来完成认证,通过该类的 doAuthenticate方法来获取Realms信息。如果是单Realms直接将 token 和 realm 中的数据进行比较,判断是否认证成功。如果是多Realms,那么需要Authentication Strategy(认证策略) 来完成对应的认证工作。
SecurityManager接口的实现类继承图如下:
三、JdbcRealm 与 Authentication Strategy认证策略
1、使用shiro框架来完成认证工作,默认情况下使用的是IniRealm。如果需要使用其他Realm,那么需要进行相关的配置。
2、ini配置文件讲解:
[main] section是你配置应用程序的SecurityManager实例及任何它的依赖组件(如Realms)的地方。
[main] myRealm=cn.sun.realm.MyRealm #依赖注入 securityManager.realm=$myRealm |
[users] section允许你定义一组静态的用户账户。在这大部分拥有少数用户账户或用户账户不需要运行时被动态地创建的环境下是很有用的。
[users] zhangsan=1111 lisi=2222,role1,role2 |
[roles] section允许你把定义在[users] section中的角色与权限关联起来。另外,这在大部分拥有少数用户账户或用户账户不需要再运行时被动态地创建的环境下是很有用的。
[users] zhangsan=1111,role1 [roles] role1=user:add,user:delete |
3、使用JdbcRealm来完成身份认证
通过观察JdbcRealm可知,要实现JdbcRealm:
(1)需要为jdbcRealm设置dataSource
(2)在指定的dataSource所对应的数据库中应有用户表users,该表中应该有username,password,password_salt等
4、实现步骤
(1)新建数据库表
(2)配置shiro.ini文件
[main]
#配置数据源
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.url=jdbc:mysql://127.0.0.1:3306/shiro?serverTimezone=GMT%2B8
dataSource.driverClassName=com.mysql.cj.jdbc.Driver
dataSource.username=root
dataSource.password=root
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
# $表示引用对象
jdbcRealm.dataSource=$dataSource
securityManager.realm=$jdbcRealm
说明:特别要注意连接池配置关键字:url,driverClassName,username,password
(3)测试
JdbcDemo.java
package com.sun123.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.util.Factory;
//java.lang中有SecurityManager的包,需要改成Apache的
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
public class JdbcDemo {
public static void main(String[] args) {
// 1、创建SecurityManager工厂,读取相应的配置文件
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 2、通过SecurityManager工厂获取SecurityManager的实例
SecurityManager securityManager = factory.getInstance();
// 3、将securityManager对象设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
// 4、通过SecurityUtils获取主体Subject
Subject subject = SecurityUtils.getSubject();
// 5、加入登录的用户名zhangsan和1111,这个地方的zhangsan和1111表示用户登录时输入的信息
// 而shiro.ini文件中的信息相当于数据库中存放的信息
// UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "111");
UsernamePasswordToken token = new UsernamePasswordToken("wangwu", "111");
try {
// 6、进行用户身份验证
subject.login(token);
// 7、通过subject来判断用户是否通过验证
if (subject.isAuthenticated()) {
System.out.println("验证通过");
}
} catch (UnknownAccountException e) {// AuthenticationException为父异常 Ctrl+T看子异常
System.out.println("用户名/密码不正确");
} catch (IncorrectCredentialsException e) {// AuthenticationException为父异常 Ctrl+T看子异常
System.out.println("用户名或密码不正确");
}
}
}
5、Authentication Strategy:认证策略,在shiro中有3种认证策略
(1)AtleastOneSuccessfulStrategy:如果一个(或更多)Realm 验证成功, 则整体的尝试被认为是成功的。 如果没有一个验证成功,则整体尝试失败。
(2)FirstSuccessfulStrategy:只要有第一个成功地验证的 Realm返回的信息将被使用 。所有进一步的Realm将被忽略。如果没有一个验证成功, 则整体尝试失败。
(3)AIISucessfulStrategy:为了整体的尝试成功,所有配置的Realm必须验证成功。如果没有一个验证成功,则整体尝试失 败。
默认的策略是:AtleastOneSuccessfulStrategy
6、设置认证策略
#验证策略设置 authenticationStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy securityManager.authenticator.authenticationStrategy=$authenticationStrategy |
四、自定义Realm来实现身份验证
1、jdbcReam已经实现了从数据库中获取用户的验证信息,但是jdbcRealm灵活性太差。如果要实现自己的一些特殊应用时,将不能支持。这时,可以通过自定义Realm来实现身份的认证功能。
2、Realm是一个接口,在接口中定义了根据token获得认证信息的方法。Shiro内容实现了一系列的realm。这些不同的Realm实现类提供了不同的功能。AuthenticatingRealm实现了获取身份信息的功能。AuthorizingRealm(继承了AuthenticatingRealm)实现了获取权限信息的功能。通常自定义Realm需要继承AuthorizingRealm,这样既可以提供身份认证的自定义方法,也可以实现授权的自定义方法。
package com.sun123.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class UserRealm extends AuthorizingRealm {
/**
* 自定义realm的实现 该realm类提供了两个方法
* doGetAuthorizationInfo 获取认证信息
* doGetAuthenticationInfo 获取权限信息
*/
@Override
public String getName() {
// 自定义
return "userRealm";
}
// 完成身份认证,并且返回认证信息
// 如果身份认证失败,返回null
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户输入的用户名
String username = (String) token.getPrincipal();// 获取身份信息
System.out.println("username:" + username);
// 根据用户名到数据库查询密码信息——模拟
// 假定从数据库获取的密码为1111
String pwd = "1111";
// 将从数据库中查询的信息封装到SimpleAuthenticationInfo中
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, pwd, getName());
return info;
}
// 授权的信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
// TODO Auto-generated method stub
return null;
}
}
|
五、散列算法(加密算法)
1、在身份认证过程中往往会涉及加密。如果不加密那么数据信息不安全。Shiro内容实现比较多的散列算法。如MD5,SHA等。并且提供了加盐功能。比如"1111"的MD5码为"b59c67bf196a4758191e42f76670ceba",这个MD5码可以在很多破解网站上找到对应的原密码。但是如果为"1111"+姓名,那么能找到原密码的难度就会增加。
2、测试MD5案例
package com.sun123.shiro;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
public class Md5Demo {
public static void main(String[] args) {
//使用MD5加密算法 加密
Md5Hash md5 = new Md5Hash("1111");
System.out.println("1111=="+md5.toString());
//加盐
md5 = new Md5Hash("1111","sxt");
System.out.println("1111=="+md5.toString());
//迭代次数
md5 = new Md5Hash("1111","sxt",2);
System.out.println("1111=="+md5.toString());
SimpleHash hash = new SimpleHash("md5","1111","sxt",2);
System.out.println("1111=="+hash.toString());
}
}
运行结果:
3、在自定义的Realm中使用散列算法
Realm的实现:
package com.sun123.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
public class UserRealm extends AuthorizingRealm {
/**
* 自定义realm的实现 该realm类提供了两个方法
* doGetAuthorizationInfo 获取认证信息
* doGetAuthenticationInfo 获取权限信息
*/
@Override
public String getName() {
// 自定义
return "userRealm";
}
// 完成身份认证,并且返回认证信息
// 如果身份认证失败,返回null
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户输入的用户名
String username = (String) token.getPrincipal();// 获取身份信息
System.out.println("username:" + username);
// 根据用户名到数据库查询密码信息——模拟
// 假定从数据库获取的密码为1111和盐值
String pwd = "e41cd85110c7533e3f93b729b25235c3";
String salt = "sxt";
// 将从数据库中查询的信息封装到SimpleAuthenticationInfo中
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, pwd,ByteSource.Util.bytes(salt),getName());
return info;
}
// 授权的信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
// TODO Auto-generated method stub
return null;
}
}
shiro.ini
[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
userRealm=com.sun123.realm.UserRealm
userRealm.credentialsMatcher=$credentialsMatcher
securityManager.realm=$userRealm