security在响应式与servlet下登出的区别处理

123 阅读3分钟

1、gateway 配置与 server 区别

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-security-oauth2-client</artifactId>
            <groupId>org.springframework.security</groupId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
    <version>6.0.2</version>
    <scope>compile</scope>
</dependency>

依赖的区别主要是网关多了

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

2、配置核心

gateway配置


import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseCookie;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Mono;

import java.time.Duration;

import static org.springframework.security.config.Customizer.withDefaults;

/**
 * @author Joe Grandja
 * @since 0.0.1
 */
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@Slf4j
public class OAuth2LoginSecurityConfig {

   @Resource
   MyMatcher myMatcher;
   /**
    * 默认的白名单,结合数据库存储的
    */
   private static final String[] AUTH_WHITELIST = new String[]{
         "/actuator/health",
         // outh2user 信息端点
         "/api/user/info",
         "/login/**",
         "/logout/**",

         "/websocket/**",
         // swagger 相关
         "/favicon.ico",
         "/v3/api-docs/**",
         "/*/v3/api-docs",
         "/webjars/css/*.css",
         "/webjars/js/*.js",
         "/swagger-ui/**l",
//       "/doc.html"
   };

   @Bean
   public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
      http
         .authorizeExchange(authorize ->{
            authorize.matchers(myMatcher).permitAll();
            authorize
                  .pathMatchers(AUTH_WHITELIST).permitAll()
                  // 下面不能注释, 否者会报 Access Deny
                  .anyExchange().authenticated();
            }
         )
         .oauth2Login(withDefaults())
         .logout(fromLogout -> {
            // post 定义退出的端点 前端收到200响应后通过window.location.href 定向到首页,会自动跳转到登入中心
            fromLogout.logoutUrl("/logout")
            .logoutHandler(new ServerLogoutHandler() {
               @Override
               public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
                  return exchange.getExchange().getSession().flatMap(webSession -> {
                     MultiValueMap<String, ResponseCookie> cookiesMap = exchange.getExchange().getResponse().getCookies();
                     cookiesMap.add("JSESSIONID", ResponseCookie.from("JSESSIONID").maxAge(Duration.ZERO).httpOnly(true).path("/").sameSite("Lax").build());
                     cookiesMap.add("SESSION", ResponseCookie.from("SESSION").maxAge(Duration.ZERO).httpOnly(true).path("/").sameSite("Lax").build());

                     // 取消认证
                     SecurityContext context = SecurityContextHolder.getContext();
                     context.setAuthentication(null);
                     // 清理上下文
                     SecurityContextHolder.clearContext();
                     ReactiveSecurityContextHolder.clearContext();
                     return webSession.invalidate();
                  });
               }
            })
            ;
         })
         .cors().disable();

      http.csrf(csrf -> csrf.disable());
      return http.build();
   }

}

白名单及扩展的匹配器都会有效,退出登录也一样会把cookie 置失效 servlet端配置

/*
 * Copyright 2020-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseCookie;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.time.Duration;

import static org.springframework.security.config.Customizer.withDefaults;

/**
 * @author Joe Grandja
 * @since 0.0.1
 */
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@Slf4j
public class OAuth2LoginSecurityConfig {

   /**
    * 默认的白名单,结合数据库存储的
    */
   private static final String[] AUTH_WHITELIST = new String[]{
         "/actuator/health",
         // outh2user 信息端点
         "/api/user/info",

         // swagger 相关
         "/favicon.ico",
         "/v3/api-docs/**",
         "/*/v3/api-docs",
         "/webjars/css/*.css",
         "/webjars/js/*.js",
         "/swagger-ui/**l",
         "/doc.html"
   };

   /**
    * 非响应式的白名单此处有效
    * @return
    */
   @Bean
   WebSecurityCustomizer webSecurityCustomizer() {
      return web -> web.ignoring().requestMatchers(AUTH_WHITELIST);
   }


// @Bean
// SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//    http
//       .logout()
//       .invalidateHttpSession(true)
//       .logoutSuccessHandler(new LogoutSuccessHandler() {
//          @Override
//          public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//             System.out.println();
//          }
//       });
////            .authorizeHttpRequests(authorize ->
////                  authorize.anyRequest().authenticated()
////            )
////            .oauth2Login(oauth2Login ->
////                  oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
////            .oauth2Client(withDefaults());
//    return http.build();
// }

   @Bean
   public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
      http
         // ReactiveClientRegistrationRepository bean 找不到的处理办法
         .oauth2Login(
            oAuth2LoginSpec -> {
               oAuth2LoginSpec.clientRegistrationRepository(new ReactiveClientRegistrationRepository() {
                  @Override
                  public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
                     return null;
                  }
               });
            }
         )
         // 无效的
         .logout(fromLogout -> {
            ....
          })
         .cors().disable();

      http.csrf(csrf -> csrf.disable());
      return http.build();
   }

}

servelet 的白名单通过WebSecurityCustomizer 开放SecurityFilterChain不能做定义登出端点用默认的

CsrfFilter 要diy开放post支持

private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
    private final HashSet<String> allowedMethods = new HashSet(Arrays.asList("GET", "DELETE", "PUT", "POST", "HEAD", "TRACE", "OPTIONS"));
    ...

3、servlet 登出成功后清除cookie配置

package org.springframework.security.web.authentication.logout;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import java.util.UUID;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler;

/**
 * @author liangguohun
 * @todo
 * @date
 **/
public class SimpleUrlLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {
    public SimpleUrlLogoutSuccessHandler() {
    }

    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        Cookie JSESSIONID = new Cookie("JSESSIONID", UUID.randomUUID().toString());
        JSESSIONID.setPath("/");
        JSESSIONID.setHttpOnly(true);
        JSESSIONID.setMaxAge(Duration.ZERO.getNano());
        response.addCookie(JSESSIONID);

        Cookie AfterAuth = new Cookie("AfterAuth", UUID.randomUUID().toString());
        AfterAuth.setPath("/");
        AfterAuth.setHttpOnly(true);
        AfterAuth.setMaxAge(Duration.ZERO.getNano());
        response.addCookie(AfterAuth);

        super.handle(request, response, authentication);
    }
}

4、访问Nginx配置

        # 访问后端接口或退出
        location ~ /(api|logout) {
            proxy_pass        http://172.31.100.111:8989;
            proxy_http_version 1.1;
            proxy_read_timeout   3600s;
            proxy_set_header Host $host:$server_port;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            # websocket
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }

5、登出前端细节

post "logout" 后响应 清除cookie 前端监听到重定向到"login?logout"后重定向到首页即可

1691552362516.png

6、调用链区别如下

servlet

   o.s.security.web.FilterChainProxy [223]- Securing POST /logout
       o.s.s.w.a.logout.LogoutFilter [101]- Logging out [null]
   o.s.s.web.DefaultRedirectStrategy [56]- Redirecting to /login?logout
w.s.m.m.a.HttpEntityMethodProcessor [275]- Using 'application/vnd.spring-boot.actuator.v3+json', given [text/plain, text/*, */*] and supported [application/vnd.spring-boot.actuator.v3+json, application/vnd.spring-boot.actuator.v2+json, application/json]
  o.s.web.servlet.DispatcherServlet [1128]- Completed 200 OK, headers={masked}

gateway

op-5-1] tternParserServerWebExchangeMatcher [95]- Checking match of request : '/logout'; against '/logout'
-5-1] .w.s.u.m.OrServerWebExchangeMatcher [60]- matched
-5-1] sionServerSecurityContextRepository [89]- Found SecurityContext 'SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [admin], Granted Authorities: [[OAUTH2_USER, SCOPE_haas.read, SCOPE_haas.write, SCOPE_message.read]], User Attributes: [{userValid=true, wxworkId=null, dingtalkId=null, loginType=null, loginName=admin, userPhone=19176161894, userMail=null, name=admin, remark=null, userName=admin, userId=1}], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[OAUTH2_USER, SCOPE_haas.read, SCOPE_haas.write, SCOPE_message.read]]]' in WebSession: 'org.springframework.session.web.server.session.SpringSessionWebSessionStore$SpringSessionWebSession@71f5d552'
-5-1]  o.s.s.w.s.a.logout.LogoutWebFilter [74]- Logging out user 'OAuth2AuthenticationToken 
[Principal=Name: [admin], Granted Authorities: [[OAUTH2_USER, ... ]]' and transferring to logout destination
-5-1] s.w.s.DefaultServerRedirectStrategy [54]- Redirecting to '/login?logout'
-5-1]  o.s.w.s.s.DefaultWebSessionManager [112]- WebSession expired or has been invalidated
tternParserServerWebExchangeMatcher [87]- Request 'GET /login' doesn't match 'null /oauth2/authorization/{registrationId}'
tternParserServerWebExchangeMatcher [87]- Request 'GET /login' doesn't match 'null /login/oauth2/code/{registrationId}'
.w.s.u.m.OrServerWebExchangeMatcher [57]- Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/login', method=GET}
tternParserServerWebExchangeMatcher [95]- Checking match of request : '/login'; against '/login'
.w.s.u.m.OrServerWebExchangeMatcher [60]- matched
tternParserServerWebExchangeMatcher [87]- Request 'GET /' doesn't match 'null /oauth2/authorization/{registrationId}'