升级Spring Cloud Hoxton & Alibaba,认证授权改用Oauth2。

This commit is contained in:
macro
2020-08-15 10:51:40 +08:00
parent d004888a10
commit dae82f62ed
258 changed files with 4671 additions and 3092 deletions

View File

@@ -0,0 +1,104 @@
package com.macro.mall.authorization;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.macro.mall.common.constant.AuthConstant;
import com.macro.mall.common.domain.UserDto;
import com.macro.mall.config.IgnoreUrlsConfig;
import com.nimbusds.jose.JWSObject;
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;
import java.util.stream.Collectors;
/**
* 鉴权管理器,用于判断是否有资源的访问权限
* Created by macro on 2020/6/19.
*/
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@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> ignoreUrls = ignoreUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
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.isEmpty(token)){
return Mono.just(new AuthorizationDecision(false));
}
String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
UserDto userDto = JSONUtil.toBean(userStr, UserDto.class);
if (AuthConstant.ADMIN_CLIENT_ID.equals(userDto.getClientId()) && !pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
return Mono.just(new AuthorizationDecision(false));
}
if (AuthConstant.PORTAL_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)));
}
}
authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
//认证通过且角色匹配的用户可访问当前路径
return mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
}

View File

@@ -0,0 +1,35 @@
package com.macro.mall.component;
import cn.hutool.json.JSONUtil;
import com.macro.mall.common.api.CommonResult;
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过期时
* Created by macro on 2020/6/18.
*/
@Component
public class RestAuthenticationEntryPoint 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(CommonResult.unauthorized(e.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}

View File

@@ -0,0 +1,36 @@
package com.macro.mall.component;
import cn.hutool.json.JSONUtil;
import com.macro.mall.common.api.CommonResult;
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;
/**
* 自定义返回结果:没有权限访问时
* Created by macro on 2018/4/26.
*/
@Component
public class RestfulAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
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(CommonResult.forbidden(denied.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}

View File

@@ -21,7 +21,7 @@ public class GlobalCorsConfig {
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);

View File

@@ -0,0 +1,20 @@
package com.macro.mall.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 网关白名单配置
* Created by macro on 2020/6/17.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Component
@ConfigurationProperties(prefix="secure.ignore")
public class IgnoreUrlsConfig {
private List<String> urls;
}

View File

@@ -0,0 +1,19 @@
package com.macro.mall.config;
import com.macro.mall.common.config.BaseRedisConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis相关配置
* Created by macro on 2020/6/19.
*/
@Configuration
public class RedisConfig extends BaseRedisConfig {
}

View File

@@ -0,0 +1,66 @@
package com.macro.mall.config;
import cn.hutool.core.util.ArrayUtil;
import com.macro.mall.authorization.AuthorizationManager;
import com.macro.mall.common.constant.AuthConstant;
import com.macro.mall.component.RestAuthenticationEntryPoint;
import com.macro.mall.component.RestfulAccessDeniedHandler;
import com.macro.mall.filter.IgnoreUrlsRemoveJwtFilter;
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;
/**
* 资源服务器配置
* Created by macro on 2020/6/19.
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
//自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
//对白名单路径直接移除JWT请求头
http.addFilterBefore(ignoreUrlsRemoveJwtFilter,SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
.anyExchange().access(authorizationManager)//鉴权管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
.authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
.and().csrf().disable();
return http.build();
}
@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,55 @@
package com.macro.mall.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* Swagger资源配置
* Created by macro on 2020/7/9.
*/
@Slf4j
@Component
@Primary
@AllArgsConstructor
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
//获取所有路由的ID
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
//过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成api-docs路径->生成SwaggerResource
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
route.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("**", "v2/api-docs"))));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
log.info("name:{},location:{}", name, location);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}

View File

@@ -0,0 +1,51 @@
package com.macro.mall.filter;
import cn.hutool.core.util.StrUtil;
import com.macro.mall.common.constant.AuthConstant;
import com.nimbusds.jose.JWSObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import java.text.ParseException;
/**
* 将登录用户的JWT转化成用户信息的全局过滤器
* Created by macro on 2020/6/17.
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
if (StrUtil.isEmpty(token)) {
return chain.filter(exchange);
}
try {
//从token中解析用户信息并设置到Header中去
String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr);
ServerHttpRequest request = exchange.getRequest().mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).build();
exchange = exchange.mutate().request(request).build();
} catch (ParseException e) {
e.printStackTrace();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}

View File

@@ -0,0 +1,43 @@
package com.macro.mall.filter;
import com.macro.mall.common.constant.AuthConstant;
import com.macro.mall.config.IgnoreUrlsConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
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请求头
* Created by macro on 2020/7/24.
*/
@Component
public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@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 = ignoreUrlsConfig.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);
}
}

View File

@@ -0,0 +1,58 @@
package com.macro.mall.handler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
* 自定义Swagger的各个配置节点
* Created by macro on 2020/7/9.
*/
@RestController
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
/**
* Swagger安全配置支持oauth和apiKey设置
*/
@GetMapping("/swagger-resources/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/**
* Swagger UI配置
*/
@GetMapping("/swagger-resources/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/**
* Swagger资源配置微服务中这各个服务的api-docs信息
*/
@GetMapping("/swagger-resources")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}

View File

@@ -1,5 +0,0 @@
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka/
registry-fetch-interval-seconds: 10

View File

@@ -1,7 +0,0 @@
eureka:
client:
service-url:
defaultZone: http://mall-registry:8001/eureka/
registry-fetch-interval-seconds: 10
instance:
prefer-ip-address: true

View File

@@ -1,16 +1,77 @@
server:
port: 8201
spring:
application:
name: mall-gateway
profiles:
active: dev
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true #使用小写service-id
routes: #配置路由路径
- id: mall-auth
uri: lb://mall-auth
predicates:
- Path=/mall-auth/**
filters:
- StripPrefix=1
- id: mall-admin
uri: lb://mall-admin
predicates:
- Path=/mall-admin/**
filters:
- StripPrefix=1
- id: mall-portal
uri: lb://mall-portal
predicates:
- Path=/mall-portal/**
filters:
- StripPrefix=1
- id: mall-search
uri: lb://mall-search
predicates:
- Path=/mall-search/**
filters:
- StripPrefix=1
- id: mall-demo
uri: lb://mall-demo
predicates:
- Path=/mall-demo/**
filters:
- StripPrefix=1
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:8201/mall-auth/rsa/publicKey' #配置RSA的公钥访问地址
redis:
database: 0
port: 6379
host: localhost
password:
secure:
ignore:
urls: #配置白名单路径
- "/doc.html"
- "/swagger-resources/**"
- "/swagger/**"
- "/**/v2/api-docs"
- "/**/*.js"
- "/**/*.css"
- "/**/*.png"
- "/**/*.ico"
- "/webjars/springfox-swagger-ui/**"
- "/actuator/**"
- "/mall-auth/oauth/token"
- "/mall-auth/rsa/publicKey"
- "/mall-search/**"
- "/mall-portal/sso/**"
- "/mall-portal/home/**"
- "/mall-portal/product/**"
- "/mall-portal/brand/**"
- "/mall-admin/admin/login"
- "/mall-admin/admin/register"
- "/mall-admin/admin/info"
- "/mall-admin/minio/upload"
management: #开启SpringBoot Admin的监控
endpoints:
web:
@@ -19,6 +80,3 @@ management: #开启SpringBoot Admin的监控
endpoint:
health:
show-details: always
logging:
level:
org.springframework.cloud.gateway: debug

View File

@@ -0,0 +1,8 @@
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
config:
server-addr: http://localhost:8848
file-extension: yaml

View File

@@ -0,0 +1,8 @@
spring:
cloud:
nacos:
discovery:
server-addr: http://nacos-registry:8848
config:
server-addr: http://nacos-registry:8848
file-extension: yaml

View File

@@ -0,0 +1,5 @@
spring:
profiles:
active: dev
application:
name: mall-gateway