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;