preface
The implementation of resource server authentication is simple. In the case that JWT is an access_token, you only need to configure JwtTokenStore to use @preauthorize and @PostAuthorize annotations to implement role-based authorization.
However, in Spring Cloud Gateway, Web Flux based authentication is used, which is different in configuration and implementation. In addition, we need to implement URL-based authentication on the gateway, so we need to customize the implementation.
The overall structure
Gateway to the request of the main process including ReactiveAuthenticationManager – > ReactiveAuthorizationManager – > Gateway Filters. The ReactiveAuthenticationManager encapsulates JWT OAuth2Authentication and judging the validity of the Token; ReactiveAuthorizationManager for authentication based on URL; Gateway Filters are the filtering processes of gateways.
ReactiveAuthorizationManager implementation
This interface is mainly used for Authentication. We use URL Authentication to authenticate the user. After obtaining the Authorities of user Authentication, we can determine whether to allow access to the path.
AccessManager implementation
This is simply to determine whether it is a cross-domain check request, if yes, it is directly allowed. The specific authentication implementation is in UrlAuthorityChecker.
/** * Description: Authentication manager **@author xhsf
* @create2020/11/26 11:26 * /
@Component
public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final UrlAuthorityChecker urlAuthorityChecker;
public AccessManager(UrlAuthorityChecker urlAuthorityChecker){
this.urlAuthorityChecker = urlAuthorityChecker;
}
/** * implement permission verification judgment */
@Override
public Mono<AuthorizationDecision> check(Mono
authenticationMono, AuthorizationContext authorizationContext)
{
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
// Allow the cross-domain precheck request directly
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
// Perform authentication
String url = request.getURI().getPath();
return authenticationMono
.map(auth -> new AuthorizationDecision(urlAuthorityChecker.check(auth.getAuthorities(), url)))
.defaultIfEmpty(new AuthorizationDecision(false)); }}Copy the code
UrlAuthorityChecker implementation
There are two important points here. One is the relationship between the permission name and the authorization path. For example, the get_user permission can access the /users/* path. /oauth/** is the path in the whitelist. But we don’t want to directly write the two configuration in code to death, because may need to modify the permission and authorization path corresponding relation and white list, so we add timing tasks, regularly update the two list, while the white list as a service, you can through the corresponding interface (or graphical interface).
/** * Description: URL-based permission checker **@author xhsf
* @create2020/11/26 he * /
@Component
@EnableScheduling
public class UrlAuthorityChecker {
@Reference
private PermissionService permissionService;
@Reference
private WhiteListService whiteListService;
/ * * / * * refresh permissionNameAuthorizationUrlMap intervals
private static final int REFRESH_DELAY = 60000;
/** * Mapping between permission name and authorization URL */
private Map<String, String> permissionNameAuthorizationUrlMap;
/** * Path whitelist */
private List<String> whiteList;
private final AntPathMatcher antPathMatcher;
public UrlAuthorityChecker(AntPathMatcher antPathMatcher) {
this.antPathMatcher = antPathMatcher;
}
/** * Authentication is performed based on the user's permissions **@paramAuthorities List *@paramUrl Indicates the requested URL *@returnAuthentication */
public boolean check(Collection<? extends GrantedAuthority> authorities, String url) {
// Check whether the path is in the whitelist. If yes, the path is allowed
for (String permittedUrl : whiteList) {
if (antPathMatcher.match(permittedUrl, url)) {
return true; }}// Check whether the user has permissions sufficient to obtain the path resource
for (GrantedAuthority authority : authorities) {
String authorizationUrl = permissionNameAuthorizationUrlMap.get(authority.getAuthority());
if(authorizationUrl ! =null && antPathMatcher.match(authorizationUrl, url)) {
return true; }}return false;
}
/ * * * create newPermissionNameAuthorizationUrlMap and replace the old * add timing task here, will refresh every REFRESH_DELAY milliseconds permissionNameAuthorizationUrlMap * /
@Scheduled(initialDelay = 0, fixedDelay = REFRESH_DELAY)
private void updatePermissionNameAuthorizationUrlMap(a) {
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;
}
/** * create newWhiteList and replace the old *. Add a timed task to refresh whiteList */ every time REFRESH_DELAY milliseconds
@Scheduled(initialDelay = 0, fixedDelay = REFRESH_DELAY)
private void updateWhiteList(a) { Result<List<String>> getWhiteListResult = whiteListService.getWhiteList(); whiteList = getWhiteListResult.getData(); }}Copy the code
Cross-domain configuration
Here we simply add a CorsConfigurationSource, which is automatically recognized by Spring Boot and constructs a CorsWebFilter for cross-domain filtering, as long as cORS () is configured in ServerHttpSecurity.
/** * Description: Cross-domain configuration **@author xhsf
* @create2020/11/28 0:34 * /
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource(a) {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/ * *", corsConfiguration);
returnsource; }}Copy the code
ResourceServerConfig configuration
The gateway is used as a resource server for authentication. This class is used to configure various filters to implement specific authentication logic. Specific code comments are as follows.
/** * Description: Resource server configuration **@author xhsf
* @create2020/11/27 bowing * /
@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 filter chain configuration *@param http ServerHttpSecurity
* @return SecurityWebFilterChain
*/
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
/ / JWT processing
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
// Custom processing of JWT request header expiration or signature error results
http.oauth2ResourceServer().authenticationEntryPoint(customServerAuthenticationEntryPoint);
// Add an authentication filter
http.authorizeExchange()
.anyExchange().access(authorizationManager)
.and()
// Handle authentication exceptions
.exceptionHandling()
.accessDeniedHandler(customServerAccessDeniedHandler) // Handle unauthorized requests
.authenticationEntryPoint(customServerAuthenticationEntryPoint) // Handle unauthenticated
.and()
.csrf().disable()
.cors();
return http.build();
}
/** * ServerHttpSecurity does not add authorities to JWT Claim as Authentication * Redefine ReactiveAuthenticationManager permissions manager, add the default converter 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 newReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); }}Copy the code
Other code
CustomServerAccessDeniedHandler
Catch AccessDeniedException and return custom response results.
/** * Description: Has no access to the custom response **@author xhsf
* @createBank against 2020/11/26 * /
@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));
returnresponse.writeWith(Mono.just(buffer)); }}Copy the code
CustomServerAuthenticationEntryPoint
Catch AuthenticationException and return custom response results.
/** * Description: Invalid token/ Token expired custom response **@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));
returnresponse.writeWith(Mono.just(buffer)); }}Copy the code