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:资源服务器集合。

目前业务大致流程如下:

  1. Client发出请求至授权服务器获取token
  2. Clinet获取token后,携带token请求资源
  3. Gateway收到请求,分三步
    • 认证:对token解析校验
    • 鉴权:当前用户是否有权限获取目标资源
    • 转发:通过Repath Filter将请求转发到目标资源服务器
  4. 资源服务器进行业务处理

下面主要介绍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
  1. ServerAuthenticationFailureHandlerServerAuthenticationEntryPoint的区别就在于EntryPoint是未认证,产生的原因可能是converter没有返回对应的authentication,AuthenticationManager没有处理到,进入ServerAuthenticationEntryPoint
    FailureHanlder是鉴权失败,进入了AuthenticationManager,处理完成并抛出了Authentication异常,进入ServerAuthenticationFailureHandler

  2. ReactiveAuthorizationManager中的Authentication是由ReactiveAuthenticationManager传递的

image

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));
    }
}
posted @   AaronTanooo  阅读(381)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
点击右上角即可分享
微信分享提示