title: Shiro之实现认证
tags: shiro
categories: shiro
若图片无法显示,请前往我的博客查看,相应文章链接:http://codingxiaxw.cn/2016/11/21/48-shiro-authentication/#more
与其它java开源框架类似,将shiro的jar包加入项目就可以使用shiro提供的功能了。shiro-core是核心包必须选用,还提供了与web整合的shiro-web、与spring整合的shiro-spring、与任务调度quartz整合的shiro-quartz等jar包。
写在前面的话:本篇文章只是通过一个入门程序教大家学会使用Shiro实现认证功能(包括普通认证和经过散列算法对密码进行加密后的认证),没有将Shiro同Spring整合起来使用(后面的文章我们会讲解Shiro整合web开发环境的项目中如何实现认证功能,见Shiro整合Web项目及整合后的开发),所以我们采用的jar包很少:shiro-core.jar、junit测试jar包、commons-logging.jar。
1.认证的概念
身份认证,即在应用中谁能证明他就是他本人。一般提供如他们的身份ID一些标识信息来表明他就是他本人,如提供身份证,用户名/密码来证明。
在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份:
- principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
- credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的principals和credentials组合就是用户名/密码了。接下来先通过一个入门程序进行一个基本的身份认证。
另外两个相关的概念是之前提到的Subject及Realm,分别是主体及验证主体的数据源。
2.Shiro认证流程
Shiro认证流程图如下:[图片上传失败...(image-292a3f-1526380804010)]
文字描述认证流程:
- 首先调用Subject.login(token)进行登录,其会自动委托给SecurityManager,调用之前必须通过SecurityUtils. setSecurityManager()设置。
- SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证。
- Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现。
- Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证。
- Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
通过Shiro认证流程图,我们在用Shiro完成认证功能时就要完全按照这个流程去写代码。导入上述所说的3个jar包后我们就可以开始写Shiro认证的入门程序了。
3.Shiro认证入门程序
本入门程序是通过模仿用户的登录与退出来完成认证。我们将用户登录时输入的信息与数据库中的信息进行比较,只有二者信息符合时就说明用户认证通过了。
3.1创建shiro-first.ini
在src包下创建名为shiro-first.ini文件,程序中我们没有连接到数据库,所以采用后缀为ini的文件来模拟数据库中的数据。该文件中的数据如下:[图片上传失败...(image-df059d-1526380804010)]
在.ini文件中通过[users]
指定了两个主体(Subject):用户名zhangsan和密码111111、用户名lisi和密码22222。这样在.ini文件中配置就相当于模拟了数据库中的两条用户信息。
3.2编写入门程序
我们接下来就参照上述认证流程图来写认证的程序,如下:
public class AuthenticationTest
{
//用户的登录和退出
@Test
public void testLoginAndLogout()
{
//创建securityManager工厂
Factory factory=new IniSecurityManagerFactory(
"classpath:shiro-first.ini");
//创建SecurityManager
SecurityManager securityManager = factory.getInstance();
//将securityManager设置到当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);
//丛SecurityUtils里边的到一个subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备token(令牌)
// 模拟用户输入的账号和密码,将来是由用户输入进去从页面传送过来
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
"111111");
try {
// 执行认证提交
subject.login(token);
} catch (AuthenticationException e) {
// 认证失败
e.printStackTrace();
}
// 是否认证通过
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("是否认证通过:" + isAuthenticated);
// 退出操作
subject.logout();
}
}
运行程序,输出是否认证通过:true
。因为该用户输入的信息和数据库中的信息匹配,所以认证成功。代码讲解:
- 首先通过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进行退出操作。
从上面的测试类中我们发现的几个问题:1、用户名/密码硬编码在ini配置文件,以后需要改成如数据库存储,且密码需要加密存储。2、用户身份Token可能不仅仅是用户名/密码,也可能还有其他的,如登录时允许用户名/邮箱/手机号同时登录。
上面我们是直接访问数据库(.ini文件)的,而我们真正用到Shiro时是通过Realm来访问数据库的,在这里也应该是通过Realm来访问.ini配置文件。所以下面我们来讲讲通过上面提到的Realm完成认证功能。
4.自定义Realm
实际开发中我们通过realm从数据库中查询用户信息,所以realm的作用可想而知:根据token中的身份信息去查询数据库(入门程序我们使用ini配置文件模拟数据库),如果查到用户则返回认证信息,如果查询不到就返回null。
在Shiro架构中,realm接口中的java代码如下:
String getName(); //返回一个唯一的Realm名字
boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException; //根据Token获取认证信息
而往往我们都是自定义Realm,所以我们需要自定义一个CustomRealm.java文件并让它继承AuthorizingRealm抽象类,在sric包下创建一个realm包,在realm包中创建我们的自定义Realm,java代码如下:
public class CustomRealm extends AuthorizingRealm
{
//设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealm");
}
//用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//token是用户输入的
//第一步:丛token中取出身份信息
String userCode= (String) token.getPrincipal();
//第二步:根据用户输入的userCode丛数据库查询
//模拟从数据库查询到的密码
String password="111111";
//如果查不到返回null,
//如果查询到,返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo=new
SimpleAuthenticationInfo(userCode,password,this.getName());
return simpleAuthenticationInfo;
}
//用于授权,该功能在下篇文章中进行讲解
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
4.1配置自定义Realm
自定义Realm的代码实现后,我们就需要将这个自定义的Realm进行配置,在src包下创建一个shiro-reaml.ini文件(你也可以在先前我们创建的shiro-first.ini文件下进行修改),而该配置文件跟我们入门程序中的配置文件是不一样的,入门程序中的配置文件是模拟的数据库,而这里的配置文件是将Realm进行配置,数据库中的信息我们在自定义Reaml的代码中已进行模拟,内容如下:
[图片上传失败...(image-c541ea-1526380804010)]
内容中的解释也在注释中给出,内容中的属性值customRealm任意取,不一定要跟Reaml实现java代码中方法setName()的值一样,当然若是你自定义了多个Realm,那么配置文件中的内容如下:
#声明多个realm
customRealm1=realm.CustomRealme2
customRealme=realm.CustomRealme2
#指定securityManager的realms实现
securityManager.realms=$myRealm1,$myRealm2
完成自定义Realm的代码实现并配置后,我们便可进行该Reaml的测试了,测试代码如下,仍然模拟的用户登录认证:
//用户的登录和退出
@Test
public void testCustomRealm()
{
//创建securityManager工厂
Factory factory=new IniSecurityManagerFactory(
"classpath:shiro-realm.ini");
//创建SecurityManager
SecurityManager securityManager = factory.getInstance();
//将securityManager设置到当前的运行环境汇中
SecurityUtils.setSecurityManager(securityManager);
//丛SecurityUtils里边创建一个
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备token(令牌)
// 这里的账号和密码 将来是由用户输入进去
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
"111111");
try {
// 执行认证提交
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 是否认证通过
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("是否认证通过:" + isAuthenticated);
}
运行程序,控制台输出认证通过,说明用户输入的信息与数据库(配置文件)中的信息符合,所以用户认证成功。这样我们便通过Shiro完成了认证功能的实现。
扩展:看到这里相信你就会使用Shiro完成认证功能了,但是这里我们是通过用户输入的密码直接去查询数据库中的明文密码,而往往为了保护用户信息所以数据库中的密码都是经过了MD5的算法进行加密后才放入数据库的,所以实际开发中我们需要通过Shiro获取数据库中经过md5加密后的密码来和用户输入的密码进行比对。所以接下来我要讲Shiro中是如何将用户输入的信息与数据库中的加密信息进行对比从而实现的认证。
5.散列算法
实际开发中为了保护用户信息的安全,我们需要对用户在注册时输入的密码进行加密后再保存到数据库,当用户登录时我们也要将用户输入的密码进行加密后再与数据库中的密码进行比对。即需要对密码进行散列,常用的散列方法有md5、sha。
用md5算法对密码进行散列的问题:如果知道散列后的值可以通过穷举算法得到md5密码对应的明文。解决方法:建议对md5进行散列时加salt(盐),进行加密相当于对原始密码+盐进行散列。
5.1自定义realm支持散列算法
那么接下来我们就讲解上述reaml支持散列算法的测试,在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;
}
}
5.2在realm中配置凭证匹配器
然后对该自定义支持md5算法的Realm进行配置,在src包下创建shiro-realm-md5.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
5.3测试
然后便可进行我们支持md5算法的realm测试了,测试代码如下:
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的认证功能(包括普通认证和实际开发中的经过散列算法后的认证),当然只是一个简单的入门程序,将来我会讲解实际开发中(整合了Shiro的项目)如何通过Shiro查询数据库为大家深入讲解,下篇文章我将介绍Shiro的授权功能。见下篇文章Shiro之实现授权
2018.3.19更
欢迎加入我的Java交流1群:659957958。群里目前已有1800人,每天都非常活跃,但为了筛选掉那些不怀好意的朋友进来搞破坏,所以目前入群方式已改成了付费方式,你只需要支付9块钱,即可获取到群文件中的所有干货以及群里面各位前辈们的疑惑解答;为了鼓励良好风气的发展,让每个新人提出的问题都得到解决,所以我将得到的入群收费收入都以红包的形式发放到那些主动给新手们解决疑惑的朋友手中。在这里,我们除了谈技术,还谈生活、谈理想;在这里,我们为你的学习方向指明方向,为你以后的求职道路提供指路明灯;在这里,我们把所有好用的干货都与你分享。还在等什么,快加入我们吧!
2018.4.21更:如果群1已满或者无法加入,请加Java学习交流2群:305335626 。群2作为群1的附属群,除了日常的技术交流、资料分享、学习方向指明外,还会在每年互联网的秋春招时节在群内发布大量的互联网内推方式,话不多说,快上车吧!
6.联系
If you have some questions after you see this article,you can tell your doubts in the comments area or you can find some info by clicking these links.
Blog@codingXiaxw's blog
Weibo@codingXiaxw
Zhihu@codingXiaxw
Github@codingXiaxw