for 830 (#832)

* for 830

* For 830

* For 830

* For 830

* For 830

* For 830
diff --git a/dubbo-admin-server/pom.xml b/dubbo-admin-server/pom.xml
index 9973cd6..2eaa4f3 100644
--- a/dubbo-admin-server/pom.xml
+++ b/dubbo-admin-server/pom.xml
@@ -33,6 +33,8 @@
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>1.8</java.version>
         <mockito-version>2.23.4</mockito-version>
+        <jwt-version>3.4.1</jwt-version>
+        <jjwt-version>0.6.0</jjwt-version>
     </properties>
 
     <dependencies>
@@ -189,6 +191,19 @@
             <artifactId>zookeeper</artifactId>
         </dependency>
 
+        <!--JWT-->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>${jwt-version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>${jjwt-version}</version>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/authentication/impl/DefaultPreHandle.java b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/authentication/impl/DefaultPreHandle.java
index 5ee83c7..d66fd1f 100644
--- a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/authentication/impl/DefaultPreHandle.java
+++ b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/authentication/impl/DefaultPreHandle.java
@@ -19,10 +19,10 @@
 
 import org.apache.dubbo.admin.annotation.Authority;
 import org.apache.dubbo.admin.authentication.InterceptorAuthentication;
-import org.apache.dubbo.admin.controller.UserController;
 import org.apache.dubbo.admin.interceptor.AuthInterceptor;
+import org.apache.dubbo.admin.utils.JwtTokenUtil;
+import org.apache.dubbo.admin.utils.SpringBeanUtils;
 
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.util.StringUtils;
 import org.springframework.web.method.HandlerMethod;
 
@@ -30,14 +30,9 @@
 import javax.servlet.http.HttpServletResponse;
 import java.lang.reflect.Method;
 
-
 public class DefaultPreHandle implements InterceptorAuthentication {
-    //make session timeout configurable
-    //default to be an hour:1000 * 60 * 60
-    @Value("${admin.check.sessionTimeoutMilli:3600000}")
-    private long sessionTimeoutMilli;
 
-    private AuthInterceptor authInterceptor = new AuthInterceptor();
+    private JwtTokenUtil jwtTokenUtil = SpringBeanUtils.getBean(JwtTokenUtil.class);
 
     @Override
     public boolean authentication(HttpServletRequest request, HttpServletResponse response, Object handler) {
@@ -48,25 +43,21 @@
             authority = method.getDeclaringClass().getDeclaredAnnotation(Authority.class);
         }
 
-        String authorization = request.getHeader("Authorization");
+        String token = request.getHeader("Authorization");
+
         if (null != authority && authority.needLogin()) {
             //check if 'authorization' is empty to prevent NullPointException
-            //since UserController.tokenMap is an instance of ConcurrentHashMap.
-            if (StringUtils.isEmpty(authorization)) {
+            if (StringUtils.isEmpty(token)) {
                 //While authentication is required and 'Authorization' string is missing in the request headers,
                 //reject this request(http403).
-                authInterceptor.rejectedResponse(response);
+                AuthInterceptor.authRejectedResponse(response);
                 return false;
             }
-
-            UserController.User user = UserController.tokenMap.get(authorization);
-            if (null != user && System.currentTimeMillis() - user.getLastUpdateTime() <= sessionTimeoutMilli) {
-                user.setLastUpdateTime(System.currentTimeMillis());
+            if (jwtTokenUtil.canTokenBeExpiration(token)) {
                 return true;
             }
-
-            //while user not found, or session timeout, reject this request(http403).
-            authInterceptor.rejectedResponse(response);
+            //while user not found, or token timeout, reject this request(http401).
+            AuthInterceptor.loginFailResponse(response);
             return false;
         } else {
             return true;
diff --git a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/controller/UserController.java b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/controller/UserController.java
index 82e5031..9a10dfd 100644
--- a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/controller/UserController.java
+++ b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/controller/UserController.java
@@ -18,107 +18,67 @@
 
 import org.apache.dubbo.admin.annotation.Authority;
 import org.apache.dubbo.admin.authentication.LoginAuthentication;
-
-import org.apache.commons.lang3.StringUtils;
+import org.apache.dubbo.admin.interceptor.AuthInterceptor;
+import org.apache.dubbo.admin.utils.JwtTokenUtil;
 import org.apache.dubbo.common.extension.ExtensionLoader;
+import org.apache.commons.lang3.StringUtils;
+
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.context.request.RequestContextHolder;
-import org.springframework.web.context.request.ServletRequestAttributes;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import java.util.Iterator;
-import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
 
 @RestController
 @RequestMapping("/api/{env}/user")
 public class UserController {
-    public static Map<String /*token*/, User /*user info*/> tokenMap = new ConcurrentHashMap<>();
 
     @Value("${admin.root.user.name:}")
     private String rootUserName;
     @Value("${admin.root.user.password:}")
     private String rootUserPassword;
-    //make session timeout configurable
-    //default to be an hour:1000 * 60 * 60
-    @Value("${admin.check.sessionTimeoutMilli:3600000}")
-    private long sessionTimeoutMilli;
+
+    @Autowired
+    private JwtTokenUtil jwtTokenUtil;
 
     @RequestMapping(value = "/login", method = RequestMethod.GET)
-    public String login(HttpServletRequest httpServletRequest, @RequestParam String userName, @RequestParam String password) {
+    public String login(HttpServletRequest httpServletRequest, HttpServletResponse response, @RequestParam String userName, @RequestParam String password) {
         ExtensionLoader<LoginAuthentication> extensionLoader = ExtensionLoader.getExtensionLoader(LoginAuthentication.class);
         Set<LoginAuthentication> supportedExtensionInstances = extensionLoader.getSupportedExtensionInstances();
         Iterator<LoginAuthentication> iterator = supportedExtensionInstances.iterator();
         boolean flag = true;
-        if (iterator == null) {
+        if (iterator != null && !iterator.hasNext()) {
             if (StringUtils.isBlank(rootUserName) || (rootUserName.equals(userName) && rootUserPassword.equals(password))) {
-                return creatToken(rootUserName);
+                return jwtTokenUtil.generateToken(userName);
+            } else {
+                flag = false;
             }
         }
         while (iterator.hasNext()) {
             LoginAuthentication loginAuthentication = iterator.next();
             boolean b = loginAuthentication.authentication(httpServletRequest, userName, password);
             flag = b & flag;
-            if (flag == false) {
+            if (!flag) {
                 break;
             }
         }
         if (flag) {
-            return creatToken(userName);
+            return jwtTokenUtil.generateToken(userName);
         }
+        AuthInterceptor.loginFailResponse(response);
         return null;
     }
 
     @Authority(needLogin = true)
     @RequestMapping(value = "/logout", method = RequestMethod.DELETE)
     public boolean logout() {
-        HttpServletRequest request =
-                ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
-        String token = request.getHeader("Authorization");
-        return null != tokenMap.remove(token);
+        return true;
     }
 
-    @Scheduled(cron= "0 5 * * * ?")
-    public void clearExpiredToken() {
-        tokenMap.entrySet().removeIf(entry -> entry.getValue() == null || System.currentTimeMillis() - entry.getValue().getLastUpdateTime() > sessionTimeoutMilli);
-    }
-
-    public static class User {
-        private String userName;
-        private long lastUpdateTime;
-
-        public String getUserName() {
-            return userName;
-        }
-
-        public void setUserName(String userName) {
-            this.userName = userName;
-        }
-
-        public long getLastUpdateTime() {
-            return lastUpdateTime;
-        }
-
-        public void setLastUpdateTime(long lastUpdateTime) {
-            this.lastUpdateTime = lastUpdateTime;
-        }
-    }
-
-    public String creatToken(String userName) {
-        UUID uuid = UUID.randomUUID();
-        String token = uuid.toString();
-        User user = new User();
-        user.setUserName(userName);
-        user.setLastUpdateTime(System.currentTimeMillis());
-        tokenMap.put(token, user);
-        return token;
-    }
 }
diff --git a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/interceptor/AuthInterceptor.java b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/interceptor/AuthInterceptor.java
index a45de4e..8184822 100644
--- a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/interceptor/AuthInterceptor.java
+++ b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/interceptor/AuthInterceptor.java
@@ -35,11 +35,7 @@
 public class AuthInterceptor extends HandlerInterceptorAdapter {
     @Value("${admin.check.authority:true}")
     private boolean checkAuthority;
-    
-    //make session timeout configurable
-    //default to be an hour:1000 * 60 * 60
-    @Value("${admin.check.sessionTimeoutMilli:3600000}")
-    private long sessionTimeoutMilli;
+
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         if (!(handler instanceof HandlerMethod) || !checkAuthority) {
@@ -53,14 +49,18 @@
             InterceptorAuthentication interceptorAuthentication = iterator.next();
             boolean b = interceptorAuthentication.authentication(request, response, handler);
             flag = b & flag;
-            if (flag == false) {
+            if (!flag) {
                 break;
             }
         }
         return flag;
     }
 
-    public static void rejectedResponse(@NotNull HttpServletResponse response) {
+    public static void loginFailResponse(@NotNull HttpServletResponse response) {
         response.setStatus(HttpStatus.UNAUTHORIZED.value());
     }
+
+    public static void authRejectedResponse(@NotNull HttpServletResponse response) {
+        response.setStatus(HttpStatus.FORBIDDEN.value());
+    }
 }
diff --git a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/utils/JwtTokenUtil.java b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/utils/JwtTokenUtil.java
new file mode 100644
index 0000000..d9f5b7c
--- /dev/null
+++ b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/utils/JwtTokenUtil.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dubbo.admin.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Jwt token tool class.
+ */
+@Component
+public class JwtTokenUtil {
+    /**
+     * Jwt signingKey configurable
+     */
+    @Value("${admin.check.signSecret:}")
+    public String secret;
+
+    /**
+     * token timeout configurable
+     * default to be an hour: 1000 * 60 * 60
+     */
+    @Value("${admin.check.tokenTimeoutMilli:}")
+    public long expiration;
+
+    /**
+     * default SignatureAlgorithm
+     */
+    public static final SignatureAlgorithm defaultAlgorithm = SignatureAlgorithm.HS512;
+
+    /**
+     * Generate the token
+     *
+     * @return token
+     * @param rootUserName
+     */
+    public String generateToken(String rootUserName) {
+        Map<String, Object> claims = new HashMap<>(1);
+        claims.put("sub", rootUserName);
+        return Jwts.builder()
+                .setClaims(claims)
+                .setExpiration(new Date(System.currentTimeMillis() + expiration))
+                .setIssuedAt(new Date(System.currentTimeMillis()))
+                .signWith(defaultAlgorithm, secret)
+                .compact();
+    }
+
+    /**
+     * Check whether the token is invalid
+     *
+     * @return boolean type
+     * @param token
+     */
+    public Boolean canTokenBeExpiration(String token) {
+        Claims claims;
+        try {
+            claims = Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+            final Date exp = claims.getExpiration();
+            if (exp.before(new Date(System.currentTimeMillis()))) {
+                return false;
+            }
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+}
diff --git a/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/utils/SpringBeanUtils.java b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/utils/SpringBeanUtils.java
new file mode 100644
index 0000000..87a445d
--- /dev/null
+++ b/dubbo-admin-server/src/main/java/org/apache/dubbo/admin/utils/SpringBeanUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dubbo.admin.utils;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * get spring bean tool class.
+ */
+@Component
+public class SpringBeanUtils implements ApplicationContextAware {
+    /**
+     * spring applicationContext
+     */
+    public static ApplicationContext applicationContext;
+
+    /**
+     * get spring bean
+     *
+     * @return spring bean
+     * @param clazz
+     */
+    public static <T> T getBean(Class<T> clazz){
+        return applicationContext.getBean(clazz);
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        SpringBeanUtils.applicationContext = applicationContext;
+    }
+}
diff --git a/dubbo-admin-server/src/main/resources/application.properties b/dubbo-admin-server/src/main/resources/application.properties
index df17468..da49bf2 100644
--- a/dubbo-admin-server/src/main/resources/application.properties
+++ b/dubbo-admin-server/src/main/resources/application.properties
@@ -56,3 +56,8 @@
 server.compression.enabled=true
 server.compression.mime-types=text/css,text/javascript,application/javascript
 server.compression.min-response-size=10240
+
+#token timeout, default is one hour
+admin.check.tokenTimeoutMilli=3600000
+#Jwt signingKey
+admin.check.signSecret=86295dd0c4ef69a1036b0b0c15158d77
diff --git a/dubbo-admin-server/src/test/java/org/apache/dubbo/admin/utils/JwtTokenUtilTest.java b/dubbo-admin-server/src/test/java/org/apache/dubbo/admin/utils/JwtTokenUtilTest.java
new file mode 100644
index 0000000..90a15a8
--- /dev/null
+++ b/dubbo-admin-server/src/test/java/org/apache/dubbo/admin/utils/JwtTokenUtilTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dubbo.admin.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import org.junit.Test;
+
+import java.util.Date;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+
+public class JwtTokenUtilTest {
+    public final String defaultSecret = "86295dd0c4ef69a1036b0b0c15158d77";
+    public final long defaultExpire = 1000 * 60 * 60;
+    public String testToken = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MzM4NTI2" +
+            "MDQsInN1YiI6InRlc3QiLCJpYXQiOjE2MzM4NDkwMDR9.e1UqT-3W3EZcI6" +
+            "Dt-35b0Q_MA9ZhARAq59ZvkOYNlWL0Fa-RFk1ZQKs15Hk7LATfVH2DAo0JL" +
+            "rHcY-79jDFnfQ";
+    public long testIat = 1633849279000L;
+    public long testExp = 1633852879000L;
+    public String userName = "test";
+
+}