spring gateway集成spring security

spring gateway

分布式开发时,微服务会有很多,但是网关是请求的第一入口,所以一般会把客户端请求的权限验证统一放在网关进行认证与鉴权。SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

注意:

由于web容器不同,在gateway项目中使用的webflux,是不能和spring-web混合使用的。

Spring MVC和WebFlux的区别
11772383-b70d80a3893f3a04.png

依赖:

        
            org.springframework.cloud
            spring-cloud-starter-security
        

        
            org.springframework.cloud
            spring-cloud-starter-gateway
        

配置spring security

spring security设置要采用响应式配置,基于WebFlux中WebFilter实现,与Spring MVC的Security是通过Servlet的Filter实现类似,也是一系列filter组成的过滤链。

  1. 部分概念是对应的:
Reactive Web
@EnableWebFluxSecurity @EnableWebSecurity
ReactiveSecurityContextHolder SecurityContextHolder
AuthenticationWebFilter FilterSecurityInterceptor
ReactiveAuthenticationManager AuthenticationManager
ReactiveUserDetailsService UserDetailsService
ReactiveAuthorizationManager AccessDecisionManager
  1. 首先需要配置@EnableWebFluxSecurity注解,开启Spring WebFlux Security的支持
import java.util.LinkedList;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;

/**
* @Author: pilsy
* @Date: 2020/6/29 0029 16:54
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

   @Autowired
   private AuthenticationConverter authenticationConverter;

   @Autowired
   private AuthorizeConfigManager authorizeConfigManager;

   @Autowired
   private AuthEntryPointException serverAuthenticationEntryPoint;

   @Autowired
   private JsonServerAuthenticationSuccessHandler jsonServerAuthenticationSuccessHandler;

   @Autowired
   private JsonServerAuthenticationFailureHandler jsonServerAuthenticationFailureHandler;

   @Autowired
   private JsonServerLogoutSuccessHandler jsonServerLogoutSuccessHandler;

   @Autowired
   private AuthenticationManager authenticationManager;

   private static final String[] AUTH_WHITELIST = new String[]{"/login", "/logout"};

   @Bean
   public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
       SecurityWebFilterChain chain = http.formLogin()
               .loginPage("/login")
               // 登录成功handler
               .authenticationSuccessHandler(jsonServerAuthenticationSuccessHandler)
               // 登陆失败handler
               .authenticationFailureHandler(jsonServerAuthenticationFailureHandler)
               // 无访问权限handler
               .authenticationEntryPoint(serverAuthenticationEntryPoint)
               .and()
               .logout()
               // 登出成功handler
               .logoutSuccessHandler(jsonServerLogoutSuccessHandler)
               .and()
               .csrf().disable()
               .httpBasic().disable()
               .authorizeExchange()
               // 白名单放行
               .pathMatchers(AUTH_WHITELIST).permitAll()
               // 访问权限控制
               .anyExchange().access(authorizeConfigManager)
               .and().build();
       // 设置自定义登录参数转换器
       chain.getWebFilters()
               .filter(webFilter -> webFilter instanceof AuthenticationWebFilter)
               .subscribe(webFilter -> {
                   AuthenticationWebFilter filter = (AuthenticationWebFilter) webFilter;
                   filter.setServerAuthenticationConverter(authenticationConverter);
               });
       return chain;
   }

   /**
    * 注册用户信息验证管理器,可按需求添加多个按顺序执行
    * @return 
    */
   @Bean
   ReactiveAuthenticationManager reactiveAuthenticationManager() {
       LinkedList managers = new LinkedList<>();
       managers.add(authenticationManager);
       return new DelegatingReactiveAuthenticationManager(managers);
   }


   /**
    * BCrypt密码编码
    * @return 
    */
   @Bean
   public BCryptPasswordEncoder bcryptPasswordEncoder() {
       return new BCryptPasswordEncoder();
   }

}
  1. 特殊handler的实现
  • JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import io.netty.util.CharsetUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
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;

/**
 * @Author: pilsy
 * @Date: 2020/6/29 0029 17:39
 */
@Component
public class JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {

    @Override
    public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        // 登录成功后可以放入一些参数到session中
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        String body = JSONObject.toJSONString(AjaxResult.ok("登录成功!"));
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

  • JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
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;

/**
 * @Author: pilsy
 * @Date: 2020/6/29 0029 17:44
 */
@Component
public class JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {

    private static final String USER_NOT_EXISTS = "用户不存在!";

    private static final String USERNAME_PASSWORD_ERROR = "用户密码错误!";

    private static final String USER_LOCKED = "用户锁定!";

    @Override
    public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        if (exception instanceof UsernameNotFoundException) {
            return writeErrorMessage(response, USER_NOT_EXISTS);
        } else if (exception instanceof BadCredentialsException) {
            return writeErrorMessage(response, USERNAME_PASSWORD_ERROR);
        } else if (exception instanceof LockedException) {
            return writeErrorMessage(response, USER_LOCKED);
        }
        return writeErrorMessage(response, exception.getMessage());
    }

    private Mono writeErrorMessage(ServerHttpResponse response, String message) {
        String result = JSONObject.toJSONString(AjaxResult.restResult(message, ApiErrorCode.FAILED));
        DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

  • JsonServerLogoutSuccessHandler implements ServerLogoutSuccessHandler
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;

import io.netty.util.CharsetUtil;
import reactor.core.publisher.Mono;

/**
 * @Author: pilsy
 * @Date: 2020/7/10 0010 15:05
 */
@Component
public class JsonServerLogoutSuccessHandler implements ServerLogoutSuccessHandler {
    @Override
    public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
        ServerHttpResponse response = exchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        String result = JSONObject.toJSONString(AjaxResult.restResult("注销成功", ApiErrorCode.SUCCESS));
        DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}
  • AuthEntryPointException implements ServerAuthenticationEntryPoint
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
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.Flux;
import reactor.core.publisher.Mono;

/**
 * 无访问权限的返回结果
 *
 * @author pilsy
 */
@Component
public class AuthEntryPointException implements ServerAuthenticationEntryPoint {

    @Override
    public Mono commence(ServerWebExchange exchange, AuthenticationException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        AjaxResult ajaxResult = AjaxResult.restResult(e.getMessage(), ApiErrorCode.FAILED);
        String body = JSONObject.toJSONString(ajaxResult);
        DataBuffer wrap = exchange.getResponse().bufferFactory().wrap(body.getBytes(CharsetUtil.UTF_8));
        return exchange.getResponse().writeWith(Flux.just(wrap));
    }
}
  1. 表单登陆时security默认只会获取了username,password参数,但有时候需要一些特殊属性,所以需要覆盖默认获取的表单参数的Converter
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 将表单参数转换为AuthenticationToken
 *
 * @Author: pilsy
 * @Date: 2020/7/15 0015 15:41
 */
@Component
public class AuthenticationConverter extends ServerFormLoginAuthenticationConverter {

    private String usernameParameter = "username";

    private String passwordParameter = "password";

    @Override
    public Mono convert(ServerWebExchange exchange) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        String tenant = headers.getFirst("_tenant");
        String host = headers.getHost().getHostName();
        return exchange.getFormData()
                .map(data -> {
                    String username = data.getFirst(this.usernameParameter);
                    String password = data.getFirst(this.passwordParameter);
                    return new AuthenticationToken(username, password, tenant, host);
                });
    }

}
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * 存储用户信息的token
 *
 * @Author: pilsy
 * @Date: 2020/7/15 0015 16:08
 */
@SuppressWarnings("serial")
@Getter
@Setter
public class AuthenticationToken extends UsernamePasswordAuthenticationToken {

    private String tenant;

    private String host;

    public AuthenticationToken(Object principal, Object credentials, String tenant, String host) {
        super(principal, credentials);
        this.tenant = tenant;
        this.host = host;
    }

    public AuthenticationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public AuthenticationToken(Object principal, Object credentials, Collection authorities) {
        super(principal, credentials, authorities);
    }
}

  1. 验证用户身份
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * 验证用户
 *
 * @Author: pilsy
 * @Date: 2020/7/15 0015 16:43
 */
@Component
public class AuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {

    private Scheduler scheduler = Schedulers.boundedElastic();

    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Autowired
    private MySqlReactiveUserDetailsServiceImpl mySqlReactiveUserDetailsService;

    @Override
    public Mono authenticate(Authentication authentication) {
        AuthenticationToken token = (AuthenticationToken) authentication;
        final String username = authentication.getName();
        final String presentedPassword = (String) authentication.getCredentials();
        final String tenant = token.getTenant();
        final String host = token.getHost();
        return retrieveUser(username)
                .publishOn(scheduler)
                .filter(u -> passwordEncoder.matches(presentedPassword, u.getPassword()))
                .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
                .flatMap(u -> {
                    boolean upgradeEncoding = mySqlReactiveUserDetailsService != null
                            && passwordEncoder.upgradeEncoding(u.getPassword());
                    if (upgradeEncoding) {
                        String newPassword = passwordEncoder.encode(presentedPassword);
                        return mySqlReactiveUserDetailsService.updatePassword(u, newPassword);
                    }
                    return Mono.just(u);
                })
                .flatMap(userDetails -> {
                    // 省略业务代码
                    return Mono.just(userDetails);
                })
                .map(u -> new AuthenticationToken(u, u.getPassword(), u.getAuthorities()));
    }

    @Override
    protected Mono retrieveUser(String username) {
        return mySqlReactiveUserDetailsService.findByUsername(username);
    }
}
import com.gsoft.foa.gateway.repository.AccountInfoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * 身份认证类
 *
 * @Author: pilsy
 * @Date: 2020/6/29 0029 18:01
 */
@Slf4j
@Component
public class MySqlReactiveUserDetailsServiceImpl implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {

    private static final String USER_NOT_EXISTS = "用户不存在!";

    private final AccountInfoRepository accountInfoRepository;

    public MySqlReactiveUserDetailsServiceImpl(AccountInfoRepository accountInfoRepository) {
        this.accountInfoRepository = accountInfoRepository;
    }

    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public Mono findByUsername(String username) {
        return accountInfoRepository.findByUsername(username)
                .switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(USER_NOT_EXISTS))))
                .doOnNext(u -> log.info(
                        String.format("查询账号成功  user:%s password:%s", u.getUsername(), u.getPassword())))
                .cast(UserDetails.class);
    }

    @Override
    public Mono updatePassword(UserDetails user, String newPassword) {
        return accountInfoRepository.findByUsername(user.getUsername())
                .switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(USER_NOT_EXISTS))))
                .map(foundedUser -> {
                    foundedUser.setPassword(bCryptPasswordEncoder.encode(newPassword));
                    return foundedUser;
                })
                .flatMap(updatedUser -> accountInfoRepository.save(updatedUser))
                .cast(UserDetails.class);
    }
}
  1. 鉴权

import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
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;

/**
 * API请求权限校验配置类
 *
 * @Author: pilsy
 * @Date: 2020/7/1 0001 18:27
 */
@Slf4j
@Component
public class AuthorizeConfigManager implements ReactiveAuthorizationManager {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono check(Mono authentication,
                                             AuthorizationContext authorizationContext) {
        return authentication.map(auth -> {
            ServerWebExchange exchange = authorizationContext.getExchange();
            ServerHttpRequest request = exchange.getRequest();

            Collection authorities = auth.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                String authorityAuthority = authority.getAuthority();
                String path = request.getURI().getPath();
                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 verify(Mono authentication, AuthorizationContext object) {
        return check(authentication, object)
                .filter(d -> d.isGranted())
                .switchIfEmpty(Mono.defer(() -> {
                    AjaxResult ajaxResult = AjaxResult.restResult("当前用户没有访问权限! ", ApiErrorCode.FAILED);
                    String body = JSONObject.toJSONString(ajaxResult);
                    return Mono.error(new AccessDeniedException(body));
                }))
                .flatMap(d -> Mono.empty());
    }
}

你可能感兴趣的:(spring gateway集成spring security)