一、shiro是什么
shiro是一个Apache Shiro是java的一个安全框架,具有轻量,操作简单,学习成本低等特点,在今天的系统中,认证和权限都是不可或缺的关键部分,所以掌握一个权限框架是尤为重要的一件事。
在shiro的官网中是这样介绍shiro的:是一个功能强大且易于使用的Java安全框架,它执行身份验证,授权,加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的Web和企业应用程序。
下面是shiro的体系架构图:
shiro整体架构如上图所示
subject是主体,他可以是访问系统的一段程序,可以是一个人(用户);
securityManager是处理安全认证的容器;
Authenticator:是shiro中的认证器;
Authorizer:是shiro中的权限认证器;
sessionManager:shiro将一次访问作为一次会话存入session中进行管理;
Pluggable Realms:是我们在做认证和权限管理时与数据库进行的交互,可以是持久型数据库或nosql;
最右边还有一个Cryptography:是shiro为我们封装的一个对明文密码进行加密的;
CacheManager:是shiro中的缓存,他会将用户存在缓存中,如果用户在访问资源服务时,可以从缓存中查找用户信息,减轻对数据库的访问压力
二、shiro的认证实现
1..ini文件获取用户信息形式
shiro用户认证的简单实现,先从最基础的入门开始
shiro提供了一种为.ini的文件格式可以用来存储用户的信息,文件格式如下:
[users]
zhangsan=123
lisi=1234
注意,以上文件格式是固定的不可更改的,都为键值对存储
下面上代码:
1.首先是目录结构:
可以看到在resources包中放置了一个.ini文件里面存储用户信息:这是.ini文件中的内容
[users]
lisi=123
zhangsan=1234
wangwu=123456
下面是认证的代码demo:
package com.csdn;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
public class TestAuthenticator {
public static void main(String[] args) {
//1.创建安全管理器SecurityManager容器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.给安全管理容器设置一个域对象(因为我们是通过读取配置文件方式获取用户信息,所以用IniRealm并且通过指示文件位置方式告诉安全管理容器)
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3.接着在shiro中为我们提供了一个安全管理工具类来操作这些对象,但是要讲容器set给这个工具类
SecurityUtils.setSecurityManager(securityManager);
//4.然后我们就可以通过这个安全管理工具类找到主体对象subject
Subject subject = SecurityUtils.getSubject();
//5.我们通过new一个UsernamePassword对象来做用户登录输入用户名密码,创建token,传入两个参数:用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken("lisi","123");
//6.调用主体subject的login方法登录,并传入token
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
/**
* 对subject的login方法进行异常捕捉
* UnknownAccountException:用户名不存在时抛出的异常
* IncorrectCredentialsException:密码错误时抛出的异常
*/
}
}
可以看到我们在token中输入的用户名和密码是正确的,我们可以看一下效果,看控制台打印结果:
然后我们修改一下token中传入的用户名,写一个.ini文件中没有的用户名,再看一下效果:
抛出了UnKnownAccountExceotion也就是用户名不存在异常
最后再试一下用户名输入正确,但是密码错误的情况:
三种情况都符合我们的预期。
2.自定义Realm形式读取用户信息
下面为大家介绍一下通过自定义Realm的形式获取用户信息,因为我们如果去做一个系统,不可能将所有用户都存储在一个.ini文件中,以后一定是通过数据库来获取用户的信息,所以我们在以后一定是会通过自定义Realm的形式来获取用户信息完成认证,但在接下来的Demo中我先直接将用户信息写死在代码中,在后面的与springboot集成的文章中会为大家介绍shiro和springboot整合,以及从数据库中获取用户信息。
下面为大家上自定义Realm的代码,如果我们使用了自定义Realm,.ini文件就不需要了,认证的代码也要稍作改动
再来看一下目录结构:
realm包中放的是我们自己自定义写的MyRealm,TestMyRealmAuthenticator是新写的认证类,下面看自定义Realm的代码实现
package com.csdn.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;
/**
* 自定义Realm
*/
public class MyRealm extends AuthorizingRealm {//自定义Realm首先我们要继承一个AuthorizingRealm 并且重写他的认证和授权方法
/**
* 授权
* @param principalCollection 授权的内容会在稍后的文章中介绍,首先大家先看下面的方法
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException 下面的这个方法就是重写了认证的方法
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//首先用传入得参数AuthenticationToken调用.getPrincipal()方法拿到用户名,因为我们知道用户名是String字符串,所以将他强转为String
String principal = (String) authenticationToken.getPrincipal();//登录的用户名
//下面做一个判断,shiro底层就是用的equals来做的用户名匹配判断,所以我们这里也用equals来做判断
if(principal.equals("lisi")){//如果用户名能匹配上,进入方法new一个SimpleAuthenticationInfo并用他的构造方法来存放用户信息
//传入SimpleAuthenticationInfo的三个参数分别为用户的用户名,密码,以及当前Realm的名字,后期我们一定都是通过数据库查询到的,目前先写死
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
然后是新写的认证类将Realm域对象换成我们自己写的MyRealm:
package com.csdn;
import com.csdn.realm.MyRealm;
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.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
public class TestMyRealmAuthenticator {
public static void main(String[] args) {
//1.创建安全管理器SecurityManager容器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.给安全管理容器设置一个域对象(这次往容器中放的是我们自己写的MyRealm)
securityManager.setRealm(new MyRealm());
//3.接着在shiro中为我们提供了一个安全管理工具类来操作这些对象,但是要讲容器set给这个工具类
SecurityUtils.setSecurityManager(securityManager);
//4.然后我们就可以通过这个安全管理工具类找到主体对象subject
Subject subject = SecurityUtils.getSubject();
//5.我们通过new一个UsernamePassword对象来做用户登录输入用户名密码,创建token,传入两个参数:用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken("lisi","123");
//6.调用主体subject的login方法登录,并传入token
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
/**
* 对subject的login方法进行异常捕捉
* UnknownAccountException:用户名不存在时抛出的异常
* IncorrectCredentialsException:密码错误时抛出的异常
*/
}
}
效果就不赘述了,仍然和第一个demo一样。
3.shiro的密码处理
前两个demo都是将密码进行的明文存储,这在一个系统中是非常不合理的,shiro在内部也为我们整合了密码的处理,这里我们用的是md5加密方式
md5加密的特点就是密码不可逆,并且同样的字符串加密都是一样的结果。
密码加密的三个阶段:
阶段一:将密码进行一次md5加密
阶段二:将密码进行md5加密后加盐,
(盐:为什么要加盐,因为虽然我们对明文密码进行了一次md5加密,但是因为所有的字符串进行MD5加密后所得的值都是一样的,为了避免一些用户设置的密码简单,被恶意攻击者对简单的密码进行穷举法进行密码破解我们还要在加密后的md5密码中进行加盐处理[默认是加在加密后的md5的内容前面,保证用户数据的安全性])
阶段三:将密码进行MD5加密后加盐之后再进行散列,并且可以指定散列次数,保证密码更加安全
下面写一段三个阶段的md5加密的代码并且保存下来一会儿用:
package com.csdn.md5utils;
import org.apache.shiro.crypto.hash.Md5Hash;
public class Md5Utils {
public static void main(String[] args) {
//先新建一个md5Hash对象,并利用他的构造方法传入要加密的密码
Md5Hash md5Hash = new Md5Hash("123");
//这个结果是只对字符串进行md5加密的结果
System.out.println(md5Hash.toHex());//202cb962ac59075b964b07152d234b70
//接着我们还可以利用构造方法加入盐值,这里我们这个盐值就用密码进行md5加密后的值作为盐
Md5Hash md5Hash1 = new Md5Hash("123",md5Hash.toHex());
//这个结果是对字符串进行md5加密后并且将mad5值作为盐值再加入的结果
System.out.println(md5Hash1.toHex());//babc156ac796828d0d08625f86f6dc55
Md5Hash md5Hash2 = new Md5Hash("123",md5Hash.toHex(),1024);
//这个结果是对字符串进行MD5加密后并且将md5值作为盐值再加入的结果,并且再散列1024次
System.out.println(md5Hash2.toHex());//a98c57bc4231c5b9a0efb48787575e8a
}
}
上面代码就是对同一个字符串进行不同处理后的结果我都在输出语句后面跟上了结果一会儿用
下面就对处理后的密码的认证类做一下改造:
先再来看一下目录结构:
然后是处理密码后认证类的代码:
package com.csdn.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;
/**
* 处理密码后的MyRealm
*/
public class MyMd5Realm extends AuthorizingRealm {
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
if (principal.equals("lisi")){
//因为这里我们是假设的数据库中查询出的数据所以要将输入密码的位置将处理后的md5密码放入第二个参数的位置
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"202cb962ac59075b964b07152d234b70",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
MD5处理的认证类:
package com.csdn;
import com.csdn.realm.MyMd5Realm;
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.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
public class TestMyRealmMd5Authenticator {
public static void main(String[] args) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
MyMd5Realm myMd5Realm = new MyMd5Realm();
//因为密码进行了md5加密处理,所以要new一个HashedCredentialsMatcher对象出来,告诉shiro我是用什么方式进行加密的
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密的方式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//最后将这个处理密码的对象再set给Realm
myMd5Realm.setCredentialsMatcher(hashedCredentialsMatcher);
securityManager.setRealm(myMd5Realm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("lisi","123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}
md5密码处理完了之后,盐值也是一样的代码,因为盐值的处理是自动完成的,只是在自定义Realm中有不一样的地方,下面为大家贴出来加盐的自定义Realm的代码:
package com.csdn.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.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
/**
* 处理密码后的MyRealm
*/
public class MyMd5Realm extends AuthorizingRealm {
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
//因为之前的代码中我有介绍,我们会将进行md5加密后的字符串作为盐放入,所以先取出处理后的md5结果
Md5Hash md5Hash = new Md5Hash("123");
String s = md5Hash.toHex();
/**
* 作为盐直接放入SimpleAuthenticationInfo对象中的第三个参数中即可,但是放的时候有一个比较特殊的地方,就是
* 必须用ByteSource.Util.bytes(s)方法对字符串进行处理之后才可以作为盐值放入
*/
if (principal.equals("lisi")){
//因为这里我们是假设的数据库中查询出的数据所以要将输入密码的位置将处理后的md5密码放入第二个参数的位置
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"babc156ac796828d0d08625f86f6dc55", ByteSource.Util.bytes(s),this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
以上两种单md5加密处理以及加盐处理就都讲解完毕了,最后一种加散列的再为大家贴代码
散列的话也很简单,只需要在认证测试类中加入一个散列即可,在自定义Realm中不需要操作,下面为大家贴出代码:
package com.csdn;
import com.csdn.realm.MyMd5Realm;
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.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
public class TestMyRealmMd5Authenticator {
public static void main(String[] args) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
MyMd5Realm myMd5Realm = new MyMd5Realm();
//因为密码进行了md5加密处理,所以要new一个HashedCredentialsMatcher对象出来,告诉shiro我是用什么方式进行加密的
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密的方式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//这里只需要再调用hashedCredentialsMatcher的.setHashIterations方法传入散列的次数即可
hashedCredentialsMatcher.setHashIterations(1024);
//最后将这个处理密码的对象再set给Realm
myMd5Realm.setCredentialsMatcher(hashedCredentialsMatcher);
securityManager.setRealm(myMd5Realm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("lisi","123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}
好了,到此为止我们的shiro认证的具体流程和demo实现就写完了,其实很多代码都是很冗余的,不需要重复粘贴出来的,但我就是希望第一次接触shiro的童鞋能够更细致的看下来这个认证的流程,以及对不同情况的各种操作,能够细细的理解shiro的各种实现以及大致的功能,能为我们做什么事。
有兴趣的话可以通篇看下来之后理解一下各个代码段的不同之处,理解shiro是怎么样一步一步的为了我们系统安全而做出的各种设计,好了shiro认证的入门demo部分就写到这里了,剩余的关于shiro的更多内容会在后续为大家一一写出,谢谢大家。
shiro授权部分传送门:https://blog.csdn.net/JavaJieRui/article/details/108495686