目前后端项目基础框架涉及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:
- 客户端从资源拥有者那里请求授权。授权请求能够直接发送给资源拥有者,或者间接地通过授权服务器这样的中介,而后者更为可取。
- 客户端收到一个访问许可,它代表由资源服务器提供的授权。
- 客户端使用它自己的私有证书到授权服务器上验证,并出示访问许可,来请求一个访问令牌。
- 授权服务器验证客户端私有证书和访问许可的有效性,如果验证通过则分发一个访问令牌。
- 客户端通过出示访问令牌向资源服务器请求受保护资源。
- 资源服务器验证访问令牌的有效性,如果验证通过则响应这个资源请求。
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