• Spring Security HTTP认证


    本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。

            HTTP提供了用户权限控制和认证的通用方式,这种认证方式通过HTTP请求头来提供认证信息,而不是通过表单登录。最为小伙伴熟知的应该是HTTP Basic Authentication,另外一种是相对安全的HTTP Digest Authentication。Spring Security中对于这两种认证方式都提供了相应的支持。接下来我们来学习下这两种认证的具体使用方法以及各自的优缺点。

    HTTP Basic Authentication

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

            在RFC7235https://tools.ietf.org/html/rfc7235)规范中定义的认证方式,方客户端发起一个请求之后,服务端可以针对该请求返回一个质询信息,然后客户端再提供用户的凭证信息。具体的质询和应答流程如下图:

             从上图我们可以看到,HTTP基本认证流程是这样的:

    • 首先客户端(浏览器)发起请求
      1. GET /hello HTTP/1.1
      2. Host: localhost:8080
    • 服务端收到请求,发现用户没有认证,于是给出如下响应。状态码401表示用户未完成认证,WWW-Authenticate响应头定义了使用何种认证方式去完成身份认证。最简单的、常见的的就是我们使用的HTTP基本认证(Basic)、Bearer(OAuth2.0认证)、Digest(HTTP摘要认证)等等取值。
      1. HTTP/1.1 401
      2. WWW-Authenticate: Basic realm="Realm"
    • 客户端收到服务器的响应之后,将用户名/密码使用Base64编码之后,放在请求头中再次发起请求
      1. GET /hello HTTP/1.1
      2. Host: localhost:8080
      3. Authorization: Basic amF2YWJveToxMjM=
    • 服务器端解析Authorization字段,完成用户身份的校验,最后将资环返回给客户端

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

      上面就是整个HTTP Basic Authenticaiton认证流程。

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

    基本用法

            Spring Security中开启HTTP基本认证非常容易,继承WebSecurityConfigurerAdapter重写configure(HttpSecurity http) 方法,配置配置如下:

    1. @Override
    2. protected void configure(HttpSecurity http) throws Exception {
    3. http
    4. .authorizeRequests()
    5. .anyRequest().authenticated()
    6. .and()
    7. .httpBasic()
    8. .and().httpBasic()
    9. ;
    10. }

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

            

     源码分析

            接下来我们看看Spring Security中是如何实现HTTP基本认证的。实现整体分为两部分:

    • 对没有认证的请求发出咨询
    • 解析携带了认证信息的请求

    质询

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

    1. private void registerDefaultEntryPoint(B http, RequestMatcher preferredMatcher) {
    2. ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
    3. if (exceptionHandling == null) {
    4. return;
    5. }
    6. exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(this.authenticationEntryPoint),
    7. preferredMatcher);
    8. }

            这里配置到exceptionHandling中的authenticationEntryPoint是一个代理对象,具体代理的是BasicAuthenticationEntryPoint(启动项目的是debug时就能看到)。

            简而言之,如果一个请求没有携带认证信息的话,最终将被BasicAuthenticationEntryPoint实例处理,我们来看下该类的主要实现:

    1. public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
    2. private String realmName;
    3. @Override
    4. public void afterPropertiesSet() {
    5. Assert.hasText(this.realmName, "realmName must be specified");
    6. }
    7. @Override
    8. public void commence(HttpServletRequest request, HttpServletResponse response,
    9. AuthenticationException authException) throws IOException {
    10. response.addHeader("WWW-Authenticate", "Basic realm=\"" + this.realmName + "\"");
    11. response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
    12. }
    13. public String getRealmName() {
    14. return this.realmName;
    15. }
    16. public void setRealmName(String realmName) {
    17. this.realmName = realmName;
    18. }
    19. }

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

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

    请求分析

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

    1. @Override
    2. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    3. throws IOException, ServletException {
    4. try {
    5. UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
    6. if (authRequest == null) {
    7. this.logger.trace("Did not process authentication request since failed to find "
    8. + "username and password in Basic Authorization header");
    9. chain.doFilter(request, response);
    10. return;
    11. }
    12. String username = authRequest.getName();
    13. this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
    14. if (authenticationIsRequired(username)) {
    15. Authentication authResult = this.authenticationManager.authenticate(authRequest);
    16. SecurityContext context = SecurityContextHolder.createEmptyContext();
    17. context.setAuthentication(authResult);
    18. SecurityContextHolder.setContext(context);
    19. if (this.logger.isDebugEnabled()) {
    20. this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    21. }
    22. this.rememberMeServices.loginSuccess(request, response, authResult);
    23. this.securityContextRepository.saveContext(context, request, response);
    24. onSuccessfulAuthentication(request, response, authResult);
    25. }
    26. }
    27. catch (AuthenticationException ex) {
    28. SecurityContextHolder.clearContext();
    29. this.logger.debug("Failed to process authentication request", ex);
    30. this.rememberMeServices.loginFail(request, response);
    31. onUnsuccessfulAuthentication(request, response, ex);
    32. if (this.ignoreFailure) {
    33. chain.doFilter(request, response);
    34. }
    35. else {
    36. this.authenticationEntryPoint.commence(request, response, ex);
    37. }
    38. return;
    39. }
    40. chain.doFilter(request, response);
    41. }

            该方法执行流程如下:

    1. 首先调用authenticationConverter的convert方法从请求头中的Authorization字段进行解析,经过Base64解码之后的用户名和密码是一个用 ":" 隔开的字符串,例如用户使用tianluhua/123进行登录,那么这里解码后得到的字符串就是tianluhua:123,然后根据用户名和密码构造一个UsernamePasswordAuthenticationToken对象出来。
    2. authRequest 为null,说明请求头中没有包含Http基本认证的信息,改方法到此为止。那么接下来执行其他的过滤器即可。
    3. 从authRequest对象中提取出用户名,然后调用authenticationIsRequired方法判断是否有必要进行认证,如果没必要认证就执行下面的过滤器。
    4. 如果有必要认证,就调用authenticationManager的authenticate方法完成用户认证,同时将信息存入SecurityContextHolder中;如果配合了rememberMeServices,也行进相应的处理,最后回调一个onSuccessfulAuthentication方法,不过改方法没有做任何实现。
    5. 如果认证过程中抛出异常,则进行相应的处理即可
    6. 最后执行接下来的过滤器。在后续的过滤器中执行的过程中,由于SecurityContextHolder中已经保存了登录用户的信息了,相当于用户已经完成了登录了,因此就和普通的请求一样,不会被“半路拦截”。

  • 相关阅读:
    Learn Prompt-ChatGPT 精选案例:代码助理
    css3 中选择器间有空格与没空格的区别?
    Golang反射修改变量值
    SSH配置免密登录
    自制手机app的51蓝牙+循迹+OLED显示速度小车
    SpringMVC——SpringMVC框架的基础知识概括
    ssm课堂考勤管理毕业设计-附源码191617
    匿名信v1.4.4源码下载,安装教程
    【博客489】prometheus-----PromQL数据类型与Metrics数据类型
    1901_野火FreeRTOS教程之任务链表以及调度部分阅读
  • 原文地址:https://blog.csdn.net/qq_27062249/article/details/128187708