1 摘要
对于微服务后台而言,网关层作为所有网络请求的入口。一般基于安全考虑,会在网关层做权限认证,但是对于一些例如登录、注册等接口以及一些资源数据,这些是不需要有认证信息,因此需要在网关层设计一个白名单的功能。本文将基于 Spring Cloud Gateway 2.X 实现白名单功能。
注意事项 : Gateway 网关层的白名单实现原理是在过滤器内判断请求地址是否符合白名单,如果通过则跳过当前过滤器。如果有多个过滤器,则需要在每一个过滤器里边添加白名单判断。
2 核心 Maven 依赖
./cloud-alibaba-gateway-filter/pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>${spring-cloud-gateway.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson-spring.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
依赖对应的版本为:
<lombok.version>1.18.24</lombok.version>
<spring-cloud-gateway.version>2.2.5.RELEASE</spring-cloud-gateway.version>
<servlet-api.version>4.0.1</servlet-api.version>
<hutool.version>5.7.8</hutool.version>
<mysql.version>8.0.27</mysql.version>
<mybatis-plus.version>3.4.3.4</mybatis-plus.version>
<redisson-spring.version>3.17.5</redisson-spring.version>
3 白名单数据库设计
./doc/sql/gateway-database-create.sql
create table gateway_white_list
(
id bigint unsigned not null comment 'id',
route_type varchar(32) comment '路由类型',
path varchar(128) comment '请求路径',
comment varchar(128) comment '说明',
create_date bigint unsigned comment '创建时间',
update_date bigint unsigned comment '更新时间',
primary key (id)
)
engine = innodb default
charset = utf8mb4;
alter table gateway_white_list comment '网关路由白名单';
4 核心代码
4.1 白名单实体类
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/model/WhiteListEntity.java
package com.ljq.demo.springboot.alibaba.gateway.filter.model;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName(value = "gateway_white_list")
public class WhiteListEntity implements Serializable {
private static final long serialVersionUID = -854919732121208131L;
@TableId(type = IdType.NONE)
private Long id;
private String routeType;
private String path;
private String comment;
private Long createDate;
private Long updateDate;
}
4.2 白名单 DAO 接口
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/dao/WhiteListMapper.java
package com.ljq.demo.springboot.alibaba.gateway.filter.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.WhiteListEntity;
import org.springframework.stereotype.Repository;
@Repository
public interface WhiteListMapper extends BaseMapper<WhiteListEntity> {
}
4.3 初始化加载白名单
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/common/component/WhiteListHandler.java
package com.ljq.demo.springboot.alibaba.gateway.filter.common.component;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.constant.RedisKeyConst;
import com.ljq.demo.springboot.alibaba.gateway.filter.dao.WhiteListMapper;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.WhiteListEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class WhiteListHandler implements ApplicationRunner {
@Autowired
private WhiteListMapper whiteListMapper;
@Autowired
private RedisComponent redisComponent;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("-------------加载网关路由白名单------------------");
List<WhiteListEntity> whiteListList = whiteListMapper.selectList(Wrappers.emptyWrapper());
Map<String, Object> whiteListMap = new HashMap<>(16);
whiteListList.forEach(whiteList -> whiteListMap.put(whiteList.getId().toString(), whiteList));
redisComponent.mapPutBatch(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, whiteListMap);
}
}
4.4 权限过滤器
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/interceptor/AuthFilter.java
package com.ljq.demo.springboot.alibaba.gateway.filter.interceptor;
import cn.hutool.json.JSONUtil;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.component.RedisComponent;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.constant.RedisKeyConst;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.WhiteListEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final String TOKEN_KEY = "token";
@Autowired
private RedisComponent redisComponent;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestPath = exchange.getRequest().getPath().value();
log.info("requestPath: {}",requestPath);
if (validateWhiteList(requestPath)) {
return chain.filter(exchange);
}
List<String> tokenList = exchange.getRequest().getHeaders().get(TOKEN_KEY);
log.info("token: {}", tokenList);
if (CollectionUtils.isEmpty(tokenList) || tokenList.get(0).trim().isEmpty()) {
ServerHttpResponse response = exchange.getResponse();
byte[] data = JSONUtil.toJsonStr(ApiResult.fail("Token is null")).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(data);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return response.writeWith(Mono.just(buffer));
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
public boolean validateWhiteList(String requestPath) {
List<WhiteListEntity> whiteListList = redisComponent.mapGetAll(RedisKeyConst.KEY_GATEWAY_WHITE_LIST,
WhiteListEntity.class);
for (WhiteListEntity whiteList : whiteListList) {
if (requestPath.contains(whiteList.getPath()) || requestPath.matches(whiteList.getPath())) {
return true;
}
}
return false;
}
}
必须在进入过滤器后首先进行白名单校验
白名单规则支持正则表达式
4.5 白名单 Service 层
Service 接口
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/service/WhiteListService.java
package com.ljq.demo.springboot.alibaba.gateway.filter.service;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.*;
public interface WhiteListService {
ApiResult add(WhiteListAddParam addParam);
ApiResult info(WhiteListInfoParam infoParam);
ApiResult page(WhiteListPageParam pageParam);
ApiResult update(WhiteListUpdateParam updateParam);
ApiResult delete(WhiteListDeleteParam deleteParam);
}
Service 业务实现类
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/service/impl/WhiteListServiceImpl.java
package com.ljq.demo.springboot.alibaba.gateway.filter.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.component.RedisComponent;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.constant.RedisKeyConst;
import com.ljq.demo.springboot.alibaba.gateway.filter.dao.WhiteListMapper;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.*;
import com.ljq.demo.springboot.alibaba.gateway.filter.service.WhiteListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
@Service
public class WhiteListServiceImpl extends ServiceImpl<WhiteListMapper, WhiteListEntity> implements WhiteListService {
@Autowired
private RedisComponent redisComponent;
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public ApiResult add(WhiteListAddParam addParam) {
WhiteListEntity whiteListParam = new WhiteListEntity();
BeanUtil.copyProperties(addParam, whiteListParam, CopyOptions.create().ignoreError().ignoreNullValue());
long nowTime = System.currentTimeMillis();
whiteListParam.setCreateDate(nowTime);
whiteListParam.setUpdateDate(nowTime);
this.save(whiteListParam);
redisComponent.mapPut(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, whiteListParam.getId().toString(), whiteListParam);
return ApiResult.success(whiteListParam);
}
@Override
public ApiResult info(WhiteListInfoParam infoParam) {
WhiteListEntity whiteList = redisComponent.mapGet(RedisKeyConst.KEY_GATEWAY_WHITE_LIST,
infoParam.getId().toString(), WhiteListEntity.class);
if (Objects.isNull(whiteList)) {
whiteList = this.getById(infoParam.getId());
}
return ApiResult.success(whiteList);
}
@Override
public ApiResult page(WhiteListPageParam pageParam) {
IPage<WhiteListEntity> page = new Page<>(pageParam.getCurrentPage(), pageParam.getPageSize());
LambdaQueryWrapper<WhiteListEntity> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(StrUtil.isNotBlank(pageParam.getRouteType()), WhiteListEntity::getRouteType,
pageParam.getRouteType())
.like(StrUtil.isNotBlank(pageParam.getPath()), WhiteListEntity::getPath, pageParam.getPath())
.like(StrUtil.isNotBlank(pageParam.getComment()), WhiteListEntity::getComment, pageParam.getComment());
return ApiResult.success(this.page(page, queryWrapper));
}
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public ApiResult update(WhiteListUpdateParam updateParam) {
WhiteListEntity whiteListParam = new WhiteListEntity();
BeanUtil.copyProperties(updateParam, whiteListParam, CopyOptions.create().ignoreError().ignoreNullValue());
long nowTime = System.currentTimeMillis();
whiteListParam.setUpdateDate(nowTime);
boolean flag = this.updateById(whiteListParam);
if (flag) {
redisComponent.mapPut(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, whiteListParam.getId().toString(),
whiteListParam);
}
return ApiResult.success(flag);
}
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public ApiResult delete(WhiteListDeleteParam deleteParam) {
boolean flag = this.removeById(deleteParam.getId());
if (flag) {
redisComponent.mapRemove(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, deleteParam.getId().toString());
}
return ApiResult.success(flag);
}
}
白名单的写操作都需要同步到缓存中
4.6 白名单控制层
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/controller/WhiteListController.java
package com.ljq.demo.springboot.alibaba.gateway.filter.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.*;
import com.ljq.demo.springboot.alibaba.gateway.filter.service.WhiteListService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping(value = "/api/gateway/whitelist")
public class WhiteListController {
@Autowired
private WhiteListService whiteListService;
@PostMapping(value = "/add", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<WhiteListEntity>> add(@RequestBody @Validated WhiteListAddParam addParam) {
log.info("/add,新增白名单参数: {}", addParam);
return ResponseEntity.ok(whiteListService.add(addParam));
}
@GetMapping(value = "/info", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<WhiteListEntity>> info(@Validated WhiteListInfoParam infoParam) {
log.info("/info,查询单条白名单参数: {}", infoParam);
return ResponseEntity.ok(whiteListService.info(infoParam));
}
@GetMapping(value = "/page", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<IPage<WhiteListEntity>>> page(@Validated WhiteListPageParam pageParam) {
log.info("/page,分页查询白名单参数: {}", pageParam);
return ResponseEntity.ok(whiteListService.page(pageParam));
}
@PutMapping(value = "/update", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<Boolean>> update(@RequestBody @Validated WhiteListUpdateParam updateParam) {
log.info("/update,修改白名单参数: {}", updateParam);
return ResponseEntity.ok(whiteListService.update(updateParam));
}
@DeleteMapping(value = "/delete", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<Boolean>> delete(@RequestBody @Validated WhiteListDeleteParam deleteParam) {
log.info("/delete,删除单条白名单参数: {}", deleteParam);
return ResponseEntity.ok(whiteListService.delete(deleteParam));
}
}
5 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
|