From 76208975bffec39eb62f8599a68d583a5cb6da18 Mon Sep 17 00:00:00 2001
From: leesam <leesam@leesam.cn>
Date: 星期二, 19 三月 2024 16:53:01 +0800
Subject: [PATCH] [add]支持其他平台通过ApiKey调用系统相关接口

---
 web_src/src/components/dialog/addUserApiKey.vue                                |  139 ++++++
 web_src/config/index.js                                                        |    2 
 web_src/src/components/dialog/remarkUserApiKey.vue                             |   93 ++++
 src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java                |  106 ++++
 web_src/src/components/UserManager.vue                                         |    7 
 web_src/src/components/UserApiKeyManager.vue                                   |  296 +++++++++++++
 src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java                      |    2 
 数据库/初始化-mysql.sql                                                              |   12 
 src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java          |    5 
 数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql                                    |   12 
 src/main/java/com/genersoft/iot/vmp/service/IUserService.java                  |    2 
 pom.xml                                                                        |    4 
 打包/config/wvp-application.yml                                                  |    2 
 src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java |    7 
 数据库/初始化-postgresql-kingbase.sql                                                |   12 
 src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java            |   25 +
 web_src/src/router/index.js                                                    |    8 
 src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java         |   60 ++
 src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java    |   80 +++
 数据库/2.7.0/初始化-mysql-2.7.0.sql                                                  |   11 
 src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java           |  151 ++++++
 src/main/resources/application-dev.yml                                         |    2 
 src/main/resources/application-docker.yml                                      |    2 
 src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java    |  251 +++++++++++
 24 files changed, 1,267 insertions(+), 24 deletions(-)

diff --git a/pom.xml b/pom.xml
index 7ec73b8..f38d693 100644
--- a/pom.xml
+++ b/pom.xml
@@ -101,6 +101,10 @@
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <dependency>
diff --git a/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
index be57316..262910b 100644
--- a/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
+++ b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
@@ -9,6 +9,7 @@
 import org.springframework.boot.builder.SpringApplicationBuilder;
 import org.springframework.boot.web.servlet.ServletComponentScan;
 import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
@@ -24,6 +25,7 @@
 @ServletComponentScan("com.genersoft.iot.vmp.conf")
 @SpringBootApplication
 @EnableScheduling
+@EnableCaching
 public class VManageBootstrap extends SpringBootServletInitializer {
 
 	private final static Logger logger = LoggerFactory.getLogger(VManageBootstrap.class);
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java
index f45f89a..274a19f 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java
@@ -51,8 +51,11 @@
         if (StringUtils.isBlank(jwt)) {
             jwt = request.getParameter(JwtUtils.getHeader());
             if (StringUtils.isBlank(jwt)) {
-                chain.doFilter(request, response);
-                return;
+                jwt = request.getHeader(JwtUtils.getApiKeyHeader());
+                if (StringUtils.isBlank(jwt)) {
+                    chain.doFilter(request, response);
+                    return;
+                }
             }
         }
 
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java
index fcd1946..949bcba 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java
@@ -1,8 +1,12 @@
 package com.genersoft.iot.vmp.conf.security;
 
 import com.genersoft.iot.vmp.conf.security.dto.JwtUser;
+import com.genersoft.iot.vmp.service.IUserApiKeyService;
 import com.genersoft.iot.vmp.service.IUserService;
 import com.genersoft.iot.vmp.storager.dao.dto.User;
+import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey;
+import org.jose4j.jwk.JsonWebKey;
+import org.jose4j.jwk.JsonWebKeySet;
 import org.jose4j.jwk.RsaJsonWebKey;
 import org.jose4j.jwk.RsaJwkGenerator;
 import org.jose4j.jws.AlgorithmIdentifiers;
@@ -20,8 +24,18 @@
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
+import java.util.List;
+import java.util.Map;
 
 @Component
 public class JwtUtils implements InitializingBean {
@@ -30,6 +44,8 @@
 
     public static final String HEADER = "access-token";
 
+    public static final String API_KEY_HEADER = "api-key";
+
     private static final String AUDIENCE = "Audience";
 
     private static final String keyId = "3e79646c4dbc408383a9eed09f2b85ae";
@@ -37,15 +53,26 @@
     /**
      * token杩囨湡鏃堕棿(鍒嗛挓)
      */
-    public static final long expirationTime = 30 * 24 * 60;
+    public static final long EXPIRATION_TIME = 30 * 24 * 60;
 
     private static RsaJsonWebKey rsaJsonWebKey;
 
     private static IUserService userService;
 
+    private static IUserApiKeyService userApiKeyService;
+
+    public static String getApiKeyHeader() {
+        return API_KEY_HEADER;
+    }
+
     @Resource
     public void setUserService(IUserService userService) {
         JwtUtils.userService = userService;
+    }
+
+    @Resource
+    public void setUserApiKeyService(IUserApiKeyService userApiKeyService) {
+        JwtUtils.userApiKeyService = userApiKeyService;
     }
 
     @Override
@@ -62,14 +89,38 @@
      * @throws JoseException JoseException
      */
     private RsaJsonWebKey generateRsaJsonWebKey() throws JoseException {
-        // 鐢熸垚涓�涓猂SA瀵嗛挜瀵癸紝璇ュ瘑閽ュ灏嗙敤浜嶫WT鐨勭鍚嶅拰楠岃瘉锛屽寘瑁呭湪JWK涓�
-        RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
-        // 缁橨WK涓�涓瘑閽D
-        rsaJsonWebKey.setKeyId(keyId);
+        RsaJsonWebKey rsaJsonWebKey = null;
+        try {
+            URL url = getClass().getClassLoader().getResource("jwk.json");
+            if (url != null) {
+                URI uri = url.toURI();
+                Path path = Paths.get(uri);
+                if (Files.exists(path)) {
+                    byte[] allBytes = Files.readAllBytes(path);
+                    String jwkJson = new String(allBytes, StandardCharsets.UTF_8);
+                    final JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jwkJson);
+                    List<JsonWebKey> jsonWebKeys = jsonWebKeySet.getJsonWebKeys();
+                    if (!jsonWebKeys.isEmpty()) {
+                        JsonWebKey jsonWebKey = jsonWebKeys.get(0);
+                        if (jsonWebKey instanceof RsaJsonWebKey) {
+                            rsaJsonWebKey = (RsaJsonWebKey) jsonWebKey;
+                        }
+                    }
+                }
+            }
+        } catch (URISyntaxException | IOException e) {
+            // ignored
+        }
+        if (rsaJsonWebKey == null) {
+            // 鐢熸垚涓�涓猂SA瀵嗛挜瀵癸紝璇ュ瘑閽ュ灏嗙敤浜嶫WT鐨勭鍚嶅拰楠岃瘉锛屽寘瑁呭湪JWK涓�
+            rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
+            // 缁橨WK涓�涓瘑閽D
+            rsaJsonWebKey.setKeyId(keyId);
+        }
         return rsaJsonWebKey;
     }
 
-    public static String createToken(String username) {
+    public static String createToken(String username, Long expirationTime, Map<String, Object> extra) {
         try {
             /*
              * 鈥渋ss鈥� (issuer)  鍙戣浜�
@@ -83,13 +134,17 @@
             claims.setGeneratedJwtId();
             claims.setIssuedAtToNow();
             // 浠ょ墝灏嗚繃鏈熺殑鏃堕棿 鍒嗛挓
-            claims.setExpirationTimeMinutesInTheFuture(expirationTime);
+            if (expirationTime != null) {
+                claims.setExpirationTimeMinutesInTheFuture(expirationTime);
+            }
             claims.setNotBeforeMinutesInThePast(0);
             claims.setSubject("login");
             claims.setAudience(AUDIENCE);
             //娣诲姞鑷畾涔夊弬鏁�,蹇呴』鏄瓧绗︿覆绫诲瀷
             claims.setClaim("userName", username);
-
+            if (extra != null) {
+                extra.forEach(claims::setClaim);
+            }
             //jws
             JsonWebSignature jws = new JsonWebSignature();
             //绛惧悕绠楁硶RS256
@@ -104,8 +159,15 @@
         } catch (JoseException e) {
             logger.error("[Token鐢熸垚澶辫触]锛� {}", e.getMessage());
         }
-
         return null;
+    }
+
+    public static String createToken(String username, Long expirationTime) {
+        return createToken(username, expirationTime, null);
+    }
+
+    public static String createToken(String username) {
+        return createToken(username, EXPIRATION_TIME);
     }
 
     public static String getHeader() {
@@ -118,8 +180,8 @@
 
         try {
             JwtConsumer consumer = new JwtConsumerBuilder()
-                    .setRequireExpirationTime()
-                    .setMaxFutureValidityInMinutes(5256000)
+                    //.setRequireExpirationTime()
+                    //.setMaxFutureValidityInMinutes(5256000)
                     .setAllowedClockSkewInSeconds(30)
                     .setRequireSubject()
                     //.setExpectedIssuer("")
@@ -129,15 +191,27 @@
 
             JwtClaims claims = consumer.processToClaims(token);
             NumericDate expirationTime = claims.getExpirationTime();
-            // 鍒ゆ柇鏄惁鍗冲皢杩囨湡, 榛樿鍓╀綑鏃堕棿灏忎簬5鍒嗛挓鏈嵆灏嗚繃鏈�
-            // 鍓╀綑鏃堕棿 锛堢锛�
-            long timeRemaining = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)) - expirationTime.getValue();
-            if (timeRemaining < 5 * 60) {
-                jwtUser.setStatus(JwtUser.TokenStatus.EXPIRING_SOON);
+            if (expirationTime != null) {
+                // 鍒ゆ柇鏄惁鍗冲皢杩囨湡, 榛樿鍓╀綑鏃堕棿灏忎簬5鍒嗛挓鏈嵆灏嗚繃鏈�
+                // 鍓╀綑鏃堕棿 锛堢锛�
+                long timeRemaining = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)) - expirationTime.getValue();
+                if (timeRemaining < 5 * 60) {
+                    jwtUser.setStatus(JwtUser.TokenStatus.EXPIRING_SOON);
+                } else {
+                    jwtUser.setStatus(JwtUser.TokenStatus.NORMAL);
+                }
             } else {
                 jwtUser.setStatus(JwtUser.TokenStatus.NORMAL);
             }
 
+            Long apiKeyId = claims.getClaimValue("apiKeyId", Long.class);
+            if (apiKeyId != null) {
+                UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(apiKeyId.intValue());
+                if (userApiKey == null || !userApiKey.isEnable()) {
+                    jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED);
+                }
+            }
+
             String username = (String) claims.getClaimValue("userName");
             User user = userService.getUserByUsername(username);
 
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java b/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java
new file mode 100644
index 0000000..b3cc580
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java
@@ -0,0 +1,25 @@
+package com.genersoft.iot.vmp.service;
+
+import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey;
+import com.github.pagehelper.PageInfo;
+
+public interface IUserApiKeyService {
+    int addApiKey(UserApiKey userApiKey);
+
+    boolean isApiKeyExists(String apiKey);
+
+    PageInfo<UserApiKey> getUserApiKeys(int page, int count);
+
+    int enable(Integer id);
+
+    int disable(Integer id);
+
+    int remark(Integer id, String remark);
+
+    int delete(Integer id);
+
+    UserApiKey getUserApiKeyById(Integer id);
+
+    int reset(Integer id, String apiKey);
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IUserService.java b/src/main/java/com/genersoft/iot/vmp/service/IUserService.java
index 7e2a839..1e9b724 100755
--- a/src/main/java/com/genersoft/iot/vmp/service/IUserService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/IUserService.java
@@ -11,6 +11,8 @@
 
     boolean changePassword(int id, String password);
 
+    User getUserById(int id);
+
     User getUserByUsername(String username);
 
     int addUser(User user);
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java
new file mode 100644
index 0000000..85ee4f0
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java
@@ -0,0 +1,80 @@
+package com.genersoft.iot.vmp.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.genersoft.iot.vmp.service.IUserApiKeyService;
+import com.genersoft.iot.vmp.storager.dao.UserApiKeyMapper;
+import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@DS("master")
+public class UserApiKeyServiceImpl implements IUserApiKeyService {
+
+    @Autowired
+    UserApiKeyMapper userApiKeyMapper;
+
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    @Override
+    public int addApiKey(UserApiKey userApiKey) {
+        return userApiKeyMapper.add(userApiKey);
+    }
+
+    @Override
+    public boolean isApiKeyExists(String apiKey) {
+        return userApiKeyMapper.isApiKeyExists(apiKey);
+    }
+
+    @Override
+    public PageInfo<UserApiKey> getUserApiKeys(int page, int count) {
+        PageHelper.startPage(page, count);
+        List<UserApiKey> userApiKeys = userApiKeyMapper.getUserApiKeys();
+        return new PageInfo<>(userApiKeys);
+    }
+
+    @Cacheable(cacheNames = "userApiKey", key = "#id", sync = true)
+    @Override
+    public UserApiKey getUserApiKeyById(Integer id) {
+        return userApiKeyMapper.selectById(id);
+    }
+
+    @CacheEvict(cacheNames = "userApiKey", key = "#id")
+    @Override
+    public int enable(Integer id) {
+        return userApiKeyMapper.enable(id);
+    }
+
+    @CacheEvict(cacheNames = "userApiKey", key = "#id")
+    @Override
+    public int disable(Integer id) {
+        return userApiKeyMapper.disable(id);
+    }
+
+    @CacheEvict(cacheNames = "userApiKey", key = "#id")
+    @Override
+    public int remark(Integer id, String remark) {
+        return userApiKeyMapper.remark(id, remark);
+    }
+
+    @CacheEvict(cacheNames = "userApiKey", key = "#id")
+    @Override
+    public int delete(Integer id) {
+        return userApiKeyMapper.delete(id);
+    }
+
+    @CacheEvict(cacheNames = "userApiKey", key = "#id")
+    @Override
+    public int reset(Integer id, String apiKey) {
+        return userApiKeyMapper.apiKey(id, apiKey);
+    }
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java
index 400b19b..fb97db9 100755
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java
@@ -32,6 +32,11 @@
     }
 
     @Override
+    public User getUserById(int id) {
+        return userMapper.selectById(id);
+    }
+
+    @Override
     public User getUserByUsername(String username) {
         return userMapper.getUserByUsername(username);
     }
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java
new file mode 100644
index 0000000..d235475
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java
@@ -0,0 +1,60 @@
+package com.genersoft.iot.vmp.storager.dao;
+
+import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey;
+import org.apache.ibatis.annotations.*;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Mapper
+@Repository
+public interface UserApiKeyMapper {
+
+    @SelectKey(statement = "SELECT LAST_INSERT_ID() AS id", keyProperty = "id", before = false, resultType = Integer.class)
+    @Insert("INSERT INTO wvp_user_api_key (user_id, app, api_key, expired_at, remark, enable, create_time, update_time) VALUES" +
+            "(#{userId}, #{app}, #{apiKey}, #{expiredAt}, #{remark}, #{enable}, #{createTime}, #{updateTime})")
+    int add(UserApiKey userApiKey);
+
+    @Update(value = {"<script>" +
+            "UPDATE wvp_user_api_key " +
+            "SET update_time = #{updateTime} " +
+            "<if test=\"app != null\">, app = #{app}</if>" +
+            "<if test=\"apiKey != null\">, api_key = #{apiKey}</if>" +
+            "<if test=\"expiredAt != null\">, expired_at = #{expiredAt}</if>" +
+            "<if test=\"remark != null\">, username = #{remark}</if>" +
+            "<if test=\"enable != null\">, enable = #{enable}</if>" +
+            "WHERE id = #{id}" +
+            " </script>"})
+    int update(UserApiKey userApiKey);
+
+    @Update("UPDATE wvp_user_api_key SET enable = true WHERE id = #{id}")
+    int enable(@Param("id") int id);
+
+    @Update("UPDATE wvp_user_api_key SET enable = false WHERE id = #{id}")
+    int disable(@Param("id") int id);
+
+    @Update("UPDATE wvp_user_api_key SET api_key = #{apiKey} WHERE id = #{id}")
+    int apiKey(@Param("id") int id, @Param("apiKey") String apiKey);
+
+    @Update("UPDATE wvp_user_api_key SET remark = #{remark} WHERE id = #{id}")
+    int remark(@Param("id") int id, @Param("remark") String remark);
+
+    @Delete("DELETE FROM wvp_user_api_key WHERE id = #{id}")
+    int delete(@Param("id") int id);
+
+    @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id WHERE uak.id = #{id}")
+    UserApiKey selectById(@Param("id") int id);
+
+    @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id WHERE uak.api_key = #{apiKey}")
+    UserApiKey selectByApiKey(@Param("apiKey") String apiKey);
+
+    @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id")
+    List<UserApiKey> selectAll();
+
+    @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id")
+    List<UserApiKey> getUserApiKeys();
+
+    @Select("SELECT COUNT(0) FROM wvp_user_api_key WHERE api_key = #{apiKey}")
+    boolean isApiKeyExists(@Param("apiKey") String apiKey);
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java
new file mode 100644
index 0000000..470c8b6
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java
@@ -0,0 +1,151 @@
+package com.genersoft.iot.vmp.storager.dao.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.io.Serializable;
+
+/**
+ * 鐢ㄦ埛淇℃伅
+ */
+@Schema(description = "鐢ㄦ埛ApiKey淇℃伅")
+public class UserApiKey implements Serializable {
+
+    /**
+     * Id
+     */
+    @Schema(description = "Id")
+    private int id;
+
+    /**
+     * 鐢ㄦ埛Id
+     */
+    @Schema(description = "鐢ㄦ埛Id")
+    private int userId;
+
+    /**
+     * 搴旂敤鍚�
+     */
+    @Schema(description = "搴旂敤鍚�")
+    private String app;
+
+    /**
+     * ApiKey
+     */
+    @Schema(description = "ApiKey")
+    private String apiKey;
+
+    /**
+     * 杩囨湡鏃堕棿锛坣ull=姘镐笉杩囨湡锛�
+     */
+    @Schema(description = "杩囨湡鏃堕棿锛坣ull=姘镐笉杩囨湡锛�")
+    private String expiredAt;
+
+    /**
+     * 澶囨敞淇℃伅
+     */
+    @Schema(description = "澶囨敞淇℃伅")
+    private String remark;
+
+    /**
+     * 鏄惁鍚敤
+     */
+    @Schema(description = "鏄惁鍚敤")
+    private boolean enable;
+
+    /**
+     * 鍒涘缓鏃堕棿
+     */
+    @Schema(description = "鍒涘缓鏃堕棿")
+    private String createTime;
+
+    /**
+     * 鏇存柊鏃堕棿
+     */
+    @Schema(description = "鏇存柊鏃堕棿")
+    private String updateTime;
+
+    /**
+     * 鐢ㄦ埛鍚�
+     */
+    private String username;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public int getUserId() {
+        return userId;
+    }
+
+    public void setUserId(int userId) {
+        this.userId = userId;
+    }
+
+    public String getApp() {
+        return app;
+    }
+
+    public void setApp(String app) {
+        this.app = app;
+    }
+
+    public String getApiKey() {
+        return apiKey;
+    }
+
+    public void setApiKey(String apiKey) {
+        this.apiKey = apiKey;
+    }
+
+    public String getExpiredAt() {
+        return expiredAt;
+    }
+
+    public void setExpiredAt(String expiredAt) {
+        this.expiredAt = expiredAt;
+    }
+
+    public String getRemark() {
+        return remark;
+    }
+
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    public boolean isEnable() {
+        return enable;
+    }
+
+    public void setEnable(boolean enable) {
+        this.enable = enable;
+    }
+
+    public String getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(String createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(String updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java
new file mode 100644
index 0000000..44690ad
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java
@@ -0,0 +1,251 @@
+package com.genersoft.iot.vmp.vmanager.user;
+
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
+import com.genersoft.iot.vmp.conf.security.SecurityUtils;
+import com.genersoft.iot.vmp.service.IUserApiKeyService;
+import com.genersoft.iot.vmp.service.IUserService;
+import com.genersoft.iot.vmp.storager.dao.dto.User;
+import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey;
+import com.genersoft.iot.vmp.utils.DateUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.github.pagehelper.PageInfo;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Tag(name = "鐢ㄦ埛ApiKey绠$悊")
+@RestController
+@RequestMapping("/api/userApiKey")
+public class UserApiKeyController {
+
+    public static final int EXPIRATION_TIME = Integer.MAX_VALUE;
+    @Autowired
+    private IUserService userService;
+
+    @Autowired
+    private IUserApiKeyService userApiKeyService;
+
+    /**
+     * 娣诲姞鐢ㄦ埛ApiKey
+     *
+     * @param userId
+     * @param app
+     * @param remark
+     * @param expiresAt
+     * @param enable
+     */
+    @PostMapping("/add")
+    @Operation(summary = "娣诲姞鐢ㄦ埛ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "userId", description = "鐢ㄦ埛Id", required = true)
+    @Parameter(name = "app", description = "搴旂敤鍚嶇О", required = false)
+    @Parameter(name = "remark", description = "澶囨敞淇℃伅", required = false)
+    @Parameter(name = "expiredAt", description = "杩囨湡鏃堕棿锛堜笉浼犱唬琛ㄦ案涓嶈繃鏈燂級", required = false)
+    @Transactional
+    public synchronized void add(
+            @RequestParam(required = true) int userId,
+            @RequestParam(required = false) String app,
+            @RequestParam(required = false) String remark,
+            @RequestParam(required = false) String expiresAt,
+            @RequestParam(required = false) Boolean enable
+    ) {
+        User user = userService.getUserById(userId);
+        if (user == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "鐢ㄦ埛涓嶅瓨鍦�");
+        }
+
+        Long expirationTime = null;
+        if (expiresAt != null) {
+            long timestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(expiresAt);
+            expirationTime = (timestamp - System.currentTimeMillis()) / (60 * 1000);
+            if (expirationTime < 0) {
+                throw new ControllerException(ErrorCode.ERROR400.getCode(), "杩囨湡鏃堕棿涓嶈兘鏃╀簬褰撳墠鏃堕棿");
+            }
+        }
+
+        UserApiKey userApiKey = new UserApiKey();
+        userApiKey.setUserId(userId);
+        userApiKey.setApp(app);
+        userApiKey.setApiKey(null);
+        userApiKey.setRemark(remark);
+        userApiKey.setExpiredAt(expiresAt);
+        userApiKey.setEnable(enable != null ? enable : false);
+        userApiKey.setCreateTime(DateUtil.getNow());
+        userApiKey.setUpdateTime(DateUtil.getNow());
+
+        int addResult = userApiKeyService.addApiKey(userApiKey);
+
+        if (addResult <= 0) {
+            throw new ControllerException(ErrorCode.ERROR100);
+        }
+
+        String apiKey;
+        do {
+            Map<String, Object> extra = new HashMap<>(1);
+            extra.put("apiKeyId", userApiKey.getId());
+            apiKey = JwtUtils.createToken(user.getUsername(), expirationTime, extra);
+        } while (userApiKeyService.isApiKeyExists(apiKey));
+
+        int resetResult = userApiKeyService.reset(userApiKey.getId(), apiKey);
+
+        if (resetResult <= 0) {
+            throw new ControllerException(ErrorCode.ERROR100);
+        }
+    }
+
+    /**
+     * 鍒嗛〉鏌ヨApiKey
+     *
+     * @param page  褰撳墠椤�
+     * @param count 姣忛〉鏌ヨ鏁伴噺
+     * @return 鍒嗛〉ApiKey鍒楄〃
+     */
+    @GetMapping("/userApiKeys")
+    @Operation(summary = "鍒嗛〉鏌ヨ鐢ㄦ埛", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "page", description = "褰撳墠椤�", required = true)
+    @Parameter(name = "count", description = "姣忛〉鏌ヨ鏁伴噺", required = true)
+    @Transactional
+    public PageInfo<UserApiKey> userApiKeys(@RequestParam(required = true) int page, @RequestParam(required = true) int count) {
+        return userApiKeyService.getUserApiKeys(page, count);
+    }
+
+    @PostMapping("/enable")
+    @Operation(summary = "鍚敤鐢ㄦ埛ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "id", description = "鐢ㄦ埛ApiKeyId", required = true)
+    @Transactional
+    public void enable(@RequestParam(required = true) Integer id) {
+        // 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛id
+        int currenRoleId = SecurityUtils.getUserInfo().getRole().getId();
+        if (currenRoleId != 1) {
+            // 鍙敤瑙掕壊id涓�1鎵嶅彲浠ョ鐞哢serApiKey
+            throw new ControllerException(ErrorCode.ERROR403);
+        }
+        UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id);
+        if (userApiKey == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey涓嶅瓨鍦�");
+        }
+
+        int enableResult = userApiKeyService.enable(id);
+
+        if (enableResult <= 0) {
+            throw new ControllerException(ErrorCode.ERROR100);
+        }
+    }
+
+    @PostMapping("/disable")
+    @Operation(summary = "鍋滅敤鐢ㄦ埛ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "id", description = "鐢ㄦ埛ApiKeyId", required = true)
+    @Transactional
+    public void disable(@RequestParam(required = true) Integer id) {
+        // 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛id
+        int currenRoleId = SecurityUtils.getUserInfo().getRole().getId();
+        if (currenRoleId != 1) {
+            // 鍙敤瑙掕壊id涓�1鎵嶅彲浠ョ鐞哢serApiKey
+            throw new ControllerException(ErrorCode.ERROR403);
+        }
+        UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id);
+        if (userApiKey == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey涓嶅瓨鍦�");
+        }
+
+        int disableResult = userApiKeyService.disable(id);
+
+        if (disableResult <= 0) {
+            throw new ControllerException(ErrorCode.ERROR100);
+        }
+    }
+
+    @PostMapping("/reset")
+    @Operation(summary = "閲嶇疆鐢ㄦ埛ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "id", description = "鐢ㄦ埛ApiKeyId", required = true)
+    @Transactional
+    public void reset(@RequestParam(required = true) Integer id) {
+        // 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛id
+        int currenRoleId = SecurityUtils.getUserInfo().getRole().getId();
+        if (currenRoleId != 1) {
+            // 鍙敤瑙掕壊id涓�1鎵嶅彲浠ョ鐞哢serApiKey
+            throw new ControllerException(ErrorCode.ERROR403);
+        }
+        UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id);
+        if (userApiKey == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey涓嶅瓨鍦�");
+        }
+        User user = userService.getUserById(userApiKey.getUserId());
+        if (user == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "鐢ㄦ埛涓嶅瓨鍦�");
+        }
+        Long expirationTime = null;
+        if (userApiKey.getExpiredAt() != null) {
+            long timestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(userApiKey.getExpiredAt());
+            expirationTime = (timestamp - System.currentTimeMillis()) / (60 * 1000);
+            if (expirationTime < 0) {
+                throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey宸插け鏁�");
+            }
+        }
+        String apiKey;
+        do {
+            Map<String, Object> extra = new HashMap<>(1);
+            extra.put("apiKeyId", userApiKey.getId());
+            apiKey = JwtUtils.createToken(user.getUsername(), expirationTime, extra);
+        } while (userApiKeyService.isApiKeyExists(apiKey));
+
+        int resetResult = userApiKeyService.reset(id, apiKey);
+
+        if (resetResult <= 0) {
+            throw new ControllerException(ErrorCode.ERROR100);
+        }
+    }
+
+    @PostMapping("/remark")
+    @Operation(summary = "澶囨敞鐢ㄦ埛ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "id", description = "鐢ㄦ埛ApiKeyId", required = true)
+    @Parameter(name = "remark", description = "鐢ㄦ埛ApiKey澶囨敞", required = false)
+    @Transactional
+    public void remark(@RequestParam(required = true) Integer id, @RequestParam(required = false) String remark) {
+        // 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛id
+        int currenRoleId = SecurityUtils.getUserInfo().getRole().getId();
+        if (currenRoleId != 1) {
+            // 鍙敤瑙掕壊id涓�1鎵嶅彲浠ョ鐞哢serApiKey
+            throw new ControllerException(ErrorCode.ERROR403);
+        }
+        UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id);
+        if (userApiKey == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey涓嶅瓨鍦�");
+        }
+        int remarkResult = userApiKeyService.remark(id, remark);
+
+        if (remarkResult <= 0) {
+            throw new ControllerException(ErrorCode.ERROR100);
+        }
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "鍒犻櫎鐢ㄦ埛ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "id", description = "鐢ㄦ埛ApiKeyId", required = true)
+    @Transactional
+    public void delete(@RequestParam(required = true) Integer id) {
+        // 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛id
+        int currenRoleId = SecurityUtils.getUserInfo().getRole().getId();
+        if (currenRoleId != 1) {
+            // 鍙敤瑙掕壊id涓�1鎵嶅彲浠ョ鐞哢serApiKey
+            throw new ControllerException(ErrorCode.ERROR403);
+        }
+        UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id);
+        if (userApiKey == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey涓嶅瓨鍦�");
+        }
+
+        int deleteResult = userApiKeyService.delete(id);
+
+        if (deleteResult <= 0) {
+            throw new ControllerException(ErrorCode.ERROR100);
+        }
+    }
+}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 8f9661b..558bd14 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -10,6 +10,8 @@
     multipart:
       max-file-size: 10MB
       max-request-size: 100MB
+  cache:
+    type: redis
   # REDIS鏁版嵁搴撻厤缃�
   redis:
     # [蹇呴』淇敼] Redis鏈嶅姟鍣↖P, REDIS瀹夎鍦ㄦ湰鏈虹殑,浣跨敤127.0.0.1
diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml
index aeabc8d..51f7226 100644
--- a/src/main/resources/application-docker.yml
+++ b/src/main/resources/application-docker.yml
@@ -4,6 +4,8 @@
         multipart:
             max-file-size: 10MB
             max-request-size: 100MB
+    cache:
+        type: redis
     # REDIS鏁版嵁搴撻厤缃�
     redis:
         # [蹇呴』淇敼] Redis鏈嶅姟鍣↖P, REDIS瀹夎鍦ㄦ湰鏈虹殑,浣跨敤127.0.0.1
diff --git a/web_src/config/index.js b/web_src/config/index.js
index 9d24d1b..ad5f940 100644
--- a/web_src/config/index.js
+++ b/web_src/config/index.js
@@ -12,7 +12,7 @@
     assetsPublicPath: '/',
     proxyTable: {
       '/debug': {
-        target: 'http://127.0.0.1:18082',
+        target: 'http://127.0.0.1:8080',
         changeOrigin: true,
         pathRewrite: {
           '^/debug': '/'
diff --git a/web_src/src/components/UserApiKeyManager.vue b/web_src/src/components/UserApiKeyManager.vue
new file mode 100644
index 0000000..b3eb80d
--- /dev/null
+++ b/web_src/src/components/UserApiKeyManager.vue
@@ -0,0 +1,296 @@
+<template>
+  <div id="app" style="width: 100%">
+    <div class="page-header" style="margin-bottom: 0">
+      <div class="page-title">
+        <el-page-header @back="goBack" content="ApiKey鍒楄〃"></el-page-header>
+      </div>
+      <div class="page-header-btn">
+        <el-button icon="el-icon-plus" size="mini" style="margin-right: 1rem;" type="primary" @click="addUserApiKey">
+          娣诲姞ApiKey
+        </el-button>
+      </div>
+    </div>
+    <!--ApiKey鍒楄〃-->
+    <el-table :data="userList" style="width: 100%;font-size: 12px;" :height="winHeight"
+              header-row-class-name="table-header">
+      <el-table-column prop="user.username" label="鐢ㄦ埛鍚�" min-width="120"/>
+      <el-table-column prop="app" label="搴旂敤鍚�" min-width="160"/>
+      <el-table-column prop="apiKey" label="ApiKey" min-width="480"/>
+      <el-table-column prop="enable" label="鍚敤" width="120">
+        <template #default="scope">
+          <el-tag v-if="scope.row.enable">
+            鍚敤
+          </el-tag>
+          <el-tag v-else type="info">
+            鍋滅敤
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="expiredAt" label="杩囨湡鏃堕棿" width="160"/>
+      <el-table-column prop="remark" label="澶囨敞淇℃伅" min-width="160"/>
+      <el-table-column label="鎿嶄綔" min-width="160" fixed="right">
+        <template #default="scope">
+          <el-button v-if="scope.row.enable"
+                     size="medium" icon="el-icon-circle-close" type="text" @click="disableUserApiKey(scope.row)">
+            鍋滅敤
+          </el-button>
+          <el-button v-else
+                     size="medium" icon="el-icon-circle-check" type="text" @click="enableUserApiKey(scope.row)">
+            鍚敤
+          </el-button>
+          <el-divider direction="vertical"></el-divider>
+          <el-button size="medium" icon="el-icon-refresh" type="text" @click="resetUserApiKey(scope.row)">
+            閲嶇疆
+          </el-button>
+          <el-divider direction="vertical"></el-divider>
+          <el-button size="medium" icon="el-icon-edit" type="text" @click="remarkUserApiKey(scope.row)">
+            澶囨敞
+          </el-button>
+          <el-divider direction="vertical"></el-divider>
+          <el-button size="medium" icon="el-icon-delete" type="text" @click="deleteUserApiKey(scope.row)"
+                     style="color: #f56c6c">
+            鍒犻櫎
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <addUserApiKey ref="addUserApiKey"></addUserApiKey>
+    <remarkUserApiKey ref="remarkUserApiKey"></remarkUserApiKey>
+    <el-pagination
+      style="float: right"
+      @size-change="handleSizeChange"
+      @current-change="currentChange"
+      :current-page="currentPage"
+      :page-size="count"
+      :page-sizes="[15, 25, 35, 50]"
+      layout="total, sizes, prev, pager, next"
+      :total="total">
+    </el-pagination>
+  </div>
+</template>
+
+<script>
+import uiHeader from '../layout/UiHeader.vue'
+import addUserApiKey from "./dialog/addUserApiKey.vue";
+import remarkUserApiKey from './dialog/remarkUserApiKey.vue'
+
+export default {
+  name: 'userApiKeyManager',
+  components: {
+    uiHeader,
+    addUserApiKey,
+    remarkUserApiKey
+  },
+  data() {
+    return {
+      userList: [], //璁惧鍒楄〃
+      currentUser: {}, //褰撳墠鎿嶄綔璁惧瀵硅薄
+      winHeight: window.innerHeight - 200,
+      currentPage: 1,
+      count: 15,
+      total: 0,
+      getUserApiKeyListLoading: false
+    };
+  },
+  mounted() {
+    this.initParam();
+    this.initData();
+  },
+  methods: {
+    goBack() {
+      this.$router.back()
+    },
+    initParam() {
+      this.userId = this.$route.params.userId;
+    },
+    initData() {
+      this.getUserApiKeyList();
+    },
+    currentChange(val) {
+      this.currentPage = val;
+      this.getUserApiKeyList();
+    },
+    handleSizeChange(val) {
+      this.count = val;
+      this.getUserApiKeyList();
+    },
+    getUserApiKeyList() {
+      let that = this;
+      this.getUserApiKeyListLoading = true;
+      this.$axios({
+        method: 'get',
+        url: `/api/userApiKey/userApiKeys`,
+        params: {
+          page: that.currentPage,
+          count: that.count
+        }
+      }).then((res) => {
+        if (res.data.code === 0) {
+          that.total = res.data.data.total;
+          that.userList = res.data.data.list;
+        }
+        that.getUserApiKeyListLoading = false;
+      }).catch((error) => {
+        that.getUserApiKeyListLoading = false;
+      });
+    },
+    addUserApiKey() {
+      this.$refs.addUserApiKey.openDialog(this.userId, () => {
+        this.$refs.addUserApiKey.close();
+        this.$message({
+          showClose: true,
+          message: "ApiKey娣诲姞鎴愬姛",
+          type: "success",
+        });
+        setTimeout(this.getUserApiKeyList, 200)
+      })
+    },
+    remarkUserApiKey(row) {
+      this.$refs.remarkUserApiKey.openDialog(row.id, () => {
+        this.$refs.remarkUserApiKey.close();
+        this.$message({
+          showClose: true,
+          message: "澶囨敞淇敼鎴愬姛",
+          type: "success",
+        });
+        setTimeout(this.getUserApiKeyList, 200)
+      })
+    },
+    enableUserApiKey(row) {
+      let msg = "纭畾鍚敤姝piKey锛�"
+      if (row.online !== 0) {
+        msg = "<strong>纭畾鍚敤姝piKey锛�</strong>"
+      }
+      this.$confirm(msg, '鎻愮ず', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        center: true,
+        type: 'warning'
+      }).then(() => {
+        this.$axios({
+          method: 'post',
+          url: `/api/userApiKey/enable?id=${row.id}`
+        }).then((res) => {
+          this.$message({
+            showClose: true,
+            message: '鍚敤鎴愬姛',
+            type: 'success'
+          });
+          this.getUserApiKeyList();
+        }).catch((error) => {
+          this.$message({
+            showClose: true,
+            message: '鍚敤澶辫触',
+            type: 'error'
+          });
+          console.error(error);
+        });
+      }).catch(() => {
+      });
+    },
+    disableUserApiKey(row) {
+      let msg = "纭畾鍋滅敤姝piKey锛�"
+      if (row.online !== 0) {
+        msg = "<strong>纭畾鍋滅敤姝piKey锛�</strong>"
+      }
+      this.$confirm(msg, '鎻愮ず', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        center: true,
+        type: 'warning'
+      }).then(() => {
+        this.$axios({
+          method: 'post',
+          url: `/api/userApiKey/disable?id=${row.id}`
+        }).then((res) => {
+          this.$message({
+            showClose: true,
+            message: '鍋滅敤鎴愬姛',
+            type: 'success'
+          });
+          this.getUserApiKeyList();
+        }).catch((error) => {
+          this.$message({
+            showClose: true,
+            message: '鍋滅敤澶辫触',
+            type: 'error'
+          });
+          console.error(error);
+        });
+      }).catch(() => {
+      });
+    },
+    resetUserApiKey(row) {
+      let msg = "纭畾閲嶇疆姝piKey锛�"
+      if (row.online !== 0) {
+        msg = "<strong>纭畾閲嶇疆姝piKey锛�</strong>"
+      }
+      this.$confirm(msg, '鎻愮ず', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        center: true,
+        type: 'warning'
+      }).then(() => {
+        this.$axios({
+          method: 'post',
+          url: `/api/userApiKey/reset?id=${row.id}`
+        }).then((res) => {
+          this.$message({
+            showClose: true,
+            message: '閲嶇疆鎴愬姛',
+            type: 'success'
+          });
+          this.getUserApiKeyList();
+        }).catch((error) => {
+          this.$message({
+            showClose: true,
+            message: '閲嶇疆澶辫触',
+            type: 'error'
+          });
+          console.error(error);
+        });
+      }).catch(() => {
+      });
+    },
+    deleteUserApiKey(row) {
+      let msg = "纭畾鍒犻櫎姝piKey锛�"
+      if (row.online !== 0) {
+        msg = "<strong>纭畾鍒犻櫎姝piKey锛�</strong>"
+      }
+      this.$confirm(msg, '鎻愮ず', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        center: true,
+        type: 'warning'
+      }).then(() => {
+        this.$axios({
+          method: 'delete',
+          url: `/api/userApiKey/delete?id=${row.id}`
+        }).then((res) => {
+          this.$message({
+            showClose: true,
+            message: '鍒犻櫎鎴愬姛',
+            type: 'success'
+          });
+          this.getUserApiKeyList();
+        }).catch((error) => {
+          this.$message({
+            showClose: true,
+            message: '鍒犻櫎澶辫触',
+            type: 'error'
+          });
+          console.error(error);
+        });
+      }).catch(() => {
+      });
+    },
+  }
+}
+</script>
+<style>
+
+</style>
diff --git a/web_src/src/components/UserManager.vue b/web_src/src/components/UserManager.vue
index c0fa695..19ba087 100755
--- a/web_src/src/components/UserManager.vue
+++ b/web_src/src/components/UserManager.vue
@@ -23,6 +23,8 @@
           <el-divider direction="vertical"></el-divider>
           <el-button size="medium" icon="el-icon-edit" type="text" @click="changePushKey(scope.row)">淇敼pushkey</el-button>
           <el-divider direction="vertical"></el-divider>
+          <el-button size="medium" icon="el-icon-edit" type="text" @click="showUserApiKeyManager(scope.row)">绠$悊ApiKey</el-button>
+          <el-divider direction="vertical"></el-divider>
           <el-button size="medium" icon="el-icon-delete" type="text" @click="deleteUser(scope.row)"
                      style="color: #f56c6c">鍒犻櫎
           </el-button>
@@ -178,7 +180,10 @@
         setTimeout(this.getUserList, 200)
 
       })
-    }
+    },
+    showUserApiKeyManager: function (row) {
+      this.$router.push(`/userApiKeyManager/${row.id}`)
+    },
   }
 }
 </script>
diff --git a/web_src/src/components/dialog/addUserApiKey.vue b/web_src/src/components/dialog/addUserApiKey.vue
new file mode 100644
index 0000000..5dd94c0
--- /dev/null
+++ b/web_src/src/components/dialog/addUserApiKey.vue
@@ -0,0 +1,139 @@
+<template>
+  <div id="addUserApiKey" v-loading="isLoading">
+    <el-dialog
+      title="娣诲姞ApiKey"
+      width="40%"
+      top="2rem"
+      :close-on-click-modal="false"
+      :visible.sync="showDialog"
+      :destroy-on-close="true"
+      @close="close()"
+    >
+      <div id="shared" style="margin-right: 20px;">
+        <el-form ref="formRef" :model="form" :rules="rules" status-icon label-width="80px">
+          <el-form-item label="搴旂敤鍚�" prop="app">
+            <el-input
+              v-model="form.app"
+              property="app"
+              autocomplete="off"/>
+          </el-form-item>
+          <el-form-item label="鍚敤鐘舵��" prop="enable" style="text-align: left">
+            <el-switch
+              v-model="form.enable"
+              property="enable"
+              active-text="鍚敤"
+              inactive-text="鍋滅敤"/>
+          </el-form-item>
+          <el-form-item label="杩囨湡鏃堕棿" prop="expiresAt" style="text-align: left">
+            <el-date-picker v-model="form.expiresAt"
+                            style="width: 100%"
+                            property="expiresAt"
+                            type="datetime"
+                            value-format="yyyy-MM-dd HH:mm:ss"
+                            format="yyyy-MM-dd HH:mm:ss"
+                            placeholder="閫夋嫨杩囨湡鏃堕棿"/>
+          </el-form-item>
+          <el-form-item label="澶囨敞淇℃伅" prop="remark">
+            <el-input v-model="form.remark"
+                      type="textarea"
+                      property="remark"
+                      autocomplete="off"
+                      :autosize="{ minRows: 5}"
+                      maxlength="255"
+                      show-word-limit/>
+          </el-form-item>
+          <el-form-item>
+            <div style="float: right;">
+              <el-button type="primary" @click="onSubmit">淇濆瓨</el-button>
+              <el-button @click="close">鍙栨秷</el-button>
+            </div>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'addUserApiKey',
+  props: {},
+  computed: {},
+  created() {
+  },
+  data() {
+    return {
+      userId: null,
+      form: {
+        app: null,
+        enable: true,
+        expiresAt: null,
+        remark: null
+      },
+      rules: {
+        app: [{required: true, trigger: 'blur', message: '搴旂敤鍚嶄笉鑳戒负绌�'}]
+      },
+      listChangeCallback: null,
+      showDialog: false,
+      isLoading: false
+    };
+  },
+  methods: {
+    resetForm() {
+      this.form = {
+        app: null,
+        enable: true,
+        expiresAt: null,
+        remark: null
+      }
+    },
+    openDialog(userId, callback) {
+      this.resetForm()
+      this.userId = userId
+      this.listChangeCallback = callback
+      this.showDialog = true
+    },
+    onSubmit() {
+      this.$refs.formRef.validate((valid) => {
+        if (valid) {
+          this.$axios({
+            method: 'post',
+            url: '/api/userApiKey/add',
+            params: {
+              userId: this.userId,
+              app: this.form.app,
+              enable: this.form.enable,
+              expiresAt: this.form.expiresAt,
+              remark: this.form.remark,
+            }
+          }).then((res) => {
+            if (res.data.code === 0) {
+              this.$message({
+                showClose: true,
+                message: '娣诲姞鎴愬姛',
+                type: 'success'
+              });
+              this.showDialog = false
+              if (this.listChangeCallback) {
+                this.listChangeCallback()
+              }
+            } else {
+              this.$message({
+                showClose: true,
+                message: res.data.msg,
+                type: 'error'
+              });
+            }
+          }).catch((error) => {
+            console.error(error)
+          });
+        }
+      });
+    },
+    close() {
+      this.showDialog = false
+    }
+  },
+};
+</script>
diff --git a/web_src/src/components/dialog/remarkUserApiKey.vue b/web_src/src/components/dialog/remarkUserApiKey.vue
new file mode 100644
index 0000000..6236a2e
--- /dev/null
+++ b/web_src/src/components/dialog/remarkUserApiKey.vue
@@ -0,0 +1,93 @@
+<template>
+  <div id="remarkUserApiKey" v-loading="isLoading">
+    <el-dialog
+      title="ApiKey澶囨敞"
+      width="40%"
+      top="2rem"
+      :close-on-click-modal="false"
+      :visible.sync="showDialog"
+      :destroy-on-close="true"
+      @close="close()"
+    >
+      <div id="shared" style="margin-right: 20px;">
+        <el-form ref="form" :rules="rules" status-icon label-width="80px">
+          <el-form-item label="澶囨敞" prop="oldPassword">
+            <el-input type="textarea" v-model="form.remark" autocomplete="off" :autosize="{ minRows: 5}" maxlength="255" show-word-limit></el-input>
+          </el-form-item>
+          <el-form-item>
+            <div style="float: right;">
+              <el-button type="primary" @click="onSubmit">淇濆瓨</el-button>
+              <el-button @click="close">鍙栨秷</el-button>
+            </div>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "remarkUserApiKey",
+  props: {},
+  computed: {},
+  created() {
+  },
+  data() {
+    return {
+      userApiKeyId: null,
+      form: {
+        remark: null
+      },
+      rules: {},
+      listChangeCallback: null,
+      showDialog: false,
+      isLoading: false
+    };
+  },
+  methods: {
+    resetForm() {
+      this.form = {
+        remark: null
+      }
+    },
+    openDialog(userApiKeyId, callback) {
+      this.resetForm()
+      this.userApiKeyId = userApiKeyId
+      this.listChangeCallback = callback
+      this.showDialog = true
+    },
+    onSubmit() {
+      this.$axios({
+        method: 'post',
+        url: "/api/userApiKey/remark",
+        params: {
+          id: this.userApiKeyId,
+          remark: this.form.remark
+        }
+      }).then((res) => {
+        if (res.data.code === 0) {
+          this.$message({
+            showClose: true,
+            message: '澶囨敞淇敼鎴愬姛!',
+            type: 'success'
+          });
+          this.showDialog = false;
+          this.listChangeCallback()
+        } else {
+          this.$message({
+            showClose: true,
+            message: '澶囨敞淇敼澶辫触',
+            type: 'error'
+          });
+        }
+      }).catch((error) => {
+        console.error(error)
+      });
+    },
+    close() {
+      this.showDialog = false
+    },
+  },
+};
+</script>
diff --git a/web_src/src/router/index.js b/web_src/src/router/index.js
index d5e9f7e..fa96b2b 100755
--- a/web_src/src/router/index.js
+++ b/web_src/src/router/index.js
@@ -20,7 +20,7 @@
 import live from '../components/live.vue'
 import deviceTree from '../components/common/DeviceTree.vue'
 import userManager from '../components/UserManager.vue'
-
+import userApiKeyManager from '../components/UserApiKeyManager.vue'
 import wasmPlayer from '../components/common/jessibuca.vue'
 import rtcPlayer from '../components/dialog/rtcPlayer.vue'
 
@@ -125,7 +125,13 @@
           path: '/userManager',
           name: 'userManager',
           component: userManager,
+        },
+        {
+          path: '/userApiKeyManager/:userId',
+          name: 'userApiKeyManager',
+          component: userApiKeyManager,
         }
+        ,
         ]
     },
     {
diff --git "a/\346\211\223\345\214\205/config/wvp-application.yml" "b/\346\211\223\345\214\205/config/wvp-application.yml"
index 8083e36..71b5da2 100755
--- "a/\346\211\223\345\214\205/config/wvp-application.yml"
+++ "b/\346\211\223\345\214\205/config/wvp-application.yml"
@@ -4,6 +4,8 @@
         multipart:
             max-file-size: 10MB
             max-request-size: 100MB
+    cache:
+        type: redis
     # REDIS鏁版嵁搴撻厤缃�
     redis:
         # [鍙�塢 瓒呮椂鏃堕棿
diff --git "a/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-mysql-2.7.0.sql" "b/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-mysql-2.7.0.sql"
index a3f4a1d..6e1f83b 100644
--- "a/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-mysql-2.7.0.sql"
+++ "b/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-mysql-2.7.0.sql"
@@ -314,6 +314,17 @@
                                     parentId integer,
                                     path character varying(255)
 );
+create table wvp_user_api_key (
+                                    id serial primary key ,
+                                    user_id bigint,
+                                    app character varying(255) ,
+                                    api_key text,
+                                    expired_at bigint,
+                                    remark character varying(255),
+                                    enable bool default true,
+                                    create_time character varying(50),
+                                    update_time character varying(50)
+);
 
 
 /*鍒濆鏁版嵁*/
diff --git "a/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-postgresql-kingbase-2.7.0.sql" "b/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-postgresql-kingbase-2.7.0.sql"
index 9f41667..17ef270 100644
--- "a/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-postgresql-kingbase-2.7.0.sql"
+++ "b/\346\225\260\346\215\256\345\272\223/2.7.0/\345\210\235\345\247\213\345\214\226-postgresql-kingbase-2.7.0.sql"
@@ -314,7 +314,17 @@
                                     parentId integer,
                                     path character varying(255)
 );
-
+create table wvp_user_api_key (
+                                  id serial primary key ,
+                                  user_id bigint,
+                                  app character varying(255) ,
+                                  api_key text,
+                                  expired_at bigint,
+                                  remark character varying(255),
+                                  enable bool default true,
+                                  create_time character varying(50),
+                                  update_time character varying(50)
+);
 
 /*鍒濆鏁版嵁*/
 INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3');
diff --git "a/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-mysql.sql" "b/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-mysql.sql"
index a3f4a1d..f3247c1 100644
--- "a/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-mysql.sql"
+++ "b/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-mysql.sql"
@@ -314,7 +314,17 @@
                                     parentId integer,
                                     path character varying(255)
 );
-
+create table wvp_user_api_key (
+                                  id serial primary key ,
+                                  user_id bigint,
+                                  app character varying(255) ,
+                                  api_key text,
+                                  expired_at bigint,
+                                  remark character varying(255),
+                                  enable bool default true,
+                                  create_time character varying(50),
+                                  update_time character varying(50)
+);
 
 /*鍒濆鏁版嵁*/
 INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3');
diff --git "a/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-postgresql-kingbase.sql" "b/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-postgresql-kingbase.sql"
index 9f41667..17ef270 100644
--- "a/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-postgresql-kingbase.sql"
+++ "b/\346\225\260\346\215\256\345\272\223/\345\210\235\345\247\213\345\214\226-postgresql-kingbase.sql"
@@ -314,7 +314,17 @@
                                     parentId integer,
                                     path character varying(255)
 );
-
+create table wvp_user_api_key (
+                                  id serial primary key ,
+                                  user_id bigint,
+                                  app character varying(255) ,
+                                  api_key text,
+                                  expired_at bigint,
+                                  remark character varying(255),
+                                  enable bool default true,
+                                  create_time character varying(50),
+                                  update_time character varying(50)
+);
 
 /*鍒濆鏁版嵁*/
 INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3');

--
Gitblit v1.8.0