一. 身份认证
身份验证,即在应用中谁能证明他就是他本人。一般提供如他们的身份ID一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。
在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:
①principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。
②credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
最常见的 principals 和 credentials 组合就是用户名 / 密码了,另外还有两个相关的概念就是Subject 及 Realm,分别是主体及验证主体的数据源。
二. 身份认证流程
身份认证流程如下:
流程如下:
-
首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
-
SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
-
Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
-
Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
-
Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
三. 身份认证简单实现
1.环境准备
给项目导入junit、common-logging 及 shiro-core 依赖。笔者使用的是maven,直接下载jar包然后导入项目也可以。
<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>
2.配置用户身份/凭据
本文简单程序没有涉及到数据库,所以需要在src/main/java下创建shiro.ini文件,用于保存用户账户名密码等信息。
[users]
zhang=123
wang=456
在这里使用 ini 配置文件,通过 [users] 指定了两个主体:zhang/123、wang/456,模拟数据库中用户名zhang密码123、用户名wang密码456的两条用户信息。
3.编写测试用例
public class AuthenticationTest {
@Test
public void testLoginAndLogout() {
// 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) {
// 5、身份验证失败
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated()); // 断言用户已经登录
// 6、退出
subject.logout();
}
}
上面代码过程如下:
-
首先通过 new IniSecurityManagerFactory 并指定一个 ini 配置文件来创建一个 SecurityManager 工厂;
-
接着获取 SecurityManager 并绑定到 SecurityUtils,这是一个全局设置,设置一次即可;
-
通过 SecurityUtils 得到 Subject,其会自动绑定到当前线程;如果在 web 环境在请求结束时需要解除绑定;然后获取身份验证的 Token,如用户名 / 密码;
-
调用 subject.login 方法进行登录,其会自动委托给 SecurityManager.login 方法进行登录;
-
如果身份验证失败请捕获 AuthenticationException 或其子类,常见的如: DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如 “用户名 / 密码错误” 而不是 “用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;
-
最后可以调用 subject.logout 退出,其会自动委托给 SecurityManager.logout 方法退出。
从如上代码可总结出身份验证的步骤:
①收集用户身份 / 凭证,即如用户名 / 密码;
②调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功;
③最后调用 Subject.logout 进行退出操作。
从上面的测试类中我们发现的几个问题:
①用户名 / 密码硬编码在 ini 配置文件,以后需要改成如数据库存储,且密码需要加密存储;
②用户身份 Token 可能不仅仅是用户名 / 密码,也可能还有其他的,如登录时允许用户名 / 邮箱 / 手机号同时登录。
上面的例子是直接使用ini配置文件保存用户名密码,但在日常应用开发中我们肯定是要使用数据库存储的。而要使用数据库存储,如何使用Shiro来访问数据库,就需要借助下面我们要将的Realm了。
四. 身份认证Realm配置
Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作。
在Shiro架构中,org.apache.shiro.realm.Realm 接口如下:
String getName(); //返回一个唯一的Realm名字
boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException; //根据Token获取认证信息
1.单Realm配置
通常我们自定义Realm,可以通过实现上面的Realm接口,也可以通过继承AuthorizingRealm抽象类。
① 实现Realm接口:
public class MyRealm1 implements Realm {
public String getName() {
return "myrealm1";
}
public boolean supports(AuthenticationToken token) {
// 仅支持UsernamePasswordToken类型的Token
return token instanceof UsernamePasswordToken;
}
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());
}
}
②继承AuthorizingRealm抽象类:
public class MyRealm2 extends AuthorizingRealm {
//设置realm的名称
@Override
public void setName(String name) {
super.setName("myrealm2");
}
//用于认证
@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(); // 如果密码错误
}
// 如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
//用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
无论是通过哪种方式实现的自定义Realm,我们都要配置ini 配置文件来指定securityManager的realm实现:
#声明一个realm
myRealm1=org.dan.realm.MyRealm1
#指定securityManager的realms实现
securityManager.realms=$myRealm1
2.多Realm配置
ini配置文件:
#声明一个realm
myRealm1=org.dan.realm.MyRealm1
myRealm2=org.dan.realm.MyRealm2
#指定securityManager的realms实现
securityManager.realms=$myRealm1,$myRealm2
securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,如果删除 “securityManager.realms=$myRealm1,$myRealm2”,那么securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现),当我们显示指定 realm 后,其他没有指定 realm 将被忽略,如 “securityManager.realms=$myRealm1”,那么 myRealm2 不会被自动设置进去。
到这里我们已经能够使用Shiro完成认证功能了,但是这里我们是通过用户输入的密码直接去查询数据库中的明文密码,而往往为了保护用户信息所以数据库中的密码都是经过了MD5的算法进行加密后才放入数据库的,所以实际开发中我们需要通过Shiro获取数据库中经过md5加密后的密码来和用户输入的密码进行比对。所以接下来我们将学习Shiro中是如何将用户输入的信息与数据库中的加密信息进行对比从而实现的认证。
五. 身份加密认证
实际开发中为了保护用户信息的安全,我们需要对用户在注册时输入的密码进行加密后再保存到数据库,当用户登录时我们也要将用户输入的密码进行加密后再与数据库中的密码进行比对。即需要对密码进行散列,常用的散列方法有md5、sha。
用md5算法对密码进行散列的问题:如果知道散列后的值可以通过穷举算法得到md5密码对应的明文。解决方法:建议对md5进行散列时加salt(盐),进行加密相当于对原始密码+盐进行散列。
接下来通过一个测试例子来看怎么实现加密认证。
1.自定义realm支持散列算法
创建CustomRealmMd5.java类,并在里面模仿数据库中的用户名和加密密文,内容如下:
public class CustomRealmMd5 extends AuthorizingRealm {
// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealmMd5");
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// token是用户输入的
// 第一步从token中取出身份信息
String userCode = (String) token.getPrincipal();
// 第二步:根据用户输入的userCode从数据库查询
// ....
// 如果查询不到返回null
// 数据库中用户账号是zhangsansan
/*
* if(!userCode.equals("zhangsansan")){// return null; }
*/
// 模拟根据用户名从数据库查询到的密码,散列值
String password = "f3694f162729b7d0254c6e40260bf15c";
// 从数据库获取salt
String salt = "qwerty";
//上边散列值和盐对应的明文:111111
// 如果查询到返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userCode, password, ByteSource.Util.bytes(salt), this.getName());
return simpleAuthenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
2.定义ini配置文件
[main]
#自定凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列的算法
credentialsMatcher.hashAlgorithmName=md5
#散列的次数
credentialsMatcher.hashIterations=1
#将凭证匹配器设置到我们定义的realm
customRealm=realm.CustomRealmMd5
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm
3.测试
public class MD5Test {
//注解用的main方法进行测试,你也可以通过junit.jar进行测试
public static void main(String[] args) {
//模拟用户输入的密码
String source="111111";
//加入我们的盐salt
String salt="qwerty";
//密码11111经过散列1次得到的密码:f3694f162729b7d0254c6e40260bf15c
int hashIterations=1;
//构造方法中:
//第一个参数:明文,原始密码
//第二个参数:盐,通过使用随机数
//第三个参数:散列的次数,比如散列两次,相当 于md5(md5(''))
Md5Hash md5Hash=new Md5Hash(source,salt,hashIterations);
String password_md5=md5Hash.toString();
System.out.println(password_md5);
}
}
自此,我们就完成了简单的Shiro身份认证实现(包括普通认证和实际开发中的经过散列算法后的认证)。