introduce

Spring-Cloud-Gatewway

The Spring Cloud Gateway is built based on Spring Boot 2.x, Spring WebFlux, and Project Reactor. As a result, many of the familiar synchronization libraries (for example, Spring Data and Spring Security) and patterns may not apply when you use the Spring Cloud Gateway. If you are not familiar with these projects, it is recommended that you read their documentation to familiarize yourself with some of the new concepts before using the Spring Cloud Gateway.

Spring-Security

Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first-class support for imperative and responsive applications, it is the de facto standard for securing Spring-based applications.

Spring-Webflux

The original Web framework included in the Spring Framework, Spring Web MVC, was built specifically for the Servlet API and Servlet container. Spring WebFlux, a responsive stack Web framework, was added later in version 5.0. It is completely non-blocking, supports Reactive Streams backpressure, and runs on servers such as Netty, Undertow, and Servlet 3.1+ containers.

Both Web frameworks reflect the names of their source modules (Spring-WebMVC and Spring-WebFlux) and coexist in the Spring Framework. Each module is optional. Applications can use one module, two modules, or in some cases, two modules, such as the Spring MVC controller WebClient with React.

Pay attention to

The WebFlux used in the Gateway project cannot be mixed with Spring-Web because of the different Web containers. Differences between Spring MVC and WebFlux:


coding

Project Environment Version

  1. Spring – Cloud: 2020.0.1
  2. Spring – the Boot: 2.4.3

Gradle rely on

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

Spring ws-security configuration

Spring Security should be configured in a responsive manner, based on WebFilter in WebFlux. Similar to spring MVC’s security is implemented through Filter of Servlet, it is also a Filter chain composed of a series of filters.

Reactor corresponds to a traditional MVC configuration:

webflux mvc role
@EnableWebFluxSecurity @EnableWebSecurity Enabling the Security Configuration
ServerAuthenticationSuccessHandler AuthenticationSuccessHandler Login successful Handler
ServerAuthenticationFailureHandler AuthenticationFailureHandler Logon failure Handler
ReactiveAuthorizationManager AuthorizationManager Authentication management
ServerSecurityContextRepository SecurityContextHolder Authentication information storage management
ReactiveUserDetailsService UserDetailsService The user login
ReactiveAuthorizationManager AccessDecisionManager Authentication management
ServerAuthenticationEntryPoint AuthenticationEntryPoint No authentication Handler
ServerAccessDeniedHandler AccessDeniedHandler Authentication failed Handler

1. Security core configuration

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
 * @descriptionWebflux Security core configuration class */
@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;
    
    /** * Custom filtering permission */
    @Value("${security.noFilter}")
    private String noFilter;
     
     @Bean
     public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
        httpSecurity
                // Login authentication processing
                .authenticationManager(reactiveAuthenticationManager())
                .securityContextRepository(defaultSecurityContextRepository)
                // Request interception processing
                .authorizeExchange(exchange -> exchange
                        .pathMatchers(noFilter).permitAll()
                        .pathMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyExchange().access(defaultAuthorizationManager)
                )
                .formLogin()
                // Custom processing
                .authenticationSuccessHandler(defaultAuthenticationSuccessHandler)
                        .authenticationFailureHandler(defaultAuthenticationFailureHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(defaultAuthenticationEntryPoint)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(defaultAccessDeniedHandler)
                .and()
                .csrf().disable()
        ;
        return httpSecurity.build();
     }
     
     /** * BCrypt */
     @Bean("passwordEncoder")
     public PasswordEncoder passwordEncoder(a) {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
     }
     
     /** * Register user information validation manager, can be added as required in order to execute */
     @Bean
     ReactiveAuthenticationManager reactiveAuthenticationManager(a) {
            LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
         managers.add(authentication -> {
                    // Other login methods (such as mobile verification code login) can be set here not to throw exceptions or mono.error
                    return Mono.empty();
         });
         If the user name and password are incorrect, this AuthenticationManager will call Mono.error and the following AuthenticationManager will not take effect
         managers.add(new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsServiceImpl));
         managers.add(tokenAuthenticationManager);
         return newDelegatingReactiveAuthenticationManager(managers); }}Copy the code

2. User authentication

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
 * @descriptionCustomize user information */
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(a) {
        return userId;
    }
    
    public void setUserId(Long userId) {
        this.userId = userId; }}Copy the code
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
 * @descriptionUser login processing *@version1.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
        );
        returnMono.just(securityUserDetails); }}Copy the code

3.1 Customizing successful Login Handler

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
 * @descriptionSuccessful login */
@Component
public class DefaultAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {

    /** * Token expiration time */
    @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();
            // Generate a 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());
            returnresponse.writeWith(Mono.just(dataBuffer)); })); }}Copy the code

3.2 User-defined Login Failure Handler

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
 * @dateWholly 2021/3/11 *@descriptionLogin failure processing */
@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();
            // The account does not exist
            if (exception instanceof UsernameNotFoundException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_NOT_EXIST);
                // The username or password is incorrect
            } else if (exception instanceof BadCredentialsException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.LOGIN_PASSWORD_ERROR);
                // The account has expired
            } else if (exception instanceof AccountExpiredException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_EXPIRED);
                // The account is locked
            } else if (exception instanceof LockedException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_LOCKED);
                // The user credential is invalid
            } else if (exception instanceof CredentialsExpiredException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_CREDENTIAL_EXPIRED);
                // The account has been disabled
            } else if (exception instanceof DisabledException) {
                resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_DISABLE);
            }
            DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(resultVO).getBytes());
            returnresponse.writeWith(Mono.just(dataBuffer)); })); }}Copy the code

3.3 Customizing an Unauthenticated Handler

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
 * @dateBetter 2021/3/11 *@descriptionUnauthenticated processing */
@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()));
                returnresponse.writeWith(Mono.just(buffer)); }); }}Copy the code

3.4 User-defined Authentication Failed Handler

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
 * @date2021/3/11 and *@descriptionAuthentication Management */
@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()));
                    returnresponse.writeWith(Mono.just(buffer)); }); }}Copy the code

4. Customize JWT Token authentication management

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
 * @descriptionStorage authentication and authorization information */
@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); }}}returnMono.empty(); }}Copy the code
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
 * @date2021/3/11 notice *@descriptionToken authentication processing */
@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 ); }); }}Copy the code

5. Customize authentication management

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
 * @date2021/3/11 10 *@descriptionUser permission authentication */
@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
                // Query roles required for user access for comparison
                if (antPathMatcher.match(authorityAuthority, path)) {
                    log.info(String.format(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(newAccessDeniedException(body)); })).flatMap(d -> Mono.empty()); }}Copy the code

Github repository address (remember to switch to dev-1.0.0 branch)

Welcome you who love programming to communicate and improve together, remember to click on the collection oh!