网关安全
微服务整体架构图
相关功能组件
- 服务注册与发现:consul,etcd,zookeeper,eureka
- 配置中心:Spring Cloud Config,携程的阿波罗,duic
OAuth2
OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息
主要角色及步骤
角色:
- 用户
- 客户端应用
- 认证服务器
- 订单服务
认证步骤
- 用户直接访问客户端应用(可能是一个WebApp,也可能是一个手机App)
- 客户端应用从认证服务器获取令牌
- 客户端应用访问资源服务器
- 资源服务访问认证服务器来验证从客户端应用获取的请求中的令牌
Oauth认证服务器服务端配置
使用Spring Security OAuth2 构建认证服务
- 创建相关数据库表文件
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
create table oauth_client_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
create table oauth_access_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication BLOB,
refresh_token VARCHAR(256)
);
create table oauth_refresh_token (
token_id VARCHAR(256),
token BLOB,
authentication BLOB
);
create table oauth_code (
code VARCHAR(256), authentication BLOB
);
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt DATETIME,
lastModifiedAt DATETIME
);
- 配置认证服务基础类AuthorizationServerConfigurerAdapter
@Configuration
// 设置当前服务器用来作为授权服务
@EnableAuthorizationServer
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {
// 配置的认证控制器,自定义认证的流程,比如获取到用户名,查询数据库然后比较验证
@Autowired
private AuthenticationManager authenticationManager;
// 配置的数据库信息
@Autowired
private DataSource dataSource;
// 设置存储token为mysql数据库,默认存在内存中
// 也可设置为Redis
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
}
// 配置客户端应用信息
// 使用数据mysql数据库存储
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
// 设置服务的策略为认证通过才能继续
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()");
}
}
- 配置具体认证流程
@Configuration
@EnableWebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 自定义的认证类,返回一个UserDetails对象
@Autowired
private UserDetailsService userDetailsService;
// 密码转换
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 继承 WebSecurityConfigurerAdapter 的验证方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
// 继承 WebSecurityConfigurerAdapter 的验证方式
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- 配置具体的用户信息类,返回UserDetails
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
// UserDetailsService的方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这儿写死了,应该调用DB查询相关信息,然后返回对应的用户信息
return User.withUsername(username)
.password(passwordEncoder.encode("12345"))
.authorities("ROLE_ADMIN")
.build();
}
}
- 写具体的对象信息继承UserDetails,实现其中的方法
Oauth2服务使用验证
- 配置服务为一个资源服务,相当于加了一个过滤器,到来的请求会发送到相应认证服务器服务端
@Configuration
// 声明为资源服务器
@EnableResourceServer
public class Oauth2ResourceServiceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 设置当前资源服务器的名称
resources.resourceId("order-server");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
// 这里可以自定义配置路由规则
// http.authorizeRequests().antMatchers("/haha").permitAll();
}
}
- 配置认证服务器连接地址信息
@EnableWebSecurity
@Configuration
public class Oauth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public ResourceServerTokenServices tokenServices(){
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("orderService");
tokenServices.setClientSecret("12345");
tokenServices.setCheckTokenEndpointUrl("http://localhost:9090/oauth/check_token");
return tokenServices;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
OAuth2AuthenticationManager auth2AuthenticationManager = new OAuth2AuthenticationManager();
auth2AuthenticationManager.setTokenServices(tokenServices());
return auth2AuthenticationManager;
}
}
这种认证方法会使得系统很臃肿,并且不好维护,一般我们都是在网关的地方进行认证。如下。
引入网关
将认证信息写在特定的资源服务里面的缺点:
- 认证代码与业务代码耦合度高,不便于修改
- 服务之间调用复杂
所以需要将认证的步骤放在网关内。便于控制
简单测试
引入zuul相关包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
配置对应yaml文件
zuul:
routes:
token:
url: http://localhost:9090
order:
url: http://localhost:9080
server:
port: 9079
这样每次访问9070/token的接口都会转发到对应认证服务
正式步骤
- 配置认证过滤器,继承ZuulFilter实现相应方法
- 配置日志过滤器,记录响应访问日志
- 配置授权过滤器,对响应请求授权
- 配置流控设置
- 将获取到的用户信息设置到请求头中,转发到对应服务器
配置认证过滤器
@Slf4j
@Component
public class OAuthFilter extends ZuulFilter {
private RestTemplate restTemplate = new RestTemplate();
// 判断过滤器是不是起作用
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
log.info("oauth start");
// 获取请求对象
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
// 发往token服务器的请求,直接过去,因为现在还没有token
if(StringUtils.startsWith(request.getRequestURI(), "/token")) {
return null;
}
// 不是发往认证服务器的,就需要校验token
String authHeader = request.getHeader("Authorization");
// 没有认证信息 也继续往下走
if(StringUtils.isBlank(authHeader)) {
return null;
}
// 判断是否是Bearer认证
if(!StringUtils.startsWithIgnoreCase(authHeader, "bearer ")) {
return null;
}
try {
// 获取token中的相关信息
TokenInfo info = getTokenInfo(authHeader);
request.setAttribute("tokenInfo", info);
} catch (Exception e) {
log.error("get token info fail", e);
}
return null;
}
// 发送请求到认证服务器,验证获取相关token信息
// 这里的配置信息可以注入其他Bean来实现
// 也可用consul等配置中心配置
private TokenInfo getTokenInfo(String authHeader) {
String token = StringUtils.substringAfter(authHeader, "Bearer ");
String oauthServiceUrl = "http://localhost:9090/oauth/check_token";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth("gateway", "12345");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("token", token);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);
ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);
log.info("token info :" + response.getBody().toString());
return response.getBody();
}
// 过滤器类型 有四种 pre,post,error,route
// pre: 业务逻辑执行之前,执行run里的逻辑
// post : 执行之后...
// error : 报错之后,执行...
@Override
public String filterType() {
return "pre";
}
// 过滤器执行顺序
@Override
public int filterOrder() {
return 1;
}
}
配置日志过滤器
@Slf4j
@Component
public class AuditLogFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
// 业务处理插入相关日志信息
@Override
public Object run() throws ZuulException {
log.info("audit log insert");
return null;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 2;
}
}
配置授权过滤器
@Slf4j
@Component
public class AuthorizationFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
// 主要是将认证服务返回的token信息进行权限认证
// 没有对应权限的话就调用 requestContext.setSendZuulResponse(false);
// 设置zull网关不通过
// 有对应权限就放开,发送到具体的地方
@Override
public Object run() throws ZuulException {
log.info("authorization start");
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
if(isNeedAuth(request)) {
TokenInfo tokenInfo = (TokenInfo)request.getAttribute("tokenInfo");
if(tokenInfo != null && tokenInfo.isActive()) {
if(!hasPermission(tokenInfo, request)) {
log.info("audit log update fail 403");
handleError(403, requestContext);
}
requestContext.addZuulRequestHeader("username", tokenInfo.getUser_name());
}else {
// 去获取token的请求不做校验
if(!StringUtils.startsWith(request.getRequestURI(), "/token")) {
log.info("audit log update fail 401");
handleError(401, requestContext);
}
}
}
return null;
}
private void handleError(int status, RequestContext requestContext) {
requestContext.getResponse().setContentType("application/json");
requestContext.setResponseStatusCode(status);
requestContext.setResponseBody("{"message":"auth fail"}");
// 调用这句话就不会向下执行了
requestContext.setSendZuulResponse(false);
}
private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) {
return true; //RandomUtils.nextInt() % 2 == 0;
}
private boolean isNeedAuth(HttpServletRequest request) {
return true;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 3;
}
}
流控配置
- 引入相关包
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
- 配置对应application.yaml
zuul:
routes:
token:
url: http://localhost:9090
order:
url: http://localhost:9080
# 默认不会转发请求头包含Authorization 、cookie的服务
# 这儿设置为空,都会转发
sensitive-headers:
ratelimit:
enabled: true
# 这里可以设置存储的方式
repository: JPA
default-policy-list:
# 3秒中最多2个请求 限制请求数
- limit: 2
# 2个请求的处理时间加起来不能超过1s,在请求数量少,但处理时间长的情景中使用
quota: 1
refresh-interval: 3
# 根据url、请求方式、ip去限制流量
type:
- url
- httpmethod
#- origin
上述的方法会根据自己的策略生成对应请求的限流控制,这种策略可以自己设置, 根据自己的规则,设置相关的key
@Component
public class MyKeyGen extends DefaultRateLimitKeyGenerator {
public MyKeyGen(RateLimitProperties properties,
RateLimitUtils rateLimitUtils) {
super(properties, rateLimitUtils);
}
@Override
public String key(HttpServletRequest request, Route route, Policy policy) {
// TODO Auto-generated method stub
return super.key(request, route, policy);
}
}
当请求数操作限制被拦截的时候,可以自定义错误处理
@Component
public class MyRateLimitErrorHandler extends DefaultRateLimiterErrorHandler {
@Override
public void handleError(String msg, Exception e) {
super.handleError(msg, e);
}
}