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"后重定向到首页即可
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}'