Spring Cloud Gateway интегрирует унифицированную аутентификацию Spring Security

Spring Cloud

представлять

Spring-Cloud-Gatewway

Spring Cloud Gateway основан наSpring Boot 2.x,Spring WebFluxа такжеProject Reactorпостроен. В результате многие из ваших знакомых библиотек синхронизации (например, Spring Data и Spring Security) и шаблоны могут не применяться при использовании Spring Cloud Gateway. Если вы не знакомы с этими проектами, рекомендуется прочитать их документацию, чтобы ознакомиться с некоторыми новыми концепциями, прежде чем использовать Spring Cloud Gateway.

Spring-Security

Spring Security — это фреймворк, обеспечивающий аутентификацию, авторизацию и защиту от распространенных атак. Благодаря первоклассной поддержке императивных и реактивных приложений он является стандартом де-факто для защиты приложений на основе Spring.

Spring-Webflux

Spring Web MVC, исходная веб-инфраструктура, включенная в Spring Framework, была создана специально для API сервлетов и контейнера сервлетов. Веб-фреймворк реактивного стека Spring WebFlux был добавлен позже в версии 5.0. Он полностью не блокирует и поддерживаетReactive StreamsПоддерживается и работает на таких серверах, как контейнеры Netty, Undertow и Servlet 3.1+.

Оба веб-фреймворка отражают имена своих исходных модулей (spring-webmvcа такжеspring-webflux) и сосуществуют в Spring Framework. Каждый модуль является необязательным. Приложение может использовать один модуль или два модуля, а в некоторых случаях и два модуля, например контроллеры Spring MVC с реагирующимиWebClient.

注意

Из-за разных веб-контейнеров WebFlux, используемый в проекте Gateway, нельзя смешивать с Spring-Web. Различия между Spring MVC и WebFlux:image.png


кодирование

Версия среды проекта

  1. Весеннее облако: 2020.0.1
  2. Spring-Boot: 2.4.3

градиентные зависимости

dependencies {
 implementation(
            'org.springframework.cloud:spring-cloud-starter-gateway',
            'org.springframework.boot:spring-boot-starter-security'
 )
}

Конфигурация Spring-Security

Параметры безопасности Spring должны быть настроены реактивным образом на основе реализации WebFilter в WebFlux, которая аналогична безопасности Spring MVC, реализованной через фильтр сервлетов, а также представляет собой цепочку фильтров, состоящую из ряда фильтров.

Reactor соответствует традиционной конфигурации MVC:

webflux mvc эффект
@EnableWebFluxSecurity @EnableWebSecurity Включить настройку безопасности
ServerAuthenticationSuccessHandler AuthenticationSuccessHandler Успешный вход в систему Обработчик
ServerAuthenticationFailureHandler AuthenticationFailureHandler Ошибка входа в систему Обработчик
ReactiveAuthorizationManager AuthorizationManager Управление сертификацией
ServerSecurityContextRepository SecurityContextHolder Управление хранилищем информации для аутентификации
ReactiveUserDetailsService UserDetailsService Логин пользователя
ReactiveAuthorizationManager AccessDecisionManager Управление аутентификацией
ServerAuthenticationEntryPoint AuthenticationEntryPoint Неаутентифицированный обработчик
ServerAccessDeniedHandler AccessDeniedHandler Обработчик неудачной аутентификации

1. Конфигурация ядра безопасности

package com.pluto.gateway.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.LinkedList;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 10:56
 * @description webflux security核心配置类
 */
@EnableWebFluxSecurity
public class WebfluxSecurityConfig {
    @Resource
    private DefaultAuthorizationManager defaultAuthorizationManager;
    
    @Resource
    private UserDetailsServiceImpl userDetailsServiceImpl;
    
    @Resource
    private DefaultAuthenticationSuccessHandler defaultAuthenticationSuccessHandler;
    
    @Resource
    private DefaultAuthenticationFailureHandler defaultAuthenticationFailureHandler;
    
    @Resource
    private TokenAuthenticationManager tokenAuthenticationManager;
    
    @Resource
    private DefaultSecurityContextRepository defaultSecurityContextRepository;
    
    @Resource
    private DefaultAuthenticationEntryPoint defaultAuthenticationEntryPoint;
    
    @Resource
    private DefaultAccessDeniedHandler defaultAccessDeniedHandler;
    
    /**
    * 自定义过滤权限
    */
    @Value("${security.noFilter}")
    private String noFilter;
     
     @Bean
     public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
        httpSecurity
                // 登录认证处理
                .authenticationManager(reactiveAuthenticationManager())
                .securityContextRepository(defaultSecurityContextRepository)
                // 请求拦截处理
                .authorizeExchange(exchange -> exchange
                        .pathMatchers(noFilter).permitAll()
                        .pathMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyExchange().access(defaultAuthorizationManager)
                )
                .formLogin()
                // 自定义处理
                .authenticationSuccessHandler(defaultAuthenticationSuccessHandler)
                        .authenticationFailureHandler(defaultAuthenticationFailureHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(defaultAuthenticationEntryPoint)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(defaultAccessDeniedHandler)
                .and()
                .csrf().disable()
        ;
        return httpSecurity.build();
     }
     
     /**
     * BCrypt密码编码
     */
     @Bean("passwordEncoder")
     public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
     }
     
     /**
     * 注册用户信息验证管理器,可按需求添加多个按顺序执行
     */
     @Bean
     ReactiveAuthenticationManager reactiveAuthenticationManager() {
            LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
         managers.add(authentication -> {
                    // 其他登陆方式 (比如手机号验证码登陆) 可在此设置不得抛出异常或者 Mono.error 
                    return Mono.empty();
         });
         // 必须放最后不然会优先使用用户名密码校验但是用户名密码不对时此 AuthenticationManager 会调用 Mono.error 造成后面的 AuthenticationManager 不生效
         managers.add(new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsServiceImpl));
         managers.add(tokenAuthenticationManager);
         return new DelegatingReactiveAuthenticationManager(managers);
     }
}

2. Аутентификация пользователя

package com.pluto.gateway.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.io.Serializable;
import java.util.Collection;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/10 13:15
 * @description 自定义用户信息
 */
public class SecurityUserDetails extends User implements Serializable {

    private Long userId;
    
    public SecurityUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, Long userId) {
        super(username, password, authorities);
        this.userId = userId;
    }
    
    public SecurityUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, Long userId) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        this.userId = userId;
    }
    
    public Long getUserId() {
        return userId;
    }
    
    public void setUserId(Long userId) {
        this.userId = userId;
    }
}

package com.pluto.gateway.security;

import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.ArrayList;

/**
 * @author ceshi
 * @date 2021/3/9 14:03
 * @description 用户登录处理
 * @version 1.0.0
 */@Service
public class UserDetailsServiceImpl implements ReactiveUserDetailsService {

    @Resource
    private PasswordEncoder passwordEncoder;
    
    @Override
    public Mono<UserDetails> findByUsername(String username) {
        SecurityUserDetails securityUserDetails = new SecurityUserDetails(
                    "user",
                    passwordEncoder.encode("user"),
                    true, true, true, true, new ArrayList<>(),
                    1L
        );
        return Mono.just(securityUserDetails);
    }
}

3.1 Пользовательский обработчик успешного входа в систему

package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.utils.JwtTokenUtil;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 15:00
 * @description 登录成功处理
 */
@Component
public class DefaultAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {

    /**
    * token 过期时间
    */
    @Value("${jwt.token.expired}")
    private int jwtTokenExpired;
    
    /**
    * 刷新token 时间
    */
    @Value("${jwt.token.refresh.expired}")
    private int jwtTokenRefreshExpired;
    
    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        return Mono.defer(() -> Mono.just(webFilterExchange.getExchange().getResponse()).flatMap(response -> {
            DataBufferFactory dataBufferFactory = response.bufferFactory();
            // 生成JWT token
            Map<String, Object> map = new HashMap<>(2);
            SecurityUserDetails userDetails = (SecurityUserDetails) authentication.getPrincipal();
            map.put("userId", userDetails.getUserId());
            map.put("username", userDetails.getUsername());
            map.put("roles",userDetails.getAuthorities());
            String token = JwtTokenUtil.generateToken(map, userDetails.getUsername(), jwtTokenExpired);
            String refreshToken = JwtTokenUtil.generateToken(map, userDetails.getUsername(), jwtTokenRefreshExpired);
            Map<String, Object> tokenMap = new HashMap<>(2);
            tokenMap.put("token", token);
            tokenMap.put("refreshToken", refreshToken);
            DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(ResultVoUtil.success(tokenMap)).getBytes());
            return response.writeWith(Mono.just(dataBuffer));
        }));
     }
}

3.2 Пользовательский обработчик входа в систему

package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import com.pluto.common.basic.vo.ResultVO;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Map;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 15:14
 * @description 登录失败处理
 */
@Component
public class DefaultAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {

    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
        return Mono.defer(() -> Mono.just(webFilterExchange.getExchange()
                                    .getResponse()).flatMap(response -> {
            DataBufferFactory dataBufferFactory = response.bufferFactory();
            ResultVO<Map<String, Object>> resultVO = ResultVoUtil.error();
            // 账号不存在
            if (exception instanceof UsernameNotFoundException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_NOT_EXIST);
                // 用户名或密码错误
            } else if (exception instanceof BadCredentialsException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.LOGIN_PASSWORD_ERROR);
                // 账号已过期
            } else if (exception instanceof AccountExpiredException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_EXPIRED);
                // 账号已被锁定
            } else if (exception instanceof LockedException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_LOCKED);
                // 用户凭证已失效
            } else if (exception instanceof CredentialsExpiredException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_CREDENTIAL_EXPIRED);
                // 账号已被禁用
            } else if (exception instanceof DisabledException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_DISABLE);
            }
            DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(resultVO).getBytes());
            return response.writeWith(Mono.just(dataBuffer));
        }));
    }
}

3.3 Настройка неаутентифицированного обработчика

package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 15:17
 * @description 未认证处理
 */
@Component
public class DefaultAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
        return Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(response -> {  
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                DataBufferFactory dataBufferFactory = response.bufferFactory();
                String result = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.USER_UNAUTHORIZED));
                DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
                                    Charset.defaultCharset()));
                return response.writeWith(Mono.just(buffer));
        });
    }
}

3.4 Пользовательский обработчик ошибок аутентификации

package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 11:12
 * @description 鉴权管理
 */
@Component
public class DefaultAccessDeniedHandler implements ServerAccessDeniedHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
        return Mono.defer(() -> Mono.just(exchange.getResponse()))
                .flatMap(response -> {
                    response.setStatusCode(HttpStatus.OK);
                    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                    DataBufferFactory dataBufferFactory = response.bufferFactory();
                    String result = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.PERMISSION_DENIED));
                    DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
                                    Charset.defaultCharset()));
                    return response.writeWith(Mono.just(buffer));
        });
    }
}

4. Настройте управление аутентификацией токена JWT.

package com.pluto.gateway.security;

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.List;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 16:27
 * @description 存储认证授权的相关信息
 */
@Component
public class DefaultSecurityContextRepository implements ServerSecurityContextRepository {

    public final static String TOKEN_HEADER = "Authorization";
    
    public final static String BEARER = "Bearer ";
    
    @Resource
    private TokenAuthenticationManager tokenAuthenticationManager;
    
    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
        return Mono.empty();
    }
    
    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        List<String> headers = request.getHeaders().get(TOKEN_HEADER);
        if (!CollectionUtils.isEmpty(headers)) {
            String authorization = headers.get(0);
            if (StringUtils.isNotEmpty(authorization)) {
                String token = authorization.substring(BEARER.length());
                if (StringUtils.isNotEmpty(token)) {
                    return tokenAuthenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(token, null)
                    ).map(SecurityContextImpl::new);
                }
            }
        }
        return Mono.empty();
    }
}
package com.pluto.gateway.security;

import com.pluto.common.basic.utils.JwtTokenUtil;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Collection;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 13:23
 * @description token 认证处理
 */
@Component
@Primary
public class TokenAuthenticationManager implements ReactiveAuthenticationManager {

    @Override
    @SuppressWarnings("unchecked")
    public Mono<Authentication> authenticate(Authentication authentication) {
        return Mono.just(authentication)
                .map(auth -> JwtTokenUtil.parseJwtRsa256(auth.getPrincipal().toString()))
                .map(claims -> {
                    Collection<? extends GrantedAuthority> roles = (Collection<? extends GrantedAuthority>)                     claims.get("roles");
                    return new UsernamePasswordAuthenticationToken(
                            claims.getSubject(),
                            null,
                            roles
                    );
                });
    }
}

5. Пользовательское управление аутентификацией

package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collection;

/**
 * @author ShiLei
 * @version 1.0.0
 * @date 2021/3/11 13:10
 * @description 用户权限鉴权处理
 */
@Component
@Slf4j
public class DefaultAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
        return authentication.map(auth -> {
            ServerWebExchange exchange = authorizationContext.getExchange();
            ServerHttpRequest request = exchange.getRequest();
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                String authorityAuthority = authority.getAuthority();
                String path = request.getURI().getPath();
                // TODO
                // 查询用户访问所需角色进行对比
                if (antPathMatcher.match(authorityAuthority, path)) {
                    log.info(String.format("用户请求API校验通过,GrantedAuthority:{%s}  Path:{%s} ", authorityAuthority, path));
                return new AuthorizationDecision(true);
                }
            }
                return new AuthorizationDecision(false);
        }).defaultIfEmpty(new AuthorizationDecision(false));
    }
    
    @Override
    public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
        return check(authentication, object)
                .filter(AuthorizationDecision::isGranted)
                .switchIfEmpty(Mono.defer(() -> {
                    String body = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.PERMISSION_DENIED));
                    return Mono.error(new AccessDeniedException(body));
                })).flatMap(d -> Mono.empty());
    }
}

Адрес репозитория Github (не забудьте переключиться на ветку dev-1.0.0)

Приветствую всех друзей, которые любят программирование, чтобы общаться и развиваться вместе, не забудьте нажать на коллекцию!