diff --git a/pom.xml b/pom.xml index df841d52..e129b784 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,10 @@ org.springframework.boot spring-boot-starter-data-redis + + org.springframework.boot + spring-boot-starter-cache + org.springframework.boot spring-boot-starter-web diff --git a/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java index be573166..262910b0 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.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); diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java index 03ef0994..7f25a362 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java @@ -1,6 +1,8 @@ package com.genersoft.iot.vmp.conf; import org.apache.ibatis.logging.stdout.StdOutImpl; +import org.apache.ibatis.mapping.DatabaseIdProvider; +import org.apache.ibatis.mapping.VendorDatabaseIdProvider; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.factory.annotation.Autowired; @@ -9,6 +11,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import javax.sql.DataSource; +import java.util.Properties; /** * 配置mybatis @@ -21,7 +24,29 @@ public class MybatisConfig { private UserSetting userSetting; @Bean - public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + public DatabaseIdProvider databaseIdProvider() { + VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); + Properties properties = new Properties(); + properties.setProperty("Oracle", "oracle"); + properties.setProperty("MySQL", "mysql"); + properties.setProperty("DB2", "db2"); + properties.setProperty("Derby", "derby"); + properties.setProperty("H2", "h2"); + properties.setProperty("HSQL", "hsql"); + properties.setProperty("Informix", "informix"); + properties.setProperty("MS-SQL", "ms-sql"); + properties.setProperty("PostgreSQL", "postgresql"); + properties.setProperty("Sybase", "sybase"); + properties.setProperty("Hana", "hana"); + properties.setProperty("DM", "dm"); + properties.setProperty("KingbaseES", "kingbase"); + properties.setProperty("KingBase8", "kingbase"); + databaseIdProvider.setProperties(properties); + return databaseIdProvider; + } + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource, DatabaseIdProvider databaseIdProvider) throws Exception { final SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); @@ -30,6 +55,7 @@ public class MybatisConfig { } config.setMapUnderscoreToCamelCase(true); sqlSessionFactory.setConfiguration(config); + sqlSessionFactory.setDatabaseIdProvider(databaseIdProvider); return sqlSessionFactory.getObject(); } 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 f45f89a1..274a19f8 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 @@ 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; + } } } 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 fcd19461..eacff188 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,13 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; @Component public class JwtUtils implements InitializingBean { @@ -30,6 +39,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 +48,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 { @@ -59,17 +81,34 @@ 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 (BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("/jwk.json"), StandardCharsets.UTF_8))) { + String jwkJson = reader.readLine(); + JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jwkJson); + List jsonWebKeys = jsonWebKeySet.getJsonWebKeys(); + if (!jsonWebKeys.isEmpty()) { + JsonWebKey jsonWebKey = jsonWebKeys.get(0); + if (jsonWebKey instanceof RsaJsonWebKey) { + rsaJsonWebKey = (RsaJsonWebKey) jsonWebKey; + } + } + } catch (Exception 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 extra) { try { /* * “iss” (issuer) 发行人 @@ -83,13 +122,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 +147,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 +168,8 @@ public class JwtUtils implements InitializingBean { try { JwtConsumer consumer = new JwtConsumerBuilder() - .setRequireExpirationTime() - .setMaxFutureValidityInMinutes(5256000) + //.setRequireExpirationTime() + //.setMaxFutureValidityInMinutes(5256000) .setAllowedClockSkewInSeconds(30) .setRequireSubject() //.setExpectedIssuer("") @@ -129,15 +179,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); 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 00000000..b3cc5805 --- /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 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 7e2a8395..1e9b7247 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 @@ public interface IUserService { 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 00000000..85ee4f0f --- /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 redisTemplate; + + @Override + public int addApiKey(UserApiKey userApiKey) { + return userApiKeyMapper.add(userApiKey); + } + + @Override + public boolean isApiKeyExists(String apiKey) { + return userApiKeyMapper.isApiKeyExists(apiKey); + } + + @Override + public PageInfo getUserApiKeys(int page, int count) { + PageHelper.startPage(page, count); + List 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 400b19bc..fb97db93 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 @@ -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); 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 00000000..18539f1a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java @@ -0,0 +1,61 @@ +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(databaseId = "postgresql", statement = "SELECT currval('wvp_user_api_key_id_seq'::regclass) AS id", keyProperty = "id", before = false, resultType = Integer.class) + @SelectKey(databaseId = "mysql", 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 = {""}) + 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 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 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 00000000..b631295c --- /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; + + /** + * 过期时间(null=永不过期) + */ + @Schema(description = "过期时间(null=永不过期)") + private long 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 long getExpiredAt() { + return expiredAt; + } + + public void setExpiredAt(long 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 00000000..0de8496c --- /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(expirationTime != null ? expirationTime : 0); + 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 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 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() > 0) { + long timestamp = userApiKey.getExpiredAt(); + expirationTime = (timestamp - System.currentTimeMillis()) / (60 * 1000); + if (expirationTime < 0) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey已失效"); + } + } + String apiKey; + do { + Map 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); + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5be3036f..ddbf237a 100644 --- a/src/main/resources/application-dev.yml +++ b/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 diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml index aeabc8d3..51f72267 100644 --- a/src/main/resources/application-docker.yml +++ b/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 diff --git a/src/main/resources/jwk.json b/src/main/resources/jwk.json new file mode 100644 index 00000000..912b8f39 --- /dev/null +++ b/src/main/resources/jwk.json @@ -0,0 +1 @@ +{"keys":[{"kty":"RSA","kid":"3e79646c4dbc408383a9eed09f2b85ae","n":"rThRAlbMRceko3NkymeSoN2ICVaDlNBLWv3cyLUeixjWcmuhnPv2JpXmgoxezKZfhH_0sChBof--BaaqSUukl9wWMW1bWCyFyU5qNczhQk3ANlhaLiSgXsqD-NKI3ObJjB-26fnOZb9QskCqrPW1lEtwgb9-skMAfGlh5kaDOKjYKI64DPSMMXpSiJEDM-7DK-TFfm0QfPcoH-k-1C02NHlGWehVUn9FUJ0TAiDxpKj28qOmYh7s1M7OU_h-Sso7LM-5zbftpcO6SINe81Gw9JPd7rKPCRxkw8ROSCCq-JH_zshM80kTK2nWcseGvhQ_4vKQIBp9PrAgCrGJHM160w","e":"AQAB","d":"AwS2NKo6iQS_k7GREg3X-kGh-zest00h4wYFcOHnFFlsczX47PlfArEeASxdAofrpi1soB0zd5UzRHnxAbH1vkexg076hoDQG__nzeQyEKu2K7xCZgdxW_V_cziH9gF3hZ-P2mfl9tPsng6OatElRt5BqaEingyY15ImiJK1-qi_LTx4gfwRfquKLbUgqJR4Tf6eKlwOzEo41Ilo26gnojNzWryB_XHG7lj6SngPDBJp7ty32je4Fv3A3hXt7JHDwloww6-xiRtUflDpSec4A-o-PHgbfoYLyM7mM4BDt4PM54EHm4u8WzypG0wNKDTiq4KSapei5xDbiG3RpngvAQ","p":"5kUHkGxnZvZT762Ex-0De2nYodAbbZNVR-eIPx2ng2VZmEbAU3cp_DxigpXWyQ0FwJ2Me8GvxnlbxJ7k7d-4AV2X8q6Q-UqXajHdudRU_QX05kPEgZ3xtPk5ekI0-u1BEQT7pY_gxlZC2mzXAcVLd-LwbVPuQEba5S4JMsjcHUE","q":"wJNa06-qZ2tWncGl7cfJdO-SJ_H3taowMhh-RsJmeVefjjN3pfVjjE0wG_rIP-BjjCB9OhvSnI8LDjoNu8uIg090DYnA6IUfZpWo3zjgedeyqQyXFVjjVQkn98zgp5NFLpuitZsl9-EHhh7JaZDCwaJ527MN3VCoQxeI75ggjxM","dp":"HQTH_kBbC5OxYjwIxrUswinFnia-viFaFvSrq-CN0rY8Az-vTxVuWhY2B-TgK3gTqIFyScpP34A9u1qW2Q9fffSQiInNRU1MJZrhKWED0NsmULprkjYYVsktoCWlzZWGpKFvIR8voW8Pf71FnziA2TvlNrHkDX-gaE9T422Cp8E","dq":"owJYqMWS1dYLTKBlx0ANbHl6W2u7xb_Y6h7HjTfzLBWazvEL_6QW7uVLqvN-XGuheDTsK6rvfWyr7BACHgvsc1JnJyqK64f8C4b1mnZ3tUt7RROONBi43ftRJLX9GHxV3F0LvvQkkI2gI8ydq0lJQkU5J1qKiuNCewBJ_p3kOZc","qi":"hNAZV6aWEEWfB1HkrfdtO6sjq9ceEod55ez82I1ZNgoKle8gpRkh3vw2EIJ_5lcw57s5rw8G-sCQPG1AQSZ6u9aURwHkIXjpIhLAlv6gvKkCh0smPPvnSiltJKOJsuHkrD6rGkV1f-MlCS51lKlk9xShQzkRidkNd4BUh0a7ktA"}]} \ No newline at end of file diff --git a/web_src/config/index.js b/web_src/config/index.js index 9d24d1ba..ad5f9409 100644 --- a/web_src/config/index.js +++ b/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': '/' diff --git a/web_src/src/components/UserApiKeyManager.vue b/web_src/src/components/UserApiKeyManager.vue new file mode 100644 index 00000000..b3eb80dd --- /dev/null +++ b/web_src/src/components/UserApiKeyManager.vue @@ -0,0 +1,296 @@ + + + + diff --git a/web_src/src/components/UserManager.vue b/web_src/src/components/UserManager.vue index c0fa695a..19ba0871 100755 --- a/web_src/src/components/UserManager.vue +++ b/web_src/src/components/UserManager.vue @@ -23,6 +23,8 @@ 修改pushkey + 管理ApiKey + 删除 @@ -178,7 +180,10 @@ export default { setTimeout(this.getUserList, 200) }) - } + }, + showUserApiKeyManager: function (row) { + this.$router.push(`/userApiKeyManager/${row.id}`) + }, } } diff --git a/web_src/src/components/dialog/addUserApiKey.vue b/web_src/src/components/dialog/addUserApiKey.vue new file mode 100644 index 00000000..5dd94c06 --- /dev/null +++ b/web_src/src/components/dialog/addUserApiKey.vue @@ -0,0 +1,139 @@ + + + diff --git a/web_src/src/components/dialog/remarkUserApiKey.vue b/web_src/src/components/dialog/remarkUserApiKey.vue new file mode 100644 index 00000000..6236a2e9 --- /dev/null +++ b/web_src/src/components/dialog/remarkUserApiKey.vue @@ -0,0 +1,93 @@ + + + diff --git a/web_src/src/router/index.js b/web_src/src/router/index.js index d5e9f7ed..fa96b2b5 100755 --- a/web_src/src/router/index.js +++ b/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, } + , ] }, { diff --git a/打包/config/wvp-application.yml b/打包/config/wvp-application.yml index 8083e369..71b5da29 100755 --- a/打包/config/wvp-application.yml +++ b/打包/config/wvp-application.yml @@ -4,6 +4,8 @@ spring: multipart: max-file-size: 10MB max-request-size: 100MB + cache: + type: redis # REDIS数据库配置 redis: # [可选] 超时时间 diff --git a/数据库/2.7.0/初始化-mysql-2.7.0.sql b/数据库/2.7.0/初始化-mysql-2.7.0.sql index 2cd88e72..edab7dc1 100644 --- a/数据库/2.7.0/初始化-mysql-2.7.0.sql +++ b/数据库/2.7.0/初始化-mysql-2.7.0.sql @@ -315,6 +315,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) +); /*初始数据*/ diff --git a/数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql b/数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql index 5cda9457..452c36c5 100644 --- a/数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql +++ b/数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql @@ -315,7 +315,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'); diff --git a/数据库/2.7.1/初始化-mysql-2.7.0.sql b/数据库/2.7.1/初始化-mysql-2.7.0.sql index d59256b5..cf614d91 100644 --- a/数据库/2.7.1/初始化-mysql-2.7.0.sql +++ b/数据库/2.7.1/初始化-mysql-2.7.0.sql @@ -320,7 +320,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'); diff --git a/数据库/2.7.1/初始化-postgresql-kingbase-2.7.0.sql b/数据库/2.7.1/初始化-postgresql-kingbase-2.7.0.sql index 992a3cf9..317e9e09 100644 --- a/数据库/2.7.1/初始化-postgresql-kingbase-2.7.0.sql +++ b/数据库/2.7.1/初始化-postgresql-kingbase-2.7.0.sql @@ -320,7 +320,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');