示例项目地址: https://git.code.tencent.com/xinzhang0618/oa.git
参考文档:
使用JDK生成公钥: https://blog.csdn.net/mmingxiang/article/details/108611390
公钥的使用: https://blog.csdn.net/qq_15973399/article/details/106903103

实现思路

(使用oauth2+jwt+gateway+rsa)
做认证主要是登录, 访问刷新, token过期, 登出

  1. 登录, 前端组装oauth2的url访问gateway, 直接跳转auth服务获取token;
  2. 访问刷新, gateway添加后置过滤器, 当token快过期时, 重新请求auth服务获取token, 请求头添加token刷新的事件, 前端接收后刷新token
  3. token过期, jwt过期
  4. 登录, redis添加token的黑名单, 过期时间为token的过期时间, 每次访问校验token不能存在于黑名单

尴尬点:

  1. oauth2本用作第三方登录, 用在这里只为做一个token生成器, 若应用有多个前端(web, app等), 则更有应用场景一些; 且oauth2有诸多限制, 获取token以及刷新有固定的url, 导致appSecret暴露在url且刷新极为不便; 导致这套体系之下, token的刷新使用”重新获取”更便利些; 所以, 建议在一般的分布式认证中, 直接用jwt就够了
  2. 在gateway重新获取token依然不便, 不能将用户密码以及appSecret等暴露在jwt中, 因此重新获取token, 还是需要在gateway请求auth服务拿到用户以及客户端信息, 然后拼接url重新请求oauth2
  3. jwt是无状态的, 但是用户登陆必须要可以登出; 此处有两种方案, 一是利用短时效的token, 允许用户登出后token依然有效; 另一种就是借助redis给token加上过期的状态, 但每次访问就都要走redis校验了
  4. 此外oauth2的sso方案之前尝试过, 配置虽然简单, 但资源服务器会在每次请求时都访问鉴权服务器以校验token, 增加了延时, 不建议用
  5. 纯吐槽, oauth2的配置网上的版本颇多, 甚至不同依赖不同配置, 坑也非常非常多, 这部分我尽量写详细

配置解析

项目采用通用的结构

  • auth-认证鉴权服务
  • gateway-网关
  • web-资源服务

    认证鉴权服务

    1. <!--oauth2-->
    2. <dependency>
    3. <groupId>org.springframework.cloud</groupId>
    4. <artifactId>spring-cloud-starter-oauth2</artifactId>
    5. <version>Greenwich.SR3</version>
    6. </dependency>
    7. <!--jwt-->
    8. <dependency>
    9. <groupId>com.auth0</groupId>
    10. <artifactId>java-jwt</artifactId>
    11. <version>3.10.3</version>
    12. </dependency>
    AuthServerConfig, oauth2认证服务器配置
  1. @EnableResourceServer, @EnableAuthorizationServer俩注解都不能掉, 它是认证服务器的同事也是资源服务器, 这样才能保证携带token能访问auth服务(便于后续的token刷新/重新获取)
  2. auth服务存放.jks文件(gateway存放pubkey.txt), 至于秘钥库和公钥的生成, 以及配置, 详见文档顶部的参考文档
  3. 自定义客户端的获取(oauth2的客户端模式通用)—>CustomClientDetailsService, 自定义用户信息的获取(oauth2的密码模式通用)—>CustomUserDetailsService, JWT的使用—>JWTTokenEnhancer
  4. AuthenticationManager这个bean一定要定义, 参见下方WebSecurityConfig中定义了 ```java package top.xinzhang0618.oa.config;

import java.security.KeyPair; import java.util.ArrayList; import java.util.List;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.bootstrap.encrypt.KeyProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.Resource;

/**

  • oauth2认证服务器配置 *
  • @author xinzhang
  • @date 2020/11/16 17:19 */ @Configuration @EnableAuthorizationServer @EnableResourceServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { @Resource(name = “keyProp”) private KeyProperties keyProperties;

    @Bean(“keyProp”) public KeyProperties keyProperties() {

    1. return new KeyProperties();

    }

    @Autowired private AuthenticationManager authenticationManager; @Autowired private JWTTokenEnhancer jwtTokenEnhancer; @Autowired private CustomClientDetailsService clientDetailsService; @Autowired private CustomUserDetailsService userDetailsService;

// /* // 辅助排查问题, 保留 // @return // / // @Bean // public WebResponseExceptionTranslator loggingExceptionTranslator() { // return new DefaultWebResponseExceptionTranslator() { // @Override // public ResponseEntity translate(Exception e) throws Exception { // e.printStackTrace(); // ResponseEntity responseEntity = super.translate(e); // HttpHeaders headers = new HttpHeaders(); // headers.setAll(responseEntity.getHeaders().toSingleValueMap()); // OAuth2Exception excBody = responseEntity.getBody(); // return new ResponseEntity<>(excBody, headers, responseEntity.getStatusCode()); // } // }; // }

  1. @Bean
  2. public JwtAccessTokenConverter accessTokenConverter() {
  3. JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  4. KeyPair keyPair = (new KeyStoreKeyFactory(this.keyProperties.getKeyStore().getLocation(),
  5. this.keyProperties.getKeyStore().getSecret().toCharArray())).getKeyPair(this.keyProperties.getKeyStore().getAlias(),
  6. this.keyProperties.getKeyStore().getPassword().toCharArray());
  7. converter.setKeyPair(keyPair);
  8. return converter;
  9. }
  10. @Bean
  11. public JwtTokenStore jwtTokenStore() {
  12. return new JwtTokenStore(accessTokenConverter());
  13. }
  14. @Override
  15. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  16. TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
  17. List<TokenEnhancer> enhancers = new ArrayList<>(2);
  18. enhancers.add(jwtTokenEnhancer);
  19. enhancers.add(accessTokenConverter());
  20. tokenEnhancerChain.setTokenEnhancers(enhancers);
  21. endpoints.tokenStore(jwtTokenStore())
  22. .tokenEnhancer(tokenEnhancerChain)
  23. .authenticationManager(authenticationManager)
  24. .accessTokenConverter(accessTokenConverter())
  25. .userDetailsService(userDetailsService)
  26. .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);

// .exceptionTranslator(loggingExceptionTranslator());

  1. }
  2. @Override
  3. public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
  4. oauthServer.tokenKeyAccess("permitAll()")
  5. .checkTokenAccess("permitAll()")
  6. .allowFormAuthenticationForClients();
  7. }
  8. @Override
  9. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  10. clients.withClientDetails(clientDetailsService);
  11. }

}

  1. **WebSecurityConfig, web安全配置**
  2. 1. PasswordEncoder这个bean**一定要定义**
  3. 1. AuthenticationManager这个bean**一定要定义**
  4. ```java
  5. package top.xinzhang0618.oa.config;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.security.authentication.AuthenticationManager;
  9. import org.springframework.security.config.BeanIds;
  10. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  11. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  12. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  13. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  14. import org.springframework.security.crypto.password.NoOpPasswordEncoder;
  15. import org.springframework.security.crypto.password.PasswordEncoder;
  16. /**
  17. * @author xinzhang
  18. * @date 2020/11/16 18:13
  19. */
  20. @Configuration
  21. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  22. @Bean
  23. public PasswordEncoder passwordEncoder() {
  24. return new BCryptPasswordEncoder();
  25. }
  26. @Bean
  27. @Override
  28. protected AuthenticationManager authenticationManager() throws Exception {
  29. return super.authenticationManager();
  30. }
  31. @Override
  32. protected void configure(HttpSecurity http) throws Exception {
  33. http.authorizeRequests()
  34. .antMatchers("/auth/oauth/**").permitAll()
  35. .and()
  36. .csrf().disable();
  37. }
  38. }

上面俩爹已经搞定了, 剩余的配置基本就贴代码了, 比较简单, 不多说
CustomClientDetailsService, 注意客户端密码加密

  1. package top.xinzhang0618.oa.config;
  2. import java.util.Arrays;
  3. import java.util.Collections;
  4. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  7. import org.springframework.security.oauth2.provider.ClientDetails;
  8. import org.springframework.security.oauth2.provider.ClientDetailsService;
  9. import org.springframework.security.oauth2.provider.ClientRegistrationException;
  10. import org.springframework.security.oauth2.provider.client.BaseClientDetails;
  11. import org.springframework.stereotype.Service;
  12. import top.xinzhang0618.oa.domain.base.Client;
  13. import top.xinzhang0618.oa.service.base.ClientService;
  14. /**
  15. * @author xinzhang
  16. * @date 2020/11/16 18:05
  17. */
  18. @Service
  19. public class CustomClientDetailsService implements ClientDetailsService {
  20. @Autowired
  21. private ClientService clientService;
  22. @Override
  23. public ClientDetails loadClientByClientId(String appKey) throws ClientRegistrationException {
  24. Client client = clientService.getOne(new LambdaQueryWrapper<Client>().eq(Client::getAppKey, appKey));
  25. BaseClientDetails clientDetails = new BaseClientDetails();
  26. clientDetails.setClientId(appKey);
  27. clientDetails.setClientSecret( new BCryptPasswordEncoder().encode(client.getAppSecret()));
  28. clientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getGrantType().split(",")));
  29. clientDetails.setScope(Collections.singletonList("all"));
  30. clientDetails.setAccessTokenValiditySeconds(client.getTokenValidityHours() * 60 * 60);
  31. clientDetails.setRefreshTokenValiditySeconds(client.getRefreshTokenValidityHours() * 60 * 60);
  32. return clientDetails;
  33. }
  34. }

客户端表结构设计

  1. CREATE TABLE `oa_client` (
  2. `client_id` bigint(20) unsigned NOT NULL COMMENT '客户端id',
  3. `created_time` datetime NOT NULL COMMENT '创建时间',
  4. `modified_time` datetime NOT NULL COMMENT '更新时间',
  5. `tenant_id` bigint(20) unsigned NOT NULL COMMENT '租户id',
  6. `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
  7. `app_key` varchar(50) NOT NULL COMMENT '应用key',
  8. `app_secret` varchar(100) NOT NULL COMMENT '应用密码',
  9. `grant_type` varchar(200) NOT NULL COMMENT '授权类型',
  10. `token_validity_hours` int(11) NOT NULL COMMENT 'token有效期',
  11. `refresh_token_validity_hours` int(11) NOT NULL COMMENT 'refreshToken有效期',
  12. PRIMARY KEY (`client_id`)
  13. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户端';

CustomUserDetailsService, 注意用户名密码加密, 以及此处权限的处理

  1. package top.xinzhang0618.oa.config;
  2. import java.util.List;
  3. import java.util.Set;
  4. import java.util.stream.Collectors;
  5. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  8. import org.springframework.security.core.userdetails.UserDetails;
  9. import org.springframework.security.core.userdetails.UserDetailsService;
  10. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  11. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  12. import org.springframework.security.crypto.password.PasswordEncoder;
  13. import org.springframework.stereotype.Service;
  14. import top.xinzhang0618.oa.Assert;
  15. import top.xinzhang0618.oa.domain.base.User;
  16. import top.xinzhang0618.oa.service.base.PrivilegeService;
  17. import top.xinzhang0618.oa.service.base.UserService;
  18. import javax.annotation.Resource;
  19. /**
  20. * @author xinzhang
  21. * @date 2020/11/16 17:53
  22. */
  23. @Service
  24. public class CustomUserDetailsService implements UserDetailsService {
  25. @Autowired
  26. private UserService userService;
  27. @Autowired
  28. private PrivilegeService privilegeService;
  29. @Override
  30. public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
  31. User user = userService.getOne(new LambdaQueryWrapper<User>()
  32. .eq(User::getUserName, userName)
  33. .eq(User::isEnable, true));
  34. if (Assert.isNull(user)) {
  35. throw new UsernameNotFoundException("用户不存在");
  36. }
  37. // String encode = new BCryptPasswordEncoder().encode(user.getPassword());
  38. user.setPassword( new BCryptPasswordEncoder().encode(user.getPassword()));
  39. List<Long> privileges = privilegeService.listUserPrivileges(user.getUserId());
  40. Set<SimpleGrantedAuthority> authorities =
  41. privileges.stream().map(p -> new SimpleGrantedAuthority(String.valueOf(privileges))).collect(Collectors.toSet());
  42. return new UserBO(user,authorities);
  43. }
  44. }

UserBO

  1. package top.xinzhang0618.oa.config;
  2. import org.springframework.security.core.GrantedAuthority;
  3. import org.springframework.security.core.userdetails.UserDetails;
  4. import top.xinzhang0618.oa.domain.base.User;
  5. import java.util.Collection;
  6. import java.util.Set;
  7. /**
  8. * @author xinzhang
  9. * @date 2020/11/17 19:28
  10. */
  11. public class UserBO implements UserDetails {
  12. public UserBO(User user, Collection<? extends GrantedAuthority> authorities) {
  13. this.userId = user.getUserId();
  14. this.userName = user.getUserName();
  15. this.password = user.getPassword();
  16. }
  17. private Long userId;
  18. private String userName;
  19. private String password;
  20. private String tenantId;
  21. private Collection<? extends GrantedAuthority> authorities;
  22. @Override
  23. public Collection<? extends GrantedAuthority> getAuthorities() {
  24. return this.authorities;
  25. }
  26. @Override
  27. public String getPassword() {
  28. return this.password;
  29. }
  30. @Override
  31. public String getUsername() {
  32. return this.userName;
  33. }
  34. @Override
  35. public boolean isAccountNonExpired() {
  36. return true;
  37. }
  38. @Override
  39. public boolean isAccountNonLocked() {
  40. return true;
  41. }
  42. @Override
  43. public boolean isCredentialsNonExpired() {
  44. return true;
  45. }
  46. @Override
  47. public boolean isEnabled() {
  48. return true;
  49. }
  50. public Long getUserId() {
  51. return userId;
  52. }
  53. public void setUserId(Long userId) {
  54. this.userId = userId;
  55. }
  56. public String getUserName() {
  57. return userName;
  58. }
  59. public void setUserName(String userName) {
  60. this.userName = userName;
  61. }
  62. public void setPassword(String password) {
  63. this.password = password;
  64. }
  65. public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
  66. this.authorities = authorities;
  67. }
  68. public String getTenantId() {
  69. return tenantId;
  70. }
  71. public void setTenantId(String tenantId) {
  72. this.tenantId = tenantId;
  73. }
  74. }

其余
AuthController

  1. package top.xinzhang0618.oa;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.data.redis.core.RedisTemplate;
  5. import org.springframework.security.oauth2.common.OAuth2AccessToken;
  6. import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
  7. import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
  8. import org.springframework.web.bind.annotation.*;
  9. import top.xinzhang0618.oa.bo.auth.AuthInfoBO;
  10. import top.xinzhang0618.oa.constant.AuthConstant;
  11. import top.xinzhang0618.oa.domain.base.Client;
  12. import top.xinzhang0618.oa.domain.base.User;
  13. import top.xinzhang0618.oa.service.base.ClientService;
  14. import top.xinzhang0618.oa.service.base.UserService;
  15. import java.time.LocalDateTime;
  16. import java.util.concurrent.TimeUnit;
  17. /**
  18. * @author xinzhang
  19. * @date 2020/11/19 14:19
  20. */
  21. @RestController
  22. public class AuthController {
  23. @Autowired
  24. private UserService userService;
  25. @Autowired
  26. private ClientService clientService;
  27. @Autowired
  28. private RedisTemplate<String, Object> redisTemplate;
  29. @Autowired
  30. private JwtAccessTokenConverter accessTokenConverter;
  31. @GetMapping("/info/{appKey}/{userId}")
  32. public AuthInfoBO getAuthInfo(@PathVariable("appKey") String appKey, @PathVariable("userId") Long userId) {
  33. User user = userService.getById(userId);
  34. Client client = clientService.getOne(new LambdaQueryWrapper<Client>().eq(Client::getAppKey, appKey));
  35. AuthInfoBO authInfoBO = new AuthInfoBO();
  36. authInfoBO.setUserName(user.getUserName());
  37. authInfoBO.setPassword(user.getPassword());
  38. authInfoBO.setAppKey(client.getAppKey());
  39. authInfoBO.setAppSecret(client.getAppSecret());
  40. return authInfoBO;
  41. }
  42. @PostMapping("/exit")
  43. public void logout(@RequestHeader("Authorization") String authorization) {
  44. String token = authorization.replace(AuthConstant.TOKEN_PREFIX, "");
  45. OAuth2AccessToken oAuth2AccessToken = new JwtTokenStore(accessTokenConverter).readAccessToken(token);
  46. String key = AuthConstant.TOKEN_BLACK_LIST_PREFIX + authorization;
  47. redisTemplate.opsForValue().set(key, LocalDateTime.now().toString());
  48. redisTemplate.expire(key, oAuth2AccessToken.getExpiresIn(), TimeUnit.SECONDS);
  49. }
  50. }

.jks的配置

  1. encrypt:
  2. key-store:
  3. location: classpath:config/oa.jks
  4. secret: xxx
  5. alias: xx
  6. password: xxx

获取token的路径示例
注意其中客户端以及密码均采用密文

  1. http://localhost:40002/auth/oauth/token?grant_type=password&client_id=test&client_secret=$2a$10$ATXY1ablVzcML1aNFkZrbuD3oVvddBw62JXfOSZ4zQgrJAOqOfOM2&username=test&password=$2a$10$ATXY1ablVzcML1aNFkZrbuD3oVvddBw62JXfOSZ4zQgrJAOqOfOM2

image.png

网关

  1. <!--webflux-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-webflux</artifactId>
  5. </dependency>
  6. <!--gateway-->
  7. <dependency>
  8. <groupId>org.springframework.cloud</groupId>
  9. <artifactId>spring-cloud-starter-gateway</artifactId>
  10. </dependency>
  11. <!--oauth2-->
  12. <dependency>
  13. <groupId>org.springframework.cloud</groupId>
  14. <artifactId>spring-cloud-starter-oauth2</artifactId>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework.security</groupId>
  18. <artifactId>spring-security-oauth2-resource-server</artifactId>
  19. </dependency>
  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. # web服务路由
  6. - id: web-router
  7. uri: lb://WEB
  8. predicates:
  9. - Path=/api/**
  10. # auth服务路由
  11. - id: auth-router
  12. uri: lb://AUTH
  13. predicates:
  14. - Path=/auth/**
  15. redis:
  16. host: 39.106.55.179
  17. port: 6379
  18. database: 0
  19. password: xxx
  20. hystrix:
  21. command:
  22. default:
  23. execution:
  24. isolation:
  25. thread:
  26. timeoutInMilliseconds: 5000

以下的几个核心配置类相当僵硬, 稍有改动可能会导致Bean加载顺序出问题而报错, 但仍有优化空间
ResourceServerConfig, oauth2资源服务器配置

  1. 配置全局跨域
  2. gateway存放pubkey.txt(auth服务存放.jks文件), 配置token转换器方便解析token
  3. 注册权限验证器ReactiveAuthenticationManager和认证验证器ReactiveAuthorizationManager ```java package top.xinzhang0618.oa.config;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.AuthenticationWebFilter; import org.springframework.web.cors.reactive.CorsUtils; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import top.xinzhang0618.oa.util.FileUtils;

/**

  • @author xinzhang
  • @date 2020/11/17 19:43 */ @Configuration @EnableWebFluxSecurity public class ResourceServerConfig {

    private static final String MAX_AGE = “18000L”;

    @Autowired private AccessManager accessManager; @Autowired private RedisTemplate redisTemplate;

    /**

    • 跨域配置 */ public WebFilter corsFilter() { return (ServerWebExchange ctx, WebFilterChain chain) -> {

      1. ServerHttpRequest request = ctx.getRequest();
      2. if (CorsUtils.isCorsRequest(request)) {
      3. HttpHeaders requestHeaders = request.getHeaders();
      4. ServerHttpResponse response = ctx.getResponse();
      5. HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
      6. HttpHeaders headers = response.getHeaders();
      7. headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
      8. headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
      9. requestHeaders.getAccessControlRequestHeaders());
      10. if (requestMethod != null) {
      11. headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
      12. }
      13. headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
      14. headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
      15. headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
      16. if (request.getMethod() == HttpMethod.OPTIONS) {
      17. response.setStatusCode(HttpStatus.OK);
      18. return Mono.empty();
      19. }
      20. }
      21. return chain.filter(ctx);

      }; }

      @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(FileUtils.read(“config/pubkey.txt”)); return converter; }

      @Bean SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) { //token管理器-jwt实现类 ReactiveAuthenticationManager tokenAuthenticationManager =

      1. new JwtAuthenticationManager(new JwtTokenStore(accessTokenConverter()),redisTemplate);

      //认证过滤器 AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager); authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());

      http.httpBasic().disable()

      1. .csrf().disable()
      2. .authorizeExchange()
      3. .pathMatchers(HttpMethod.OPTIONS).permitAll()
      4. .anyExchange().access(accessManager)
      5. .and()
      6. .addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS)
      7. .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);

      return http.build(); } }

  1. **AccessManager, 实现ReactiveAuthorizationManager<AuthorizationContext>接口做鉴权**<br />这里可以设定放开的静态资源路径, 对客户端做路径的权限控制, 利用正则对用户请求路径做权限控制等, <br />对于分布式后端做鉴权, 两种方案:
  2. 1. 使用restFul风格的url, 那只能使用正则表达式以实现对资源路径的校验;
  3. 1. 全部使用POST作为请求方式, 同样数据库维护url的资源权限;
  4. 不论哪种, 维护都较为繁琐, 且使用正则性能低, 目前个人没发现好的解决方案, 因此示例项目这部分没做, 仅由前端控制权限
  5. ```java
  6. package top.xinzhang0618.oa.config;
  7. import org.springframework.security.authorization.AuthorizationDecision;
  8. import org.springframework.security.authorization.ReactiveAuthorizationManager;
  9. import org.springframework.security.core.Authentication;
  10. import org.springframework.security.oauth2.provider.OAuth2Authentication;
  11. import org.springframework.security.web.server.authorization.AuthorizationContext;
  12. import org.springframework.stereotype.Component;
  13. import org.springframework.util.AntPathMatcher;
  14. import org.springframework.web.server.ServerWebExchange;
  15. import reactor.core.publisher.Mono;
  16. import java.util.HashSet;
  17. import java.util.Set;
  18. /**
  19. * @author xinzhang
  20. * @date 2020/11/18 17:40
  21. */
  22. @Component
  23. public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
  24. private final Set<String> permitAll = new HashSet<>();
  25. private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
  26. public AccessManager() {
  27. permitAll.add("/");
  28. permitAll.add("/auth/oauth/**");
  29. }
  30. @Override
  31. public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
  32. ServerWebExchange exchange = authorizationContext.getExchange();
  33. String requestPath = exchange.getRequest().getURI().getPath();
  34. if (permitAll(requestPath)) {
  35. return Mono.just(new AuthorizationDecision(true));
  36. }
  37. return mono.map(auth -> new AuthorizationDecision(checkAuthorities(auth, requestPath)))
  38. .defaultIfEmpty(new AuthorizationDecision(false));
  39. }
  40. /**
  41. * 校验是否属于静态资源
  42. *
  43. * @param requestPath 请求路径
  44. * @return
  45. */
  46. private boolean permitAll(String requestPath) {
  47. return permitAll.stream().anyMatch(r -> antPathMatcher.match(r, requestPath));
  48. }
  49. /**
  50. * 权限校验
  51. *
  52. * @param auth Authentication
  53. * @param requestPath 请求路径
  54. * @return
  55. */
  56. private boolean checkAuthorities(Authentication auth, String requestPath) {
  57. if (auth instanceof OAuth2Authentication) {
  58. OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) auth;
  59. String clientId = oAuth2Authentication.getOAuth2Request().getClientId();
  60. // 权限校验
  61. }
  62. return true;
  63. }
  64. }

JwtAuthenticationManager实现ReactiveAuthenticationManager接口做认证
认证就一个authenticate方法较为简单, 注意实现的逻辑有:

  1. 先过滤redis黑名单
  2. jwt的解析
  3. token的过期校验
  4. token鉴权 ```java package top.xinzhang0618.oa.config;

import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import reactor.core.publisher.Mono;

/**

  • @author xinzhang
  • @date 2020/11/17 19:53 */ public class JwtAuthenticationManager implements ReactiveAuthenticationManager {

    private RedisTemplate redisTemplate; private TokenStore tokenStore;

    public JwtAuthenticationManager(TokenStore tokenStore, RedisTemplate redisTemplate) {

    1. this.tokenStore = tokenStore;
    2. this.redisTemplate = redisTemplate;

    }

    @Override public Mono authenticate(Authentication authentication) {

    1. return Mono.justOrEmpty(authentication)
    2. .filter(a -> a instanceof BearerTokenAuthenticationToken)
    3. .cast(BearerTokenAuthenticationToken.class)
    4. .map(BearerTokenAuthenticationToken::getToken)
    5. .flatMap((accessToken -> {
    6. // redis黑名单
    7. if (redisTemplate.opsForValue().get("TOKEN_BLACK_LIST:Bearer " + accessToken) != null) {
    8. return Mono.error(new InvalidTokenException("请重新登录"));
    9. }
    10. OAuth2AccessToken oAuth2AccessToken;
    11. try {
    12. oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);
    13. } catch (Exception e) {
    14. return Mono.error(new InvalidTokenException("token无效, 请重新登录"));
    15. }
    16. if (oAuth2AccessToken == null) {
    17. return Mono.error(new InvalidTokenException("请先登录"));
    18. } else if (oAuth2AccessToken.isExpired()) {
    19. return Mono.error(new InvalidTokenException("登录已过期, 请重新登录"));
    20. }
    21. OAuth2Authentication oAuth2Authentication = this.tokenStore.readAuthentication(accessToken);
    22. if (oAuth2Authentication == null) {
    23. return Mono.error(new InvalidTokenException("登录无效, 请重新登录"));
    24. } else {
    25. return Mono.just(oAuth2Authentication);
    26. }
    27. })).cast(Authentication.class);

    } }

  1. **AuthGlobalFilter实现GlobalFilter, 作为校验通过后的过滤器**
  2. 1. 注意这里有个骚操作是"往SecurityContext中塞入token, 方便restTemplate取出并携带", 是为了后续访问auth服务重新获取token所必须的, oauth2+webflux下的的SecurityContext中啥都没有
  3. 1. 判断token有效期, 只剩5分钟时, 重新获取token, 并在请求头添加token刷新的事件
  4. 1. 解析jwt内容塞入上下文
  5. ```java
  6. package top.xinzhang0618.oa.config;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  9. import org.springframework.cloud.gateway.filter.GlobalFilter;
  10. import org.springframework.core.Ordered;
  11. import org.springframework.http.HttpHeaders;
  12. import org.springframework.security.core.context.SecurityContext;
  13. import org.springframework.security.core.context.SecurityContextHolder;
  14. import org.springframework.security.oauth2.common.OAuth2AccessToken;
  15. import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
  16. import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
  17. import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
  18. import org.springframework.stereotype.Component;
  19. import org.springframework.web.server.ServerWebExchange;
  20. import reactor.core.publisher.Mono;
  21. import top.xinzhang0618.oa.BizContext;
  22. import top.xinzhang0618.oa.constant.GatewayConstant;
  23. import top.xinzhang0618.oa.util.StringUtils;
  24. import top.xinzhang0618.oa.util.TokenUtil;
  25. import java.util.Map;
  26. /**
  27. * @author xinzhang
  28. * @date 2020/11/17 19:55
  29. */
  30. @Component
  31. public class AuthGlobalFilter implements GlobalFilter, Ordered {
  32. @Autowired
  33. private JwtAccessTokenConverter accessTokenConverter;
  34. @Autowired
  35. private TokenUtil tokenUtil;
  36. @Override
  37. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  38. String token = exchange.getRequest().getHeaders().getFirst(GatewayConstant.AUTHORIZATION);
  39. if (StringUtils.isEmpty(token)) {
  40. return chain.filter(exchange);
  41. }
  42. // 往SecurityContext中塞入token, 方便restTemplate取出并携带
  43. SecurityContext context = SecurityContextHolder.getContext();
  44. context.setAuthentication(new BearerTokenAuthenticationToken(token));
  45. OAuth2AccessToken oAuth2AccessToken = new JwtTokenStore(accessTokenConverter).readAccessToken(token.replace(
  46. GatewayConstant.TOKEN_PREFIX, ""));
  47. Map<String, Object> map = oAuth2AccessToken.getAdditionalInformation();
  48. String userId = (String) map.get(GatewayConstant.USER_ID);
  49. String appKey = (String) map.get(GatewayConstant.APP_KEY);
  50. // token有效期只剩5分钟时刷新token
  51. if (oAuth2AccessToken.getExpiresIn() < GatewayConstant.TOKEN_REFRESH_TIME_LIMIT) {
  52. String newToken = tokenUtil.generateToken((String) map.get(GatewayConstant.USER_ID), (String) map.get(GatewayConstant.APP_KEY));
  53. HttpHeaders headers = exchange.getResponse().getHeaders();
  54. headers.add(GatewayConstant.ACCESS_TOKEN, newToken);
  55. headers.add(GatewayConstant.EVENT, GatewayConstant.TOKEN_REFRESHED);
  56. }
  57. // 设置上下文
  58. BizContext.setUserId((Long) map.get(GatewayConstant.USER_ID));
  59. BizContext.setUserName((String) map.get(GatewayConstant.USER_NAME));
  60. BizContext.setTenantId((Long) map.get(GatewayConstant.TENANT_ID));
  61. return chain.filter(exchange);
  62. }
  63. @Override
  64. public int getOrder() {
  65. return 0;
  66. }
  67. }

补充:
RestTemplateConfig, 注意下拦截器, 从SecurityContext中取出token并携带

  1. package top.xinzhang0618.oa.config.rest;
  2. import com.alibaba.fastjson.serializer.SerializerFeature;
  3. import com.alibaba.fastjson.support.config.FastJsonConfig;
  4. import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
  5. import java.nio.charset.StandardCharsets;
  6. import java.util.Arrays;
  7. import java.util.List;
  8. import org.apache.http.client.HttpClient;
  9. import org.apache.http.impl.client.HttpClientBuilder;
  10. import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
  11. import org.slf4j.Logger;
  12. import org.slf4j.LoggerFactory;
  13. import org.springframework.cloud.client.loadbalancer.LoadBalanced;
  14. import org.springframework.context.annotation.Bean;
  15. import org.springframework.context.annotation.Configuration;
  16. import org.springframework.http.HttpHeaders;
  17. import org.springframework.http.MediaType;
  18. import org.springframework.http.client.ClientHttpRequestInterceptor;
  19. import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
  20. import org.springframework.http.converter.HttpMessageConverter;
  21. import org.springframework.http.converter.StringHttpMessageConverter;
  22. import org.springframework.security.core.context.SecurityContextHolder;
  23. import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
  24. import org.springframework.web.client.RestTemplate;
  25. import top.xinzhang0618.oa.constant.GatewayConstant;
  26. /**
  27. * RestTemplate配置类
  28. *
  29. * @author gavin
  30. * @date 2020-07-01
  31. * 文档: https://docs.spring.io/spring/docs/4.3.9.RELEASE/spring-framework-reference/html/remoting.html#rest-client-access
  32. * api: https://docs.spring.io/spring-framework/docs/4.3.9
  33. * .RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
  34. */
  35. @Configuration
  36. public class RestTemplateConfig {
  37. private static final Logger LOGGER = LoggerFactory.getLogger(RestTemplateConfig.class);
  38. @Bean
  39. @LoadBalanced
  40. public RestTemplate restTemplate() {
  41. RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());
  42. restTemplate.getInterceptors().add(authorizedRequestInterceptor());
  43. replaceJackson2FastJson(restTemplate);
  44. return restTemplate;
  45. }
  46. /**
  47. * 自定义拦截器携带authorization
  48. */
  49. @Bean(name = "authorizedRequestInterceptor")
  50. public ClientHttpRequestInterceptor authorizedRequestInterceptor() {
  51. return (httpRequest, bytes, clientHttpRequestExecution) -> {
  52. HttpHeaders headers = httpRequest.getHeaders();
  53. BearerTokenAuthenticationToken authentication =
  54. (BearerTokenAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
  55. headers.add(GatewayConstant.AUTHORIZATION, authentication.getToken());
  56. return clientHttpRequestExecution.execute(httpRequest, bytes);
  57. };
  58. }
  59. /**
  60. * 替换默认的jackson转换器为fastJson转换器
  61. */
  62. private void replaceJackson2FastJson(RestTemplate restTemplate) {
  63. List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
  64. //原有的String是ISO-8859-1编码 替换成 UTF-8编码
  65. converters.removeIf(c -> c instanceof StringHttpMessageConverter);
  66. converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
  67. converters.add(0, fastJsonHttpMessageConverter());
  68. }
  69. /**
  70. * 配置fastJson转换器
  71. */
  72. @Bean
  73. public HttpMessageConverter fastJsonHttpMessageConverter() {
  74. FastJsonConfig fastJsonConfig = new FastJsonConfig();
  75. fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue, SerializerFeature.QuoteFieldNames,
  76. SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullListAsEmpty,
  77. SerializerFeature.DisableCircularReferenceDetect);
  78. FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
  79. fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_UTF8,
  80. MediaType.TEXT_HTML));
  81. fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
  82. return fastJsonHttpMessageConverter;
  83. }
  84. /**
  85. * 配置clientHttpRequestFactory
  86. */
  87. @Bean
  88. public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
  89. try {
  90. HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
  91. //设置连接池
  92. PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
  93. //最大连接数
  94. connectionManager.setMaxTotal(20);
  95. //同路由并发数
  96. connectionManager.setDefaultMaxPerRoute(10);
  97. httpClientBuilder.setConnectionManager(connectionManager);
  98. HttpClient httpClient = httpClientBuilder.build();
  99. // httpClient连接配置
  100. HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(
  101. httpClient);
  102. //连接超时
  103. requestFactory.setConnectTimeout(60 * 1000);
  104. //数据读取超时时间
  105. requestFactory.setReadTimeout(60 * 1000);
  106. //连接不够用的等待时间
  107. requestFactory.setConnectionRequestTimeout(60 * 1000);
  108. return requestFactory;
  109. } catch (Exception e) {
  110. LOGGER.error(String.format("初始化clientHttpRequestFactory失败, 错误信息: %s", e));
  111. }
  112. return null;
  113. }
  114. }

网关全局异常处理
ExceptionConfig, 主要是定义ErrorWebExceptionHandler的Bean

  1. package top.xinzhang0618.oa.config.exception;
  2. import org.springframework.beans.factory.ObjectProvider;
  3. import org.springframework.boot.autoconfigure.web.ResourceProperties;
  4. import org.springframework.boot.autoconfigure.web.ServerProperties;
  5. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  6. import org.springframework.boot.web.reactive.error.ErrorAttributes;
  7. import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
  8. import org.springframework.context.ApplicationContext;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.core.Ordered;
  12. import org.springframework.core.annotation.Order;
  13. import org.springframework.http.codec.ServerCodecConfigurer;
  14. import org.springframework.web.reactive.result.view.ViewResolver;
  15. import java.util.Collections;
  16. import java.util.List;
  17. /**
  18. * @author xinzhang
  19. * @date 2020/11/20 16:22
  20. */
  21. @Configuration
  22. @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
  23. public class ExceptionConfig {
  24. private final ServerProperties serverProperties;
  25. private final ApplicationContext applicationContext;
  26. private final ResourceProperties resourceProperties;
  27. private final List<ViewResolver> viewResolvers;
  28. private final ServerCodecConfigurer serverCodecConfigurer;
  29. public ExceptionConfig(ServerProperties serverProperties,
  30. ResourceProperties resourceProperties,
  31. ObjectProvider<List<ViewResolver>> viewResolversProvider,
  32. ServerCodecConfigurer serverCodecConfigurer,
  33. ApplicationContext applicationContext) {
  34. this.serverProperties = serverProperties;
  35. this.applicationContext = applicationContext;
  36. this.resourceProperties = resourceProperties;
  37. this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
  38. this.serverCodecConfigurer = serverCodecConfigurer;
  39. }
  40. @Bean
  41. @Order(Ordered.HIGHEST_PRECEDENCE)
  42. public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
  43. JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
  44. errorAttributes,
  45. this.resourceProperties,
  46. this.serverProperties.getError(),
  47. this.applicationContext);
  48. exceptionHandler.setViewResolvers(this.viewResolvers);
  49. exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
  50. exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
  51. return exceptionHandler;
  52. }
  53. }

JsonExceptionHandler, 实现默认的DefaultErrorWebExceptionHandler异常处理器, 重写方法
此处的status可能有线程安全问题, 待优化, 这个status是请求返回的http状态码, 但项目中我自定义的异常码都是10000-10003这种, 在getErrorAttributes方法又覆盖了原有的返回值, 因此会导致getHttpStatus方法报错, 暂时存放一个类变量以保存原有的status码

  1. package top.xinzhang0618.oa.config.exception;
  2. import com.alibaba.fastjson.JSON;
  3. import org.springframework.boot.autoconfigure.web.ErrorProperties;
  4. import org.springframework.boot.autoconfigure.web.ResourceProperties;
  5. import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
  6. import org.springframework.boot.web.reactive.error.ErrorAttributes;
  7. import org.springframework.context.ApplicationContext;
  8. import org.springframework.http.HttpStatus;
  9. import org.springframework.web.reactive.function.server.*;
  10. import top.xinzhang0618.oa.constant.GatewayConstant;
  11. import top.xinzhang0618.oa.rest.response.ErrorCode;
  12. import top.xinzhang0618.oa.rest.response.RestResponse;
  13. import java.util.Map;
  14. /**
  15. * 自定义异常处理器
  16. * 参考: https://cloud.tencent.com/developer/article/1650123
  17. * 参考: https://blog.csdn.net/github_38924695/article/details/104374037
  18. *
  19. * @author xinzhang
  20. * @date 2020/11/20 16:02
  21. */
  22. public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
  23. /**
  24. * 原ErrorAttributes中状态码
  25. */
  26. private int status;
  27. public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
  28. ErrorProperties errorProperties, ApplicationContext applicationContext) {
  29. super(errorAttributes, resourceProperties, errorProperties, applicationContext);
  30. }
  31. /**
  32. * 获取异常属性
  33. */
  34. @Override
  35. protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
  36. Throwable error = super.getError(request);
  37. this.status= (int) super.getErrorAttributes(request, includeStackTrace).get(GatewayConstant.STATUS);
  38. return JSON.parseObject(JSON.toJSONString(RestResponse.failure(ErrorCode.UNKNOWN_ERROR.getValue(),
  39. error.getMessage())));
  40. }
  41. /**
  42. * 指定响应处理方法为JSON处理的方法
  43. *
  44. * @param errorAttributes
  45. */
  46. @Override
  47. protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
  48. return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
  49. }
  50. /**
  51. * 根据code获取对应的HttpStatus
  52. *
  53. * @param errorAttributes
  54. */
  55. @Override
  56. protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
  57. return HttpStatus.valueOf(status);
  58. }
  59. }

日志处理
网关可以添加拦截器, 记录请求的参数以及返回值, 个人觉得不太好, 增加了响应时间, 日志应该在各服务业务处理的地方做记录比较合适
断路器
hystrix是一定要配置的, 详见上面配置
限流
暂不配置, 略