Spring Security实现HTTP认证

作者:Tony-devj 时间:2021-10-31 14:21:47 

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

前言

除系统内维护的用户名和密码认证技术外,Spring Security还支持HTTP层面的认证,包括HTTP基本认证和HTTP摘要认证

一、HTTP基本认证是什么?

HTTP基本认证是在RFC2616中定义的一种认证模式。

二、HTTP基本认证流程

  • 客户端发起一条没有携带认证信息的请求。

  • 服务器返回一条401 Unauthorized响应, 并在WWW-Authentication首部说明认证形式, 当进行HTTP基本认证时, WWW-Authentication会被设置为Basic realm=“被保护页面”。

  • 客户端收到401 Unauthorized 响应后, 弹出对话框, 询问用户名和密码。 当用户完成后, 客户端将用户名和密码使用冒号拼接并编码为Base64形式, 然后放入请求的Authorization首部发送给服务器。

  • 服务器解码得到客户端发来的用户名和密码,并在验证它们是正确的之后,返回客户端请求的报文

Spring Security实现HTTP认证

有上面可以看出只需要验证Authentication即可,因此如果不使用浏览器访问HTTP基本认证保护的页面,则自行在请求头中设置Authorization也是可以.

HTTP基本认证是一种无状态的认证方式,与表单认证相比,HTTP基本认证是一种基于HTTP层面的认证方式,无法携带session,即无法实现Remember-ME功能。另外,用户名和密码在传递时仅做一次简单的Base64编码,几乎等同于明文传输,极易出现密码被窃听和重放攻击等安全性问题,在实际系统开发中很少使用这种方式来进行安全验证。 如果有必要,也应使用加密的传输层HTTPS来保障安全.

一.Spring Security使用HTTP基本认证

1.创建项目spring-security-http-auth

pom.xml:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.创建配置文件WebSecurityConfig

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
   }
}

上面的配置最后添加了httpBasic(),使用http基本认证

3.运行项目

访问本地项目,http://localhost:8080

Spring Security实现HTTP认证

会弹出登陆框,我们看到调试工具中返回了401无权限。

Spring Security实现HTTP认证

我们使用Spring Security提供的默认的用户名和密码登陆。

Spring Security实现HTTP认证

登陆成功后,header中就会有Authorization: Basic dXNlcjo0NWU2NzViOC1hZGYwLTQzNzMtYjA2MS02MGE0YzkzZjA2ZGU=

二.Spring Security HTTP基本认证原理

上面我们实现了HTTP基本认证,我们看看其中Spring Security中是如何做到的?
我们使用HTTP基本认证的时候,在配置类中使用httpBasic()进行处理。
httpBasic方法:

public HttpBasicConfigurer<HttpSecurity> httpBasic() throws Exception {
       return (HttpBasicConfigurer)this.getOrApply(new HttpBasicConfigurer());
   }

上面可以看出,Spring Security进行HTTP基本认证是使用HttpBasicConfigurer配置类进行的。
HttpBasicConfigurer.class:

//构建HttpBasicConfigurer
public HttpBasicConfigurer() {
       this.realmName("Realm");
       LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap();
       entryPoints.put(X_REQUESTED_WITH, new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
       DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
       defaultEntryPoint.setDefaultEntryPoint(this.basicAuthEntryPoint);
       this.authenticationEntryPoint = defaultEntryPoint;
   }
//进行配置
public void configure(B http) {
//进行认证管理
       AuthenticationManager authenticationManager = (AuthenticationManager)http.getSharedObject(AuthenticationManager.class);
       //声明basic认证 *
       BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authenticationManager, this.authenticationEntryPoint);
       if (this.authenticationDetailsSource != null) {
           basicAuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
       }
//注册一个RememberMeServices
       RememberMeServices rememberMeServices = (RememberMeServices)http.getSharedObject(RememberMeServices.class);
       if (rememberMeServices != null) {
//设置rememberMeServices      
           basicAuthenticationFilter.setRememberMeServices(rememberMeServices);
       }
//申明basicAuthenticationFilter过滤器
       basicAuthenticationFilter = (BasicAuthenticationFilter)this.postProcess(basicAuthenticationFilter);
       http.addFilter(basicAuthenticationFilter);
   }

上面声明BasicAuthenticationFilter并添加到 * 链中
BasicAuthenticationFilter.class:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
       try {
       //获取token
           UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
           //authRequest为空直接放行
           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 (this.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.onSuccessfulAuthentication(request, response, authResult);
           }
       } catch (AuthenticationException var8) {
           SecurityContextHolder.clearContext();
           this.logger.debug("Failed to process authentication request", var8);
           this.rememberMeServices.loginFail(request, response);
           this.onUnsuccessfulAuthentication(request, response, var8);
           if (this.ignoreFailure) {
               chain.doFilter(request, response);
           } else {
               this.authenticationEntryPoint.commence(request, response, var8);
           }

return;
       }

chain.doFilter(request, response);
   }

BasicAuthenticationEntryPoint返回进行响应的处理

public 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());
   }

三.HTTP摘要认证是什么?

HTTP摘要认证和HTTP基本认证一样,也是在RFC2616中定义的认证模式,RFC2617专门对这两种认证模式做了规定。与 HTTP 基本认证相比,HTTP 摘要认证使用对通信双方都可知的口令进行校验,且最终的传输数据并非明文形式。

摘要认证是一种协议规定的Web服务器用来同网页浏览器进行认证信息协商的方法。它在密码发出前,先对其应用哈希函数,这相对于HTTP基本认证发送明文而言,更安全。

从技术上讲,摘要认证是使用随机数来阻止进行密码分析的MD5加密哈希函数应用。
HTTP摘要认证流程:

Spring Security实现HTTP认证

HTTP摘要认证中的相关参数:

  • username: 用户名。

  • password: 用户密码。

  • realm: 认证域, 由服务器返回。

  • opaque: 透传字符串, 客户端应原样返回。

  • method: 请求的方法。

  • nonce: 由服务器生成的随机字符串。

  • nc: 即nonce-count, 指请求的次数, 用于计数, 防止重放攻击。 qop被指定时, nc也必须被指定。

  • cnonce: 客户端发给服务器的随机字符串, qop被指定时, cnonce也必须被指定。

  • qop: 保护级别, 客户端根据此参数指定摘要算法。 若取值为auth, 则只进行身份验证; 若取

  • 值为auth-int, 则还需要校验内容完整性。

  • uri: 请求的uri。

  • response:客户端根据算法算出的摘要值。

  • algorithm:摘要算法, 目前仅支持MD5。

  • entity-body:页面实体,非消息实体,仅在auth-int中支持。

  • 通常服务器携带的数据包括realm、 opaque、 nonce、 qop等字段, 如果客户端需要做出验证回应,就必须按照一定的算法计算得到一些新的数据并一起返回。

四.Spring Security使用HTTP摘要认证流程?

在Spring Security中没有像HTTP基础认证那样,通过httpBasic()方法进行集成HTTP摘要认证,但是Spring Security提供了像BasicAuthenticationEntryPoint一样的DigestAuthenticationEntryPoint.就是我们需要将DigestAuthenticationEntryPoint添加到filter过滤器中去处理。
代码如下:
WebSecurityConfig类:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
   private DigestAuthenticationEntryPoint digestAuthenticationEntryPoint;

@Autowired
   private UserDetailsService userDetailsService;

@Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests().anyRequest().authenticated().and()
               .exceptionHandling()
               .authenticationEntryPoint(digestAuthenticationEntryPoint)
               .and().addFilter(digestAuthenticationFilter());
   }

public DigestAuthenticationFilter digestAuthenticationFilter(){
       DigestAuthenticationFilter digestAuthenticationFilter = new DigestAuthenticationFilter();
       digestAuthenticationFilter.setUserDetailsService(userDetailsService);
       digestAuthenticationFilter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint);
       return digestAuthenticationFilter;
   }

}

申明DigestAuthenticationEntryPointBean:

@Bean
   public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint(){
       DigestAuthenticationEntryPoint digestAuthenticationEntryPoint = new DigestAuthenticationEntryPoint();
       digestAuthenticationEntryPoint.setRealmName("realName");
       digestAuthenticationEntryPoint.setKey("tony");
       return digestAuthenticationEntryPoint;
   }
@Bean
   public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint(){
       DigestAuthenticationEntryPoint digestAuthenticationEntryPoint = new DigestAuthenticationEntryPoint();
       digestAuthenticationEntryPoint.setRealmName("realm");
       digestAuthenticationEntryPoint.setKey("tony");
       return digestAuthenticationEntryPoint;
   }

@Bean
   public UserDetailsService userDetailsService() {
       InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
       manager.createUser(User.withUsername("tony").password("123456").roles("admin").build());
       return manager;
   }

@Bean
   public PasswordEncoder passwordEncoder() {
       return NoOpPasswordEncoder.getInstance();
   }

运行项目

访问主页,http://localhost:8080,返回如下页面:

Spring Security实现HTTP认证

我们输入用户名和密码登陆。

Spring Security实现HTTP认证

当长时间未登录,随机字符串到期了也登陆不上。
默认的过期时间为300s,我们可以通过设置时间。
DigestAuthenticationEntryPoint中realmName和key是必须要设置的。
相关源码:

public void afterPropertiesSet() {
       Assert.hasLength(this.realmName, "realmName must be specified");
       Assert.hasLength(this.key, "key must be specified");
   }

public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
//计算过期时间
       long expiryTime = System.currentTimeMillis() + (long)(this.nonceValiditySeconds * 1000);
       //计算签名值
       String signatureValue = DigestAuthUtils.md5Hex(expiryTime + ":" + this.key);
       //随机字符串
       String nonceValue = expiryTime + ":" + signatureValue;
       //随机字符串base64
       String nonceValueBase64 = new String(Base64.getEncoder().encode(nonceValue.getBytes()));
       String authenticateHeader = "Digest realm=\"" + this.realmName + "\", qop=\"auth\", nonce=\"" + nonceValueBase64 + "\"";
       if (authException instanceof NonceExpiredException) {
           authenticateHeader = authenticateHeader + ", stale=\"true\"";
       }

logger.debug(LogMessage.format("WWW-Authenticate header sent to user agent: %s", authenticateHeader));
       response.addHeader("WWW-Authenticate", authenticateHeader);
       response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
   }

进行处理的时候使用DigestAuthenticationFilter进行处理

public void afterPropertiesSet() {
//必须设置userDetailsService
       Assert.notNull(this.userDetailsService, "A UserDetailsService is required");
       //必须设置authenticationEntryPoint
       Assert.notNull(this.authenticationEntryPoint, "A DigestAuthenticationEntryPoint is required");
   }

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
       String header = request.getHeader("Authorization");
       if (header != null && header.startsWith("Digest ")) {
           logger.debug(LogMessage.format("Digest Authorization header received from user agent: %s", header));
           DigestAuthenticationFilter.DigestData digestAuth = new DigestAuthenticationFilter.DigestData(header);

try {
//验证并且解密
               digestAuth.validateAndDecode(this.authenticationEntryPoint.getKey(), this.authenticationEntryPoint.getRealmName());
           } catch (BadCredentialsException var11) {
               this.fail(request, response, var11);
               return;
           }
           //缓存
           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);
               }
//服务器md5摘要
               serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(), request.getMethod());
               if (!serverDigestMd5.equals(digestAuth.getResponse()) && cacheWasUsed) {
                   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 var12) {
               String message = this.messages.getMessage("DigestAuthenticationFilter.usernameNotFound", new Object[]{digestAuth.getUsername()}, "Username {0} not found");
               this.fail(request, response, new BadCredentialsException(message));
               return;
           }

String message;
           if (!serverDigestMd5.equals(digestAuth.getResponse())) {
               logger.debug(LogMessage.format("Expected response: '%s' but received: '%s'; is AuthenticationDao returning clear text passwords?", serverDigestMd5, digestAuth.getResponse()));
               message = this.messages.getMessage("DigestAuthenticationFilter.incorrectResponse", "Incorrect response");
               this.fail(request, response, new BadCredentialsException(message));
           } else if (digestAuth.isNonceExpired()) {
               message = this.messages.getMessage("DigestAuthenticationFilter.nonceExpired", "Nonce has expired/timed out");
               this.fail(request, response, new NonceExpiredException(message));
           } else {
               logger.debug(LogMessage.format("Authentication success for user: '%s' with response: '%s'", digestAuth.getUsername(), digestAuth.getResponse()));
               Authentication authentication = this.createSuccessfulAuthentication(request, user);
               SecurityContext context = SecurityContextHolder.createEmptyContext();
               context.setAuthentication(authentication);
               SecurityContextHolder.setContext(context);
               chain.doFilter(request, response);
           }
       } else {
           chain.doFilter(request, response);
       }
   }

DigestData为摘要数据:

private class DigestData {
//用户名
   private final String username;
   //认证域
   private final String realm;
   //随机字符串
   private final String nonce;
   private final String uri;
   private final String response;
   //保护级别
   private final String qop;
   //即nonce-count, 指请求的次数, 用于计数, 防止重放攻击
   private final String nc;
   private final String cnonce;
   private final String section212response;
   private long nonceExpiryTime;

DigestData(String header) {
       this.section212response = header.substring(7);
       String[] headerEntries = DigestAuthUtils.splitIgnoringQuotes(this.section212response, ',');
       Map<String, String> headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\"");
       this.username = (String)headerMap.get("username");
       this.realm = (String)headerMap.get("realm");
       this.nonce = (String)headerMap.get("nonce");
       this.uri = (String)headerMap.get("uri");
       this.response = (String)headerMap.get("response");
       this.qop = (String)headerMap.get("qop");
       this.nc = (String)headerMap.get("nc");
       this.cnonce = (String)headerMap.get("cnonce");
       DigestAuthenticationFilter.logger.debug(LogMessage.format("Extracted username: '%s'; realm: '%s'; nonce: '%s'; uri: '%s'; response: '%s'", new Object[]{this.username, this.realm, this.nonce, this.uri, this.response}));
   }
  //验证和解密
   void validateAndDecode(String entryPointKey, String expectedRealm) throws BadCredentialsException {
       if (this.username != null && this.realm != null && this.nonce != null && this.uri != null && this.response != null) {
           if ("auth".equals(this.qop) && (this.nc == null || this.cnonce == null)) {
               DigestAuthenticationFilter.logger.debug(LogMessage.format("extracted nc: '%s'; cnonce: '%s'", this.nc, this.cnonce));
               throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.missingAuth", new Object[]{this.section212response}, "Missing mandatory digest value; received header {0}"));
           } else if (!expectedRealm.equals(this.realm)) {
               throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.incorrectRealm", new Object[]{this.realm, expectedRealm}, "Response realm name '{0}' does not match system realm name of '{1}'"));
           } else {
               byte[] nonceBytes;
               try {
                   nonceBytes = Base64.getDecoder().decode(this.nonce.getBytes());
               } catch (IllegalArgumentException var8) {
                   throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceEncoding", new Object[]{this.nonce}, "Nonce is not encoded in Base64; received nonce {0}"));
               }

String nonceAsPlainText = new String(nonceBytes);
               String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":");
               if (nonceTokens.length != 2) {
                   throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceNotTwoTokens", new Object[]{nonceAsPlainText}, "Nonce should have yielded two tokens but was {0}"));
               } else {
                   try {
                       this.nonceExpiryTime = new Long(nonceTokens[0]);
                   } catch (NumberFormatException var7) {
                       throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceNotNumeric", new Object[]{nonceAsPlainText}, "Nonce token should have yielded a numeric first token, but was {0}"));
                   }

String expectedNonceSignature = DigestAuthUtils.md5Hex(this.nonceExpiryTime + ":" + entryPointKey);
                   if (!expectedNonceSignature.equals(nonceTokens[1])) {
                       throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[]{nonceAsPlainText}, "Nonce token compromised {0}"));
                   }
               }
           }
       } else {
           throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.missingMandatory", new Object[]{this.section212response}, "Missing mandatory digest value; received header {0}"));
       }
   }
//计算服务摘要
   String calculateServerDigest(String password, String httpMethod) {
   //生产摘要
       return DigestAuthUtils.generateDigest(DigestAuthenticationFilter.this.passwordAlreadyEncoded, this.username, this.realm, password, httpMethod, this.uri, this.qop, this.nonce, this.nc, this.cnonce);
   }
//判断随机数是否到期
   boolean isNonceExpired() {
       long now = System.currentTimeMillis();
       return this.nonceExpiryTime < now;
   }

String getUsername() {
       return this.username;
   }

String getResponse() {
       return this.response;
   }
}

来源:https://blog.csdn.net/qian1314520hu/article/details/125043179

标签:Spring,Security,HTTP,认证
0
投稿

猜你喜欢

  • C#观察者模式(Observer Pattern)实例教程

    2021-07-13 02:53:39
  • Java设计模式之抽象工厂模式实例详解

    2023-11-29 04:04:57
  • C#判断字符是否为汉字的三种方法分享

    2022-05-24 07:59:41
  • GSON实现Java对象与JSON格式对象相互转换的完全教程

    2023-11-23 09:23:37
  • JVM内存管理之JAVA语言的内存管理详解

    2021-11-01 12:00:10
  • Java代码实现矩形覆盖实例

    2022-02-04 22:02:02
  • java比较器comparator使用示例分享

    2022-07-18 22:45:15
  • Java实现带图形界面的聊天程序

    2022-04-25 22:41:55
  • spring mvc实现登录账号单浏览器登录

    2022-06-28 22:29:47
  • Java BufferWriter写文件写不进去或缺失数据的解决

    2023-07-20 14:57:02
  • java实现马踏棋盘的算法

    2023-11-29 17:00:29
  • Java线程池Executor用法详解

    2022-02-13 01:21:16
  • 微服务搭建集成Spring Cloud Turbine详解

    2023-03-29 12:39:07
  • Java面试题-实现复杂链表的复制代码分享

    2023-11-23 20:05:39
  • 梳理总结Java static关键字的方法作用

    2023-06-09 04:06:17
  • 详解Mybatis中的PooledDataSource

    2022-10-24 18:08:11
  • Flutter 剪裁组件的使用

    2023-06-18 13:15:04
  • 详解SpringBoot中Session超时原理说明

    2022-01-24 06:40:49
  • 如何使用两个栈实现队列Java

    2023-11-29 17:48:09
  • Java Socket通信之聊天室功能

    2022-02-04 11:47:35
  • asp之家 软件编程 m.aspxhome.com