initial commit

This commit is contained in:
2025-12-23 19:47:02 +08:00
commit 7e439d0bed
79 changed files with 5120 additions and 0 deletions

18
public-common/pom.xml Normal file
View File

@@ -0,0 +1,18 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yelink.example</groupId>
<artifactId>rainyhon-xzt</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>public-common</artifactId>
<packaging>pom</packaging>
<modules>
<module>yelink-redis-starter</module>
<module>yelink-security-starter</module>
<module>yelink-minio-starter</module>
<module>yelink-doc-starter</module>
</modules>
</project>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.yelink.example</groupId>
<artifactId>public-common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yelink-doc-starter</artifactId>
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,82 @@
package com.yelink.doc.configure;
import com.google.common.collect.Lists;
import com.yelink.doc.properties.DocProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
/**
* DocAutoconfigure.
* @author cwp
*/
@Configuration
@EnableSwagger2
@EnableConfigurationProperties(DocProperties.class)
@ConditionalOnProperty(value = "doc.enable", havingValue = "true", matchIfMissing = true)
public class DocAutoconfigure {
private final DocProperties properties;
public DocAutoconfigure(DocProperties properties) {
this.properties = properties;
}
@Bean
@Order(-1)
public Docket groupRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(groupApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage(properties.getBasePackage()))
.paths(PathSelectors.any())
.build().securityContexts(Lists.newArrayList(securityContext())).securitySchemes(Lists.<SecurityScheme>newArrayList(apiKey()));
}
private ApiInfo groupApiInfo() {
String description = String.format("<div style='font-size:%spx;color:%s;'>%s</div>",
properties.getDescriptionFontSize(), properties.getDescriptionColor(), properties.getDescription());
Contact contact = new Contact(properties.getName(), properties.getUrl(), properties.getEmail());
return new ApiInfoBuilder()
.title(properties.getTitle())
.description(description)
.termsOfServiceUrl(properties.getTermsOfServiceUrl())
.contact(contact)
.license(properties.getLicense())
.licenseUrl(properties.getLicenseUrl())
.version(properties.getVersion())
.build();
}
private ApiKey apiKey() {
return new ApiKey("BearerToken", "Authorization", "header");
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("/.*"))
.build();
}
List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Lists.newArrayList(new SecurityReference("BearerToken", authorizationScopes));
}
}

View File

@@ -0,0 +1,81 @@
package com.yelink.doc.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* DocProperties.
*
* @author cwp
*/
@ConfigurationProperties(prefix = "doc")
@Data
public class DocProperties {
/**
* 是否开启doc功能.
*/
private Boolean enable = true;
/**
* 接口扫描路径如Controller路径.
*/
private String basePackage;
/**
* 文档标题.
*/
private String title;
/**
* 文档描述.
*/
private String description;
/**
* 文档描述颜色.
*/
private String descriptionColor = "#42b983";
/**
* 文档描述字体大小.
*/
private String descriptionFontSize = "14";
/**
* 服务url.
*/
private String termsOfServiceUrl;
/**
* 联系方式:姓名.
*/
private String name;
/**
* 联系方式个人网站url.
*/
private String url;
/**
* 联系方式:邮箱.
*/
private String email;
/**
* 协议.
*/
private String license;
/**
* 协议地址.
*/
private String licenseUrl;
/**
* 版本.
*/
private String version;
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yelink.doc.configure.DocAutoconfigure

View File

@@ -0,0 +1,45 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yelink.example</groupId>
<artifactId>public-common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>yelink-minio-starter</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,27 @@
package com.yelink.minio.configure;
import com.yelink.minio.properties.MinioConfig;
import com.yelink.minio.service.MinioService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MinioAutoconfigure.
*
* @author cwp
* @date 2024-05-16 17:45
*/
@Configuration
@EnableConfigurationProperties({MinioConfig.class})
@Slf4j
@RequiredArgsConstructor
public class MinioAutoconfigure {
@Bean
public MinioService minioService(MinioConfig minioConfig) {
return new MinioService(minioConfig);
}
}

View File

@@ -0,0 +1,24 @@
package com.yelink.minio.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* MinioConfig.
* @author cwp
* @date 2024-05-16 17:19
*/
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioConfig {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucket;
private String applicationName;
}

View File

@@ -0,0 +1,277 @@
package com.yelink.minio.service;
import com.yelink.minio.properties.MinioConfig;
import io.minio.CopyObjectArgs;
import io.minio.CopySource;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* MinioService.
*
* @author cwp
* @date 2024-05-16 17:33
*/
@Slf4j
public class MinioService {
private static final Map<String, String> MIME_TYPES = new HashMap<>();
static {
MIME_TYPES.put("txt", "text/plain");
MIME_TYPES.put("html", "text/html");
MIME_TYPES.put("css", "text/css");
MIME_TYPES.put("js", "application/javascript");
MIME_TYPES.put("json", "application/json");
MIME_TYPES.put("xml", "application/xml");
MIME_TYPES.put("jpg", "image/jpeg");
MIME_TYPES.put("jpeg", "image/jpeg");
MIME_TYPES.put("png", "image/png");
MIME_TYPES.put("gif", "image/gif");
MIME_TYPES.put("bmp", "image/bmp");
MIME_TYPES.put("pdf", "application/pdf");
// 添加更多扩展名和 Content-Type 对应关系
}
private final MinioConfig config;
private final SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS");
private MinioClient client;
public MinioService(MinioConfig config) {
this.config = config;
try {
client = MinioClient.builder().endpoint(config.getEndpoint())
.credentials(config.getAccessKey(), config.getSecretKey()).build();
} catch (Exception ex) {
log.error("Minio 初始化失败Trace", ex);
}
}
/**
* 根据文件扩展名获取 Content-Type 不使用MultipartFile.getContentType()是因为它基于客户端传输的content_type不一定准确.
*
* @param fileName 文件名,包含后缀
* @return MIME 类型
*/
private String getContentType(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
return MIME_TYPES.getOrDefault(extension, "application/octet-stream");
}
/**
* 文件上传.
*
* @param file 文件
* @param path minio 远程路径
* @return 公网访问 url
*/
public String upload(MultipartFile file, String path) throws Exception {
String contentType;
if (ObjectUtils.isEmpty(file.getOriginalFilename())) {
contentType = file.getContentType();
} else {
contentType = getContentType(file.getOriginalFilename());
}
return upload(file, path, contentType);
}
/**
* 文件上传.
*
* @param file 文件
* @param path minio 远程路径
* @param contentType 文件类型
* @return 公网访问 url
*/
public String upload(MultipartFile file, String path, String contentType) throws Exception {
String filename = genPath(path, getExtension(file));
return upload(file.getInputStream(), filename, contentType);
}
/**
* 文件上传.
*
* @param content 文件内容
* @param fileExtension 后缀名
* @return 内网访问 url
*/
public String upload(String content, String fileExtension) throws Exception {
// 转换字符串为输入流
ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
// 构建对象名称
String objectName = config.getApplicationName() + "/" + format.format(new Date()) + "." + fileExtension;
return upload(inputStream, objectName, "text/plain");
}
/**
* 文件上传.
*
* @param inputStream inputStream
* @param objectName 文件名
* @param contentType 文件类型
* @return 内网访问 url
* @throws Exception e
*/
public String upload(InputStream inputStream, String objectName, String contentType) throws Exception {
try {
PutObjectArgs args = PutObjectArgs.builder().contentType(contentType).object(objectName)
.bucket(config.getBucket()).stream(inputStream, inputStream.available(), -1).build();
client.putObject(args);
} finally {
inputStream.close();
}
return config.getBucket() + "/" + objectName;
}
/**
* 根据完整 URL 来移除 minio 服务器中的对象.
*
* @param url 例:<a href="http://127.0.0.1:9000/furniture/materials/20230315/7375e567-d771-46a7-bccf-bb48886ab3d2.png">...</a>
*/
public void removeObjectByUrl(String url) {
removeObject(extractObjectName(url));
}
/**
* 根据对象名移除 minio 服务器中的对象.
*
* @param name 例materials/20230315/7375e567-d771-46a7-bccf-bb48886ab3d2.png
*/
public void removeObject(String name) {
try {
RemoveObjectArgs args = RemoveObjectArgs.builder().bucket(config.getBucket()).object(name).build();
client.removeObject(args);
} catch (Exception e) {
log.error("MinIO 文件删除错误", e);
}
}
/**
* 获取对象文件公网访问 url.
*
* @param filename 对象名称
* @return 公网访问 url
*/
public String minioPublicUrl(String filename) {
return config.getBucket() + "/" + filename;
}
/**
* 生成对象路径.
*
* @param basicPath 根路径
* @param extension 文件扩展名
* @return basicPath/yyyyMMdd/uuid.extension
*/
public String genPath(String basicPath, String extension) {
return "iot/" + basicPath + extension;
}
/**
* 获取文件的扩展名.
*
* @param file 文件
* @return 例:.jpg
*/
public String getExtension(MultipartFile file) {
return getExtension(Objects.requireNonNull(file.getOriginalFilename()));
}
/**
* 获取文件的扩展名.
*
* @param name 文件名称
* @return 例:.jpg
*/
public String getExtension(String name) {
return name.substring(name.lastIndexOf('.'));
}
/**
* 下载文件,用于受保护的 minio 服务器 此方法未测试.
*/
public String download(String object) {
GetObjectArgs args = GetObjectArgs.builder().bucket(config.getBucket()).object(object).build();
try {
// 读取对象内容
try (InputStream inputStream = client.getObject(args)) {
// Read the input stream and print to the console till EOF.
byte[] buf = new byte[16384];
int bytesRead;
StringBuilder content = new StringBuilder();
while ((bytesRead = inputStream.read(buf, 0, buf.length)) >= 0) {
String readContent = new String(buf, 0, bytesRead, StandardCharsets.UTF_8);
content.append(readContent);
}
return content.toString();
}
} catch (Exception ex) {
log.error("MinIO 文件下载错误:", ex);
throw new RuntimeException(ex.getMessage());
}
}
/**
* 根据url下载文件 此方法未测试.
*/
public String downloadByUrl(String url) {
return download(extractObjectName(url));
}
/**
* 提取 url 中的 minio 对象路径.
*
* @param url 例:<a href="http://127.0.0.1:9000/furniture/materials/20230315/7375e567-d771-46a7-bccf-bb48886ab3d2.png">...</a>
* @return 例:/materials/20230315/7375e567-d771-46a7-bccf-bb48886ab3d2.png
*/
public String extractObjectName(String url) {
return url.substring(minioPublicUrl("").length());
}
/**
* 将一个对象拷贝到指定路径.
*
* @param objectName minio 中需要拷贝的对象
* @param path minio 要拷贝到的路径
* @return 拷贝后路径的公网访问 url
*/
public String copy(String objectName, String path) {
String extension = getExtension(objectName);
String targetObject = genPath(path, extension);
CopyObjectArgs args = CopyObjectArgs.builder().bucket(config.getBucket()).object(targetObject)
.source(CopySource.builder().bucket(config.getBucket()).object(objectName).build()).build();
try {
client.copyObject(args);
} catch (Exception ex) {
log.error("minIO 文件拷贝失败:", ex);
throw new RuntimeException(ex.getMessage());
}
return minioPublicUrl(targetObject);
}
/**
* 通过 url 拷贝.
*
* @see MinioService#copy(String, String)
*/
public String copyByUrl(String url, String path) {
return copy(extractObjectName(url), path);
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yelink.minio.configure.MinioAutoconfigure

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yelink.example</groupId>
<artifactId>public-common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>yelink-redis-starter</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,22 @@
package com.yelink.common.redis.annotation;
import com.yelink.common.redis.configure.RedisAutoConfigure;
import org.springframework.context.annotation.Import;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* EnableYtLettuceRedis.
* @author cwp
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RedisAutoConfigure.class)
public @interface EnableYtLettuceRedis {
}

View File

@@ -0,0 +1,74 @@
package com.yelink.common.redis.configure;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.yelink.common.redis.service.RedisService;
import io.micrometer.core.instrument.util.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Lettuce Redis配置.
*
* @author cwp
*/
@Configuration
public class RedisAutoConfigure {
@Bean(name = "redisTemplate")
@ConditionalOnClass(RedisOperations.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
mapper.registerModule(new JavaTimeModule());
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
@ConditionalOnBean(name = "redisTemplate")
public RedisService redisService(RedisTemplate<String, Object> redisTemplate) {
return new RedisService(redisTemplate);
}
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonSingle(RedisProperties redisProperties) {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer()
.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort())
.setDatabase(redisProperties.getDatabase())
.setConnectionMinimumIdleSize(10)
.setConnectionPoolSize(10);
if (StringUtils.isNotEmpty(redisProperties.getPassword())) {
singleServerConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
}

View File

@@ -0,0 +1,561 @@
package com.yelink.common.redis.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 定义常用的 Redis操作.
*
* @author cwp
*/
@Slf4j
@RequiredArgsConstructor
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 指定缓存失效时间.
*
* @param key 键
* @param time 时间(秒)
* @return Boolean
*/
public Boolean expire(String key, Long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 根据key获取过期时间.
*
* @param key 键 不能为 null
* @return 时间(秒) 返回 0代表为永久有效
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断 key是否存在.
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 删除缓存.
*
* @param key 可以传一个值 或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
/**
* 普通缓存获取.
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入.
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public Boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 普通缓存放入并设置时间.
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public Boolean set(String key, Object value, Long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 递增.
*
* @param key 键
* @param delta 要增加几(大于0)
* @return Long
*/
public Long incr(String key, Long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减.
*
* @param key 键
* @param delta 要减少几
* @return Long
*/
public Long decr(String key, Long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* HashGet.
*
* @param key 键 不能为 null
* @param item 项 不能为 null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取 hashKey对应的所有键值.
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet.
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public Boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* HashSet 并设置时间.
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public Boolean hmset(String key, Map<String, Object> map, Long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建.
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public Boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建.
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public Boolean hset(String key, String item, Object value, Long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 删除hash表中的值.
*
* @param key 键 不能为 null
* @param item 项 可以使多个不能为 null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值.
*
* @param key 键 不能为 null
* @param item 项 不能为 null
* @return true 存在 false不存在
*/
public Boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回.
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return Double
*/
public Double hincr(String key, String item, Double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减.
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return Double
*/
public Double hdecr(String key, String item, Double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
/**
* 根据 key获取 Set中的所有值.
*
* @param key 键
* @return Set
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error("error", e);
return null;
}
}
/**
* 根据value从一个set中查询,是否存在.
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public Boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 将数据放入set缓存.
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public Long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
log.error("error", e);
return 0L;
}
}
/**
* 将set数据放入缓存..
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public Long sSetAndTime(String key, Long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
log.error("error", e);
return 0L;
}
}
/**
* 获取set缓存的长度.
*
* @param key 键
* @return Long
*/
public Long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
log.error("error", e);
return 0L;
}
}
/**
* 移除值为value的.
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public Long setRemove(String key, Object... values) {
try {
return redisTemplate.opsForSet().remove(key, values);
} catch (Exception e) {
log.error("error", e);
return 0L;
}
}
/**
* 获取list缓存的内容.
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return List
*/
public List<Object> lGet(String key, Long start, Long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error("error", e);
return null;
}
}
/**
* 获取list缓存的长度.
*
* @param key 键
* @return Long
*/
public Long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
log.error("error", e);
return 0L;
}
}
/**
* 通过索引 获取list中的值.
*
* @param key 键
* @param index 索引 index>=0时 0 表头1 第二个元素,依次类推; index<0时-1表尾-2倒数第二个元素依次类推
* @return Object
*/
public Object lGetIndex(String key, Long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
log.error("error", e);
return null;
}
}
/**
* 将list放入缓存.
*
* @param key 键
* @param value 值
* @return Boolean
*/
public Boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 将list放入缓存.
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return Boolean
*/
public Boolean lSet(String key, Object value, Long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 将list放入缓存.
*
* @param key 键
* @param value 值
* @return Boolean
*/
public Boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 将list放入缓存.
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return Boolean
*/
public Boolean lSet(String key, List<Object> value, Long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 根据索引修改list中的某条数据.
*
* @param key 键
* @param index 索引
* @param value 值
* @return Boolean
*/
public Boolean lUpdateIndex(String key, Long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
log.error("error", e);
return false;
}
}
/**
* 移除N个值为value.
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public Long lRemove(String key, Long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
log.error("error", e);
return 0L;
}
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yelink.common.redis.configure.RedisAutoConfigure

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.yelink.example</groupId>
<artifactId>public-common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yelink-security-starter</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,22 @@
package com.yelink.security.annotation;
import com.yelink.security.configure.AppResourceServerConfigure;
import org.springframework.context.annotation.Import;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 启用该模块,需要再启动类增加@AppEnableResourceServer.
* @author cwp
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AppResourceServerConfigure.class)
public @interface AppEnableResourceServer {
}

View File

@@ -0,0 +1,108 @@
package com.yelink.security.configure;
import com.yelink.security.expression.WebSecurityExpressionHandler;
import com.yelink.security.handler.AccessDeniedHandler;
import com.yelink.security.handler.AuthExceptionEntryPoint;
import com.yelink.security.properties.EdgeSecurityProperties;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
/**
* @author cwp
*/
@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@EnableConfigurationProperties({EdgeSecurityProperties.class})
@EnableAutoConfiguration(exclude = UserDetailsServiceAutoConfiguration.class)
public class AppResourceServerConfigure extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
private EdgeSecurityProperties properties;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
simpleAuthorityMapper.setPrefix("");
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(buildSessionRegistry());
}
@Bean
protected SessionRegistry buildSessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
// 若enable为false则所有请求都允许访问
if (!properties.getEnable()) {
http.cors().and()
.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll();
return;
}
String[] anonUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(properties.getAnonUris(), ",");
if (ArrayUtils.isEmpty(anonUrls)) {
anonUrls = new String[]{};
}
http
.cors().and()
.csrf().disable()
.antMatcher("/**").authorizeRequests()
.antMatchers(anonUrls).permitAll()
.antMatchers("/**").access("hasAkAuth()")
.anyRequest().authenticated();
http.authorizeRequests().expressionHandler(new WebSecurityExpressionHandler());
}
@Bean
@ConditionalOnMissingBean(name = "accessDeniedHandler")
public AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler();
}
@Override
@Bean
@ConditionalOnMissingBean(name = "authenticationEntryPoint")
public AuthExceptionEntryPoint authenticationEntryPoint() {
return new AuthExceptionEntryPoint();
}
}

View File

@@ -0,0 +1,24 @@
package com.yelink.security.expression;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
public class WebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private String defaultRolePrefix = "ROLE_";
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, FilterInvocation fi) {
WebSecurityHeaderExpression root = new WebSecurityHeaderExpression(authentication, fi);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(this.defaultRolePrefix);
return root;
}
}

View File

@@ -0,0 +1,42 @@
package com.yelink.security.expression;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;
import org.springframework.util.Base64Utils;
/**
* @author cwp
*/
public class WebSecurityHeaderExpression extends WebSecurityExpressionRoot {
private Authentication a;
private static final String HEADER_ACCESS_KEY_ID = "AccessKeyId";
private static final String HEADER_GATEWAY_TOKEN = "GatewayToken";
private static final String GATEWAY_TOKEN_VALUE = "yt:gateway:123456";
public WebSecurityHeaderExpression(Authentication a, FilterInvocation fi) {
super(a, fi);
this.a = a;
}
public boolean hasAkAuth() {
boolean hasAk = request.getHeader(HEADER_ACCESS_KEY_ID) != null ||
null != request.getHeader(HEADER_ACCESS_KEY_ID.toLowerCase());
boolean hasAccessGateway = false;
String headerGatewayToken = request.getHeader(HEADER_GATEWAY_TOKEN);
if (StringUtils.isNotEmpty(headerGatewayToken)) {
String gatewayToken = new String(Base64Utils.encode(GATEWAY_TOKEN_VALUE.getBytes()));
hasAccessGateway = StringUtils.equals(gatewayToken, headerGatewayToken);
}
return hasAuth() || (hasAk && hasAccessGateway);
}
private boolean hasAuth() {
return !"anonymousUser".equals(a.getPrincipal()) && a.isAuthenticated();
}
}

View File

@@ -0,0 +1,31 @@
package com.yelink.security.handler;
import com.yelink.security.utils.ResponseUtil;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author cwp
*/
public class AccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
Map<String, Object> error = new HashMap<>();
error.put("code", 10407);
error.put("message", "没有权限访问该资源");
ResponseUtil.makeResponse(response, MediaType.APPLICATION_JSON_VALUE,
HttpServletResponse.SC_FORBIDDEN, error);
}
}

View File

@@ -0,0 +1,31 @@
package com.yelink.security.handler;
import com.yelink.security.utils.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author cwp
*/
@Slf4j
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
int status = HttpServletResponse.SC_UNAUTHORIZED;
Map<String, Object> error = new HashMap<>();
error.put("code", 10007);
error.put("message", "无效的token");
ResponseUtil.makeResponse(response, MediaType.APPLICATION_JSON_VALUE, status, error);
}
}

View File

@@ -0,0 +1,28 @@
package com.yelink.security.properties;
import com.yelink.security.utils.EndpointConstant;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author cwp
*/
@ConfigurationProperties(prefix = "app.security")
@Data
public class EdgeSecurityProperties {
/**
* 是否开启安全配置
*/
private Boolean enable = true;
/**
* 配置需要认证的uri默认为所有/**
*/
private String authUri = EndpointConstant.ALL;
/**
* 免认证资源路径,支持通配符
* 多个值时使用逗号分隔
*/
private String anonUris;
}

View File

@@ -0,0 +1,8 @@
package com.yelink.security.utils;
/**
* @author cwp
*/
public class EndpointConstant {
public static final String ALL = "/**";
}

View File

@@ -0,0 +1,28 @@
package com.yelink.security.utils;
import com.alibaba.fastjson.JSON;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author cwp
* @date 2024-08-21 17:59
*/
public class ResponseUtil {
/**
* 设置响应
*
* @param response HttpServletResponse
* @param contentType content-type
* @param status http状态码
* @param value 响应内容
* @throws IOException IOException
*/
public static void makeResponse(HttpServletResponse response, String contentType,
int status, Object value) throws IOException {
response.setContentType(contentType);
response.setStatus(status);
response.getOutputStream().write(JSON.toJSONString(value).getBytes());
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yelink.security.configure.AppResourceServerConfigure