HTTP Basic authentication

简介

HTTP Basic authentication 中文译作 HTTP 基本认证,在这种认证方式中,将用户的登录用户名/密码经过 Base64 编码之后,放在请求头的 Authorization 字段中,从而完成用户身份的认证。

这是一种在 RFC7235(https://tools.ietf.org/html/rfc7235)规范中定义的认证方式,当客户端发起一个请求之后,服务端可以针对该请求返回一个质询信息,然后客户端再提供用户的凭证信息。具体的质询与应答流程如图10-1所示。

image 2024 04 14 18 47 16 830
Figure 1. 图10-1 HTTP基本认证质询应答流程

从图10-1中我们可以看到,HTTP 基本认证的流程是下面这样的。

首先客户端(浏览器)发起请求,类似下面这样:

GET /hello HTTP/1.1
Host: localhost:8080

服务端收到请求后,发现用户还没有认证,于是给出如下响应:

HTTP/1.1 401
www-Authenticate: Basic realm="Realm"

状态码 401 表示用户未认证,WWW-Authenticate 响应头则定义了使用何种验证方式去完成身份认证。最简单、最常见的就是我们使用的 HTTP 基本认证(Basic),除了这种认证方式之外,还有 Bearer(OAuth2.0 认证)、Digest(HTTP 摘要认证)等取值。

客户端收到服务端的响应之后,将用户名/密码使用 Base64 编码之后,放在请求头中,再次发起请求:

HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 16

这就是整个 HTTP 基本认证流程。

可以看到,这种认证方式实际上非常简单,基本上所有的浏览器者都支持这种认证方式。但是我们在实际应用中,似乎很少见到这种认证方式,有的读者能在一些老路由器中见过这种认证方式:另外,在一些非公开访问的 web 应用中,可能也会见到这种认证方式。为什么很少见到这种认证方式的应用场景呢?主要还是安全问题。

HTTP 基本认证没有对传输的凭证信息进行加密,仅仅只是进行了 Base64 编码,这就造成了很大的安全隐患,所以如果用到了 HTTP 基本认证,一般都是结合HTTPS 一起使用:同时,一旦使用 HTTP 基本认证成功后,除非用户关闭浏览器或者清空浏览器缓存,否则没有办法退出登录。

具体用法

Spring Security 中开启 HTTP 基本认证非常容易,配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .csrf().disable();
    }
}

通过 httpBasic() 方法即可开启 HTTP 基本认证。配置完成后,启动项目,此时如果需要访问一个受保护的资源,浏览器就会自动弹出认证框,如图10-2所示。输入用户名/密码完成认证之后,就可以访问受保护的资源了。

image 2024 04 14 19 04 09 444
Figure 2. 图10-2 HTTP基本认证登录框

源码分析

代码实现很简单,接下来我们来看一下 Spring Security 中是如何实现 HTTP 基本认证的。实现整体上分为两部分:

  1. 对未认证的请求发出质询。

  2. 解析携带了认证信息的请求

我们分别从这两部分来分析。

质询

httpBasic() 方法开启了 HTTP 基本认证的配置,具体的配置通过 HttpBasicConfigurer 类来完成。在 HttpBasicConfigurer 配置类的 init 方法中调用了 registerDefaultEntryPoint 方法,该方法完成了失败请求失败处理类 AuthenticationEntryPoint 的配置,代码如下:

private void registerDefaultEntryPoint(B http, RequestMatcher preferredMatcher) {
    ExceptionHandlingConfigurer<B> exceptionHandling = http
            .getConfigurer(ExceptionHandlingConfigurer.class);
    if (exceptionHandling == null) {
        return;
    }
    exceptionHandling.defaultAuthenticationEntryPointFor(
            postProcess(this.authenticationEntryPoint), preferredMatcher);
}

可以看到,这里调用了 exceptionHandling 对象的方法进行配置,该对象的最终目的是配置异常过滤器 ExceptionTranslationFilter,关于该过滤器中的执行逻辑在本书第 12 章将会详细介绍,这里先不赘述。

这里配置到 exceptionHandling 中的 authenticationEntryPoint 是一个代理对象,该代理对象是在 HttpBasicConfigurer 构造方法中创建的,具体代理的就是 BasicAuthenticationEntryPoint。简而言之,如果一个请求没有携带认证信息,最终将被 BasicAuthenticationEntryPoint 的实例处理,我们来看一下该类的实现(部分):

public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint,
		InitializingBean {

	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException {
		response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
		response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
	}

}

可以看到,这个类的处理逻辑还是非常简单的,响应头中添加 WWW-Authenticate 字段,然后发送错误响应,响应码为 401。

这就是发出质询的代码。总结一下,就是一个未经认证的请求,在经过 Spring Security 过滤器链时会抛出异常,该异常会在 ExceptionTranslationFilter 过滤器中调用 BasicAuthenticationEntryPoint#commence 方法进行处理。

请求解析

HttpBasicConfigurer 类的 configure 方法中,向 Spring Security 过滤器链添加一个过滤器 BasicAuthenticationFilter,该过滤器专门用来处理 HTTP 基本认证相关的事情,我们来看一下它核心的 doFilterInternal 方法:

protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain)
                throws IOException, ServletException {

    try {
        UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);
        if (authRequest == null) {
            chain.doFilter(request, response);
            return;
        }

        String username = authRequest.getName();

        if (authenticationIsRequired(username)) {
            Authentication authResult = this.authenticationManager
                    .authenticate(authRequest);

            SecurityContextHolder.getContext().setAuthentication(authResult);

            this.rememberMeServices.loginSuccess(request, response, authResult);

            onSuccessfulAuthentication(request, response, authResult);
        }

    }
    catch (AuthenticationException failed) {
        SecurityContextHolder.clearContext();

        this.rememberMeServices.loginFail(request, response);

        onUnsuccessfulAuthentication(request, response, failed);

        if (this.ignoreFailure) {
            chain.doFilter(request, response);
        }
        else {
            this.authenticationEntryPoint.commence(request, response, failed);
        }

        return;
    }

    chain.doFilter(request, response);
}

该方法执行流程如下:

  1. 首先调用 authenticationConverter.convert 方法,对请求头中的 Authorization 字段进行解析,经过 Base64 解码后的用户名/密码是一个用 “:” 隔开的字符串,例如用户使用 javaboy/123 进行登录,那么这里解码后的结果就是 javaboy:123,然后根据拿到的用户名/密码,构造一个 UsernamePasswordAuthenticationToken 对象出来。

  2. 如果 authRequest 变量为 null,说明请求头中没有包含认证信息,那么直接执行接下来的过滤器即可,该方法也到此为止。在执行接下来的过滤器时,最终就会通过 ExceptionTranslationFilter 过滤器进入到 BasicAuthenticationEntryPoint#commence 方法中;如果 authRequest 变量不为 null,说明请求是携带了认证信息的,那么就对请求携带的认证信息进行校验。

  3. 从 authRequest 对象中提取出用户名,然后调用 authenticationIsRequired 方法判断是否有必要进行认证,如果没有必要,则直接执行剩下的过滤器即可;如果有必要进行认证,则进行用户认证。authenticationIsRequired 方法的具体逻辑就是,从SecurityContextHolder 中取出当前登录对象,判断使用是否已经登录过了,同时判断是否就是当前用户。

  4. 如果有必要进行认证,则调用 authenticationManager.authenticate 方法完成用户认证,同时将用户信息存入 SecurityContextHolder;如果配置了 rememberMeServices,也进行相应的处理,最后还有一个登录成功的回调方法 onSuccessfulAuthentication,不过该方法并未做任何实现。

  5. 如果认证过程抛出异常,则进行相应处理即可,这里逻辑比较简单。

  6. 最后继续执行接下来的过滤器。在后续过滤器的执行过程中,由于 SecurityContextHolder 中已经保存了登录用户信息了,相当于用户已经完成登录了,因此就和普通的请求一致,不会被 “半路拦截”。

这就是整个 HTTP 基本认证的实现逻辑,可以看到实现还是比较容易的。