本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。
HTTP提供了用户权限控制和认证的通用方式,这种认证方式通过HTTP请求头来提供认证信息,而不是通过表单登录。最为小伙伴熟知的应该是HTTP Basic Authentication,另外一种是相对安全的HTTP Digest Authentication。Spring Security中对于这两种认证方式都提供了相应的支持。接下来我们来学习下这两种认证的具体使用方法以及各自的优缺点。
HTTP Basic Authentication中文翻译为HTTP基本认证。在这种认证方式中,将用户的用户名/密码经过Base64编码之后,放在请求头的Authorization字段中,从而完成于用户的身份证。
在RFC7235(https://tools.ietf.org/html/rfc7235)规范中定义的认证方式,方客户端发起一个请求之后,服务端可以针对该请求返回一个质询信息,然后客户端再提供用户的凭证信息。具体的质询和应答流程如下图:
从上图我们可以看到,HTTP基本认证流程是这样的:
GET /hello HTTP/1.1
Host: localhost:8080
HTTP/1.1 401
WWW-Authenticate: Basic realm="Realm"
GET /hello HTTP/1.1
Host: localhost:8080
Authorization: Basic amF2YWJveToxMjM=
服务器端解析Authorization字段,完成用户身份的校验,最后将资环返回给客户端
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 16
上面就是整个HTTP Basic Authenticaiton认证流程。
可以看到,这种认证方式实际上非常简单,基本上所有的浏览器上都支持这种认证方式。HTTP基本认证方式没有对传输的凭证信息进行加密,仅仅只是进行了Base64编码,这就造成了很大的安全隐患,所以如果用到HTTP基本的认证一般都是结合HTTPS一起使用。同时,一旦使用HTTP基本认证,除非用户关闭浏览器或者清空浏览器缓存,否则没有办法退出登录。
Spring Security中开启HTTP基本认证非常容易,继承WebSecurityConfigurerAdapter重写configure(HttpSecurity http) 方法,配置配置如下:
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated() .and().httpBasic().and().httpBasic(); }
通过httpBasic()方法即可开启HTTP基本认证。配置完成之后,启动项目,此时如果要访问一个受保护的资源,浏览器会自动弹出一个认证框。输入正确的用户名/密码认证成功之后,就可以访问受保护的资源了。
接下来我们看看Spring Security中是如何实现HTTP基本认证的。实现整体分为两部分:
httpBasic( )方法开启了HTTP基本认证的配置,具体通过配置HttpBasicConfigurer类来完成。在HttpBasicConfigurer配置类的init方法中调用了registerDefaultEntryPoint方法,该方法完成了失败请求处理类AuthenticationEntryPoint类的配置,代码如下:
private void registerDefaultEntryPoint(B http, RequestMatcher preferredMatcher) {ExceptionHandlingConfigurer exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);if (exceptionHandling == null) {return;}exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(this.authenticationEntryPoint),preferredMatcher);}
这里配置到exceptionHandling中的authenticationEntryPoint是一个代理对象,具体代理的是BasicAuthenticationEntryPoint(启动项目的是debug时就能看到)。
简而言之,如果一个请求没有携带认证信息的话,最终将被BasicAuthenticationEntryPoint实例处理,我们来看下该类的主要实现:
public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {private String realmName;@Overridepublic void afterPropertiesSet() {Assert.hasText(this.realmName, "realmName must be specified");}@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException {response.addHeader("WWW-Authenticate", "Basic realm=\"" + this.realmName + "\"");response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());}public String getRealmName() {return this.realmName;}public void setRealmName(String realmName) {this.realmName = realmName;}
}
可以看到,这个类的处理逻辑还是很简单的,响应头中添加WWW-Authenticate字段,然后发送错误响应码为401。
这里就是发出质询的代码。总结一下,就是一个未经认证的请求,在经过Spring Security过滤器链时会抛出异常,该异常会在ExceptionTranslationFilter过滤器链中调用BasicAuthenticationEntryPoint的commence方法中进行处理。
HttpBasicConfigurer类中的configure方法中,向Spring Security过滤器链中添加一个过滤器BasicAuthenticationFilter,改过滤器专门来处理HTTP基本认证相关的事情,我们来看一下它的核心方法doFilterInternal:
@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {try {UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);if (authRequest == null) {this.logger.trace("Did not process authentication request since failed to find "+ "username and password in Basic Authorization header");chain.doFilter(request, response);return;}String username = authRequest.getName();this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));if (authenticationIsRequired(username)) {Authentication authResult = this.authenticationManager.authenticate(authRequest);SecurityContext context = SecurityContextHolder.createEmptyContext();context.setAuthentication(authResult);SecurityContextHolder.setContext(context);if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));}this.rememberMeServices.loginSuccess(request, response, authResult);this.securityContextRepository.saveContext(context, request, response);onSuccessfulAuthentication(request, response, authResult);}}catch (AuthenticationException ex) {SecurityContextHolder.clearContext();this.logger.debug("Failed to process authentication request", ex);this.rememberMeServices.loginFail(request, response);onUnsuccessfulAuthentication(request, response, ex);if (this.ignoreFailure) {chain.doFilter(request, response);}else {this.authenticationEntryPoint.commence(request, response, ex);}return;}chain.doFilter(request, response);}
该方法执行流程如下: