正如我们在最后一部分中承诺的,我们将用户帐户数据移至数据库。 此外,我们将为用户提供通过PGP证书进行身份验证的选项。 因此,我们的应用程序将具有多个备用登录选项:使用用户名/密码登录和使用证书登录。 最后,我们将强制启用备用登录选项。
换句话说,我们将展示如何创建自定义领域以及如何处理多领域方案。 我们将创建三个不同版本的SimpleShiroSecuredApplication:
- 版本,并将所有帐户信息移至数据库 ,
- 允许PGP证书作为替代身份验证机制的版本 ,
- 需要同时输入用户名/密码和PGP证书的版本 。
每个版本都有测试类RunWaitTest。 该类使用在http:// localhost:9180 / simpleshirosecuredapplication / url上部署的应用程序启动Web服务器。
注意:自第一版以来,我们更新了上一部分。 最显着的变化是新部分 ,该部分显示了如何向登录页面添加错误消息。 感谢大家的反馈。
境界
首先,我们解释什么是领域以及如何创建它们。 如果您对理论不感兴趣,请继续下一章 。
领域负责身份验证和授权。 每当用户想要登录到应用程序时,都会收集身份验证信息并将其传递到领域。 Realm验证提供的数据并决定是否应允许用户登录,访问资源或拥有特定角色。 认证信息包括两个部分:
- 主体–代表帐户唯一标识符,例如用户名,帐户ID,PGP证书,…
- 凭证–证明用户身份,例如密码,PGP证书,指纹等。
Shiro提供了能够从活动目录 , ldap , ini文件 , 属性文件和数据库中读取授权数据的领域。 在Shiro.ini文件的主要部分中配置领域:
realmName=org.apache.shiro.realm.jdbc.JdbcRealm
认证方式
所有领域都实现Realm接口。 有两种重要的接口方法:supports和getAuthenticationInfo。 两者都在身份验证令牌对象中接收主体和凭据。
Supports方法根据提供的身份验证令牌确定领域是否能够对用户进行身份验证。 例如,如果我的领域检查用户名和密码,则仅使用X509证书拒绝身份验证令牌。 方法getAuthenticationInfo本身执行身份验证。 如果来自身份验证令牌的主体和凭据表示有效的登录信息,则该方法返回身份验证信息对象。 否则,领域返回null。
授权书
如果领域也希望进行授权,则必须实现Authorizer接口。 每个Authorizer方法都将主体作为参数,并检查角色或权限。 重要的是要理解,该领域会获得所有授权请求,即使它们来自另一个领域进行了身份验证的用户也是如此。 当然,领域可以决定忽略任何授权请求。
权限以字符串或权限对象的形式提供。 除非有充分的理由,否则请使用WildcardPermissionResolver将字符串转换为权限对象。
其他选择
Shiro框架在运行时调查其他接口的领域。 如果领域实现了它们,则可以使用:
- 有关用户注销的信息,
- 有关系统启动的信息,
- 全局缓存
- 在配置文件中配置的名称 ,
- 在权限字符串和权限对象之间配置的转换器 。
这些功能可用于实现其他接口的任何领域。 无需其他配置。
自定义领域
创建新领域的最简单方法是扩展AuthenticatingRealm或AuthorizingRealm类。 它们具有上一节中提到的所有有用接口的合理实现。 如果它们不能满足您的需求,则可以扩展CachingRealm或从头开始创建新领域。
移至数据库
当前版本的SimpleShiroSecuredApplication使用默认领域进行身份验证和授权。 默认领域– IniRealm从配置文件读取用户帐户信息。 这样的存储仅对于最简单的应用是可接受的。 任何稍微复杂的事情都需要将凭据存储在更好的持久性存储中。
新要求:帐户凭据和访问权限存储在数据库中。 存储的密码经过哈希处理和加盐处理。 在本章中,我们将应用程序连接到数据库并创建表以存储所有用户帐户数据。 然后,我们将IniRealm替换为能够从数据库和salt密码读取的领域。
数据库基础架构
本节介绍示例应用程序基础结构。 它不包含有关Shiro的信息,因此您可以自由地跳过它 。
示例应用程序以嵌入式模式使用Apache Derby数据库。
我们使用Liquibase进行数据库部署和升级。 它是开源库,用于跟踪,管理和应用数据库更改。 数据库更改(新表,新列,外键)存储在数据库更改日志文件中。 启动后,Liquibase会调查数据库并应用所有新更改。 结果,数据库始终保持一致并且是最新的,而我们却没有付出任何努力。 将对Derby和Liquibase的依赖项添加到SimpleShiroSecuredApplication pom.xml中 :
<dependency><groupid>org.apache.derby</groupid><artifactid>derby</artifactid><version>10.7.1.1</version>
</dependency>
<dependency><groupid>org.liquibase</groupid><artifactid>liquibase-core</artifactid><version>2.0.1</version>
</dependency>
将jndi添加到码头:
<dependency><groupid>org.mortbay.jetty</groupid><artifactid>jetty-naming</artifactid><version>${jetty.version}</version><scope>test</scope>
</dependency>
<dependency><groupid>org.mortbay.jetty</groupid><artifactid>jetty-plus</artifactid><version>${jetty.version}</version><scope>test</scope>
</dependency>
使用数据库结构描述创建db.changelog.xml文件。 它创建用于存储用户,角色和权限的表。 它还用初始数据填充这些表。 我们使用random_salt_value_username作为盐,并使用以下方法创建哈希加盐的密码:
public static String simpleSaltedHash(String username, String password) {Sha256Hash sha256Hash = new Sha256Hash(password, (new SimpleByteSource('random_salt_value_' + username)).getBytes());String result = sha256Hash.toHex();System.out.println(username + ' simple salted hash: ' + result);return result;
}
在WEB-INF / jetty-web.xml文件中创建指向derby的数据源:
<configure class='org.mortbay.jetty.webapp.WebAppContext' id='SimpleShiroSecuredApplication'><new class='org.mortbay.jetty.plus.naming.Resource' id='SimpleShiroSecuredApplication'><arg>jdbc/SimpleShiroSecuredApplicationDB</arg><arg><new class='org.apache.derby.jdbc.EmbeddedDataSource'><set name='DatabaseName'>../SimpleShiroSecuredApplicationDatabase</set><set name='createDatabase'>create</set></new></arg></new>
</configure>
在web.xml文件中配置数据源和liquibase:
<resource-ref><description>Derby Connection</description><res-ref-name>jdbc/SimpleShiroSecuredApplicationDB</res-ref-name><res-type>javax.sql.DataSource</res-type><res-auth>Container</res-auth>
</resource-ref><context-param><param-name>liquibase.changelog</param-name><param-value>src/main/resources/db.changelog.xml</param-value>
</context-param><context-param><param-name>liquibase.datasource</param-name><param-value>jdbc/SimpleShiroSecuredApplicationDB</param-value>
</context-param><listener><listener-class>liquibase.integration.servlet.LiquibaseServletListener</listener-class>
</listener>
最终,在启用了jndi的情况下配置为读取jetty-web.xml的jetty在AbstractContainerTest类中。
创建新领域
Shiro提供的JDBCRealm能够执行身份验证和授权。 它使用可配置的SQL查询从数据库中读取用户名,密码,权限和角色。 不幸的是,该领域有两个缺点:
- 它无法从JNDI加载数据源( 未解决的问题 )。
- 它无法添加密码( 未解决的问题 )。
我们对其进行扩展,并创建新的类JNDIAndSaltAwareJdbcRealm 。 由于所有属性都可以在ini文件中进行配置,因此新属性jndiDataSourceName也将自动进行配置。 只要设置了新属性,该领域就会在JNDI中查找数据源:
protected String jndiDataSourceName;public String getJndiDataSourceName() {return jndiDataSourceName;
}public void setJndiDataSourceName(String jndiDataSourceName) {this.jndiDataSourceName = jndiDataSourceName;this.dataSource = getDataSourceFromJNDI(jndiDataSourceName);
}private DataSource getDataSourceFromJNDI(String jndiDataSourceName) {try {InitialContext ic = new InitialContext();return (DataSource) ic.lookup(jndiDataSourceName);} catch (NamingException e) {log.error('JNDI error while retrieving ' + jndiDataSourceName, e);throw new AuthorizationException(e);}
}
方法doGetAuthenticationInfo从数据库读取帐户身份验证信息,并将其转换为身份验证信息对象。 如果找不到帐户信息,则返回null。 父类AuthenticatingRealm将身份验证信息对象与原始用户提供的数据进行比较。
我们重写doGetAuthenticationInfo以从数据库中读取密码哈希和盐,并将它们存储在身份验证信息对象中:
doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {...// read password hash and salt from db PasswdSalt passwdSalt = getPasswordForUser(username);...// return salted credentialsSimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, passwdSalt.password, getName());info.setCredentialsSalt(new SimpleByteSource(passwdSalt.salt));return info;
}
这里的示例仅包含最重要的代码段。 完整的课程在Github上可用。
配置新领域
在Shiro.ini文件中配置领域和jndi名称:
[main]
# realm to be used
saltedJdbcRealm=org.meri.simpleshirosecuredapplication.realm.JNDIAndSaltAwareJdbcRealm
# any object property is automatically configurable in Shiro.ini file
saltedJdbcRealm.jndiDataSourceName=jdbc/SimpleShiroSecuredApplicationDB
# the realm should handle also authorization
saltedJdbcRealm.permissionsLookupEnabled=true
配置SQL查询:
# If not filled, subclasses of JdbcRealm assume 'select password from users where username = ?'
# first result column is password, second result column is salt
saltedJdbcRealm.authenticationQuery = select password, salt from sec_users where name = ?
# If not filled, subclasses of JdbcRealm assume 'select role_name from user_roles where username = ?'
saltedJdbcRealm.userRolesQuery = select role_name from sec_users_roles where user_name = ?
# If not filled, subclasses of JdbcRealm assume 'select permission from roles_permissions where role_name = ?'
saltedJdbcRealm.permissionsQuery = select permission from sec_roles_permissions where role_name = ?
JdbcRealm使用credetials匹配器的方式与IniRealm完全相同:
# password hashing specification
sha256Matcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
sha256Matcher.hashAlgorithmName=SHA-256
saltedJdbcRealm.credentialsMatcher = $sha256Matcher
注意:我们从配置文件中删除了[用户]和[角色]部分。 否则,Shiro将同时使用IniRealm和JdbcRealm。 这将创建超出本章范围的多领域方案。
从用户的角度来看,应用程序的工作方式与以前完全相同。 他可以登录到与以前相同的用户帐户。 但是,用户名,密码,盐,权限和角色现在存储在数据库中。
完整的源代码可在Github上的'authentication_stored_in_database'分支中找到。
备用登录–证书
某些系统允许用户登录使用多种身份验证方式。例如,用户可以提供用户名/密码,使用Google帐户,Facebook帐户或其他任何方式登录。 我们将添加与简单应用程序类似的内容。 我们将为用户提供使用PGP证书进行身份验证的选项。
新要求:应用程序支持PGP证书作为替代身份验证机制。 仅当用户不具有与应用程序帐户关联的有效证书时,才会显示登录屏幕。 如果用户具有有效的已知PGP证书,则会自动登录。 用户尝试登录应用程序时,必须提供身份验证数据。 这些数据由servlet过滤器捕获。 筛选器将数据转换为身份验证令牌,并将令牌传递给领域。 如果有任何领域希望对用户进行身份验证,它将身份验证令牌转换为身份验证信息对象。 如果该领域不希望这样做,则返回null。 开箱即用Shiro框架过滤器会忽略请求中的PGP证书。 可用的身份验证令牌无法保存它们,并且领域完全不知道PGP证书。 因此,我们必须创建:
- 身份验证令牌来移动证书,
- Servlet过滤器能够读取证书,
- 验证证书并将其与用户帐户匹配的领域。
我们的应用程序将有两个不同的领域。 一种使用名称标识帐户和密码来验证用户身份,另一种使用PGP证书两者都进行。
在开始编码之前,我们必须处理应用程序周围的PGP证书和基础结构。 如果您对设置的PGP证书不感兴趣,
基础设施
当用户访问Web应用程序时,他的Web浏览器可能会将PGP证书的副本发送到Web服务器。 证书由某个证书颁发机构或证书本身(自签名证书)签名。 Web服务器将其信任的证书列表保存在称为truststore的存储中。 如果信任库包含用户证书或对其进行签名的授权证书,则Web服务器将信任用户证书。 受信任的证书将传递到应用程序。
我们会:
- 为每个用户创建证书,
- 创建信任库,
- 配置Web服务器,
- 将证书与用户帐户关联。
在portecle中创建和管理证书。 SimpleShiroSecuredApplication的示例证书位于src \ test \ resources \ clients目录中。 所有商店和证书都具有通用密码“秘密”。
创建证书
为portecle中的每个用户创建自签名证书:
- 创建新的jks密钥库:在File-> New Keystore中,选择jks。
- 生成新证书:工具->生成密钥对。 将密码字段保留为空,证书将继承密钥库的密码。
- 导出公共证书:选择新证书->右键单击->导出,选择“头证书”。 这将创建.cer文件。
- 导出私钥和证书:选择新证书->右键单击->导出,选择私钥和证书。 这将创建.p12文件。
.cer文件仅包含公共证书,因此您可以将其提供给任何人。 另一方面,.p12文件包含用户私钥,因此必须保密。 仅将其分发给用户(例如,将其导入浏览器进行测试)。
创建信任库
创建新的信任库并将公共证书.cer文件导入到其中:
- 在文件->新密钥库中,选择jks。
- 工具->导入可信证书。
配置Web服务器
Web服务器必须请求证书,并根据信任库验证它们。 无法从Java请求证书。 每个Web服务器的配置都不同。 Github上的Look at AbstractContainerTest类中提供了Jetty配置。
将证书与帐户关联
每个证书由序列号和签署证书的证书颁发机构的名称唯一标识。 我们将它们与用户名和密码一起存储在数据库表中。 数据库更改位于db.changelog.xml文件中,有关新列,请参见changeset 3 ,有关数据初始化,请参见changeset 4 。
认证令牌
身份验证令牌表示身份验证尝试期间的用户数据和凭据。 它必须实现身份验证令牌接口,并保存我们希望在servlet过滤器和领域之间传递的所有数据。
由于我们希望同时使用用户名/密码和证书进行身份验证,因此我们扩展了UsernamePasswordToken类,并向其添加了证书属性。 新的身份验证令牌X509CertificateUsernamePasswordToken实现了新的接口X509CertificateAuthenticationToken ,两者在Github上都可用:
public class X509CertificateUsernamePasswordToken extends UsernamePasswordToken implements X509CertificateAuthenticationToken {private X509Certificate certificate;@Overridepublic X509Certificate getCertificate() {return certificate;}public void setCertificate(X509Certificate certificate) {this.certificate = certificate;}}
Servlet过滤器
Shiro过滤器将用户数据转换为身份验证令牌。 到目前为止,我们使用了FormAuthenticationFilter 。 如果传入的请求来自登录的用户,则过滤器允许用户进入。如果用户正尝试对其进行身份验证,则过滤器将创建身份验证令牌并将其传递给框架。 否则,它将用户重定向到登录屏幕。
我们的过滤器CertificateOrFormAuthenticationFilter扩展了FormAuthenticationFilter 。
首先,我们必须说服它,不仅具有用户名和密码的请求,而且具有PGP证书的任何请求都可以视为尝试登录。 其次,我们必须修改过滤器以在身份验证令牌中发送PGP证书以及用户名和密码。
方法isLoginSubmission确定请求是否表示身份验证尝试:
@Overrideprotected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {return super.isLoginSubmission(request, response) || isCertificateLogInAttempt(request, response);}private boolean isCertificateLogInAttempt(ServletRequest request, ServletResponse response) {return hasCertificate(request) && !getSubject(request, response).isAuthenticated();}private boolean hasCertificate(ServletRequest request) {return null != getCertificate(request);}private X509Certificate getCertificate(ServletRequest request) {X509Certificate[] attribute = (X509Certificate[]) request.getAttribute('javax.servlet.request.X509Certificate');return attribute==null? null : attribute[0];}
方法createToken创建身份验证令牌:
@Overrideprotected AuthenticationToken createToken(String username, String password, ServletRequest request, ServletResponse response) {boolean rememberMe = isRememberMe(request);String host = getHost(request);X509Certificate certificate = getCertificate(request);return createToken(username, password, rememberMe, host, certificate);}protected AuthenticationToken createToken(String username, String password, boolean rememberMe, String host, X509Certificate certificate) {return new X509CertificateUsernamePasswordToken(username, password, rememberMe, host, certificate);}
在配置文件中用CertificateOrFormAuthenticationFilter过滤器替换FormAuthenticationFilter:
[main]
# filter configuration
certificateFilter = org.meri.simpleshirosecuredapplication.servlet.CertificateOrFormAuthenticationFilter
# specify login page
certificateFilter.loginUrl = /simpleshirosecuredapplication/account/login.jsp
# name of request parameter with username; if not present filter assumes 'username'
certificateFilter.usernameParam = user
# name of request parameter with password; if not present filter assumes 'password'
certificateFilter.passwordParam = pass
# does the user wish to be remembered?; if not present filter assumes 'rememberMe'
certificateFilter.rememberMeParam = remember
# redirect after successful login
certificateFilter.successUrl = /simpleshirosecuredapplication/account/personalaccountpage.jsp
将所有URL重定向到新的过滤器:
[urls]
# force ssl for login page
/simpleshirosecuredapplication/account/login.jsp=ssl[8443], certificateFilter# only users with some roles are allowed to use role-specific pages
/simpleshirosecuredapplication/repairmen/**=certificateFilter, roles[repairman]
/simpleshirosecuredapplication/sales/**=certificateFilter, roles[sales]
/simpleshirosecuredapplication/scientists/**=certificateFilter, roles[scientist]
/simpleshirosecuredapplication/adminarea/**=certificateFilter, roles[Administrator]# enable certificateFilter filter for all application pages
/simpleshirosecuredapplication/**=certificateFilter
自定义领域
我们的新领域将仅负责身份验证。 授权(访问权限)将由JNDIAndSaltAwareJdbcRealm处理。 只要PGP证书将用户身份验证为与用户名/密码相同的帐户,这种配置就起作用。 否则,新领域返回的主要主体必须与JNDIAndSaltAwareJdbcRealm返回的主要主体相同。
我们的领域不需要缓存,也不需要可选接口提供的任何其他服务。 因此,我们只需要实现两个接口:Realm和Nameable。 X509CertificateRealm仅支持带有PGP证书的身份验证令牌:
@Overridepublic boolean supports(AuthenticationToken token) {if (token!=null)return token instanceof X509CertificateAuthenticationToken;return false;}
方法getAuthentcationInfo负责身份验证。 如果提供的证书有效并且与用户帐户关联,则领域将创建认证信息对象。 请记住,主要主体必须与JNDIAndSaltAwareJdbcRealm返回的主体相同:
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {// the cast is legal, since Shiro will let in only X509CertificateAuthenticationToken tokensX509CertificateAuthenticationToken certificateToken = (X509CertificateAuthenticationToken) token;X509Certificate certificate = certificateToken.getCertificate();// verify certificateif (!certificateOK(certificate)) {return null;}// the issuer name and serial number uniquely identifies certificateBigInteger serialNumber = certificate.getSerialNumber();String issuerName = certificate.getIssuerDN().getName();// find account associated with certificateString username = findUsernameToCertificate(issuerName, serialNumber);if (username == null) {// return null as no account was foundreturn null;}// sucesfull verification, return authentication inforeturn new SimpleAuthenticationInfo(username, certificate, getName());
}
请注意,领域具有两个新属性:trustStore和trustStorePassword。 两者都是PGP证书验证所必需的。 与其他任何属性一样,两者都可以在配置文件中进行配置。
将新的领域添加到Shiro.ini文件中:
[main]
certificateRealm = org.meri.simpleshirosecuredapplication.realm.X509CertificateRealm
certificateRealm.trustStore=src/main/resources/truststore
certificateRealm.trustStorePassword=secret
现在可以使用PGP证书登录到应用程序。 如果证书不可用,则用户名和密码也可以使用。
应用程序源代码在Github上的'certificates_as_alternative_log_in_method'分支中可用。
多个领域
如果配置文件包含多个领域,则将全部使用。 在这种情况下,Shiro尝试使用所有已配置的领域对用户进行身份验证,并将身份验证结果合并在一起。 负责合并的对象称为身份验证策略。 框架提供了三种身份验证策略:
- 所有成功的策略
- 至少一项成功的策略 ,
- 第一个成功的策略 。
默认情况下,使用“至少一个成功的策略”,这非常适合我们的目的。 同样,可以创建自定义身份验证策略。 例如,我们可能要求用户同时提供PGP证书和用户名/密码凭据才能登录。
新要求:用户必须同时提供PGP证书和用户名/密码凭据才能登录。
换句话说,我们需要的策略是:
- 如果某些领域不支持令牌,则失败,
- 如果某些领域无法验证用户身份,则失败,
- 如果两个领域认证不同的主体,则失败。
认证策略是一个实现认证策略接口的对象。 在身份验证尝试之后和之前调用接口方法。 我们从“所有成功策略”(可用的最接近策略)创建“ 主要主体相同的身份验证策略 ”。 在每次领域身份验证尝试之后,我们将比较主体:
@Override
public AuthenticationInfo afterAttempt(...) {validatePrimaryPrincipals(info, aggregate, realm);return super.afterAttempt(realm, token, info, aggregate, t);
}private void validatePrimaryPrincipals(...) {...Object aggregPrincipal = aggregPrincipals.getPrimaryPrincipal();Object infoPrincipal = infoPrincipals.getPrimaryPrincipal();if (!aggregPrincipal.equals(infoPrincipal)) {String message = 'All realms are required to return the same primary principal. Offending realm: ' + realm.getName();log.debug(message);throw new AuthenticationException(message);}
}
身份验证策略在Shiro.ini文件中配置:
# multi-realms strategy
authenticationStrategy=org.meri.simpleshirosecuredapplication.authc.
PrimaryPrincipalSameAuthenticationStrategy
securityManager.authenticator.authenticationStrategy = $authenticationStrategy
最后,我们必须改回CertificateOrFormAuthenticationFilter的isLoginSubmission方法。 现在仅将具有用户名和密码的请求视为登录尝试。 证书不足:
@Override
protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {return super.isLoginSubmission(request, response);
}
如果立即运行该应用程序,则必须同时使用证书和用户名/密码登录方法。
这个版本可以在Github的'certificates_as_mandatory_log_in_method'分支中找到。
结束
此部分专用于Shiro领域。 我们创建了三个不同的应用程序版本,所有版本都可以在Github上获得。 它们涵盖了基本且可能是最重要的领域功能。
如果您需要了解更多信息,请从此处链接的类开始并阅读其javadocs。 他们写得很好,内容广泛。
参考: Apache Shiro第2部分–我们的JCG合作伙伴 Maria Jurcovicova在This is Stuff博客上获得的领域,数据库和PGP证书 。
翻译自: https://www.javacodegeeks.com/2012/05/apache-shiro-part-2-realms-database-and.html