feat: 新增API访问控制
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 @@
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<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ log.debug("AccessControlGlobalFilter.filter");
+
+ // 获取 请求路径 对应的 资源
+ Collection<ConfigAttribute> 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<RequestMatcher, Collection<ConfigAttribute>> requestMap = new ConcurrentHashMap<RequestMatcher, Collection<ConfigAttribute>>();
+
+ @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<ConfigAttribute> attributes0 = new ArrayList<ConfigAttribute>();
+ attributes0.add(new SecurityConfig("user"));
+ requestMap.put(requestMatcher0, attributes0);
+
+ // FIXME: 从 后端接口 加载
+
+
+ }
+
+ public Collection<ConfigAttribute> getAttributes(ServerWebExchange exchange) throws IllegalArgumentException {
+
+ if (requestMap.isEmpty()) {
+ loadRequestMap();
+ }
+
+ ServerHttpRequest request = exchange.getRequest();
+
+
+ RequestMatcher requestMatcher;
+ for (Iterator<RequestMatcher> 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<String, String> 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<String, String> 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<String, String> 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 @@
//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;