浏览代码

[add]支持其他平台通过ApiKey调用系统相关接口

leesam 1 年之前
父节点
当前提交
76208975bf
共有 24 个文件被更改,包括 1267 次插入24 次删除
  1. 4 0
      pom.xml
  2. 2 0
      src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
  3. 5 2
      src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java
  4. 90 16
      src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java
  5. 25 0
      src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java
  6. 2 0
      src/main/java/com/genersoft/iot/vmp/service/IUserService.java
  7. 80 0
      src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java
  8. 5 0
      src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java
  9. 60 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java
  10. 151 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java
  11. 251 0
      src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java
  12. 2 0
      src/main/resources/application-dev.yml
  13. 2 0
      src/main/resources/application-docker.yml
  14. 1 1
      web_src/config/index.js
  15. 296 0
      web_src/src/components/UserApiKeyManager.vue
  16. 6 1
      web_src/src/components/UserManager.vue
  17. 139 0
      web_src/src/components/dialog/addUserApiKey.vue
  18. 93 0
      web_src/src/components/dialog/remarkUserApiKey.vue
  19. 7 1
      web_src/src/router/index.js
  20. 2 0
      打包/config/wvp-application.yml
  21. 11 0
      数据库/2.7.0/初始化-mysql-2.7.0.sql
  22. 11 1
      数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql
  23. 11 1
      数据库/初始化-mysql.sql
  24. 11 1
      数据库/初始化-postgresql-kingbase.sql

+ 4 - 0
pom.xml

@@ -99,6 +99,10 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </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>

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java

@@ -9,6 +9,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 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 @@ import java.util.Collections;
 @ServletComponentScan("com.genersoft.iot.vmp.conf")
 @SpringBootApplication
 @EnableScheduling
+@EnableCaching
 public class VManageBootstrap extends SpringBootServletInitializer {
 
 	private final static Logger logger = LoggerFactory.getLogger(VManageBootstrap.class);

+ 5 - 2
src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java

@@ -51,8 +51,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
         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;
+                }
             }
         }
 

+ 90 - 16
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.beans.factory.InitializingBean;
 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 class JwtUtils implements InitializingBean {
 
     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,17 +53,28 @@ public class JwtUtils implements InitializingBean {
     /**
      * 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
     public void afterPropertiesSet() {
         try {
@@ -62,14 +89,38 @@ public class JwtUtils implements InitializingBean {
      * @throws JoseException JoseException
      */
     private RsaJsonWebKey generateRsaJsonWebKey() throws JoseException {
-        // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中
-        RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
-        // 给JWK一个密钥ID
-        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) {
+            // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中
+            rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
+            // 给JWK一个密钥ID
+            rsaJsonWebKey.setKeyId(keyId);
+        }
         return rsaJsonWebKey;
     }
 
-    public static String createToken(String username) {
+    public static String createToken(String username, Long expirationTime, Map<String, Object> extra) {
         try {
             /*
              * “iss” (issuer)  发行人
@@ -83,13 +134,17 @@ public class JwtUtils implements InitializingBean {
             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,10 +159,17 @@ public class JwtUtils implements InitializingBean {
         } 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() {
         return HEADER;
     }
@@ -118,8 +180,8 @@ public class JwtUtils implements InitializingBean {
 
         try {
             JwtConsumer consumer = new JwtConsumerBuilder()
-                    .setRequireExpirationTime()
-                    .setMaxFutureValidityInMinutes(5256000)
+                    //.setRequireExpirationTime()
+                    //.setMaxFutureValidityInMinutes(5256000)
                     .setAllowedClockSkewInSeconds(30)
                     .setRequireSubject()
                     //.setExpectedIssuer("")
@@ -129,15 +191,27 @@ public class JwtUtils implements InitializingBean {
 
             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);
 

+ 25 - 0
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);
+
+}

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/service/IUserService.java

@@ -11,6 +11,8 @@ public interface IUserService {
 
     boolean changePassword(int id, String password);
 
+    User getUserById(int id);
+
     User getUserByUsername(String username);
 
     int addUser(User user);

+ 80 - 0
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);
+    }
+
+}

+ 5 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java

@@ -31,6 +31,11 @@ public class UserServiceImpl implements IUserService {
         return userMapper.update(user) > 0;
     }
 
+    @Override
+    public User getUserById(int id) {
+        return userMapper.selectById(id);
+    }
+
     @Override
     public User getUserByUsername(String username) {
         return userMapper.getUserByUsername(username);

+ 60 - 0
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);
+
+}

+ 151 - 0
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;
+
+    /**
+     * 过期时间(null=永不过期)
+     */
+    @Schema(description = "过期时间(null=永不过期)")
+    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;
+    }
+}

+ 251 - 0
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才可以管理UserApiKey
+            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才可以管理UserApiKey
+            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才可以管理UserApiKey
+            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才可以管理UserApiKey
+            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才可以管理UserApiKey
+            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);
+        }
+    }
+}

+ 2 - 0
src/main/resources/application-dev.yml

@@ -10,6 +10,8 @@ spring:
     multipart:
       max-file-size: 10MB
       max-request-size: 100MB
+  cache:
+    type: redis
   # REDIS数据库配置
   redis:
     # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1

+ 2 - 0
src/main/resources/application-docker.yml

@@ -4,6 +4,8 @@ spring:
         multipart:
             max-file-size: 10MB
             max-request-size: 100MB
+    cache:
+        type: redis
     # REDIS数据库配置
     redis:
         # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1

+ 1 - 1
web_src/config/index.js

@@ -12,7 +12,7 @@ module.exports = {
     assetsPublicPath: '/',
     proxyTable: {
       '/debug': {
-        target: 'http://127.0.0.1:18082',
+        target: 'http://127.0.0.1:8080',
         changeOrigin: true,
         pathRewrite: {
           '^/debug': '/'

+ 296 - 0
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 = "确定启用此ApiKey?"
+      if (row.online !== 0) {
+        msg = "<strong>确定启用此ApiKey?</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 = "确定停用此ApiKey?"
+      if (row.online !== 0) {
+        msg = "<strong>确定停用此ApiKey?</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 = "确定重置此ApiKey?"
+      if (row.online !== 0) {
+        msg = "<strong>确定重置此ApiKey?</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 = "确定删除此ApiKey?"
+      if (row.online !== 0) {
+        msg = "<strong>确定删除此ApiKey?</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>

+ 6 - 1
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 @@ export default {
         setTimeout(this.getUserList, 200)
 
       })
-    }
+    },
+    showUserApiKeyManager: function (row) {
+      this.$router.push(`/userApiKeyManager/${row.id}`)
+    },
   }
 }
 </script>

+ 139 - 0
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>

+ 93 - 0
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>

+ 7 - 1
web_src/src/router/index.js

@@ -20,7 +20,7 @@ import media from '../components/setting/Media.vue'
 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 @@ export default new VueRouter({
           path: '/userManager',
           name: 'userManager',
           component: userManager,
+        },
+        {
+          path: '/userApiKeyManager/:userId',
+          name: 'userApiKeyManager',
+          component: userApiKeyManager,
         }
+        ,
         ]
     },
     {

+ 2 - 0
打包/config/wvp-application.yml

@@ -4,6 +4,8 @@ spring:
         multipart:
             max-file-size: 10MB
             max-request-size: 100MB
+    cache:
+        type: redis
     # REDIS数据库配置
     redis:
         # [可选] 超时时间

+ 11 - 0
数据库/2.7.0/初始化-mysql-2.7.0.sql

@@ -314,6 +314,17 @@ create table wvp_resources_tree (
                                     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)
+);
 
 
 /*初始数据*/

+ 11 - 1
数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql

@@ -314,7 +314,17 @@ create table wvp_resources_tree (
                                     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');

+ 11 - 1
数据库/初始化-mysql.sql

@@ -314,7 +314,17 @@ create table wvp_resources_tree (
                                     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');

+ 11 - 1
数据库/初始化-postgresql-kingbase.sql

@@ -314,7 +314,17 @@ create table wvp_resources_tree (
                                     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');