网关整个nacos和oauth2认证资源授权测试

This commit is contained in:
XCZTH 2020-09-01 20:00:08 +08:00
parent ef56771a05
commit affa6880b2
11 changed files with 422 additions and 31 deletions

View File

@ -23,7 +23,7 @@ public class ResourceRoleRulesHolder {
Map<String, List<String>> resourceRoleMap = new TreeMap<>();
List<String> roleNames = new ArrayList<>();
roleNames.add(AuthConstant.AUTHORITY_PREFIX + "1_root");
resourceRoleMap.put("/youlai-service/users", roleNames);
resourceRoleMap.put("/admin/users", roleNames);
redisTemplate.delete(AuthConstant.RESOURCE_ROLES_MAP_KEY);
redisTemplate.opsForHash().putAll(AuthConstant.RESOURCE_ROLES_MAP_KEY, resourceRoleMap);
}

View File

@ -113,7 +113,7 @@ public class SysUserController {
if(sysUser!=null){
BeanUtil.copyProperties(sysUser,userDTO);
List<Integer> roleIds = iSysUserRoleService.list(new LambdaQueryWrapper<SysUserRole>()
.eq(SysUserRole::getUserId, sysUser)
.eq(SysUserRole::getUserId, sysUser.getId())
).stream().map(item -> item.getRoleId()).collect(Collectors.toList());
if(CollectionUtil.isNotEmpty(roleIds)){
List<String> roles = iSysRoleService.listByIds(roleIds).stream()

View File

@ -2,50 +2,86 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>youlai-mall</artifactId>
<groupId>com.youlai</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>youlai-gateway</artifactId>
<properties>
<spring-cloud.version>Hoxton.SR5</spring-cloud.version>
<youlai-common.version>1.0.0-SNAPSHOT</youlai-common.version>
</properties>
<dependencies>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>youlai-admin-api</artifactId>
<version>${youlai-common.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>youlai-common-core</artifactId>
<version>${youlai-common.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>youlai-common-auth</artifactId>
<version>${youlai-common.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>youlai-common-redis</artifactId>
<version>${youlai-common.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>-->
<!-- nacos 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- 认证依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- spring cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>

View File

@ -1,5 +1,4 @@
package com.youlai.gateway;
package com.youlai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -8,7 +7,8 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
}

View File

@ -0,0 +1,106 @@
package com.youlai.gateway.auth;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JWSObject;
import com.youlai.admin.api.dto.UserDTO;
import com.youlai.common.auth.constant.AuthConstant;
import com.youlai.gateway.config.WhiteUrlsConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 鉴权管理器用于判断是否有资源的访问权限
*/
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private WhiteUrlsConfig whiteUrlsConfig;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
URI uri = request.getURI();
//白名单路径直接放行
PathMatcher pathMatcher = new AntPathMatcher();
List<String> whiteUrls = whiteUrlsConfig.getUrls();
for (String ignoreUrl : whiteUrls) {
if (pathMatcher.match(ignoreUrl, uri.getPath())) {
return Mono.just(new AuthorizationDecision(true));
}
}
//对应跨域的预检请求直接放行
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
try {
String token = request.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
if (StrUtil.isBlank(token)) {
return Mono.just(new AuthorizationDecision(false));
}
token = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
JWSObject jwsObject = JWSObject.parse(token);
String payload = jwsObject.getPayload().toString(); // jwt 载体部分
UserDTO userDTO = JSONUtil.toBean(payload, UserDTO.class);
if (AuthConstant.ADMIN_CLIENT_ID.equals(userDTO.getClientId())
&& !pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
return Mono.just(new AuthorizationDecision(false));
}
} catch (ParseException e) {
e.printStackTrace();
return Mono.just(new AuthorizationDecision(false));
}
// 非管理端路径直接放行
if (!pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
return Mono.just(new AuthorizationDecision(true));
}
// 管理端路径需要校验权限
Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY);
Iterator<Object> iterator = resourceRolesMap.keySet().iterator();
List<String> authorities = new ArrayList<>();
while (iterator.hasNext()) {
String pattern = (String) iterator.next();
if (pathMatcher.match(pattern, uri.getPath())) {
authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
}
}
List<String> finalAuthorities = authorities;
Mono<AuthorizationDecision> authorizationDecisionMono = mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
return authorizationDecisionMono;
}
}

View File

@ -0,0 +1,35 @@
package com.youlai.gateway.component;
import cn.hutool.json.JSONUtil;
import com.youlai.common.result.Result;
import com.youlai.common.result.ResultCodeEnum;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
/**
* 自定义返回结果没有登录或token过期
*/
@Component
public class AuthServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response=exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
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 = JSONUtil.toJsonStr(Result.custom(ResultCodeEnum.USER_ACCOUNT_UNAUTHENTICATED));
DataBuffer buffer= response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}

View File

@ -0,0 +1,33 @@
package com.youlai.gateway.component;
import cn.hutool.json.JSONUtil;
import com.youlai.common.result.Result;
import com.youlai.common.result.ResultCodeEnum;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
@Component
public class ResourceServerAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
ServerHttpResponse response=exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
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= JSONUtil.toJsonStr(Result.custom(ResultCodeEnum.USER_ACCESS_UNAUTHORIZED));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}

View File

@ -0,0 +1,79 @@
package com.youlai.gateway.config;
import cn.hutool.core.util.ArrayUtil;
import com.youlai.common.auth.constant.AuthConstant;
import com.youlai.gateway.auth.AuthorizationManager;
import com.youlai.gateway.component.AuthServerAuthenticationEntryPoint;
import com.youlai.gateway.component.ResourceServerAccessDeniedHandler;
import com.youlai.gateway.filter.WhiteUrlsRemoveJwtFilter;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
/**
* 资源服务器配置
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private AuthorizationManager authorizationManager;
private ResourceServerAccessDeniedHandler resourceServerAccessDeniedHandler;
private AuthServerAuthenticationEntryPoint authServerAuthenticationEntryPoint;
private WhiteUrlsConfig whiteUrlsConfig;
private WhiteUrlsRemoveJwtFilter whiteUrlsRemoveJwtFilter;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
// 自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(authServerAuthenticationEntryPoint);
// 对白名单路径直接移除JWT请求头
http.addFilterBefore(whiteUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(whiteUrlsConfig.getUrls(),String.class)).permitAll()
.anyExchange().access(authorizationManager)
.and()
.exceptionHandling()
.accessDeniedHandler(resourceServerAccessDeniedHandler) // 处理未授权
.authenticationEntryPoint(authServerAuthenticationEntryPoint) //处理未认证
.and().csrf().disable();
return http.build();
}
/**
* https://blog.csdn.net/qq_24230139/article/details/105091273
* ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
* 需要把jwt的Claim中的authorities加入
* 方案重新定义ReactiveAuthenticationManager权限管理器默认转换器JwtGrantedAuthoritiesConverter
*
* @return
*/
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}

View File

@ -0,0 +1,15 @@
package com.youlai.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Data
@Configuration
@ConfigurationProperties(prefix = "white")
public class WhiteUrlsConfig {
private List<String> urls;
}

View File

@ -0,0 +1,43 @@
package com.youlai.gateway.filter;
import cn.hutool.core.util.StrUtil;
import com.nimbusds.jose.JWSObject;
import com.youlai.common.auth.constant.AuthConstant;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 将登录用户的JWT转换成用户信息过滤器
*/
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
if (StrUtil.isBlank(token)) {
return chain.filter(exchange);
}
token = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
JWSObject jwsObject = JWSObject.parse(token);
String userStr = jwsObject.getPayload().toString();
log.info("AuthGlobalFilter#filteruser:{}", userStr);
ServerHttpRequest request = exchange.getRequest().mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}

View File

@ -0,0 +1,44 @@
package com.youlai.gateway.filter;
import com.youlai.common.auth.constant.AuthConstant;
import com.youlai.gateway.config.WhiteUrlsConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.List;
/**
* 白名单路径移除JWT请求头
*/
@Component
public class WhiteUrlsRemoveJwtFilter implements WebFilter {
@Autowired
private WhiteUrlsConfig whiteUrlsConfig;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request=exchange.getRequest();
URI uri=request.getURI();
PathMatcher pathMatcher=new AntPathMatcher();
//白名单路径移除JWT请求头
List<String> ignoreUrls = whiteUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
if (pathMatcher.match(ignoreUrl, uri.getPath())) {
request = exchange.getRequest().mutate().header(AuthConstant.JWT_TOKEN_HEADER, "").build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
}
return chain.filter(exchange);
}
}