Java: Spring Cloud Gateway integration with Spring Security
基础概念
名词解释
- OAth2
- Spring Cloud Gateway:Spring Cloud Gateway是基于Spring Boot 2.x,Spring WebFlux和Project Reactor构建的。
- Spring Security:Spring Security是一个提供身份验证,授权和保护以防止常见攻击的框架。
- Spring Webflux:Spring框架中包含的原始Web框架Spring Web MVC是专门为Servlet API和Servlet容器而构建的。它是完全无阻塞的。
微服务认证方案
主要可以分为四个角色
- Client:需要请求服务资源
- Gateway:1、认证 2、鉴权 3、转发请求
- OAuth2.0授权服务:负责认证授权颁发令牌
- Microservices:资源服务器集合。
目前业务大致流程如下:
- Client发出请求至授权服务器获取token
- Clinet获取token后,携带token请求资源
- Gateway收到请求,分三步
- 认证:对token解析校验
- 鉴权:当前用户是否有权限获取目标资源
- 转发:通过Repath Filter将请求转发到目标资源服务器
- 资源服务器进行业务处理
下面主要介绍Gateway这个角色中的认证和鉴权这两步
WebFlux与WebMVC
WebFlux | WebMVC | 作用 |
---|---|---|
@EnableWebFluxSecurity | @EnableWebSecurity | 开启security配置 |
ReactiveAuthenticationManager | AuthorizationManager | 认证管理 |
ServerSecurityContextRepository | SecurityContextHolder | 认证信息存储管理 |
ServerAuthenticationEntryPoint | AuthenticationEntryPoint | 未认证Handler |
ServerAuthenticationSuccessHandler | AuthenticationSuccessHandler | 认证成功Handler |
ServerAuthenticationFailureHandler | AuthenticationFailureHandler | 认证失败Handler |
ReactiveUserDetailsService | UserDetailsService | 用户登录 |
ReactiveAuthorizationManager | AccessDecisionManager | 鉴权管理 |
ServerAccessDeniedHandler | AccessDeniedHandler | 鉴权失败Handler |
-
ServerAuthenticationFailureHandler
与ServerAuthenticationEntryPoint
的区别就在于EntryPoint
是未认证,产生的原因可能是converter没有返回对应的authentication,AuthenticationManager
没有处理到,进入ServerAuthenticationEntryPoint
FailureHanlder
是鉴权失败,进入了AuthenticationManager
,处理完成并抛出了Authentication异常,进入ServerAuthenticationFailureHandler
-
ReactiveAuthorizationManager
中的Authentication
是由ReactiveAuthenticationManager
传递的
Security核心配置
@Configuration
@EnableWebFluxSecurity
@Slf4j
public class WebFluxSecurityConfiguration {
@Resource
private MallAppSecurityProperties securityProperties;
@Resource
private OpenApiAuthorizationManager openApiAuthorizationManager;
@Bean
@RefreshScope
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
JwtAccessTokenProperties properties) {
// disable csrf & formLogin
http.formLogin().disable()
.httpBasic().disable()
.csrf().disable();
return http
// JWT access token filter
.addFilterAfter(jwtAuthenticationFilter(properties), SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange(exchange -> exchange
// 配置白名单,顺序不能反
.pathMatchers(securityProperties.getAuthWhiteList()).permitAll()
// 配置自定义鉴权Manger
.anyExchange().access(openApiAuthorizationManager))
// 声明异常处理,1 AuthenticationEntryPoint 2 AuthenticationFailureHandler
.exceptionHandling()
.authenticationEntryPoint(new JwtServerAuthenticationEntryPoint())
.accessDeniedHandler((exchange, denied) -> {
// 403 鉴权失败简单实现
exchange.getResponse()
.setStatusCode(HttpStatus.FORBIDDEN);
return Mono.empty();
})
.and().build();
}
/**
* create JWT access-token authentication filter instance
*/
private AuthenticationWebFilter jwtAuthenticationFilter(JwtAccessTokenProperties properties) {
final AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(new JwtAuthenticationManager(properties));
authenticationFilter.setServerAuthenticationConverter(new BearerTokenServerAuthenticationConverter());
authenticationFilter.setSecurityContextRepository(NoOpServerSecurityContextRepository.getInstance());
authenticationFilter.setAuthenticationSuccessHandler(new JwtServerAuthenticationSuccessHandler());
authenticationFilter.setAuthenticationFailureHandler(new JwtServerAuthenticationFailureHandler());
return authenticationFilter;
}
}
自定义认证Manager
@Slf4j
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
private final JwtAccessTokenProperties properties;
public JwtAuthenticationManager(JwtAccessTokenProperties properties) {
this.properties = properties;
}
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
if (!(authentication instanceof BearerTokenAuthenticationToken)) {
return Mono.empty();
}
final BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
try {
final SignedJWT signedJwt = SignedJWT.parse(token.getToken());
// debug log
log.debug("sign-check-enabled:{}", properties.getSignCheckEnabled());
if (properties.getSignCheckEnabled()) {
final JWSVerifier verifier = new MACVerifier(properties.getSecret().getBytes(StandardCharsets.UTF_8));
// validate signature
if (!signedJwt.verify(verifier)) {
throw new BadCredentialsException("Signature of token is invalid.", new BizException(ErrorCode.INVALID_SIGNATURE));
}
}
if (properties.getExpirationCheckEnabled()) {
// validate expiration time
final Date expirationTime = signedJwt.getJWTClaimsSet().getExpirationTime();
// debug log
log.debug("expiredTime:{}", expirationTime);
if (expirationTime == null) {
throw new BadCredentialsException("exp claim is required.", new BizException(ErrorCode.EXP_CLAIM_REQUIRED));
}
boolean isExpired = expirationTime.before(new Date());
if (isExpired) {
throw new BadCredentialsException("Token is expired.", new BizException(ErrorCode.TOKEN_HAS_EXPIRED));
}
}
final JwtAuthenticationToken jwtToken = new JwtAuthenticationToken(signedJwt, properties.getClaims());
// log
log.info("Information of JWT token. claims:{}", properties.getClaims());
// 利用这一点如果有多认证Manger的请况下,只要看当前authenticate是否为true,如果已认证,就可以跳过别的认证Manger
jwtToken.setAuthenticated(true);
return Mono.just(jwtToken);
} catch (JOSEException | ParseException ex) {
// error log
log.error("Error occurred.", ex);
throw new BadCredentialsException("Token is invalid. " + ex.getMessage(),
new BizException(ErrorCode.INVALID_JWT_TOKEN));
}
}
}
自定义认证成功Handler
public class JwtServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private final static String HEADER_AUTHORIZATION = "Authorization";
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
if (!(authentication instanceof JwtAuthenticationToken)) {
return webFilterExchange.getChain()
.filter(webFilterExchange.getExchange());
}
final JwtAuthenticationToken token = (JwtAuthenticationToken) authentication;
ServerHttpRequest request = webFilterExchange.getExchange().getRequest();
ServerHttpRequest.Builder reqBuilder = request.mutate();
// remove request header Authorization
reqBuilder.headers(httpHeaders -> httpHeaders.remove(HEADER_AUTHORIZATION));
// 把解析出来的token信息直接放进header里,资源服务器可以直接获取
token.getClaims().forEach(claim -> reqBuilder.header(claim.getHeader(), claim.getValue()));
ServerHttpRequest req = reqBuilder.build();
webFilterExchange.getExchange().mutate().request(req).build();
return webFilterExchange.getChain()
.filter(webFilterExchange.getExchange());
}
}
自定义认证失败Handler
@Slf4j
public class JwtServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
final ServerWebExchange exchange = webFilterExchange.getExchange();
if (!(exception instanceof BadCredentialsException)) {
return defaultResponse(exchange);
}
final ErrorMessage msg = getErrorMessage(exception);
try {
final ObjectMapper mapper = new ObjectMapper();
final String body = mapper.writeValueAsString(msg);
return Mono.defer(() -> Mono.just(exchange.getResponse())
.flatMap(response -> writeErrorMessage(body, response)));
} catch (Exception e) {
// error log
log.error("Error occurred.", e);
return defaultResponse(exchange);
}
}
未认证Handler
@Slf4j
public class JwtServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
// 401
exchange.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);
// content-type
exchange.getResponse().getHeaders()
.setContentType(MediaType.APPLICATION_JSON);
try {
ObjectMapper mapper = new ObjectMapper();
final String body = mapper.writeValueAsString(msg);
return Mono.defer(() -> Mono.just(exchange.getResponse())
.flatMap(response -> {
DataBufferFactory dataBufferFactory = response.bufferFactory();
DataBuffer dataBuffer = dataBufferFactory.wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(dataBuffer));
}));
} catch (Exception e) {
// error log
log.error("Error occurred.", e);
return Mono.empty();
}
}
}
自定义鉴权 Manager
@Component
@Slf4j
public class OpenApiAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Resource
private OpenApiAuthConfiguration openApiAuthConfiguration;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {
Boolean enable = openApiAuthConfiguration.getEnable();
if (!enable) {
return Mono.just(new AuthorizationDecision(true));
}
return authentication
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器