一般的资源服务器鉴权的实现都比较简单,在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)
{
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
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));
复制代码