前言
一般的资源服务器鉴权的实现都比较简单,在JWT作为access_token的情况下,只需要配置JwtTokenStore即可使用@PreAuthorize和@PostAuthorize等注解进行基于角色权限的鉴权。
然而在Spring Cloud Gateway中,使用得是基于Web Flux的鉴权,在配置和实现上与之前有所区别。且我们需要在网关实现基于URL的鉴权,因此需要进行自定义实现。
整体结构
网关对请求的主要处理流程包括ReactiveAuthenticationManager->ReactiveAuthorizationManager->Gateway Filters。其中ReactiveAuthenticationManager用于封装JWT为OAuth2Authentication并判断Token的有效性;ReactiveAuthorizationManager用于基于URL的鉴权;Gateway Filters是网关的过滤流程。
ReactiveAuthorizationManager实现
实现该接口主要是为了鉴权,我们在这里使用URL进行鉴权,通过获取用户Authentication里的Authorities,判断是否允许访问该路径。
AccessManager实现
这里只是简单的判断是否是跨域的检验请求,是的话直接放行。具体的鉴权实现在UrlAuthorityChecker里。
/**
* 描述:鉴权管理器
*
* @author xhsf
* @create 2020/11/26 11:26
*/
@Component
public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final UrlAuthorityChecker urlAuthorityChecker;
public AccessManager(UrlAuthorityChecker urlAuthorityChecker){
this.urlAuthorityChecker = urlAuthorityChecker;
}
/**
* 实现权限验证判断
*/
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono,
AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
// 对应跨域的预检请求直接放行
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
// 进行鉴权
String url = request.getURI().getPath();
return authenticationMono
.map(auth -> new AuthorizationDecision(urlAuthorityChecker.check(auth.getAuthorities(), url)))
.defaultIfEmpty(new AuthorizationDecision(false));
}
}
UrlAuthorityChecker实现
这里有两个重点,一个是权限名与授权路径的对应关系,比如get_user权限可以访问/users/*路径。另外一个是白名单,比如/oauth/**为白名单里面的路径。但是我们并不想直接在代码里面把这两个配置写死,因为有可能会需要修改权限名与授权路径的对应关系和白名单列表,因此我们添加定时任务,定时的更新这两个列表,同时把白名单作为一个服务,可以通过相应的接口(或者图形界面)进行修改。
/**
* 描述:基于Url的权限检查器
*
* @author xhsf
* @create 2020/11/26 15:10
*/
@Component
@EnableScheduling
public class UrlAuthorityChecker {
@Reference
private PermissionService permissionService;
@Reference
private WhiteListService whiteListService;
/**
* 刷新permissionNameAuthorizationUrlMap的间隔
*/
private static final int REFRESH_DELAY = 60000;
/**
* 权限名和授权URL的对应关系
*/
private Map<String, String> permissionNameAuthorizationUrlMap;
/**
* 路径白名单
*/
private List<String> whiteList;
private final AntPathMatcher antPathMatcher;
public UrlAuthorityChecker(AntPathMatcher antPathMatcher) {
this.antPathMatcher = antPathMatcher;
}
/**
* 通过用户拥有的权限进行鉴权
*
* @param authorities 权限列表
* @param url 请求的Url
* @return 是否通过鉴权
*/
public boolean check(Collection<? extends GrantedAuthority> authorities, String url) {
// 判断该路径是否在白名单里,如果是直接放行
for (String permittedUrl : whiteList) {
if (antPathMatcher.match(permittedUrl, url)) {
return true;
}
}
// 判断用户的权限里有没有满足获取该路径资源的
for (GrantedAuthority authority : authorities) {
String authorizationUrl = permissionNameAuthorizationUrlMap.get(authority.getAuthority());
if (authorizationUrl != null && antPathMatcher.match(authorizationUrl, url)) {
return true;
}
}
return false;
}
/**
* 创建newPermissionNameAuthorizationUrlMap并替换旧的
* 这里添加定时任务,会每REFRESH_DELAY毫秒刷新一次permissionNameAuthorizationUrlMap
*/
@Scheduled(initialDelay = 0, fixedDelay = REFRESH_DELAY)
private void updatePermissionNameAuthorizationUrlMap() {
List<PermissionDTO> permissionDTOList = permissionService.getAllPermission().getData();
Map<String, String> newPermissionNameAuthorizationUrlMap = new ConcurrentHashMap<>();
for (PermissionDTO permissionDTO : permissionDTOList) {
newPermissionNameAuthorizationUrlMap.put(
ResourceServerConstant.AUTHORITY_PREFIX + permissionDTO.getPermissionName(),
permissionDTO.getAuthorizationUrl());
}
permissionNameAuthorizationUrlMap = newPermissionNameAuthorizationUrlMap;
}
/**
* 创建newWhiteList并替换旧的
* 这里添加定时任务,会每REFRESH_DELAY毫秒刷新一次whiteList
*/
@Scheduled(initialDelay = 0, fixedDelay = REFRESH_DELAY)
private void updateWhiteList() {
Result<List<String>> getWhiteListResult = whiteListService.getWhiteList();
whiteList = getWhiteListResult.getData();
}
}
跨域配置
这里简单的添加了CorsConfigurationSource,该Bean会被Spring Boot自动识别,并构造一个CorsWebFilter进行跨域过滤,只要在ServerHttpSecurity里配置了cors()。
/**
* 描述:跨域配置
*
* @author xhsf
* @create 2020/11/28 0:34
*/
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
ResourceServerConfig配置
这里把网关作为资源服务器,进行鉴权操作。该类是用来配置各种过滤器,以实现具体的鉴权逻辑。具体如下代码注释。
/**
* 描述:资源服务器配置
*
* @author xhsf
* @create 2020/11/27 15:19
*/
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AccessManager authorizationManager;
private final CustomServerAccessDeniedHandler customServerAccessDeniedHandler;
private final CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;
public ResourceServerConfig(AccessManager authorizationManager,
CustomServerAccessDeniedHandler customServerAccessDeniedHandler,
CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint) {
this.authorizationManager = authorizationManager;
this.customServerAccessDeniedHandler = customServerAccessDeniedHandler;
this.customServerAuthenticationEntryPoint = customServerAuthenticationEntryPoint;
}
/**
* Web Security过滤器链配置
* @param http ServerHttpSecurity
* @return SecurityWebFilterChain
*/
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// JWT处理
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
// 自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(customServerAuthenticationEntryPoint);
// 添加鉴权过滤器
http.authorizeExchange()
.anyExchange().access(authorizationManager)
.and()
// 鉴权异常处理
.exceptionHandling()
.accessDeniedHandler(customServerAccessDeniedHandler) // 处理未授权
.authenticationEntryPoint(customServerAuthenticationEntryPoint) //处理未认证
.and()
.csrf().disable()
.cors();
return http.build();
}
/**
* ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
* 需要把jwt的Claim中的authorities加入
* 重新定义ReactiveAuthenticationManager权限管理器,添加默认转换器JwtGrantedAuthoritiesConverter
* @return ReactiveJwtAuthenticationConverterAdapter
*/
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(ResourceServerConstant.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(ResourceServerConstant.AUTHORITIES_CLAIM_NAME);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
其他代码
CustomServerAccessDeniedHandler
捕获AccessDeniedException并返回自定义响应结果。
/**
* 描述:无权访问自定义响应
*
* @author xhsf
* @create 2020/11/26 19:32
*/
@Component
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getHeaders().set("Access-Control-Allow-Origin", "*");
response.getHeaders().set("Cache-Control", "no-cache");
String body = JSON.toJSONString(Result.fail(ErrorCode.FORBIDDEN, e.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
CustomServerAuthenticationEntryPoint
捕获AuthenticationException并返回自定义响应结果。
/**
* 描述:无效token/token过期 自定义响应
*
* @author xhsf
* @create 2020/11/26 19:34
*/
@Component
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getHeaders().set("Access-Control-Allow-Origin", "*");
response.getHeaders().set("Cache-Control", "no-cache");
String body = JSON.toJSONString(Result.fail(ErrorCode.UNAUTHORIZED, e.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}