HTTP 认证

举报
周杰伦本人 发表于 2022/05/10 17:56:32 2022/05/10
【摘要】 HTTP 认证通过HTTP请求头来提供认证信息,而不是通过表单登录。有HTTP Basic authentication HTTP Digest authentication HTTP Basic authenticationHTTP Basic authentication将用户的登录用户名 密码经过Base64编码之后,放在请求头的Authorization字段中,从而完成用户身份的认...

HTTP 认证

通过HTTP请求头来提供认证信息,而不是通过表单登录。有HTTP Basic authentication HTTP Digest authentication

HTTP Basic authentication

HTTP Basic authentication将用户的登录用户名 密码经过Base64编码之后,放在请求头的Authorization字段中,从而完成用户身份的认证。

  1. 客户端发送请求
  2. 服务端收到请求后,发现用户还没有认证,返回状态码401 WWW-Authenticate响应头则定义了使用何种验证方式去完成身份认证,最简单最常见的是HTTP基本认证(Basic),还有Bearer(Oauth2.0 认证),Digest(HTTP摘要认证)
  3. 客户端收到服务端响应后,将用户名密码使用Base64编码后,放在请求头中,再次发送请求
  4. 服务端解析Authorization字段,完成用户身份的校验,最后将资源返回给客户端。

这种认证方式很少使用,因为有安全问题,HTTP基本认证没有对传输的凭证信息进行加密,仅仅只是进行了Base64编码。

通过httpBasic()方法即可开启HTTP基本认证。

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,authenticationEntryPoint是代理对象,在HttpBasicConfigurer构造方法中创建,具体代理的是BasicAuthenticationEntryPoint,

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过滤器

protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
   final boolean debug = this.logger.isDebugEnabled();
   try {
      UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);
      if (authRequest == null) {
         chain.doFilter(request, response);
         return;
      }

      String username = authRequest.getName();

      if (debug) {
         this.logger
               .debug("Basic Authentication Authorization header found for user '"
                     + username + "'");
      }

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

         if (debug) {
            this.logger.debug("Authentication success: " + authResult);
         }

         SecurityContextHolder.getContext().setAuthentication(authResult);

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

         onSuccessfulAuthentication(request, response, authResult);
      }

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

      if (debug) {
         this.logger.debug("Authentication request for failed!", failed);
      }

      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解码后的用户名密码用冒号隔开,然后构造出UsernamePasswordAuthenticationToken的实例authRequest
  2. 如果authRequest为null,说明请求头中没有包含认证信息,直接指向接下来的过滤器,最终通过ExceptionTranslationFilter过滤器链中调用BasicAuthenticationEntryPoint的commence方法进行处理。如果不为null,说明请求携带了认证信息,那么对认证信息进行校验。
  3. 具体校验为从authRequest中提取出用户名,然后调用authenticationIsRequired方法判断是否需要认证,不需要认证执行下一个过滤器,需要认证则进行认证。authenticationIsRequired是从SecurityContextHolder中渠道当前登录对象,判断是否已经登录过。
  4. 调用authenticationManager.authenticate(authRequest)方法完成用户认证,将用户信息存入SecurityContextHolder中

HTTP Digest authentication

需要自定义配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint(digestAuthenticationEntryPoint())
                .and()
                .addFilter(digestAuthenticationFilter());
    }
    DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
        DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
        entryPoint.setNonceValiditySeconds(3600);
        entryPoint.setRealmName("myrealm");
        entryPoint.setKey("javaboy");
        return entryPoint;
    }
    DigestAuthenticationFilter digestAuthenticationFilter() throws Exception {
        DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
        filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());
        filter.setUserDetailsService(userDetailsServiceBean());
        filter.setPasswordAlreadyEncoded(true);
        return filter;
    }
    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("javaboy").password("e7ecfd3f08e6960f154e1ff29079fbd3").roles("admin").build());
        return manager;
    }
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}
  1. 提供DigestAuthenticationEntryPoint实例,当用户发起一个没有认证的请求时,需要该实例进行处理。
  2. 创建DigestAuthenticationFilter实例,添加到Spring Security过滤器链中。

质询

HTTP摘要认证的质询由DigestAuthenticationEntryPoint的commence方法完成

public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException {
   HttpServletResponse httpResponse = response;

   // compute a nonce (do not use remote IP address due to proxy farms)
   // format of nonce is:
   // base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
   long expiryTime = System.currentTimeMillis() + (nonceValiditySeconds * 1000);
   String signatureValue = DigestAuthUtils.md5Hex(expiryTime + ":" + key);
   String nonceValue = expiryTime + ":" + signatureValue;
   String nonceValueBase64 = new String(Base64.getEncoder().encode(nonceValue.getBytes()));

   // qop is quality of protection, as defined by RFC 2617.
   // we do not use opaque due to IE violation of RFC 2617 in not
   // representing opaque on subsequent requests in same session.
   String authenticateHeader = "Digest realm=\"" + realmName + "\", "
         + "qop=\"auth\", nonce=\"" + nonceValueBase64 + "\"";

   if (authException instanceof NonceExpiredException) {
      authenticateHeader = authenticateHeader + ", stale=\"true\"";
   }

   if (logger.isDebugEnabled()) {
      logger.debug("WWW-Authenticate header sent to user agent: "
            + authenticateHeader);
   }

   httpResponse.addHeader("WWW-Authenticate", authenticateHeader);
   httpResponse.sendError(HttpStatus.UNAUTHORIZED.value(),
      HttpStatus.UNAUTHORIZED.getReasonPhrase());
}

和HTTP基本认证一样,不同的是WWW-Authenticate字段值

  • Digest表示使用HTTP摘要认证
  • realm表示服务端返回的标识访问资源的安全域
  • qop表示服务端返回的保护级别,auth表示只进行身份认证;auth-int表示除了身份认证还要校验内容完整性
  • nonce为生成的随机数 生成规则:先对过期时间和key组成的字符串计算出消息摘要signatureValue,再对过期时间和signatureValue进行Base64编码。
  • stale表示当nonce过期了包含该标记,stale=true表示客户端不必再次弹出输入框,只需要带上已有认证信息,重新发起认证请求即可。

客户端处理

客户端收到请求后,输入用户名密码,然后客户端生成response,用户密码经过各种MD5运算后,包含在response中,服务端拿到这些参数后,根据用户名去数据库中查询用户密码,然后进行MD5运算,将结果和response对比。

请求解析

过滤器DigestAuthenticationFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;

   String header = request.getHeader("Authorization");

   if (header == null || !header.startsWith("Digest ")) {
      chain.doFilter(request, response);

      return;
   }

   if (logger.isDebugEnabled()) {
      logger.debug(
            "Digest Authorization header received from user agent: " + header);
   }

   DigestData digestAuth = new DigestData(header);

   try {
      digestAuth.validateAndDecode(this.authenticationEntryPoint.getKey(),
            this.authenticationEntryPoint.getRealmName());
   }
   catch (BadCredentialsException e) {
      fail(request, response, e);

      return;
   }

   // Lookup password for presented username
   // NB: DAO-provided password MUST be clear text - not encoded/salted
   // (unless this instance's passwordAlreadyEncoded property is 'false')
   boolean cacheWasUsed = true;
   UserDetails user = this.userCache.getUserFromCache(digestAuth.getUsername());
   String serverDigestMd5;

   try {
      if (user == null) {
         cacheWasUsed = false;
         user = this.userDetailsService
               .loadUserByUsername(digestAuth.getUsername());

         if (user == null) {
            throw new AuthenticationServiceException(
                  "AuthenticationDao returned null, which is an interface contract violation");
         }

         this.userCache.putUserInCache(user);
      }

      serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(),
            request.getMethod());

      // If digest is incorrect, try refreshing from backend and recomputing
      if (!serverDigestMd5.equals(digestAuth.getResponse()) && cacheWasUsed) {
         if (logger.isDebugEnabled()) {
            logger.debug(
                  "Digest comparison failure; trying to refresh user from DAO in case password had changed");
         }

         user = this.userDetailsService
               .loadUserByUsername(digestAuth.getUsername());
         this.userCache.putUserInCache(user);
         serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(),
               request.getMethod());
      }

   }
   catch (UsernameNotFoundException notFound) {
      fail(request, response,
            new BadCredentialsException(this.messages.getMessage(
                  "DigestAuthenticationFilter.usernameNotFound",
                  new Object[] { digestAuth.getUsername() },
                  "Username {0} not found")));

      return;
   }

   // If digest is still incorrect, definitely reject authentication attempt
   if (!serverDigestMd5.equals(digestAuth.getResponse())) {
      if (logger.isDebugEnabled()) {
         logger.debug("Expected response: '" + serverDigestMd5
               + "' but received: '" + digestAuth.getResponse()
               + "'; is AuthenticationDao returning clear text passwords?");
      }

      fail(request, response,
            new BadCredentialsException(this.messages.getMessage(
                  "DigestAuthenticationFilter.incorrectResponse",
                  "Incorrect response")));
      return;
   }

   // To get this far, the digest must have been valid
   // Check the nonce has not expired
   // We do this last so we can direct the user agent its nonce is stale
   // but the request was otherwise appearing to be valid
   if (digestAuth.isNonceExpired()) {
      fail(request, response,
            new NonceExpiredException(this.messages.getMessage(
                  "DigestAuthenticationFilter.nonceExpired",
                  "Nonce has expired/timed out")));

      return;
   }

   if (logger.isDebugEnabled()) {
      logger.debug("Authentication success for user: '" + digestAuth.getUsername()
            + "' with response: '" + digestAuth.getResponse() + "'");
   }

   Authentication authentication = createSuccessfulAuthentication(request, user);
   SecurityContext context = SecurityContextHolder.createEmptyContext();
   context.setAuthentication(authentication);
   SecurityContextHolder.setContext(context);

   chain.doFilter(request, response);
}

太长了 不一一分析了。。

和HTTP基本认证相比 最大亮点就是不明文传输用户密码。客户端对密码进行MD5运算,并将运算所需参数以及运算结果发送给服务端,服务端再去校验数据是否正确。

HTTP摘要认证比较复杂 使用并不多。。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。