摘要认证

basic-authentication项目的基础上,我们构建了一个digest-authentication项目。

build.gradle

修改 build.gradle 文件,让我们的digest-authentication项目成为一个新的项目。

修改内容也比较简单,修改项目名称及版本即可。

jar {
    baseName = 'digest-authentication'
    version = '1.0.0'
}

Spring Security 的摘要认证

DigestAuthenticationFilter 能够处理在 HTTP 头中显示的摘要身份验证凭据。摘要认证尝试解决基本认证的许多弱点,特别是通过确保凭证不会以明文方式通过网络发送。许多用户代理支持摘要身份验证,包括 Mozilla Firefox 和 Internet Explorer。管理 HTTP Digest 认证的标准由 RFC 2617 定义,RFC 2617 更新了较早版本的 RFC 2069 规定的摘要认证标准。大多数用户代理实现RFC 2617。Spring Security 的 DigestAuthenticationFilter 与RFC 2617规定中的“auth”保护质量(qop)兼容,同时与RFC 2069 向后兼。如果您需要使用未加密的 HTTP(即,没有 TLS/HTTPS)并希望最大化认证过程的安全性,摘要认证是非常有吸引力的选择。同时,摘要认证是WebDAV协议的强制性要求,如RFC 2518第17.1节所述:

你不应该在现代应用程序中使用摘要,因为它不被认为是安全的。最明显的问题是您必须以明文、加密或MD5格式存储密码。所有这些存储格式都被认为不安全。相反,您应该使用单向自适应密码散列(即bCrypt、PBKDF2、SCrypt等)。

摘要认证的中心是一个“随机数”,这是服务器生成的值。 Spring Security 的随机数采用以下格式:

base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime:   随机数到期的日期和时间,以毫秒为单位
key:              用于防止随机数标记被修改的私钥

DigestAuthenticationEntryPoint 具有指定用于生成现时标记的密钥的属性,以及用于确定到期时间(默认300,等于五分钟)的nonceValiditySeconds属性。虽然该随机数有效,但是通过连接各种字符串来计算摘要,这些字符串包括用户名、密码、随机数、被请求的URI、客户端生成的随机数(只是用户代理生成每个请求的随机值)、领域名称等,然后执行 MD5 散列。服务器代理和用户代理都执行此摘要计算,如果它们对包含的值(例如密码)不同意,则导致不同的散列码。在Spring Security实现中,如果服务器生成的随机数已过期(但摘要有效),DigestAuthenticationEntryPoint 将发送一个“stale=true”头。这告诉用户代理没有必要打扰用户(因为密码和用户名等是正确的),而只是尝试再次使用一个新的随机数。

DigestAuthenticationEntryPoint 的 nonceValiditySeconds 参数的适当值取决于您的应用程序。在对安全非常重视的应用程序应该注意,截获的认证头可以用于模拟主体,直到到达随机数中包含的 expirationTime。这是选择适当设置的关键原则,但对于非常安全的应用程序,在第一次实例中不能通过TLS / HTTPS运行是不常见的。

由于 Digest 认证的更复杂的实现,常常有用户代理问题。例如,Internet Explorer 无法在同一会话中的后续请求上显示“opaque”标记。 Spring Security 过滤器因此将所有状态信息封装到“nonce”令牌中。在我们的测试中,Spring Security 的实现可靠地使用 Mozilla Firefox 和 Internet Explorer,正确处理随机数超时等。

配置

要实现 HTTP 摘要认证,需要在过滤器链中定义 DigestAuthenticationFilter。 同时需要配置 UserDetailsService,因为 DigestAuthenticationFilter 必须能够直接访问用户的明文密码。如果在 DAO 中使用编码密码,Digest 身份验证将不会工作(如果DigestAuthenticationFilter.passwordAlreadyEncoded设置为true,则可以以HEX(MD5(username:realm:password))格式对密码进行编码。但是,其他密码编码将无法使用摘要身份验证。)。 DAO 协作者以及 UserCache 通常直接与 DaoAuthenticationProvider 共享。 authenticationEntryPoint 属性必须为 DigestAuthenticationEntryPoint,以便 DigestAuthenticationFilter 可以获取正确 的realmName 和摘要计算的键。

像 BasicAuthenticationFilter 一样,如果认证成功,那么认证请求令牌将被放入 SecurityContextHolder。如果认证事件成功,或者未尝试认证,因为HTTP头不包含摘要认证请求,则过滤器链将正常继续。过滤器链将被中断的唯一时机是如果认证失败并且调用了AuthenticationEntryPoint。

摘要认证的 RFC 提供了一系列附加功能,以进一步提高安全性。例如,可以在每个请求时更改随机数。尽管如此,Spring Security 实现旨在最小化实现的复杂性(以及将出现的无疑的用户代理不兼容性),并避免需要存储服务器端状态。如果您想更详细地了解这些功能,请受邀查看RFC 2617。据我们所知,Spring Security的实现确实符合该 RFC 的最低标准。

在配置类中,我们启用摘要认证过滤器 DigestAuthenticationFilter,并自定义 DigestAuthenticationEntryPoint:

private static final String DIGEST_KEY = "waylau.com";
private static final String DIGEST_REALM_NAME = "spring security tutorial";
private static final int DIGEST_NONCE_VALIDITY_SECONDS = 240; // 过期时间 4 分钟
@Autowired
private UserDetailsService userDetailsService;
/**
 * 自定义 DigestAuthenticationEntryPoint
 *
 * @return
 */
@Bean
public DigestAuthenticationEntryPoint getDigestAuthenticationEntryPoint() {
    DigestAuthenticationEntryPoint digestEntryPoint = new DigestAuthenticationEntryPoint();
    digestEntryPoint.setKey(DIGEST_KEY);
    digestEntryPoint.setRealmName(DIGEST_REALM_NAME);
    digestEntryPoint.setNonceValiditySeconds(DIGEST_NONCE_VALIDITY_SECONDS);
    return digestEntryPoint;
}
/**
 * 摘要认证过滤器
 *
 * @param digestAuthenticationEntryPoint
 * @return
 * @throws Exception
 */
@Bean
public DigestAuthenticationFilter digestAuthenticationFilter(
        DigestAuthenticationEntryPoint digestAuthenticationEntryPoint) throws Exception {
    DigestAuthenticationFilter digestAuthenticationFilter = new DigestAuthenticationFilter();
    digestAuthenticationFilter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint);
    digestAuthenticationFilter.setUserDetailsService(userDetailsService);
    return digestAuthenticationFilter;
}

最终配置如下:

......
http
    .authorizeRequests()
        .antMatchers("/css/**", "/js/**", "/fonts/**", "/index").permitAll()  // 都可以访问
        .antMatchers("/h2-console/**").permitAll()  // 都可以访问
        .antMatchers("/users/**").hasRole("USER")   // 需要相应的角色才能访问
        .antMatchers("/admins/**").hasRole("ADMIN")   // 需要相应的角色才能访问
        .and()
    .addFilter(digestAuthenticationFilter(getDigestAuthenticationEntryPoint()))  // 使用摘要认证过滤器
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 无状态
        .and()
    .exceptionHandling().accessDeniedPage("/403") // 处理异常,拒绝访问就重定向到 403 页面
    .authenticationEntryPoint(getDigestAuthenticationEntryPoint());   // 自定义 AuthenticationEntryPoint
......

运行

用户的状态信息都是保存在客户端(本例为浏览器),所以,即使后台服务器重启了,只要用户账号还在有效期内,就无需再次登录,即可再次访问服务。

当我们试图访问受限的资源时,浏览器会弹出输入框,要求我们输入账号密码:

zhaiyaorenzheng\_1.png

输入之后,就可以在访问相关页面了。我们可以在响应头里面,看到摘要认证信息:

zhaiyaorenzheng\_2.png

当访问其他没有权限的资源时,会有相应的提示:

zhaiyaorenzheng\_3.png

如何注销账号

HttpSecurity.logout() 是清除 HttpSession 里面存储的用户信息。既然,我们是无状态(无会话),那么自然就无需调用 logout()。

如果是客户端是在浏览器,则直接关闭浏览器即可注销账号。

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「方志朋」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

results matching ""

    No results matching ""