HTTP Basic authentication
简介
HTTP Basic authentication 中文译作 HTTP 基本认证,在这种认证方式中,将用户的登录用户名/密码经过 Base64 编码之后,放在请求头的 Authorization 字段中,从而完成用户身份的认证。
这是一种在 RFC7235(https://tools.ietf.org/html/rfc7235)规范中定义的认证方式,当客户端发起一个请求之后,服务端可以针对该请求返回一个质询信息,然后客户端再提供用户的凭证信息。具体的质询与应答流程如图10-1所示。

从图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所示。输入用户名/密码完成认证之后,就可以访问受保护的资源了。

源码分析
代码实现很简单,接下来我们来看一下 Spring Security 中是如何实现 HTTP 基本认证的。实现整体上分为两部分:
-
对未认证的请求发出质询。
-
解析携带了认证信息的请求
我们分别从这两部分来分析。
质询
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);
}
该方法执行流程如下:
-
首先调用 authenticationConverter.convert 方法,对请求头中的 Authorization 字段进行解析,经过 Base64 解码后的用户名/密码是一个用 “:” 隔开的字符串,例如用户使用 javaboy/123 进行登录,那么这里解码后的结果就是 javaboy:123,然后根据拿到的用户名/密码,构造一个 UsernamePasswordAuthenticationToken 对象出来。
-
如果 authRequest 变量为 null,说明请求头中没有包含认证信息,那么直接执行接下来的过滤器即可,该方法也到此为止。在执行接下来的过滤器时,最终就会通过 ExceptionTranslationFilter 过滤器进入到 BasicAuthenticationEntryPoint#commence 方法中;如果 authRequest 变量不为 null,说明请求是携带了认证信息的,那么就对请求携带的认证信息进行校验。
-
从 authRequest 对象中提取出用户名,然后调用 authenticationIsRequired 方法判断是否有必要进行认证,如果没有必要,则直接执行剩下的过滤器即可;如果有必要进行认证,则进行用户认证。authenticationIsRequired 方法的具体逻辑就是,从SecurityContextHolder 中取出当前登录对象,判断使用是否已经登录过了,同时判断是否就是当前用户。
-
如果有必要进行认证,则调用 authenticationManager.authenticate 方法完成用户认证,同时将用户信息存入 SecurityContextHolder;如果配置了 rememberMeServices,也进行相应的处理,最后还有一个登录成功的回调方法 onSuccessfulAuthentication,不过该方法并未做任何实现。
-
如果认证过程抛出异常,则进行相应处理即可,这里逻辑比较简单。
-
最后继续执行接下来的过滤器。在后续过滤器的执行过程中,由于 SecurityContextHolder 中已经保存了登录用户信息了,相当于用户已经完成登录了,因此就和普通的请求一致,不会被 “半路拦截”。
这就是整个 HTTP 基本认证的实现逻辑,可以看到实现还是比较容易的。