From 30f4703573f86924792e8ebd3089e4f4572cd34d Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=88=98=E6=B4=AA=E9=9D=92?= Date: Sun, 22 Sep 2019 11:52:58 +0800 Subject: [PATCH] =?utf8?q?feat:=20=E6=96=B0=E5=A2=9EAPI=E8=AE=BF=E9=97=AE?= =?utf8?q?=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../configuration/GlobalFilterConfig.java | 8 + .../filter/AccessControlGlobalFilter.java | 426 ++++++++++++++++++ .../SimpleUserTransmitGlobalFilter.java | 2 +- .../userdetails/MyUserDetailsService.java | 1 - 4 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/AccessControlGlobalFilter.java diff --git a/gateway/src/main/java/com/supwisdom/institute/backend/gateway/configuration/GlobalFilterConfig.java b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/configuration/GlobalFilterConfig.java index 2e71ac7..8a76da7 100644 --- a/gateway/src/main/java/com/supwisdom/institute/backend/gateway/configuration/GlobalFilterConfig.java +++ b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/configuration/GlobalFilterConfig.java @@ -2,11 +2,19 @@ package com.supwisdom.institute.backend.gateway.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import com.supwisdom.institute.backend.gateway.filter.AccessControlGlobalFilter; import com.supwisdom.institute.backend.gateway.filter.SimpleUserTransmitGlobalFilter; @Configuration +@EnableScheduling public class GlobalFilterConfig { + + @Bean + public AccessControlGlobalFilter accessControlGlobalFilter() { + return new AccessControlGlobalFilter(); + } @Bean public SimpleUserTransmitGlobalFilter simpleUserTransmitGlobalFilter() { diff --git a/gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/AccessControlGlobalFilter.java b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/AccessControlGlobalFilter.java new file mode 100644 index 0000000..266dfa7 --- /dev/null +++ b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/AccessControlGlobalFilter.java @@ -0,0 +1,426 @@ +package com.supwisdom.institute.backend.gateway.filter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import lombok.AllArgsConstructor; +import lombok.Getter; +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.HttpMethod; +import org.springframework.http.server.RequestPath; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; + +import com.supwisdom.infras.security.core.userdetails.InfrasUser; + +import reactor.core.publisher.Mono; + +//import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; + +@Slf4j +public class AccessControlGlobalFilter implements GlobalFilter, Ordered { + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + log.debug("AccessControlGlobalFilter.filter"); + + // 获取 请求路径 对应的 资源 + Collection attributes = this.getAttributes(exchange); + log.debug("request's attributes is {}", attributes); + + // 判断 该资源 是否需要登录才能访问 + if (attributes == null) { + return chain.filter(exchange); // FIXME: + } + + // 获取 当前登录用户(包括角色信息) + + // 判断 登录用户 是否可以访问 该资源 + + return ReactiveSecurityContextHolder.getContext() + .filter(c -> { + return c.getAuthentication() != null && c.getAuthentication().isAuthenticated() && c.getAuthentication().getPrincipal() instanceof InfrasUser; + }) + .map(SecurityContext::getAuthentication) + .map(Authentication::getPrincipal) + .cast(InfrasUser.class) + .map(infrasUser -> { + log.debug("infrasUser's roles is {}", infrasUser.getRoles()); + + boolean hasPermission = false; + + for (ConfigAttribute ca : attributes) { + hasPermission = infrasUser.getRoles().contains(ca.getAttribute()); + if (hasPermission) { + log.debug("match attribute is {}", ca.getAttribute()); + break; + } + } + + if (!hasPermission) { + throw new RuntimeException("no permission"); + } + + return exchange; + }) + .flatMap(ex -> chain.filter(ex)); + } + + private Map> requestMap = new ConcurrentHashMap>(); + + @Scheduled(initialDelayString = "${sw-backend-gateway.resource.refresh-delay:10000}", fixedDelayString = "${sw-backend-gateway.resource.refresh-delay:10000}") + protected void refreshRequestMap() { + + log.debug("AccessControlGlobalFilter.refreshRequestMap"); + + loadRequestMap(); + } + + // 定时刷新 资源 与 可访问角色 的 Map + private void loadRequestMap() { + requestMap.clear(); + + AntPathRequestMatcher requestMatcher0 = new AntPathRequestMatcher("/api/**"); + Collection attributes0 = new ArrayList(); + attributes0.add(new SecurityConfig("user")); + requestMap.put(requestMatcher0, attributes0); + + // FIXME: 从 后端接口 加载 + + + } + + public Collection getAttributes(ServerWebExchange exchange) throws IllegalArgumentException { + + if (requestMap.isEmpty()) { + loadRequestMap(); + } + + ServerHttpRequest request = exchange.getRequest(); + + + RequestMatcher requestMatcher; + for (Iterator iter = requestMap.keySet().iterator(); iter.hasNext();) { + requestMatcher = iter.next(); + + if (requestMatcher.matches(request)) { + return requestMap.get(requestMatcher); + } + } + + return null; + } + + + public static interface RequestMatcher { + + boolean matches(ServerHttpRequest request); + } + + @Slf4j + public static class AntPathRequestMatcher implements RequestMatcher { + private static final String MATCH_ALL = "/**"; + + private final Matcher matcher; + + private final String pattern; + private final HttpMethod httpMethod; + private final boolean caseSensitive; + + /** + * Creates a matcher with the specific pattern which will match all HTTP methods in a + * case insensitive manner. + * + * @param pattern the ant pattern to use for matching + */ + public AntPathRequestMatcher(String pattern) { + this(pattern, null); + } + + /** + * Creates a matcher with the supplied pattern and HTTP method in a case insensitive + * manner. + * + * @param pattern the ant pattern to use for matching + * @param httpMethod the HTTP method. The {@code matches} method will return false if + * the incoming request doesn't have the same method. + */ + public AntPathRequestMatcher(String pattern, String httpMethod) { + this(pattern, httpMethod, true); + } + + /** + * Creates a matcher with the supplied pattern which will match the specified Http + * method + * + * @param pattern the ant pattern to use for matching + * @param httpMethod the HTTP method. The {@code matches} method will return false if + * the incoming request doesn't doesn't have the same method. + * @param caseSensitive true if the matcher should consider case, else false + * @param urlPathHelper if non-null, will be used for extracting the path from the HttpServletRequest + */ + public AntPathRequestMatcher(String pattern, String httpMethod, + boolean caseSensitive) { + Assert.hasText(pattern, "Pattern cannot be null or empty"); + this.caseSensitive = caseSensitive; + + if (pattern.equals(MATCH_ALL) || pattern.equals("**")) { + pattern = MATCH_ALL; + this.matcher = null; + } + else { + // If the pattern ends with {@code /**} and has no other wildcards or path + // variables, then optimize to a sub-path match + if (pattern.endsWith(MATCH_ALL) + && (pattern.indexOf('?') == -1 && pattern.indexOf('{') == -1 + && pattern.indexOf('}') == -1) + && pattern.indexOf("*") == pattern.length() - 2) { + this.matcher = new SubpathMatcher( + pattern.substring(0, pattern.length() - 3), caseSensitive); + } + else { + this.matcher = new SpringAntMatcher(pattern, caseSensitive); + } + } + + this.pattern = pattern; + this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) + : null; + } + +// @Override +// public boolean matches(ServerHttpRequest request) { +// +// RequestPath requestPath = request.getPath(); +// log.info(requestPath.pathWithinApplication().value()); +// String path = requestPath.pathWithinApplication().value(); +// +// return false; +// } + + /** + * Returns true if the configured pattern (and HTTP-Method) match those of the + * supplied request. + * + * @param request the request to match against. The ant pattern will be matched + * against the {@code servletPath} + {@code pathInfo} of the request. + */ + @Override + public boolean matches(ServerHttpRequest request) { + if (this.httpMethod != null && StringUtils.hasText(request.getMethodValue()) + && this.httpMethod != valueOf(request.getMethodValue())) { + if (log.isDebugEnabled()) { + log.debug("Request '" + request.getMethod() + " " + + getRequestPath(request) + "'" + " doesn't match '" + + this.httpMethod + " " + this.pattern); + } + + return false; + } + + if (this.pattern.equals(MATCH_ALL)) { + if (log.isDebugEnabled()) { + log.debug("Request '" + getRequestPath(request) + + "' matched by universal pattern '/**'"); + } + + return true; + } + + String url = getRequestPath(request); + + if (log.isDebugEnabled()) { + log.debug("Checking match of request : '" + url + "'; against '" + + this.pattern + "'"); + } + + return this.matcher.matches(url); + } + + + private String getRequestPath(ServerHttpRequest request) { + + RequestPath requestPath = request.getPath(); + log.info(requestPath.pathWithinApplication().value()); + String path = requestPath.pathWithinApplication().value(); + + return path; + + //String url = request.getServletPath(); + + //String pathInfo = request.getPathInfo(); + //if (pathInfo != null) { + // url = StringUtils.hasLength(url) ? url + pathInfo : pathInfo; + //} + + //return url; + } + + public String getPattern() { + return this.pattern; + } + + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AntPathRequestMatcher)) { + return false; + } + + AntPathRequestMatcher other = (AntPathRequestMatcher) obj; + return this.pattern.equals(other.pattern) && this.httpMethod == other.httpMethod + && this.caseSensitive == other.caseSensitive; + } + + @Override + public int hashCode() { + int code = 31 ^ this.pattern.hashCode(); + if (this.httpMethod != null) { + code ^= this.httpMethod.hashCode(); + } + return code; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Ant [pattern='").append(this.pattern).append("'"); + + if (this.httpMethod != null) { + sb.append(", ").append(this.httpMethod); + } + + sb.append("]"); + + return sb.toString(); + } + + + /** + * Provides a save way of obtaining the HttpMethod from a String. If the method is + * invalid, returns null. + * + * @param method the HTTP method to use. + * + * @return the HttpMethod or null if method is invalid. + */ + private static HttpMethod valueOf(String method) { + try { + return HttpMethod.valueOf(method); + } + catch (IllegalArgumentException e) { + } + + return null; + } + + private static interface Matcher { + boolean matches(String path); + + Map extractUriTemplateVariables(String path); + } + + private static class SpringAntMatcher implements Matcher { + private final AntPathMatcher antMatcher; + + private final String pattern; + + private SpringAntMatcher(String pattern, boolean caseSensitive) { + this.pattern = pattern; + this.antMatcher = createMatcher(caseSensitive); + } + + @Override + public boolean matches(String path) { + return this.antMatcher.match(this.pattern, path); + } + + @Override + public Map extractUriTemplateVariables(String path) { + return this.antMatcher.extractUriTemplateVariables(this.pattern, path); + } + + private static AntPathMatcher createMatcher(boolean caseSensitive) { + AntPathMatcher matcher = new AntPathMatcher(); + matcher.setTrimTokens(false); + matcher.setCaseSensitive(caseSensitive); + return matcher; + } + } + + /** + * Optimized matcher for trailing wildcards + */ + private static class SubpathMatcher implements Matcher { + private final String subpath; + private final int length; + private final boolean caseSensitive; + + private SubpathMatcher(String subpath, boolean caseSensitive) { + assert!subpath.contains("*"); + this.subpath = caseSensitive ? subpath : subpath.toLowerCase(); + this.length = subpath.length(); + this.caseSensitive = caseSensitive; + } + + @Override + public boolean matches(String path) { + if (!this.caseSensitive) { + path = path.toLowerCase(); + } + return path.startsWith(this.subpath) + && (path.length() == this.length || path.charAt(this.length) == '/'); + } + + @Override + public Map extractUriTemplateVariables(String path) { + return Collections.emptyMap(); + } + } + + } + + @AllArgsConstructor + @Getter + public static class RestRequestMatcher implements RequestMatcher { + + private final String path; + private final RequestMethod[] method; + private final String[] params; + private final String[] headers; + private final String[] consumes; + private final String[] produces; + + @Override + public boolean matches(ServerHttpRequest request) { + + + return false; + } + + } + +} diff --git a/gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/SimpleUserTransmitGlobalFilter.java b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/SimpleUserTransmitGlobalFilter.java index cbda00b..7c3af6f 100644 --- a/gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/SimpleUserTransmitGlobalFilter.java +++ b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/filter/SimpleUserTransmitGlobalFilter.java @@ -46,7 +46,7 @@ public class SimpleUserTransmitGlobalFilter implements GlobalFilter, Ordered { //String headerValue = new String(URLDecoder.decode(jsonUser,"UTF-8")); String headerValue = Base64.encodeBase64URLSafeString(jsonUser.getBytes("UTF-8")); - log.debug(jsonUser); + log.debug(headerValue); ServerHttpRequest request = exchange.getRequest().mutate() .header(UserContext.KEY_USER_IN_HTTP_HEADER, headerValue) diff --git a/gateway/src/main/java/com/supwisdom/institute/backend/gateway/security/core/userdetails/MyUserDetailsService.java b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/security/core/userdetails/MyUserDetailsService.java index 171f2cf..ef7d0e2 100644 --- a/gateway/src/main/java/com/supwisdom/institute/backend/gateway/security/core/userdetails/MyUserDetailsService.java +++ b/gateway/src/main/java/com/supwisdom/institute/backend/gateway/security/core/userdetails/MyUserDetailsService.java @@ -1,7 +1,6 @@ package com.supwisdom.institute.backend.gateway.security.core.userdetails; import java.util.ArrayList; -import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; -- 2.17.1