feat: 实现jwt认证接口
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/api/InfrasApiUserController.java b/gateway/src/main/java/com/supwisdom/infras/security/api/InfrasApiUserController.java
new file mode 100644
index 0000000..26eea40
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/api/InfrasApiUserController.java
@@ -0,0 +1,55 @@
+package com.supwisdom.infras.security.api;
+
+import java.security.Principal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.supwisdom.infras.security.authentication.JwtAuthenticationToken;
+import com.supwisdom.infras.security.core.userdetails.InfrasUser;
+
+@RestController
+@RequestMapping("/api/user")
+public class InfrasApiUserController {
+
+  private static final Logger logger = LoggerFactory.getLogger(InfrasApiUserController.class);
+
+  @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+  @ResponseBody
+  public InfrasUser current(Principal principal) {
+    
+    logger.debug("ApiUserController.current(Principal) is {}", principal);
+    
+    if (principal == null) {
+      throw new RuntimeException("exception.principal.is.null");
+    }
+    
+    if (principal instanceof UsernamePasswordAuthenticationToken) {
+      
+      UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) principal;
+      
+      if (token.isAuthenticated()) {
+        if (token.getPrincipal() instanceof User) {
+          return (InfrasUser) token.getPrincipal();
+        }
+      }
+    } else if (principal instanceof JwtAuthenticationToken) {
+      JwtAuthenticationToken token = (JwtAuthenticationToken) principal;
+      
+      InfrasUser user = new InfrasUser((String) token.getPrincipal(), (String) token.getToken(), token.getAuthorities(), token.getAttributes());
+      user.eraseCredentials();
+      
+      return user;
+    }
+    
+    throw new RuntimeException("exception.principal.not.correct");
+  }
+  
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/authentication/JwtAuthenticationToken.java b/gateway/src/main/java/com/supwisdom/infras/security/authentication/JwtAuthenticationToken.java
new file mode 100644
index 0000000..73a1944
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/authentication/JwtAuthenticationToken.java
@@ -0,0 +1,74 @@
+package com.supwisdom.infras.security.authentication;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+public class JwtAuthenticationToken extends AbstractAuthenticationToken {
+
+  /**
+   * 
+   */
+  private static final long serialVersionUID = 4407107653933017523L;
+  
+  private Object principal;
+  private String credentials;
+  private String token;
+  
+  private final Map<String, Object> attributes;
+  
+  public Map<String, Object> getAttributes() {
+    return this.attributes;
+  }
+  
+  public JwtAuthenticationToken(String token) {
+    super(Collections.emptyList());
+    this.token = token;
+    
+    this.attributes = Collections.emptyMap();
+    
+    setAuthenticated(false);
+  }
+  
+  public JwtAuthenticationToken(Object principal, String token, 
+      Collection<? extends GrantedAuthority> authorities) {
+    this(principal, token, authorities, Collections.emptyMap());
+  }
+
+  public JwtAuthenticationToken(Object principal, String token, 
+      Collection<? extends GrantedAuthority> authorities, 
+      Map<? extends String, ? extends Object> attributes) {
+    super(authorities);
+    this.principal = principal;
+    this.token = token;
+    
+    this.attributes = Collections.unmodifiableMap(attributes);
+    
+    setAuthenticated(true);
+  }
+  
+  @Override
+  public void setDetails(Object details) {
+    super.setDetails(details);
+    
+    this.setAuthenticated(true);
+  }
+
+  @Override
+  public Object getCredentials() {
+    return credentials;
+  }
+
+  @Override
+  public Object getPrincipal() {
+    return principal;
+  }
+  
+  public String getToken() {
+    return token;
+  }
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/core/userdetails/InfrasUser.java b/gateway/src/main/java/com/supwisdom/infras/security/core/userdetails/InfrasUser.java
new file mode 100644
index 0000000..8764974
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/core/userdetails/InfrasUser.java
@@ -0,0 +1,49 @@
+package com.supwisdom.infras.security.core.userdetails;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+public class InfrasUser extends User {
+
+  /**
+   * 
+   */
+  private static final long serialVersionUID = 6535845256804630918L;
+  
+  public InfrasUser(String username, String password,
+      Collection<? extends GrantedAuthority> authorities,
+      Map<? extends String, ? extends Object> attributes) {
+    this(username, password, true, true, true, true, authorities, attributes);
+    
+  }
+
+  public InfrasUser(String username, String password, 
+      boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, 
+      Collection<? extends GrantedAuthority> authorities,
+      Map<? extends String, ? extends Object> attributes) {
+    super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
+
+    this.attributes = Collections.unmodifiableMap(attributes);
+  }
+
+  private final Map<String, Object> attributes;
+  public Map<String, Object> getAttributes() {
+    return this.attributes;
+  }
+  
+  public List<String> getRoles() {
+    List<String> roles = new ArrayList<>();
+    for (GrantedAuthority grantedAuthority : this.getAuthorities()) {
+      roles.add(grantedAuthority.getAuthority());
+    }
+    
+    return roles;
+  }
+  
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java
index aa792e1..76cc927 100644
--- a/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java
@@ -10,7 +10,7 @@
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
 
 @Configuration
-@ConditionalOnProperty(name="infras.security.basic.enabled", havingValue="true")
+@ConditionalOnProperty(name="infras.security.basic.reactive.enabled", havingValue="true")
 public class BasicWebFluxSecurityConfiguration {
   
   @Bean
@@ -28,10 +28,9 @@
       //http.addFilterAt(webFilter, SecurityWebFiltersOrder.LAST);
       
       http.httpBasic();
-      
-      http.csrf().disable();
-      
       http.formLogin().disable();
+
+      http.csrf().disable();
       
       return http.build();
   }
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/EnableInfrasJWTWebFluxApi.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/EnableInfrasJWTWebFluxApi.java
new file mode 100644
index 0000000..51062b2
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/EnableInfrasJWTWebFluxApi.java
@@ -0,0 +1,17 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Import(JWTWebFluxConfiguration.class)
+public @interface EnableInfrasJWTWebFluxApi {
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTKeyController.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTKeyController.java
new file mode 100644
index 0000000..3c4b036
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTKeyController.java
@@ -0,0 +1,25 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+@RestController
+public class JWTKeyController {
+
+  @Autowired
+  private JWTTokenUtil jwtTokenUtil;
+  
+  /**
+   * curl -i -s -X GET 'http://localhost:8080/jwt/publicKey' 
+   * @return
+   */
+  @GetMapping(value = "/jwt/publicKey")
+  public String publicKey() {
+    
+    return jwtTokenUtil.getPublicKeyPem();
+  }
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTSecurityContextRepository.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTSecurityContextRepository.java
new file mode 100644
index 0000000..bf2f6a7
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTSecurityContextRepository.java
@@ -0,0 +1,118 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import io.jsonwebtoken.Claims;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.web.server.context.ServerSecurityContextRepository;
+import org.springframework.web.server.ServerWebExchange;
+
+import com.supwisdom.infras.security.authentication.JwtAuthenticationToken;
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+import reactor.core.publisher.Mono;
+
+public class JWTSecurityContextRepository implements ServerSecurityContextRepository {
+  
+  private static final Logger logger = LoggerFactory.getLogger(JWTSecurityContextRepository.class);
+  
+  @Value("${infras.security.jwt.token.authorization.prefix:Bearer}")
+  private String authorizationPrefix;
+  
+  private JWTTokenUtil jwtTokenUtil;
+  
+  @Autowired
+  public JWTSecurityContextRepository(JWTTokenUtil jwtTokenUtil) {
+    this.jwtTokenUtil = jwtTokenUtil;
+  }
+
+  @Override
+  public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
+    return Mono.empty();
+  }
+
+  @Override
+  public Mono<SecurityContext> load(ServerWebExchange exchange) {
+    
+    ServerHttpRequest request = exchange.getRequest();
+    
+    String authToken = null;
+    
+    String authParamter = request.getQueryParams().getFirst("token"); logger.debug("authParamter is [{}]", authParamter);
+    if (authParamter != null && !authParamter.isEmpty()) {
+      authToken = authParamter;
+    }
+
+    if (authToken == null) {
+      String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); logger.debug("authHeader is [{}]", authHeader);
+      if (authHeader != null && authHeader.toLowerCase().startsWith(authorizationPrefix.toLowerCase())) {
+        authToken = authHeader.substring(authorizationPrefix.length() + 1);
+      }
+    }
+    
+    logger.debug("authToken is [{}]", authToken);
+    
+    if (authToken != null && !authToken.isEmpty()) {
+      
+      String username = getUsernameFromToken(authToken);
+      
+      if (username != null) {
+        List<GrantedAuthority> authorities = getAuthoritiesFromToken(authToken);
+        
+        Authentication authentication = new JwtAuthenticationToken(username, authToken, authorities);
+        
+        return Mono.justOrEmpty(new SecurityContextImpl(authentication));
+      }
+      
+    }
+    
+    return Mono.empty();
+  }
+  
+  
+  /**
+   * 从令牌中获取用户名
+   *
+   * @param token 令牌
+   * @return 用户名
+   */
+  private String getUsernameFromToken(String token) {
+      String username;
+      try {
+          Claims claims = jwtTokenUtil.getClaimsFromToken(token);
+          username = claims.getSubject();
+      } catch (Exception e) {
+          username = null;
+      }
+      return username;
+  }
+  
+  private List<GrantedAuthority> getAuthoritiesFromToken(String token) {
+    List<GrantedAuthority> collAuthorities = new ArrayList<GrantedAuthority>();
+    try {
+        Claims claims = jwtTokenUtil.getClaimsFromToken(token);
+        String roles = claims.get("roles", String.class);
+        
+        for (String role : roles.split(",")) {
+          collAuthorities.add(new SimpleGrantedAuthority(role));
+        }
+    } catch (Exception e) {
+      collAuthorities = new ArrayList<GrantedAuthority>();
+    }
+    return collAuthorities;
+  }
+
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTTokenController.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTTokenController.java
new file mode 100644
index 0000000..9a357f6
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTTokenController.java
@@ -0,0 +1,232 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+import reactor.core.publisher.Mono;
+
+import com.supwisdom.infras.security.configure.jwt.util.SignUtil;
+import com.supwisdom.infras.security.core.userdetails.InfrasUser;
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+@RestController
+public class JWTTokenController {
+  
+  private static final Logger logger = LoggerFactory.getLogger(JWTTokenController.class);
+  
+  @Value("${infras.security.jwt.token.sign.enabled:false}")
+  private boolean signEnabled;
+  @Value("${infras.security.jwt.token.sign.key:}")
+  private String signKey;
+
+  @Value("${infras.security.jwt.token.authorization.prefix:Bearer}")
+  private String authorizationPrefix;
+
+  @Autowired
+  private ReactiveAuthenticationManager reactiveAuthenticationManager;
+  @Autowired
+  private ReactiveUserDetailsService reactiveUserDetailsService;
+  @Autowired
+  private JWTTokenUtil jwtTokenUtil;
+  
+  /**
+   * 用户登录
+   * 
+   * curl -i -s -X POST 'http://localhost:8080/jwt/token/login' -d 'username=user&password=user' -H 'Content-Type: application/json'
+   *
+   * @param username 用户名
+   * @param password 密码
+   * @return 操作结果
+   * @throws AuthenticationException 错误信息
+   */
+  @PostMapping(path = "/jwt/token/login", consumes = MimeTypeUtils.APPLICATION_JSON_VALUE)
+  public Mono<String> login(
+      @RequestBody LoginRequest loginRequest
+      ) throws AuthenticationException {
+
+    return Mono.just(loginRequest).flatMap(req -> {
+      try {
+        String username = req.getUsername();
+        String password = req.getPassword();
+        
+        // TODO:判断用户的登录频次
+        
+        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
+        
+        return reactiveAuthenticationManager.authenticate(upToken);
+      } catch (AuthenticationException e) {
+        // TODO: 记录失败
+        throw e;
+      }
+    }).filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof InfrasUser)
+    .map(Authentication::getPrincipal)
+    .cast(InfrasUser.class)
+    .map(userDetails -> {
+      String jwtToken = generateToken(userDetails);
+      return jwtToken;
+    })
+    ;
+    
+    // 判断签名
+//    if (signEnabled) {
+//      String timestamp = request.queryParam("timestamp").get();
+//      String sign = request.queryParam("sign").get();
+//      
+//      if (timestamp == null || timestamp.isEmpty()) {
+//        throw new BadCredentialsException("timestamp error");
+//      }
+//      
+//      if (sign == null || sign.isEmpty()) {
+//        throw new BadCredentialsException("sign error");
+//      }
+//      
+//      String data = timestamp + username + password;
+//      
+//      if (!SignUtil.checkIntervalTime(timestamp, 10000)) {
+//        throw new BadCredentialsException("timestamp error");
+//      }
+//      
+//      String currentSign = SignUtil.HMACSHA1(data, signKey);
+//      if (!currentSign.equals(sign)) {
+//        throw new BadCredentialsException("sign error");
+//      }
+//    }
+    
+//    String username = loginRequest.getUsername();
+//    String password = loginRequest.getPassword();
+//
+//    // TODO:判断用户的登录频次
+//    
+//    try {
+//      UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
+//      
+//      Mono<String> token = 
+//      reactiveAuthenticationManager.authenticate(upToken)
+//        .filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof UserDetails)
+//        .map(Authentication::getPrincipal)
+//        .cast(UserDetails.class)
+//        .map(userDetails -> {
+//          String jwtToken = generateToken(userDetails);
+//          return jwtToken;
+//        });
+//
+////      reactiveAuthenticationManager.authenticate(upToken)
+////        .filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof String)
+////        .map(Authentication::getPrincipal)
+////        .cast(String.class)
+////        .map(principal -> {
+////          return reactiveUserDetailsService.findByUsername(principal)
+////            .map(userDetails -> {
+////              String jwtToken = generateToken(userDetails);
+////              return jwtToken;
+////            });
+////        });
+//      
+//      return token.block();
+//      
+////      reactiveAuthenticationManager.authenticate(upToken)
+////        .filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof UserDetails)
+////        .map(Authentication::getPrincipal)
+////        .cast(UserDetails.class)
+////        .map(userDetails -> {
+////          String jwtToken = generateToken(userDetails);
+////          return jwtToken;
+////        })
+////      ;
+//
+//    } catch (AuthenticationException e) {
+//      // TODO: 记录失败
+//      throw e;
+//    }
+//
+//    //throw new UsernameNotFoundException(String.format("%s not found", username));
+  }
+  
+
+  /**
+   * 生成令牌
+   *
+   * @param userDetails 用户
+   * @return 令牌
+   */
+  private String generateToken(InfrasUser userDetails) {
+    String roles = "";
+    for (GrantedAuthority ga : userDetails.getAuthorities()) {
+      String role = ga.getAuthority();
+      roles += (roles.length() > 0 ? "," : "") + role;
+    }
+    
+    Map<String, Object> claims = new HashMap<>();
+    claims.put("sub", userDetails.getUsername());
+    claims.put("roles", roles);
+    claims.put("created", new Date());
+    return jwtTokenUtil.generateToken(claims);
+  }
+
+  
+  /**
+   * 刷新密钥
+   * 
+   * curl -i -s -X GET 'http://localhost:8080/jwt/token/refreshToken' -H 'Authorization: Bearer JWTToken'
+   *
+   * @param authorization 原密钥
+   * @return 新密钥
+   * @throws AuthenticationException 错误信息
+   */
+  @GetMapping(path = "/jwt/token/refreshToken")
+  public String refreshToken(@RequestHeader("Authorization") String authorization) throws AuthenticationException {
+    
+    if (authorization == null || !authorization.toLowerCase().startsWith(authorizationPrefix.toLowerCase())) {
+      return "authorization error";
+    }
+    
+    String token = authorization.substring(authorizationPrefix.length() + 1);
+    //if (!jwtTokenUtil.isTokenExpired(token)) {
+        return jwtTokenUtil.refreshToken(token);
+    //}
+    
+    //return "error";
+  }
+  
+  /**
+   * 注销密钥
+   * 
+   * curl -i -s -X GET 'http://localhost:8080/jwt/token/logout' -H 'Authorization: Bearer JWTToken'
+   * 
+   * @param authorization 原密钥
+   * @return
+   * @throws AuthenticationException
+   */
+  @GetMapping(path = "/jwt/token/logout")
+  public String expireToken(@RequestHeader("Authorization") String authorization) throws AuthenticationException {
+    
+    if (authorization == null || !authorization.toLowerCase().startsWith(authorizationPrefix.toLowerCase())) {
+      return "authorization error";
+    }
+    
+    String token = authorization.substring(authorizationPrefix.length() + 1);
+    
+    jwtTokenUtil.expireToken(token);
+    
+    return "success";
+  }
+  
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTWebFluxConfiguration.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTWebFluxConfiguration.java
new file mode 100644
index 0000000..014e483
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTWebFluxConfiguration.java
@@ -0,0 +1,141 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+@Configuration
+@ConditionalOnProperty(name="infras.security.jwt.reactive.enabled", havingValue="true")
+public class JWTWebFluxConfiguration {
+  
+  private static final Logger logger = LoggerFactory.getLogger(JWTWebFluxConfiguration.class);
+
+//  @ConditionalOnClass(RedisConnectionFactory.class)
+//  @Configuration
+//  public static class RedisStoreConfiguration {
+//    
+//    @Bean
+//    public JWTTokenRedisStore jwtTokenRedisStore(RedisConnectionFactory connectionFactory) {
+//      
+//      JWTTokenRedisStore jwtTokenRedisStore = new JWTTokenRedisStore(connectionFactory);
+//      
+//      return jwtTokenRedisStore;
+//    }
+//
+//  }
+  
+  @Autowired(required = false)
+  private ReactiveUserDetailsService reactiveUserDetailsService;
+  
+  @Autowired(required = false)
+  private PasswordEncoder passwordEncoder;
+  
+  @Bean
+  public ReactiveAuthenticationManager reactiveAuthenticationManager() {
+    if(this.reactiveUserDetailsService != null) {
+      UserDetailsRepositoryReactiveAuthenticationManager manager =
+        new UserDetailsRepositoryReactiveAuthenticationManager(this.reactiveUserDetailsService);
+      if(this.passwordEncoder != null) {
+        manager.setPasswordEncoder(this.passwordEncoder);
+      }
+      return manager;
+    }
+    return null;
+  }
+
+  @Bean
+  public JWTTokenUtil jwtTokenUtil() {
+    
+    return new JWTTokenUtil();
+  }
+
+  @Bean
+  JWTKeyController jwtKeyController() {
+    
+    JWTKeyController jwtKeyController = new JWTKeyController();
+    logger.debug("JWTWebFluxConfiguration jwtKeyController is {}", jwtKeyController);
+
+    return jwtKeyController;
+  }
+  
+  
+  
+  @Bean
+  public JWTTokenController jwtTokenController() {
+    
+    JWTTokenController jwtTokenController = new JWTTokenController();
+    logger.debug("JWTWebFluxConfiguration jwtTokenController is {}", jwtTokenController);
+    
+    return jwtTokenController;
+  }
+  
+  @Bean
+  public JWTSecurityContextRepository jwtSecurityContextRepository(JWTTokenUtil jwtTokenUtil) {
+    JWTSecurityContextRepository jwtSecurityContextRepository = new JWTSecurityContextRepository(jwtTokenUtil);
+    
+    return jwtSecurityContextRepository;
+  }
+  
+
+  @Bean
+  public SecurityWebFilterChain jwtTokenSpringSecurityFilterChain(ServerHttpSecurity http) {
+    
+    logger.debug("jwtTokenSpringSecurityFilterChain(ServerHttpSecurity)");
+  
+    http
+      .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/jwt/**"))
+      .authorizeExchange()
+        .pathMatchers(HttpMethod.OPTIONS).permitAll()
+        .pathMatchers("/jwt/publicKey").permitAll()
+        .pathMatchers("/jwt/**").permitAll()
+        .anyExchange().authenticated();
+    
+    http.httpBasic().disable();
+    http.formLogin().disable();
+
+    http.csrf().disable();
+    
+    return http.build();
+  }
+
+  
+
+  @Bean
+  public SecurityWebFilterChain jwtApiSpringSecurityFilterChain(ServerHttpSecurity http) {
+      http
+          .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/**"))
+          .authorizeExchange()
+              .pathMatchers(HttpMethod.OPTIONS).permitAll()
+              .pathMatchers("/api/public/**", "/api/open/**").permitAll()
+              .pathMatchers("/api/v*/public/**", "/api/v*/open/**").permitAll()
+              .pathMatchers("/api/*/v*/public/**", "/api/*/v*/open/**").permitAll()
+              .pathMatchers("/api/**").authenticated()
+              .anyExchange().authenticated();
+      
+      //http.addFilterAt(webFilter, SecurityWebFiltersOrder.LAST);
+      
+      http.securityContextRepository(jwtSecurityContextRepository(jwtTokenUtil()));
+      
+      http.httpBasic().disable();
+      http.formLogin().disable();
+
+      http.csrf().disable();
+
+      return http.build();
+  }
+
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/LoginRequest.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/LoginRequest.java
new file mode 100644
index 0000000..2e47c2c
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/LoginRequest.java
@@ -0,0 +1,13 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class LoginRequest {
+  private String username;
+  private String password;
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/utils/JWTTokenUtil.java b/gateway/src/main/java/com/supwisdom/infras/security/utils/JWTTokenUtil.java
new file mode 100644
index 0000000..abbf378
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/utils/JWTTokenUtil.java
@@ -0,0 +1,261 @@
+package com.supwisdom.infras.security.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+//import org.springframework.security.core.GrantedAuthority;
+//import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import com.supwisdom.infras.security.cert.CertUtil;
+import com.supwisdom.infras.security.token.store.redis.JWTTokenRedisStore;
+
+@Component
+public class JWTTokenUtil implements InitializingBean {
+
+  private static final Logger logger = LoggerFactory.getLogger(JWTTokenUtil.class);
+  
+  private static ConcurrentMap<String, Long> mapTokenExpiration = new ConcurrentHashMap<String, Long>();
+  
+  @Autowired(required = false)
+  private JWTTokenRedisStore redisTokenStore;
+
+  /**
+   * 密钥
+   */
+  //@Value("${infras.security.jwt.secret:MyJwtSecret}")
+  //private String secret;
+  
+
+  @Value("${infras.security.jwt.iss:supwisdom}")
+  private String issuer;
+  @Value("${infras.security.jwt.jti:supwisdom-jwt}")
+  private String jti;
+
+
+  @Value("${infras.security.jwt.expiration:2592000}")
+  private Long expiration;
+  
+  @Value("${infras.security.jwt.kickout.enabled:false}")
+  private boolean kickoutEnabled;
+  
+
+  @Value("${infras.security.jwt.key-alias:supwisdom-jwt-key}")
+  private String keyAlias;
+  @Value("${infras.security.jwt.key-password:kingstar}")
+  private String keyPassword;
+
+  @Value("${infras.security.jwt.key-store:}")
+  private String keyStore;
+  @Value("${infras.security.jwt.key-store-password:kingstar}")
+  private String keyStorePassword;
+  
+  
+  @Value("${infras.security.jwt.public-key-pem:}")
+  private String publicKeyPem;
+  @Value("${infras.security.jwt.private-key-pem-pkcs8:}")
+  private String privateKeyPemPKCS8;
+
+  @Override
+  public void afterPropertiesSet() throws Exception {
+    this.initKey();
+  }
+
+  private KeyPair keyPair;
+  
+  public void initKey() {
+    
+    try {
+      this.keyPair = CertUtil.initKeyFromPem(publicKeyPem, privateKeyPemPKCS8);
+      logger.debug("init keyPair from pem");
+      return;
+    } catch (NoSuchAlgorithmException e) {
+      e.printStackTrace();
+    } catch (InvalidKeySpecException e) {
+      e.printStackTrace();
+    }
+    
+    try {
+      this.keyPair = CertUtil.initKeyFromKeyStore(keyStore, keyStorePassword, keyAlias, keyPassword);
+      logger.debug("init keyPair from keyStore");
+    } catch (UnrecoverableKeyException e) {
+      e.printStackTrace();
+    } catch (KeyStoreException e) {
+      e.printStackTrace();
+    } catch (CertificateException e) {
+      e.printStackTrace();
+    } catch (IOException e) {
+      e.printStackTrace();
+    } catch (NoSuchAlgorithmException e) {
+      e.printStackTrace();
+    }
+    
+  }
+  
+  public RSAPublicKey getPublicKey() {
+    return (RSAPublicKey) this.keyPair.getPublic();
+  }
+  
+  public RSAPrivateKey getPrivateKey() {
+    return (RSAPrivateKey) this.keyPair.getPrivate();
+  }
+  
+  public String getPublicKeyPem() {
+    return CertUtil.publicKeyToPem(getPublicKey());
+  }
+
+  private void storeTokenExpiration(String token, Long expiration) {
+    if (!kickoutEnabled) {
+      return;
+    }
+    
+    logger.debug("store <token, expiration> to Map");
+    mapTokenExpiration.put(token, expiration);  // FIXME: 存储到 redis 或 数据库
+
+    if (redisTokenStore != null) {
+      logger.debug("store <token, expiration> to Redis");
+      redisTokenStore.storeTokenExpiration(token, expiration);
+    }
+  }
+  
+  private Long loadTokenExpiration(String token) {
+    if (!kickoutEnabled) {
+      return Long.MAX_VALUE;
+    }
+    
+    if (redisTokenStore != null) {
+      logger.debug("load <token, expiration> from Redis");
+      return redisTokenStore.loadTokenExpiration(token, -1L);
+    }
+    
+    logger.debug("load <token, expiration> from Map");
+    return mapTokenExpiration.getOrDefault(token, -1L);  // FIXME: 存储到 redis 或 数据库
+  }
+
+  /**
+   * 从数据声明生成令牌
+   *
+   * @param claims 数据声明
+   * @return 令牌
+   */
+  public String generateToken(Map<String, Object> claims) {
+      Date expirationDate = new Date(System.currentTimeMillis() + expiration * 1000);
+      String token = Jwts.builder()
+          .setClaims(claims)
+          .setIssuer(issuer)
+          .setId(jti)
+          .setIssuedAt(new Date(System.currentTimeMillis()))
+          .setExpiration(expirationDate)
+          .signWith(SignatureAlgorithm.RS512, this.getPrivateKey())
+          .compact();
+      
+      storeTokenExpiration(token, expirationDate.getTime());
+      
+      return token;
+  }
+
+  /**
+   * 从令牌中获取数据声明
+   *
+   * @param token 令牌
+   * @return 数据声明
+   */
+  public Claims getClaimsFromToken(String token) {
+      Claims claims;
+      try {
+          claims = Jwts.parser().setSigningKey(this.getPublicKey()).parseClaimsJws(token).getBody();
+      } catch (Exception e) {
+          claims = null;
+      }
+      return claims;
+  }
+
+  /**
+   * 判断令牌是否过期
+   *
+   * @param token 令牌
+   * @return 是否过期
+   */
+  public Boolean isTokenExpired(String token) {
+      try {
+          Claims claims = getClaimsFromToken(token);
+          Date expiration = claims.getExpiration();
+          
+          Date now = new Date();
+          
+          // 判断存储中的 token 是否过期
+          Long expirationTimeMillis = loadTokenExpiration(token);
+          if (expirationTimeMillis < now.getTime()) {
+            return true;
+          }
+          
+          return expiration.before(now);
+      } catch (Exception e) {
+          return false; // FIXME: ?
+      }
+  }
+
+  /**
+   * 刷新令牌
+   *
+   * @param token 原令牌
+   * @return 新令牌
+   */
+  public String refreshToken(String token) {
+      String refreshedToken;
+      try {
+          Claims claims = getClaimsFromToken(token);
+          claims.put("created", new Date());
+          refreshedToken = generateToken(claims);
+      } catch (Exception e) {
+          refreshedToken = null;
+      }
+      return refreshedToken;
+  }
+
+  /**
+   * 验证令牌
+   *
+   * @param token       令牌
+   * @param userDetails 用户
+   * @return 是否有效
+   */
+  public Boolean validateToken(String token, String username) {
+    Claims claims = getClaimsFromToken(token);
+    String sub = claims.getSubject();
+    
+    return (sub.equals(username) && !isTokenExpired(token));
+  }
+
+  /**
+   * 踢出令牌
+   * @param token
+   */
+  public void expireToken(String token) {
+      if (!isTokenExpired(token)) {
+          storeTokenExpiration(token, -1L);
+      }
+  }
+
+}
diff --git a/gateway/src/main/resources/META-INF/spring.factories b/gateway/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..9a872fd
--- /dev/null
+++ b/gateway/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,3 @@
+# Auto Configure
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.supwisdom.infras.security.reactive.InfrasSecurityReactiveAutoConfiguration
\ No newline at end of file