所有访问微服务的请求都要经过网关,在网关进行用户身份的认证可以将很多非法的请求拦截到微服务以外,这叫做网关认证。

下边需要明确网关的职责:
1、网站白名单维护,针对不用认证的URL全部放行。
2、校验JWT的合法性。 除了白名单剩下的就是需要认证的请求,网关需要验证JWT的合法性,JWT合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。

网关负责授权吗?
网关不负责授权,对请求的授权操作在各个微服务进行,因为微服务最清楚用户有哪些权限访问哪些接口。

新增依赖

将 Spring Security 所需依赖添加到learning-online-gateway工程下的pom文件中

pom.xml
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>

白名单

resource目录下新建白名单文件 security-whitelist.properties,内容如下

/auth/**=认证接口
/content/open/**=内容管理服务公开访问接口
/media/open/**=媒资管理服务公开访问接口
/checkcode/**=验证码服务
/learning/open/**=学习中心服务公开访问接口

配置文件

Token的签发规则应该同认证服务保持一致,在config包下创建 TokenConfig

TokenConfig
@Configuration
public class TokenConfig {

private final static String SIGNING_KEY = "sw-code";

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}

// 令牌管理服务
@Bean(name = "authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setSupportRefreshToken(true); // 支持刷新令牌
services.setTokenStore(tokenStore()); // 令牌存储策略

TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter()));
services.setTokenEnhancer(tokenEnhancerChain);

services.setAccessTokenValiditySeconds(7200); //令牌默认有效期2小时
services.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期2天
return services;
}
}

安全配置类

配置URL拦截规则

SecurityConfig
/**
* 安全配置类
*/
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {

@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/**").permitAll()
.anyExchange().authenticated()
.and().csrf().disable().build();
}
}

认证过滤器

自定义网关认证过滤器,需要实现两个接口类:GlobalFilter、Ordered

错误实体类,返回给前端

ErrorResult
/**
* 返回结果实体类
*/
@Data
public class ErrorResult implements Serializable {

private static final long serialVersionUID = 1L;
private Integer code;
private String message;
private Object data;
public ErrorResult() {}

// 返回失败
public static ErrorResult fail(Integer code, String message) {
ErrorResult result = new ErrorResult();
result.setCode(code);
result.setMessage(message);
return result;
}
}

在过滤器中,操作步骤如下:

  1. 使用配置的白名单过滤需要认证的URL;
  2. 取出Token,检查其有效性
  3. 有效,将携带JWT路由到各个微服务;无效则返回错误结果。
GatewayAuthFilter
/**
* 网关认证过滤器
*/
@Slf4j
@Component
public class GatewayAuthFilter implements GlobalFilter, Ordered {

private static List<String> whitelist = null;
private final TokenStore tokenStore;

static {
// 加载白名单
try (
InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
) {
Properties properties = new Properties();
properties.load(resourceAsStream);
Set<String> strings = properties.stringPropertyNames();
whitelist = new ArrayList<>(strings);
} catch (Exception e) {
log.error("加载/security-whitelist.properties出错", e);
}
}

public GatewayAuthFilter(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
// 白名单放行
for (String url : whitelist) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}

// 检查Token是否存在
String token = getToken(exchange);
if (StringUtils.isEmpty(token)) {
return buildReturnMono("没有认证", exchange);
}

// 校验Token有效性
OAuth2AccessToken oAuth2AccessToken;
try {
oAuth2AccessToken = tokenStore.readAccessToken(token);
boolean expired = oAuth2AccessToken.isExpired();
if (expired) {
return buildReturnMono("认证令牌已过期", exchange);
}
return chain.filter(exchange);
} catch (InvalidTokenException e) {
log.info("认证令牌无效: {}", token);
return buildReturnMono("认证令牌无效", exchange);

}
}

/**
* 获取Token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isEmpty(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isEmpty(token)) {
return null;
}
return token;
}

/**
* 构建错误返回结果
*/
private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
String jsonString = JSON.toJSONString(ErrorResult.fail(HttpStatus.UNAUTHORIZED.value(), error));
byte[] bytes = jsonString.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}

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