Apache Shiro第2部分–领域,数据库和PGP证书

这是致力于Apache Shiro的系列文章的第二部分。 我们从简单的不安全Web应用程序开始了上一部分 。 完成后,该应用程序具有基本的身份验证和授权。 用户可以登录和注销。 所有网页和按钮均已分配和实施访问权限。 授权和身份验证数据都已存储在静态配置文件中。

正如我们在最后一部分中承诺的,我们将用户帐户数据移至数据库。 此外,我们将为用户提供通过PGP证书进行身份验证的选项。 因此,我们的应用程序将具有多个备用登录选项:使用用户名/密码登录和使用证书登录。 最后,我们将强制启用备用登录选项。

换句话说,我们将展示如何创建自定义领域以及如何处理多领域方案。 我们将创建三个不同版本的SimpleShiroSecuredApplication:

每个版本都有测试类RunWaitTest。 该类使用在http:// localhost:9180 / simpleshirosecuredapplication / url上部署的应用程序启动Web服务器。

注意:自第一版以来,我们更新了上一部分。 最显着的变化是新部分 ,该部分显示了如何向登录页面添加错误消息。 感谢大家的反馈。

境界

首先,我们解释什么是领域以及如何创建它们。 如果您对理论不感兴趣,请继续下一章
领域负责身份验证和授权。 每当用户想要登录到应用程序时,都会收集身份验证信息并将其传递到领域。 Realm验证提供的数据并决定是否应允许用户登录,访问资源或拥有特定角色。 认证信息包括两个部分:

  • 主体–代表帐户唯一标识符,例如用户名,帐户ID,PGP证书,…
  • 凭证–证明用户身份,例如密码,PGP证书,指纹等。

Shiro提供了能够从活动目录ldapini文件属性文件数据库中读取授权数据的领域。 在Shiro.ini文件的主要部分中配置领域:

realmName=org.apache.shiro.realm.jdbc.JdbcRealm

认证方式

所有领域都实现Realm接口。 有两种重要的接口方法:supports和getAuthenticationInfo。 两者都在身份验证令牌对象中接收主体和凭据。
Supports方法根据提供的身份验证令牌确定领域是否能够对用户进行身份验证。 例如,如果我的领域检查用户名和密码,则仅使用X509证书拒绝身份验证令牌。 方法getAuthenticationInfo本身执行身份验证。 如果来自身份验证令牌的主体和凭据表示有效的登录信息,则该方法返回身份验证信息对象。 否则,领域返回null。

授权书

如果领域也希望进行授权,则必须实现Authorizer接口。 每个Authorizer方法都将主体作为参数,并检查角色或权限。 重要的是要理解,该领域会获得所有授权请求,即使它们来自另一个领域进行了身份验证的用户也是如此。 当然,领域可以决定忽略任何授权请求。
权限以字符串或权限对象的形式提供。 除非有充分的理由,否则请使用WildcardPermissionResolver将字符串转换为权限对象。

其他选择

Shiro框架在运行时调查其他接口的领域。 如果领域实现了它们,则可以使用:

这些功能可用于实现其他接口的任何领域。 无需其他配置。
自定义领域

创建新领域的最简单方法是扩展AuthenticatingRealmAuthorizingRealm类。 它们具有上一节中提到的所有有用接口的合理实现。 如果它们不能满足您的需求,则可以扩展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查询从数据库中读取用户名,密码,权限和角色。 不幸的是,该领域有两个缺点:

我们对其进行扩展,并创建新的类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 credentials
 SimpleAuthenticationInfo 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;

    @Override
    public X509Certificate getCertificate() {
      return certificate;
    }

    public void setCertificate(X509Certificate certificate) {
      this.certificate = certificate;
    }

}

Servlet过滤器

Shiro过滤器将用户数据转换为身份验证令牌。 到目前为止,我们使用了FormAuthenticationFilter 。 如果传入的请求来自登录的用户,则过滤器允许用户进入。如果用户正尝试对其进行身份验证,则过滤器将创建身份验证令牌并将其传递给框架。 否则,它将用户重定向到登录屏幕。
我们的过滤器CertificateOrFormAuthenticationFilter扩展了FormAuthenticationFilter

首先,我们必须说服它,不仅具有用户名和密码的请求,而且具有PGP证书的任何请求都可以视为尝试登录。 其次,我们必须修改过滤器以在身份验证令牌中发送PGP证书以及用户名和密码。
方法isLoginSubmission确定请求是否表示身份验证尝试:

@Override
    protected 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创建身份验证令牌:

@Override
    protected 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证书的身份验证令牌:

@Override
 public 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 tokens
    X509CertificateAuthenticationToken certificateToken = (X509CertificateAuthenticationToken) token;
    X509Certificate certificate = certificateToken.getCertificate();
    
    // verify certificate
    if (!certificateOK(certificate)) {
        return null;
    }
    
    // the issuer name and serial number uniquely identifies certificate
    BigInteger serialNumber = certificate.getSerialNumber();
    String issuerName = certificate.getIssuerDN().getName();
    
    // find account associated with certificate
    String username = findUsernameToCertificate(issuerName, serialNumber);
    if (username == null) {
        // return null as no account was found
        return null;
    }
    
    // sucesfull verification, return authentication info
    return 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值