1. 集成Spring
导入Spring依赖包:spring-webmvc
在web.xml中配置监听、前端控制器
配置applicationContext.xml
注解驱动
扫描包
视图转换器配前后缀
静态资源访问
测试
导入shiro包
在web环境下配置shiro
在web.xml中配置shiroFilter
在applicationContext.xml配置shiro的核心组件:
DefaultWebSecurityManager
sessionMode可删掉。
这里需要加入EHcache的jar包及配置文件。
创建realm类,实现Realm接口,配置realm
anon表示可以被匿名访问,实际上是一个过滤芯器
authc必须认证(即登录)后才可能访问的页面
一般只配Login页和/**
1.1. 工作流程
shiro提供与web集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的URL,然后进行相应的控制。
ShiroFilter类似于如struts2/springmvc这种web框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini配置文件),然后判断URL是否需要登录/权限等工作。
ShiroFilter的工作原理:
1.2. 权限URL配置细节
1.2.1. 部分细节
[urls]部分的配置,其格式是:“url=拦截器[参数], 拦截器[参数]”;
如果当前请求的url匹配[urls]部分的某个url模式,将会执行其配置的拦截器。
anon(anonymous)拦截器表示匿名访问(即不需要登录即可访问)。
authc(authentication)拦截器表示需要身份认证通过后才能访问。
1.2.2. shiro中默认的过滤器
过滤器名称: Anon
过滤器类: org.apache.shiro.web.filter.authc.AnnonymousFilter
概述: 没有参数,表示可以匿名使用
例子:/admins/**=anon
1.2.3. url匹配模式
2. 认证
2.1. 认证实现流程
(1)获取当前的subject:调用SecurityUtils.getSubject()获得。
Subject subject = SecrityUtils.getSubject();
(2)测试当前的用户是否已经被认证,即是否已登录:调用subject.isAuthenticated()。
if(!subject.isAuthenticated())
(3)若没有被认证,则把用户名和密码封装为UsernamepasswordToken对象:
1)创建一个表单页面
2)把请求提交到Controller
3)获取用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken(“admin”, “123456”);
token.setRemenberMe(true);
(4)执行登录:调用subject.login(AuthenticationToken)方法。
subject.login(token);
(5)自定义realm的方法,从数据库中获取对应的记录,返回给shiro:
1)实际上需要继承org.apache.shiro.realm.AuthenticationRealm类
2)实现doGetAuthenticationInfo(AuthenticationToken)方法
(6)由shiro完成对密码的比对。
2.2. 认证方法中的实现步骤
(1)把AuthenticationToken转换为UsernamePasswordToken
(2)从UsernamePasswordToken中来获取username
(3)调用数据库查询方法,从数据库中查询username对应的用户记录
(4)若用户不存在,则可以抛出UnknowAccountException异常
(5)根据用户信息情况,决定是否需要抛出其他的AuthenticationException异常
(6)根据用户的情况,来构建AuthenticationInfo对象并返回。
2.3. 认证方法代码
@Override
properted Authentication doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException{
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
User dbUser = UserService.getUserByName(username);
//以下信息是从数据库中获取的:
//(1) principal:认证的实体信息,可以是username,也可以是数据库对应的实体类对象
Object principal = username;
//(2) credentials:数据库返回的密码
Object credentials = dbUser.getPassword();
//(3) realmName:当前realm对象的name
String realmName = getName();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName);
return info;
}
2.4. 密码的对比
问题:
密码的比对是通过AuthenticatingRealm的credentialsMatche属性来进行密码的比对,而且还是明文直接比较的。这样其实是不安全的。shiro应该保存对密码加密后的,而不是明文的密码。
源码:
打开UsernamePasswordToken.class
类,找到getPassword()
方法,可在return password;行代码上加debug测试。测试截图:
往下:
SimpleCredentialsMatcher.class
找到下面的代码:
这里获得登录输入用户名和密码的token对象,以及数据库获得的认证赁证对象,进行比较。
再往下:
在AuthenticationRealm.class源码中找到:CredentialsMatcher()方法,用来比较密码:
credentialsMatcher(赁证匹配器)
我们发现Credentials提供了多种加密方式:
shiro密码的对比:
是通过AuthenticatingRealm类中的credenttialsMatcher属性来进行密码的比对。而且shiro默认加密方式为null,即不加密。
2.5. 密码的MD5加密
(1)什么是密码的加密?
实际上就是在数据库表中保证密码不是能是明文,比方说不能是“123456”,而应该是“123456”加密之后的一个字符串,我们还要求这个加密算法是不可逆的,就是不能将这字符串反推回来密码是什么,如果能反推的话,这个加密就没什么意义了。著名的加密算法有MD5\SHA1。
那接下来我们不禁要问:
(2)如何实现MD5加密?
首先我们先看shiro源代码:
MD5credentialsMatcher类:
该类是一个过时的类,它继承了HashedCredentialsMatcher类
。
我们想修改加密方式,可以修改它的HashAlgorithmName属性
,该属性并不是MD5credentialsMatcher类的属性,是它的父类HashedCredentialsMatcher的属性,因此,我们在配置文件中找到Realm的配置,替换当前realm的credentialsMatcher属性。直接使用HashedCredentialsMatcher对象,并设置加密算法即可。
配置如下:
配置完赁证匹配器,也要将数据库里密码由明文修改为MD5加密的字符串。
(3)怎么加密的?
这里可以查看HashedCredentialsMatcher.class类中的代码:
参数说明:
hashAlgorithmName 加密算法: MD5
credentials 加密前的密码: 123456
salt 盐值: null 表示没有盐值
hashIterations 加密次数: 加密1次是常见的,加密多次安全性越高。
打开SimpleHash.class类,查看构造方法:
可以在MyRealm中写个main方法,算下123456经过1024次MD5加密后的字符串是什么:
将得到的结果替换MyRealm中的密码:
测试登录成功,这就是Shiro中MD5加密算法的配置。
但实际上这里面也是存在问题的,什么问题?都搞了MD5加密,1024次加密,还是算法不可逆的,还不满意?
2.6. 密码的MD5盐值加密
(1)为什么使用MD5盐值加密?
我们来看一个问题:如果两个人的密码都是123456,那加密后的字符串就是一样的。
这有什么问题呢?如果是这样,我们的密码还是存在一定的风险性,那我们进一步要求:
即便密码一样的,加密后的字符串也要不一样。
怎么做到? 加点盐,味道就不一样了。
(2)如何实现盐值加密?
注意两个问题:
1)加密之后的结果,应该把盐加进去
2)在认证后得到的SimpleAuthenticationInfo 要将盐带上。
如何做到?
1)在doGetAuthenticationInfo方法返回值创建SimpleAuthenticationInfo对象的时候,需要使用最复杂的带四个参的构造方法:SimleAuthenticationInfo(principal, credentials, credentialsSalt, realeName).
2)使用ByteSource.Util.bytes()
来计算盐值。
3)盐值需要唯一:一般使用随机字符串或用户名
4)使用new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations)来计算盐值加密后的密码值。
具体代码:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
……
//(1) principal:认证的实体信息,可以是username,也可以是数据表对应的用户的实体类对象
Object principal = username;
//(2) credentials: 密码
Object credents = null; //加密的字符串
//(3),盐值
//将用户名做为盐值进行加密
ByteSource credentialsSalt = ByteSource.Util.bytes(username); //Util是内部类
//(4). realeName: 当前realm对象的name,调用父类的getName()方法即可
String realmName = getName();
SimpleAuthentictionInfo info = null;
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realeName);
return info;
}
2.7. 多 Realm 验证
(1)为什么要多Realm证明?
实际开发中,我们在某些特定场合我们需要将数据放在不同的数据库里,比方说MySQL中有,Oracle也可,那MySQL中的加密算法有MD5,而Oracle中的加密算法是sha1。那这个时候我们进行用户认证,要同时访问这两个数据库,需要多个Realm。如果有多个realm的话,还涉及到认证策略的问题。
(2)多realm怎么配?
先查看下:ModularRealmAuthenticator.java类中的doAuthenticate()方法
:
具体配置步骤:
1)再创建一个Realm文件:SecondRealm.class
区分下SecondRealm与MyRealm,改一下代码以区分,如:修改SecondRealm中的加密方式为sha1。
测试获的加密后的数据:
2)在applicatonContext.java中配置两个Realm:MyRealm和SecondRealm:
3)新建一个Bean,用来加载多个realm。注意:这里我们一开始就将MyRealm给了SecurityManager,所以下面的ModularRealmAuthentiacator就不要配了。
4)将上面“authenticator”做为SecurityManager的属性
注意:两个realm是有先后顺序的。
2.8. 认证策略
(1)为什么要认证策略?
如果有多个realm,那怎样才算认证成功呢?这就是我们所谓的认证策略。
(2)认证策略有哪些?
认证策略要实现AuthenticationStrategy接口
,还有3个默认的实现。
(1)FirstSuccesssfulStrategy:只要有一个Realm验证成功即可,只返回第一个realm身份验证成功的认证信息,其他的忽略。
(2)AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份验证成功的认证信息。
(3)AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
ModularRealmAuthenticator.class默认是AtLeastOneSuccessfulStrategy策略。
debug测试可以看到aggregate的值是AtLeastOneSuccessfulStrategy
。
(3)如何修改认证策略?
在applicationContext.xml中修改shiro的ModularRealmAuthenticatior的配置,如下:
上面代码修改了认证策略,将默认策略改成了AllSuccessfulStrategy。
2.9. 把 realms 配置给 SecurityManager
前面的代码中,我们将2个realm配置给了ModularRealmAuthenticator,如下图:
现在我们要将这2个Realm配置给SecurityManager,如下图:
(1)我们为什么需要将他们改过来?
因为我们在做授权的时候,需要SecurityManager去读Realms。
(2)为什么这样改没问题?
在认证时候,调的是SecurityManager的Realms?还是authenticator的Realms?
在认证时,调用的都是authenticator中的方法,包括调用Realms,但我们这里并没有给authenticator注入Realms,它为什么仍然可以正常读取Realms?
因为在shiro初始化时,将Realms给了authenticator。