标签:timeout 依次 tor void nal rom ret ace pass
Authentication(身份认证)是Shiro权限控制的第一步,用来告诉系统你就是你。
在提交认证的时候,我们需要给系统提交两个信息:
Principals:是一个表示用户的唯一属性,可以是用户名,邮箱之类的。
Credentials:是证明用户身份的证书,可以是密码或者指纹之类的。
认证主要分为三步:
1、收集认证信息
2、提交认证信息
3、如果认证成功,则允许访问,否则就拒绝访问或者重试。
收集认证信息:
在入门的例子中,使用了一个UsernamePasswordToken来收集用户的用户名和密码,用来登陆。
这个类支持最简单的用户名和密码登陆。实现了org.apache.shiro.authc.AuthenticationToken接口。
AuthenticationToken接口是认证系统的基础,只有getCredentials(),getPrincipal()两个方法用来获取基本的认证信息。
HostAuthenticationToken和RememberMeAuthenticationToken是它的两个子接口。
HostAuthenticationToken只有一个getHost()方法用来获取请求的地址信息。
RememberMeAuthenticationToken只有一个isRememberMe()方法用来标记用户是否需要记住我。
然后还有两个子类UsernamePasswordToken和CasToken提供了基本的实现。其中CasToken已经被废弃了。
例:
//Example using most common scenario of username/password pair: UsernamePasswordToken token = new UsernamePasswordToken(username, password); //"Remember Me" built-in: token.setRememberMe(true);
提交认证信息:
收集好认证信息之后,保存为AuthenticationToken的一个实例,我们需要提交这个认证信息来进行认证。
进行认证前,我们首先需要获取当前用户(Subject)。然后调用login方法来进行登陆。
例:
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
处理结果:
如果登陆成功的话,则不会有什么异常,此时如果调用isAuthenticated()方法,则会返回true。
如果登陆失败的话,则被抛出异常,在SHiro中,提供了很多异常类,可以用来捕捉具体的异常。
例子:
try { currentUser.login(token); } catch ( UnknownAccountException uae ) { ... } catch ( IncorrectCredentialsException ice ) { ... } catch ( LockedAccountException lae ) { ... } catch ( ExcessiveAttemptsException eae ) { ... } ... catch your own ... } catch ( AuthenticationException ae ) { //unexpected error? } //No problems, continue on as expected...
可以看到,前面出现了Remembered和Authenticated。那这两种有什么区别呢?
需要注意的是,这是两种互斥的情况,当我们在登陆的时候,登陆成功之后,我们Authenticated返回的是true。
选择Remeber me的时候,我们下次可以不登陆直接访问,而我们下次登陆之后,Remembered返回的是true,但是Authenticated返回的是false。
在登陆之后,我们可以可以用logout()方法来注销。这时候用户的信息都会被清空,包括保存在Cookie中的RemeberMe信息还有session也会被无效。
上面我们就简单的讲述了一下在代码中实现登陆验证的流程。具体实例可以参考入门的例子。
那么,在Shrio内部是怎么样的一个认证流程呢?大概可以用下图来概括:
第一步:在代码中调用login方法,传递构造好的AuthenticationToken实例。
第二步:通过一个DelegatingSubject来分发认证请求给SecurityManager
第三步:SecurityManager容器会把接收到的token简单的转发给它内部的认证器实例通过调用认证器的authenticate(token)方法。
这通常是一个ModularRealmAuthenticator实例,用来支持多个Realm。
第四步:如果有定义多个Realm则ModularRealmAuthenticator会初始化一个支持多个Realm的认证器,通过配置的AuthenticationStrategy。
第五步:每一个配置的Realm都会被检测是否支持提交的AuthenticationToken,如果支持的话就会调用getAuthenticationInfo方法,从Realm中获取数据来跟提交的token进行验证。
验证器
在SecurityManager中,默认使用ModularRealmAuthenticator实例,它不仅仅支持单Realm还支持多个Realm。
如果实在单个Realm的情况下,ModualrRealmAuthenticator会直接调用这个Realm来尝试验证。
如果我们需要定义自己的验证器的话,可以通过在配置文件的[main]中如下定义:
[main] ... authenticator = com.foo.bar.CustomAuthenticator securityManager.authenticator = $authenticator
验证策略
如果是只有一个Realms的时候,则不需要验证策略。
如果是有两个以上的Realm的时候,ModularRealmAuthenticator依赖于AuthenticationStrategy组件来决定认证成功或者失败的条件,
比如是一个成功就成功还是要都成功才是成功之类的。。。。
在Shiro里面已经有三个认证策略的实现
AtLeastOneSuccessfulStrategy:只要有一个或一个以上的认证成功就表示认证成功
FirstSuccessfulStrategy;只有第一个认证成功的时候才表示认证成功
AllSuccessfulStrategy:只有所有的都认证成功的时候才表示认证成功。
在ModularRealmAuthenticator默认使用AtLeastOneSuccessfulStrategy的验证策略。当然也可以通过下面的方式定义其他的验证策略。
[main] ... authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy securityManager.authenticator.authenticationStrategy = $authcStrategy ...
Realm认证顺序
在定义了多个Realm的时候,如果有多个Realm支持当前的AuthenticationToken,则会依次调用Realm的getAuthenticationInfo方法。
调用的顺序分为两种:
隐式的:
如果在SecurityManager中定义了如下的几个Realm,则会按照他们定义的顺序去调用。
blahRealm = com.company.blah.Realm ... fooRealm = com.company.foo.Realm ... barRealm = com.company.another.Realm
此时的效果就等于下面的语句。
securityManager.realms = $blahRealm, $fooRealm, $barRealm、
显示的:
就如上面的一样,如果在securityManager.realms中配置的时候,改变realm的配置顺序,则会按照这个配置顺序来调用,这就是显示的配置。
blahRealm = com.company.blah.Realm ... fooRealm = com.company.foo.Realm ... barRealm = com.company.another.Realm securityManager.realms = $fooRealm, $barRealm, $blahRealm ...
域认证
前面说了下Realm的认证策略和认证顺序,那么,在Realm认证的时候,究竟发生什么事情了呢?
supporting AuthenticationTokens
在Realm被用来尝试认证登陆的时候首先会调用supports方法,来检测是否能解析这个AuthenticationToken,决定是否进行认证。
Handing supported AuthenticationTokens
如果这个realm支持提交的AuthenticationTokens的话,解析器会调用Realm的getAuthenticationINfo(token)方法,来尝试认证,
这个方法大概做了下面这些事情:
1、检测token中的唯一用户标识
2、基于唯一标识 去数据源中查找
3、确保提供的证书跟数据源中保存的一样
4、如果证书一样,就封装一个AuthenticationINfo实例返回
5、如果证书不匹配,则抛出一个AuthenticationException异常
下面,我们来看看Shrio提供的Realm的类结构:
org.apache.shiro.realm.Realm(I):base
org.apache.shiro.realm.CachingRealm(Abstract):提供缓存支持
org.apache.shiro.realm.AuthenticatingRealm(Abstract):提供认证支持
org.apache.shiro.realm.AuthorizingRealm(Abstract):提供授权支持
org.apache.shiro.realm.SimpleAccountRealm(C):简单的用户名密码支持
org.apache.shiro.realm.text.TextConfigurationRealm(C):支持Text文件的简单用户名密码支持
org.apache.shiro.realm.text.IniRealm(C):通过INI文件的简单用户名密码支持(默认使用这个)
org.apache.shiro.realm.text.PropertiesRealm(C):支持属性文件的简单用户名密码支持
org.apache.shiro.realm.jdbc.JdbcRealm(C):支持通过JDBC认证
在系统默认的情况下,系统使用的是IniRealm这个实现,可以从INI配置文件的[users]和[roles]两个节中读取用户信息和权限信息。
如果我们需要定义自己的Realm实现的话,一般都是继承AuthorizingRealm。
稍候,我们将简单介绍下如果从db中来实现认证。
证书匹配:
前面我们说过,Realm需要去匹配用户提交的证书跟数据源中存储的证书是否匹配,如果匹配的话就认为是认证成功。
在获得用户唯一标识后,系统回去Realm会去检索用户的证书,然后通过CredentialsMatcher来检测证书是否匹配。
Shrio中也提供了一些证书的匹配器可以直接拿来使用,使用下面的方法变更默认的匹配器:
[main] ... customMatcher = com.company.shiro.realm.CustomCredentialsMatcher myRealm = com.company.shiro.realm.MyRealm myRealm.credentialsMatcher = $customMatcher ...
Simple Equality Check
默认情况下所有提供的Realm实现都是使用SimpleCredentialsMatcher来检测证书是否匹配。
不过一般情况下都不需要变更,因为默认的就足够了。
Hashing Credentials
相比使用原始的数据存储起来,拿来匹配,我们更愿意将证书加密进行存储来进行匹配。那么如何使用呢?
在Shiro中提供了几个HashedCredentialsMatcher的子类,用来实现这个功能。包括MD5、SHA-256等等的加密方式。
我们可以通过下面的配置方式:
[main] ... credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher # base64 encoding, not hex in this example: credentialsMatcher.storedCredentialsHexEncoded = false credentialsMatcher.hashIterations = 1024 # This next property is only needed in Shiro 1.0\. Remove it in 1.1 and later: credentialsMatcher.hashSalted = true ... myRealm = com.company..... myRealm.credentialsMatcher = $credentialsMatcher ...
需要注意的是,这种情况下在Realm的实现中需要返回一个SaltedAuthenticationInfo,而不是普通的AuthenticationInfo,因为在用户提交认证的时候,需要获取相同的salt来进行加密,进行匹配认证。
那这个salt(盐)是用来干嘛的呢?
这是因为在进行MD5之类加密的时候,还是可以进行破解的,但是如果加入一个变量来进行加密之后,就基本上是无法破解了(不知道这个SALT的情况下)。
下面我们就做一个通过JDBC来认证的登陆认证程序。点此看源码
首先我们需要一个用户表,脚本如下:
CREATE DATABASE `db_shiro` USE `db_shiro`; DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `id` int(4) NOT NULL AUTO_INCREMENT, `username` varchar(20) DEFAULT NULL, `password` varchar(100) DEFAULT NULL, UNIQUE KEY `id` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `users`(`id`,`username`,`password`) values (1,‘fuwh‘,‘123456‘);
我们需要定义一个shiro_jdbc.ini文件如下
[main] dataSource=com.mchange.v2.c3p0.ComboPooledDataSource dataSource.driverClass=com.mysql.jdbc.Driver dataSource.jdbcUrl=jdbc:mysql://localhost:3306/db_shiro dataSource.user=root dataSource.password=rootadmin ;this is comment read from mysql jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm jdbcRealm.dataSource=$dataSource securityManager.realms=$jdbcRealm
编写登陆认证代码:
package com.fuwh.demo; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ShiroDemo02 { private static Logger log=LoggerFactory.getLogger(ShiroDemo02.class); public static void main(String[] args) { //取得SecurityManager工厂 Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro_jdbc.ini"); //取得SecurityManager实例 SecurityManager securityManager=factory.getInstance(); //将securityManager绑定到SecurityUtil SecurityUtils.setSecurityManager(securityManager); /* 至此为止,简单的从mysql数据库读取realm信息的shiro环境就配置好了 */ //取得当前用户 Subject currentUser=SecurityUtils.getSubject(); //使用shiro来进行登陆验证 if(!currentUser.isAuthenticated()) { UsernamePasswordToken token=new UsernamePasswordToken("fuwh","123456"); try { currentUser.login(token); log.info("登陆成功!!!"); } catch (Exception e) { e.printStackTrace(); log.error("认证失败..."); } } currentUser.logout(); } }
执行结果:
2017-08-26 15:16:03,357 [main] INFO [com.mchange.v2.log.MLog] - MLog clients using log4j logging. 2017-08-26 15:16:04,107 [main] INFO [com.mchange.v2.c3p0.C3P0Registry] - Initializing c3p0-0.9.1.2 [built 21-May-2007 15:04:56; debug? true; trace: 10] 2017-08-26 15:16:04,436 [main] INFO [org.apache.shiro.config.IniSecurityManagerFactory] - Realms have been explicitly set on the SecurityManager instance - auto-setting of realms will not occur. 2017-08-26 15:16:04,560 [main] INFO [com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource] - Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1hgetj59q83i1dm1es8ork|67f89fa3, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1hgetj59q83i1dm1es8ork|67f89fa3, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://localhost:3306/db_shiro, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 15, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 3, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ] 2017-08-26 15:16:05,040 [main] INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler... 2017-08-26 15:16:05,056 [main] INFO [com.fuwh.demo.ShiroDemo02] - 登陆成功!!!
此时,已经可以从表中去验证登陆了。那处理流程又是什么样的呢?
首先我们使用shiro_jdbc.ini来初始化了SecurityManager,在配置文件中,我们定义了连接池信息,还有jdbcRealm,同时在SecurityManager中指定了realm为定义的JjdbcRealm,这时候,其实shiro使用的SecurityManager是一个RealmSecurityManager的实例。而当我们登陆的时候,则会通过配置的jdbcRealm来从数据库中取得用户信息来进行认证。那是怎么取得呢?我们明明没有写sql什么的。
其实,看org.apache.shiro.realm.jdbc.JdbcRealm的源码可以看到,在这个类里面定义了很多的静态sql变量,点此查看,点此查看sql内容
其中比较重要的是AuthenticationQuery这个字段,默认情况下它的值是等于Default_Authentication_query。
Default_Authentication_Query="select password from users where username=?";
所以在默认情况下,它会从users这个表中通过username这个key来查找password。从而拿来跟我们提交的密码来进行匹配验证。
如果我们不想使用默认的数据库,默认的表名,默认的列名的话,也可以通过在配置文件中重写AuthenticationQuery的值来个性化sql文。
首先修改新建一个表members:
USE `db_shiro`; DROP TABLE IF EXISTS `members`; CREATE TABLE `members` ( `id` INT(4) NOT NULL AUTO_INCREMENT, `userName` VARCHAR(20) DEFAULT NULL, `pass` VARCHAR(100) DEFAULT NULL, UNIQUE KEY `id` (`id`) ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `members`(`id`,`userName`,`pass`) VALUES (1,‘fuwh‘,‘123‘);
然后修改shiro_jdbc_sql.ini配置文件:
[main] dataSource=com.mchange.v2.c3p0.ComboPooledDataSource dataSource.driverClass=com.mysql.jdbc.Driver dataSource.jdbcUrl=jdbc:mysql://localhost:3306/db_shiro dataSource.user=root dataSource.password=rootadmin ;this is comment read from mysql jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm jdbcRealm.dataSource=$dataSource jdbcRealm.authenticationQuery=select pass from members where userName=? securityManager.realms=$jdbcRealm
修改认证程序的配置文件:
package com.fuwh.demo; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ShiroDemoSql02 { private static Logger log=LoggerFactory.getLogger(ShiroDemoSql02.class); public static void main(String[] args) { //取得SecurityManager工厂 Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro_jdbc_sql.ini"); //取得SecurityManager实例 SecurityManager securityManager=factory.getInstance(); //将securityManager绑定到SecurityUtil SecurityUtils.setSecurityManager(securityManager); /* 至此为止,简单的从mysql数据库读取realm信息的shiro环境就配置好了 */ //取得当前用户 Subject currentUser=SecurityUtils.getSubject(); //使用shiro来进行登陆验证 if(!currentUser.isAuthenticated()) { UsernamePasswordToken token=new UsernamePasswordToken("fuwh","123"); try { currentUser.login(token); log.info("登陆成功!!!"); } catch (Exception e) { e.printStackTrace(); log.error("认证失败..."); } } currentUser.logout(); } }
后面我们会讲到角色认证也是同样的道理。
自定义Realm
上面我们使用的是Shiro提供的默认的Realm,下面我们自定义一个从数据库中读取信息的Realm,通过继承AuthorizingRealm。
package com.fuwh.realm; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; 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 com.fuwh.util.DbUtil; public class MyJdbcRealm extends AuthorizingRealm{ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // TODO Auto-generated method stub return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // TODO Auto-generated method stub Connection conn=DbUtil.getConnection(); String sql="select * from members2 where username=?"; try { PreparedStatement ps=conn.prepareStatement(sql); ps.setString(1, token.getPrincipal().toString()); ResultSet rs=ps.executeQuery(); while(rs.next()) { AuthenticationInfo info=new SimpleAuthenticationInfo(rs.getString("username"),rs.getString("password"),"salt"); return info; } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } }
修改配置文件
[main] myJdbcRealm=com.fuwh.realm.MyJdbcRealm securityManager.realms=$myJdbcRealm
编写登陆类:
package com.fuwh.demo; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ShiroDemoMySql02 { private static Logger log=LoggerFactory.getLogger(ShiroDemoMySql02.class); public static void main(String[] args) { //取得SecurityManager工厂 Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro_jdbc_my_sql.ini"); //取得SecurityManager实例 SecurityManager securityManager=factory.getInstance(); //将securityManager绑定到SecurityUtil SecurityUtils.setSecurityManager(securityManager); /* 至此为止,简单的从mysql数据库读取realm信息的shiro环境就配置好了 */ //取得当前用户 Subject currentUser=SecurityUtils.getSubject(); //使用shiro来进行登陆验证 if(!currentUser.isAuthenticated()) { UsernamePasswordToken token=new UsernamePasswordToken("fuwh","123"); try { currentUser.login(token); log.info("登陆成功!!!"); } catch (Exception e) { e.printStackTrace(); log.error("认证失败..."); } } currentUser.logout(); } }
源码地址:https://github.com/oukafu/shiro
标签:timeout 依次 tor void nal rom ret ace pass
原文地址:http://www.cnblogs.com/zerotomax/p/7420100.html