架构师进阶-新旧项目如何接入微服务网关

100 阅读8分钟

目前后端项目基础框架涉及SSH、SpringMVC、SpringBoot,伴随需求的快速迭代,服务数量也在逐步增加,服务调用链路也越来越长,部分业务耦合度也越来越强,新旧项目各自的认证鉴权也参差不齐,迫切需要接入一个API网关来统一管理。

我们比较理想的愿景就是网关来负责统一认证鉴权,各业务系统专注于自己的业务逻辑而无需关注诸如认证鉴权,接口校验的工作。

网关:

API服务网关可以实现用户的验证登录、解决跨域、日志拦截、权限控制、限流、熔断、负载均衡、黑名单与白名单机制等功能

网关选型(java体系):

  • Kong(OpenResty+Lua)
  • Zuul、Zuul2
  • Spring Cloud Gateway

kong:

云原生

与平台无关,Kong可以从裸机运行到Kubernetes

日志

可以记录通过 Kong 的 HTTP,TCP,UDP 请求和响应

鉴权

权限控制,IP 黑白名单

认证

如数支持 HMAC, JWT, Basic, OAuth2.0 等常用协议

限流

基于多变量对请求进行阻塞或者限制

可用性

天然分布式支持

高性能

异步非阻塞,基于netty

插件机制

种类丰富,开箱即用的插件,可自定义

动态路由

基于OpenResty+Lua

Spring Cloud Gateway:

  • Built on Spring Framework 5, Project Reactor and Spring Boot 2.0
  • Able to match routes on any request attribute.
  • Predicates and filters are specific to routes.
  • Circuit Breaker integration.
  • Spring Cloud DiscoveryClient integration
  • Easy to write Predicates and Filters
  • Request Rate Limiting
  • Path Rewriting

Spring Cloud Gateway 使用了 Spring WebFlux 非阻塞网络框架,网络层默认使用了高性能非阻塞的 Netty Server,解决了 Spring Cloud Zuul 因为阻塞的线程模型带来的性能下降的问题。

Gateway 在启动时会创建 Netty Server,由它接收来自 Client 的请求。收到请求后根据路由的匹配条件找到第一个满足条件的路由,然后请求在被该路由配置的过滤器处理后由 Netty Client 转到目标服务。服务返回响应后会再次被过滤器处理,最后返回给 Client。

关于选型:

几种维度吧,看产品的服务对象,是toB还是toC;未来一段时间内的增长量;主力开发语言是哪些;对于性能的预期,比如一定并发下qps;未来对于选用框架的投入成本;

当然,技术并无优劣,每种框架的出现都是为了解决某个时间段遇到的问题,用业务驱动技术选型,有时候更容易做出选择

Spring cloud gateway是我们目前的最优解,后端服务基本基于Spring框架,选择Spring gateway接入成本最低,满足未来很长一段时间内的业务增长。

认证服务

oauth2:

  1. 客户端从资源拥有者那里请求授权。授权请求能够直接发送给资源拥有者,或者间接地通过授权服务器这样的中介,而后者更为可取。
  2. 客户端收到一个访问许可,它代表由资源服务器提供的授权。
  3. 客户端使用它自己的私有证书到授权服务器上验证,并出示访问许可,来请求一个访问令牌。
  4. 授权服务器验证客户端私有证书和访问许可的有效性,如果验证通过则分发一个访问令牌。
  5. 客户端通过出示访问令牌向资源服务器请求受保护资源。
  6. 资源服务器验证访问令牌的有效性,如果验证通过则响应这个资源请求。

4种授权码模式:

  • 授权码授权模式(Authorization Code Grant)
  • 隐式授权模式(简化模式)(Implicit Grant)
  • 密码授权模式(Resource Owner Password Credentials Grant)
  • 客户端凭证授权模式(Client Credentials Grant)

授权码模式:

pom.xml:添加相关依赖

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
            <version>2.6.4</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j-impl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

配置认证服务

package com.bugcoder.config;


import com.bugcoder.service.UserService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.configurers.AuthorizationServerEndpointsConfigurer;


import javax.annotation.Resource;


/**
 * @author bugcoder
 * @date 2022/12/21
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


    @Resource
    private PasswordEncoder passwordEncoder;


    @Resource
    private AuthenticationManager authenticationManager;


    @Resource
    private UserService userService;


    /**
     * 使用密码模式需要配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService);
    }


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("bugcoder")//配置client_id
                .secret(passwordEncoder.encode("123456"))//配置client-secret
                .accessTokenValiditySeconds(3600)//配置访问token的有效期
                .refreshTokenValiditySeconds(864000)//配置刷新token的有效期
                .redirectUris("http://gg.gg/12v9sy/")//配置redirect_uri,用于授权成功后跳转
                .scopes("all")//配置申请的权限范围
                .authorizedGrantTypes("authorization_code","password");//配置grant_type,表示授权类型
    }
}

配置资源服务器

package com.bugcoder.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/**
 * 配置需要保护的资源路径
 * @author bugcoder
 * @date 2022/12/21
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .requestMatchers()
                .antMatchers("/user/**");
    }
}

配置用户:本地或DB获取

package com.bugcoder.service;


import com.bugcoder.domain.User;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;


import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;


/**
 * @author bugcoder
 * @date 2022/12/21
 */
@Service
public class UserService implements UserDetailsService {


    private List<User> userList;
    @Resource
    private PasswordEncoder passwordEncoder;


    @PostConstruct
    public void initData() {
        String password = passwordEncoder.encode("123456");
        userList = new ArrayList<>();
        userList.add(new User("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")));
        userList.add(new User("bugcoder", password, AuthorityUtils.commaSeparatedStringToAuthorityList("client")));
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<User> findUserList = userList.stream().filter(user -> user.getUsername().equals(username)).collect(Collectors.toList());
        if (!CollectionUtils.isEmpty(findUserList)) {
            return findUserList.get(0);
        } else {
            throw new UsernameNotFoundException("用户名或密码错误");
        }
    }
}

浏览器输入访问地址:

http://localhost:9001/oauth/authorize?response_type=code&client_id=bugcoder&redirect_uri=http://gg.gg/12v9sy/&scope=all&state=wxbugcoder

输入账号和密码(配置用户里):

选择授权:

授权成功以后会跳转到配置的跳转地址:

https://mp.weixin.qq.com/s?__biz=MjM5NzgyODc1Mw==&mid=2647754380&idx=1&sn=d6c32c8e03a1f1efe198c93a44d5d303&chksm=bef127728986ae6484c74dae11e4cebf58436f475c16a1f2887157418c1134dbd0eecc835d59&token=1063710691&lang=zh_CN#rd

重点关注下code和state这两个参数,code是授权码

?code=Zapo7U&state=wxbugcoder

使用授权码获取access_token:

拿到token以后,就可以去访问业务的接口了

密码模式:

Spring gateway:

转发规则配置ef:

spring:
  cloud:
    gateway:
      routes:
      - id: api
        uri: lb://api
        predicates:
            - Path=/abc/**
          filters:
            - StripPrefix=1

这个转发配置的含义:

网关地址:http://localhost:9001
请求的地址:http://localhost:9001/abc/user
网关处理以后的转发地址:http://api/user

注册中心选型(java体系):

  • zookeeper(CP)
  • eureka(AP)
  • nacos(AP/CP)
  • consul(CP)
  • etcd(CP)

这几个注册中心都是spring cloud生态中的常客,还是那句话,技术没有优劣,选择一个满足自己业务的即可,这里我们选择nacos,一是我们不需要再单独引用一套配置中心,还有就是服务部署在阿里云的服务器上,再一个就是做为阿里系Spring cloud中的一员,它也算是个亲儿子,社区支持还可以。

可以从 最新稳定版本 下载 nacos-server-$version.zip 包

unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz
cd nacos/bin

启动命令(standalone代表着单机模式运行,非集群模式):

sh startup.sh -m standalone

访问地址:
http://localhost:8848/nacos,账号密码默认nacos

解决方案:Spring cloud gateway + Spring security + nacos

假如有三个服务:网关服务、认证服务、业务A(业务集群)

在认证服务中: 配置认证服务

package com.bugcoder.config;


import com.bugcoder.component.JwtTokenEnhancer;
import com.bugcoder.service.UserServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
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.rsa.crypto.KeyStoreKeyFactory;


import javax.annotation.Resource;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;


/**
 * 认证服务器
 *
 * @author bugcoder
 * @date 2022/12/19
 */
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {


    @Resource
    private  PasswordEncoder passwordEncoder;


    @Resource
    private UserServiceImpl userService;


    @Resource
    private  AuthenticationManager authenticationManager;


    @Resource
    private  JwtTokenEnhancer jwtTokenEnhancer;


    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }


    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> list = new ArrayList<>();
        list.add(jwtTokenEnhancer);
        list.add(accessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(list);
        endpoints.authenticationManager(authenticationManager).userDetailsService(userService).accessTokenConverter(accessTokenConverter()).tokenEnhancer(tokenEnhancerChain);
    }


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("client-app")
                .secret(passwordEncoder.encode("bugcoder"))
                .scopes("all").authorizedGrantTypes("password","refresh_token").accessTokenValiditySeconds(3600).refreshTokenValiditySeconds(86400);
    }


    @Bean
    public JwtAccessTokenConverter accessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(keyPair());
        return jwtAccessTokenConverter;
    }


    @Bean
    public KeyPair keyPair(){
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "bugcoder".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt", "bugcoder".toCharArray());
    }


}

在认证服务中: 配置资源管理器,初始化一些数据(用于测试)

package com.bugcoder.service;


import cn.hutool.core.collection.CollUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;


import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;


/**
 * 资源管理器
 *
 * @author bugcoder
 * @date 2022/12/19
 */
@Service
public class ResourceService {


    private Map<String, List<String>> resourceRolesMap;


    public static final String RESOURCE_ROLES_MAP = "AUTH:RESOURCE_ROLES_MAP";


    @Resource
    private RedisTemplate<String,Object> redisTemplate;


    @PostConstruct
    public void initData() {
        resourceRolesMap = new TreeMap<>();
        resourceRolesMap.put("/api/hello", CollUtil.toList("ADMIN"));
        resourceRolesMap.put("/api/currentUser", CollUtil.toList("ADMIN", "TEST"));
        resourceRolesMap.put("/api/postRequest", CollUtil.toList("ADMIN", "TEST"));
        redisTemplate.opsForHash().putAll(RESOURCE_ROLES_MAP, resourceRolesMap);
    }
}

在认证服务中: 启用安全认证

package com.bugcoder.component;


import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
 * @author bugcoder
 * @date 2022/12/20
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .antMatchers("/rsa/publicKey").permitAll()
                .anyRequest().authenticated();
    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


}

在网关服务中: 自定义全局过滤器

package com.bugcoder.filter;


import com.alibaba.cloud.commons.lang.StringUtils;
import com.nimbusds.jose.JWSObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;


import javax.annotation.Resource;
import java.text.ParseException;


/**
 * 登录用户jwt转化成用户信息
 *
 * @author bugcoder
 * @date 2022/12/20
 */
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {


    @Resource
    ModifyRequestBodyGatewayFilterFactory requestBodyGatewayFilterFactory;
    @Resource
    RequestBodyRewrite requestBodyRewrite;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isEmpty(token)){
            chain.filter(exchange);
        }
        try {
            String bearer = token.replace("bearer", "");
            JWSObject jwsObject = JWSObject.parse(bearer);
            String userStr = jwsObject.getPayload().toString();
            log.info("AuthGlobalFilter.filter() user:{}",userStr);
            ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().header("user", userStr).build();
            exchange = exchange.mutate().request(serverHttpRequest).build();
        }catch (ParseException e){
            e.printStackTrace();
        }
        return requestBodyGatewayFilterFactory
                .apply(new ModifyRequestBodyGatewayFilterFactory.Config().setRewriteFunction(String.class, String.class,requestBodyRewrite))
                .filter(exchange, chain);
    }


    @Override
    public int getOrder() {
        return 0;
    }
}

网关服务的配置application.yml

server:
  port: 9001
#  servlet:
#    context-path: gateway
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes: #配置路由规则
        - id: api
          uri: lb://api
          predicates:
            - Path=/api/**
          filters:
            - StripPrefix=1
        - id: auth2
          uri: lb://auth2
          predicates:
            - Path=/auth2/**
          filters:
            - StripPrefix=1
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能
          lower-case-service-id: true #使用小写服务名,默认是大写
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: 'http://localhost:9002/rsa/publicKey' #配置RSA的公钥访问地址
  redis:
    database: 0
    port: 6379
    host: localhost
    password:
secure:
  ignore:
    urls: #配置白名单路径
      - "/actuator/**"
      - "/auth2/oauth/token"

在业务A服务中:添加接口(GET/POST)用于测试

package com.bugcoder.controller;


import cn.hutool.json.JSONUtil;
import com.bugcoder.component.LoginHandler;
import com.bugcoder.domain.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


import javax.annotation.Resource;
import java.util.Map;


/**
 * @author bugcoder
 * @date 2022/12/20
 */
@RestController
@Slf4j
public class UserController {


    @Resource
    LoginHandler loginHandler;


    @RequestMapping(value = "/currentUser", method = RequestMethod.GET)
    public UserDTO currentUser(){
        return loginHandler.getCurrentUser();
    }


    @RequestMapping(value = "/postRequest", method = RequestMethod.POST)
    public Map<String, String> postRequest(@RequestParam Map<String, String> paramsMap){
        log.info("paramMap: {}", JSONUtil.toJsonStr(paramsMap));
        return paramsMap;
    }
}

启动网关服务(9001)、认证服务(9002)、还有业务A服务(9003),成功以后可以在nacos上看到服务列表:

网关的地址:http://localhost:9001/

前端统一访问网关,网关来负责转发到相关的业务系统

假如想要访问业务A系统(9003)的测试接口currentUser或postRequest

走网关的话,访问的地址应该是9001端口,
http://localhost:9001/api/currentUser,根据上面介绍的转发规则,网关服务会把这个请求转发到业务A系统(9003)上。

参考链接:

https://nacos.io/zh-cn/index.html
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
https://www.rfc-editor.org/rfc/rfc6749
https://www.baeldung.com/spring-cloud-gateway-url-rewriting