HTTP 认证
通过 HTTP 请求头来提供认证信息,而不是通过表单登录。有 HTTP Basic authentication HTTP Digest authentication
HTTP Basic authentication
HTTP Basic authentication 将用户的登录用户名 密码经过 Base64 编码之后,放在请求头的 Authorization 字段中,从而完成用户身份的认证。
- 客户端发送请求 
- 服务端收到请求后,发现用户还没有认证,返回状态码 401 WWW-Authenticate 响应头则定义了使用何种验证方式去完成身份认证,最简单最常见的是 HTTP 基本认证(Basic),还有 Bearer(Oauth2.0 认证),Digest(HTTP 摘要认证) 
- 客户端收到服务端响应后,将用户名密码使用 Base64 编码后,放在请求头中,再次发送请求 
- 服务端解析 Authorization 字段,完成用户身份的校验,最后将资源返回给客户端。 
这种认证方式很少使用,因为有安全问题,HTTP 基本认证没有对传输的凭证信息进行加密,仅仅只是进行了 Base64 编码。
通过 httpBasic()方法即可开启 HTTP 基本认证。
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,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);}
   复制代码
 
- 调用 authenticationConverter.convert 方法,对请求头中的 Authorization 字段进行解析,经过 Base64 解码后的用户名密码用冒号隔开,然后构造出 UsernamePasswordAuthenticationToken 的实例 authRequest 
- 如果 authRequest 为 null,说明请求头中没有包含认证信息,直接指向接下来的过滤器,最终通过 ExceptionTranslationFilter 过滤器链中调用 BasicAuthenticationEntryPoint 的 commence 方法进行处理。如果不为 null,说明请求携带了认证信息,那么对认证信息进行校验。 
- 具体校验为从 authRequest 中提取出用户名,然后调用 authenticationIsRequired 方法判断是否需要认证,不需要认证执行下一个过滤器,需要认证则进行认证。authenticationIsRequired 是从 SecurityContextHolder 中渠道当前登录对象,判断是否已经登录过。 
- 调用 authenticationManager.authenticate(authRequest)方法完成用户认证,将用户信息存入 SecurityContextHolder 中 
HTTP Digest authentication
需要自定义配置
 @Configurationpublic 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();    }}
   复制代码
 
- 提供 DigestAuthenticationEntryPoint 实例,当用户发起一个没有认证的请求时,需要该实例进行处理。 
- 创建 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 摘要认证比较复杂 使用并不多。。
评论