所有访问微服务的请求都要经过网关,在网关进行用户身份的认证可以将很多非法的请求拦截到微服务以外,这叫做网关认证。
下边需要明确网关的职责:
1、网站白名单维护,针对不用认证的URL全部放行。
2、校验JWT的合法性。 除了白名单剩下的就是需要认证的请求,网关需要验证JWT的合法性,JWT合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。
网关负责授权吗?
网关不负责授权,对请求的授权操作在各个微服务进行,因为微服务最清楚用户有哪些权限访问哪些接口。
新增依赖
将 Spring Security 所需依赖添加到learning-online-gateway
工程下的pom文件中
pom.xml <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); services.setRefreshTokenValiditySeconds(259200); 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; } }
|
在过滤器中,操作步骤如下:
- 使用配置的白名单过滤需要认证的URL;
- 取出Token,检查其有效性
- 有效,将携带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); } }
String token = getToken(exchange); if (StringUtils.isEmpty(token)) { return buildReturnMono("没有认证", exchange); }
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);
} }
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; } }
|