Spring全家桶-Spring Security之HTTP认证

2022年6月5日09:36:35

Spring全家桶-Spring Security之HTTP认证

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



前言

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


一、HTTP基本认证是什么?

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

二、HTTP基本认证流程

  1. 客户端发起一条没有携带认证信息的请求。
  2. 服务器返回一条401 Unauthorized响应, 并在WWW-Authentication首部说明认证形式, 当进行HTTP基本认证时, WWW-Authentication会被设置为Basic realm=“被保护页面”。
  3. 客户端收到401 Unauthorized 响应后, 弹出对话框, 询问用户名和密码。 当用户完成后, 客户端将用户名和密码使用冒号拼接并编码为Base64形式, 然后放入请求的Authorization首部发送给服务器。
  4. 服务器解码得到客户端发来的用户名和密码,并在验证它们是正确的之后,返回客户端请求的报文
    Spring全家桶-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

@EnableWebSecuritypublicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
        http.authorizeRequests().anyRequest().authenticated().and().httpBasic();}}

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

3.运行项目

访问本地项目,http://localhost:8080
Spring全家桶-Spring Security之HTTP认证
会弹出登陆框,我们看到调试工具中返回了401无权限。
Spring全家桶-Spring Security之HTTP认证
我们使用Spring Security提供的默认的用户名和密码登陆。
Spring全家桶-Spring Security之HTTP认证
登陆成功后,header中就会有Authorization: Basic dXNlcjo0NWU2NzViOC1hZGYwLTQzNzMtYjA2MS02MGE0YzkzZjA2ZGU=

二.Spring Security HTTP基本认证原理

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

publicHttpBasicConfigurer<HttpSecurity>httpBasic()throwsException{return(HttpBasicConfigurer)this.getOrApply(newHttpBasicConfigurer());}

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

//构建HttpBasicConfigurerpublicHttpBasicConfigurer(){this.realmName("Realm");LinkedHashMap<RequestMatcher,AuthenticationEntryPoint> entryPoints=newLinkedHashMap();
        entryPoints.put(X_REQUESTED_WITH,newHttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));DelegatingAuthenticationEntryPoint defaultEntryPoint=newDelegatingAuthenticationEntryPoint(entryPoints);
        defaultEntryPoint.setDefaultEntryPoint(this.basicAuthEntryPoint);this.authenticationEntryPoint= defaultEntryPoint;}//进行配置publicvoidconfigure(B http){//进行认证管理AuthenticationManager authenticationManager=(AuthenticationManager)http.getSharedObject(AuthenticationManager.class);//声明basic认证拦截器BasicAuthenticationFilter basicAuthenticationFilter=newBasicAuthenticationFilter(authenticationManager,this.authenticationEntryPoint);if(this.authenticationDetailsSource!=null){
            basicAuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);}//注册一个RememberMeServicesRememberMeServices 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:

protectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throwsIOException,ServletException{try{//获取tokenUsernamePasswordAuthenticationToken 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返回进行响应的处理

publicvoidcommence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException)throwsIOException{//添加响应响应头
        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全家桶-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类:

@EnableWebSecuritypublicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateDigestAuthenticationEntryPoint digestAuthenticationEntryPoint;@AutowiredprivateUserDetailsService userDetailsService;@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
        http.authorizeRequests().anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(digestAuthenticationEntryPoint).and().addFilter(digestAuthenticationFilter());}publicDigestAuthenticationFilterdigestAuthenticationFilter(){DigestAuthenticationFilter digestAuthenticationFilter=newDigestAuthenticationFilter();
        digestAuthenticationFilter.setUserDetailsService(userDetailsService);
        digestAuthenticationFilter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint);return digestAuthenticationFilter;}}

申明DigestAuthenticationEntryPointBean:

@BeanpublicDigestAuthenticationEntryPointdigestAuthenticationEntryPoint(){DigestAuthenticationEntryPoint digestAuthenticationEntryPoint=newDigestAuthenticationEntryPoint();
        digestAuthenticationEntryPoint.setRealmName("realName");
        digestAuthenticationEntryPoint.setKey("tony");return digestAuthenticationEntryPoint;}@BeanpublicDigestAuthenticationEntryPointdigestAuthenticationEntryPoint(){DigestAuthenticationEntryPoint digestAuthenticationEntryPoint=newDigestAuthenticationEntryPoint();
        digestAuthenticationEntryPoint.setRealmName("realm");
        digestAuthenticationEntryPoint.setKey("tony");return digestAuthenticationEntryPoint;}@BeanpublicUserDetailsServiceuserDetailsService(){InMemoryUserDetailsManager manager=newInMemoryUserDetailsManager();
        manager.createUser(User.withUsername("tony").password("123456").roles("admin").build());return manager;}@BeanpublicPasswordEncoderpasswordEncoder(){returnNoOpPasswordEncoder.getInstance();}

运行项目

访问主页,http://localhost:8080,返回如下页面:
Spring全家桶-Spring Security之HTTP认证
我们输入用户名和密码登陆。
Spring全家桶-Spring Security之HTTP认证

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

publicvoidafterPropertiesSet(){Assert.hasLength(this.realmName,"realmName must be specified");Assert.hasLength(this.key,"key must be specified");}publicvoidcommence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException)throwsIOException{//计算过期时间long expiryTime=System.currentTimeMillis()+(long)(this.nonceValiditySeconds*1000);//计算签名值String signatureValue=DigestAuthUtils.md5Hex(expiryTime+":"+this.key);//随机字符串String nonceValue= expiryTime+":"+ signatureValue;//随机字符串base64String nonceValueBase64=newString(Base64.getEncoder().encode(nonceValue.getBytes()));String authenticateHeader="Digest realm=\""+this.realmName+"\", qop=\"auth\", nonce=\""+ nonceValueBase64+"\"";if(authExceptioninstanceofNonceExpiredException){
            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进行处理

publicvoidafterPropertiesSet(){//必须设置userDetailsServiceAssert.notNull(this.userDetailsService,"A UserDetailsService is required");//必须设置authenticationEntryPointAssert.notNull(this.authenticationEntryPoint,"A DigestAuthenticationEntryPoint is required");}privatevoiddoFilter(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throwsIOException,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=newDigestAuthenticationFilter.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){thrownewAuthenticationServiceException("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"
  • 作者:Tony-devj
  • 原文链接:https://blog.csdn.net/qian1314520hu/article/details/125043179
    更新时间:2022年6月5日09:36:35 ,共 11260 字。