This commit is contained in:
Eric
2026-02-09 11:24:51 +08:00
commit f2173a9fa9
491 changed files with 43791 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
<?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>
<artifactId>oauth2-login-sdk</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>org.lingniu</groupId>
<packaging>jar</packaging>
<name>OAuth2 Login SDK</name>
<description>OAuth2登录SDK后端Java版本</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-security.version>6.5.7</spring-security.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>3.5.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.5.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>11.0.15</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.5.10</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.20.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.20.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.60</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.19.4</version>
<scope>compile</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>10.0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
<version>4.1.130.Final</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
<distribution>repo</distribution>
</license>
</licenses>
</project>

View File

@@ -0,0 +1,271 @@
package org.lingniu.sdk.common.redis;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* spring redis 工具类
*
* @author portal
**/
public class RedisCache
{
public final RedisTemplate redisTemplate;
public RedisCache(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
/**
* 缓存基本的对象Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
public boolean deleteCacheSetValue(final String key, final String hKey)
{
return redisTemplate.opsForSet().remove(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
public void setCacheSet(String key, String value) {
redisTemplate.opsForSet().add(key,value);
}
}

View File

@@ -0,0 +1,53 @@
package org.lingniu.sdk.common.serializer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import org.lingniu.sdk.constant.Constants;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
* Redis使用FastJson序列化
*
* @author portal
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
private Class<T> clazz;
public FastJson2JsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
}
}

View File

@@ -0,0 +1,97 @@
package org.lingniu.sdk.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;
@Configuration
public class JacksonConfiguration {
/**
* 默认日期时间格式
*/
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* 默认日期格式
*/
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
/**
* 默认时区
*/
public static final String DEFAULT_TIME_ZONE = "Asia/Shanghai";
@Bean
@Primary
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 配置序列化
objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// 配置反序列化
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
// 配置时区和日期格式
objectMapper.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIME_ZONE));
objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT));
// 注册JavaTimeModule
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addSerializer(LocalDate.class,
new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
javaTimeModule.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
/**
* 定制器方式Spring Boot推荐
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// 序列化配置
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.failOnEmptyBeans(false);
builder.failOnUnknownProperties(false);
// 日期格式配置
builder.timeZone(TimeZone.getTimeZone(DEFAULT_TIME_ZONE));
builder.simpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
// 特性配置
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
builder.featuresToEnable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
};
}
}

View File

@@ -0,0 +1,68 @@
package org.lingniu.sdk.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.lingniu.sdk.common.redis.RedisCache;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module;
@Configuration("SdkRedisConfig")
public class SdkRedisConfig {
@Bean
@ConditionalOnMissingBean(name = "sdkRedisTemplate")
@Lazy
public RedisTemplate<String, Object> sdkRedisTemplate(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper) {
// 配置 ObjectMapper 以支持多态类型
ObjectMapper redisObjectMapper = objectMapper.copy();
// 启用默认类型信息,用于反序列化
redisObjectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
// 注册 Spring Security 和 OAuth2 模块
redisObjectMapper.registerModules(
SecurityJackson2Modules.getModules(getClass().getClassLoader())
);
redisObjectMapper.registerModule(new OAuth2ClientJackson2Module());
// 创建 RedisTemplate
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 使用 GenericJackson2JsonRedisSerializer 来序列化和反序列化redis的value值
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(redisObjectMapper);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
@Bean
@ConditionalOnMissingBean(name="redisCache")
public RedisCache redisCache(@Qualifier("redisTemplate")RedisTemplate redisTemplate){
return new RedisCache(redisTemplate);
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2020-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lingniu.sdk.config;
import org.lingniu.sdk.filter.IdpAuthenticationFilter;
import org.lingniu.sdk.handler.LoginSuccessHandler;
import org.lingniu.sdk.handler.LogoutIdpSuccessHandler;
import org.lingniu.sdk.handler.RedirectHandler;
import org.lingniu.sdk.service.RedisOAuth2AuthorizedClientService;
import org.lingniu.sdk.service.TokenService;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* @author Joe Grandja
* @author Dmitriy Dubson
* @author Steve Riesenberg
* @since 0.0.1
*/
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableConfigurationProperties({OAuth2ClientProperties.class})
@Configuration
public class SecurityConfig {
private final LoginSuccessHandler loginSuccessHandler;
private final OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository;
private final RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService;
private final TokenService tokenService;
private final LogoutIdpSuccessHandler logoutIdpSuccessHandler;
private final RedirectHandler redirectHandler;
public SecurityConfig(LoginSuccessHandler loginSuccessHandler, OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository, RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService, TokenService tokenService, LogoutIdpSuccessHandler logoutIdpSuccessHandler, RedirectHandler redirectHandler) {
this.loginSuccessHandler = loginSuccessHandler;
this.oAuth2AuthorizedClientRepository = oAuth2AuthorizedClientRepository;
this.redisOAuth2AuthorizedClientService = redisOAuth2AuthorizedClientService;
this.tokenService = tokenService;
this.logoutIdpSuccessHandler = logoutIdpSuccessHandler;
this.redirectHandler = redirectHandler;
}
// @formatter:off
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtDecoder jwtDecoder) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize ->
authorize
.anyRequest().authenticated()
)
.oauth2Login(oauth2->
oauth2.authorizationEndpoint(authorization ->authorization.authorizationRedirectStrategy(redirectHandler))
.successHandler(loginSuccessHandler)
.authorizedClientRepository(oAuth2AuthorizedClientRepository)
.authorizedClientService(redisOAuth2AuthorizedClientService)
)
.exceptionHandling(withDefaults())
.addFilterBefore(new IdpAuthenticationFilter(tokenService),UsernamePasswordAuthenticationFilter.class)
.oauth2ResourceServer(resource ->resource.jwt(withDefaults()))
.logout(httpSecurityLogoutConfigurer ->
httpSecurityLogoutConfigurer
.logoutSuccessHandler(logoutIdpSuccessHandler)
);
return http.build();
}
// @formatter:on
@Bean
public JwtDecoder jwtDecoder(OAuth2ClientProperties oAuth2ClientProperties) {
RestOperations rest = new RestTemplate();
return NimbusJwtDecoder
.withJwkSetUri(oAuth2ClientProperties.getProvider().get("idp").getJwkSetUri())
.restOperations(rest)
.build();
}
}

View File

@@ -0,0 +1,24 @@
package org.lingniu.sdk.constant;
/**
* 缓存的key 常量
*
* @author portal
*/
public class CacheConstants
{
// Access Token存储: String结构
// 格式: access_token:{token}
public static final String ACCESS_TOKEN_KEY = "app_access_token:%s";
public static final String ACCESS_TOKEN_USER_KEY = "app_access_token_user:%s";
// Refresh Token存储: Hash结构
// 格式: refresh_token:{token}
public static final String REFRESH_TOKEN_KEY = "app_refresh_token:%s";
// 用户会话管理
public static final String USER_SESSIONS = "app_user_sessions:%s"; // userId -> session列表
public static final String OAUTH2_CLIENT_KEY_PREFIX = "oauth2:client:";
}

View File

@@ -0,0 +1,174 @@
package org.lingniu.sdk.constant;
import com.nimbusds.openid.connect.sdk.claims.CommonClaimsSet;
import java.util.Locale;
/**
* 通用常量信息
*
* @author portal
*/
public class Constants
{
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
public static final String GBK = "GBK";
/**
* 系统语言
*/
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
/**
* www主域
*/
public static final String WWW = "www.";
/**
* http请求
*/
public static final String HTTP = "http://";
/**
* https请求
*/
public static final String HTTPS = "https://";
/**
* 通用成功标识
*/
public static final String SUCCESS = "0";
/**
* 通用失败标识
*/
public static final String FAIL = "1";
/**
* 登录成功
*/
public static final String LOGIN_SUCCESS = "Success";
/**
* 注销
*/
public static final String LOGOUT = "Logout";
/**
* 注册
*/
public static final String REGISTER = "Register";
/**
* 登录失败
*/
public static final String LOGIN_FAIL = "Error";
/**
* 所有权限标识
*/
public static final String ALL_PERMISSION = "*:*:*";
/**
* 管理员角色权限标识
*/
public static final String SUPER_ADMIN = "admin";
/**
* 角色权限分隔符
*/
public static final String ROLE_DELIMITER = ",";
/**
* 权限标识分隔符
*/
public static final String PERMISSION_DELIMITER = ",";
/**
* 验证码有效期(分钟)
*/
public static final Integer CAPTCHA_EXPIRATION = 2;
/**
* 令牌
*/
public static final String TOKEN = "token";
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 令牌前缀
*/
public static final String LOGIN_USER_KEY = "login_user_key";
/**
* 用户ID
*/
public static final String JWT_USERID = "userid";
/**
* 用户名称
*/
public static final String JWT_USERNAME = CommonClaimsSet.SUB_CLAIM_NAME;
/**
* 用户头像
*/
public static final String JWT_AVATAR = "avatar";
/**
* 创建时间
*/
public static final String JWT_CREATED = "created";
/**
* 用户权限
*/
public static final String JWT_AUTHORITIES = "authorities";
/**
* 资源映射路径 前缀
*/
public static final String RESOURCE_PREFIX = "/profile";
/**
* RMI 远程方法调用
*/
public static final String LOOKUP_RMI = "rmi:";
/**
* LDAP 远程方法调用
*/
public static final String LOOKUP_LDAP = "ldap:";
/**
* LDAPS 远程方法调用
*/
public static final String LOOKUP_LDAPS = "ldaps:";
/**
* 自动识别json对象白名单配置仅允许解析的包名范围越小越安全
*/
public static final String[] JSON_WHITELIST_STR = { "com.portal" };
/**
* 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
*/
public static final String[] JOB_WHITELIST_STR = { "com.portal.quartz.task" };
/**
* 定时任务违规的字符
*/
public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
"org.springframework", "org.apache", "org.lingniu.idp.utils.file", "org.lingniu.idp.config", "com.portal.generator" };
}

View File

@@ -0,0 +1,81 @@
package org.lingniu.sdk.constant;
/**
* 用户常量信息
*
* @author portal
*/
public class UserConstants
{
/**
* 平台内系统用户的唯一标志
*/
public static final String SYS_USER = "SYS_USER";
/** 正常状态 */
public static final String NORMAL = "0";
/** 异常状态 */
public static final String EXCEPTION = "1";
/** 用户封禁状态 */
public static final String USER_DISABLE = "1";
/** 角色正常状态 */
public static final String ROLE_NORMAL = "0";
/** 角色封禁状态 */
public static final String ROLE_DISABLE = "1";
/** 部门正常状态 */
public static final String DEPT_NORMAL = "0";
/** 部门停用状态 */
public static final String DEPT_DISABLE = "1";
/** 字典正常状态 */
public static final String DICT_NORMAL = "0";
/** 是否为系统默认(是) */
public static final String YES = "Y";
/** 是否菜单外链(是) */
public static final String YES_FRAME = "0";
/** 是否菜单外链(否) */
public static final String NO_FRAME = "1";
/** 菜单类型(目录) */
public static final String TYPE_DIR = "M";
/** 菜单类型(菜单) */
public static final String TYPE_MENU = "C";
/** 菜单类型(按钮) */
public static final String TYPE_BUTTON = "F";
/** Layout组件标识 */
public final static String LAYOUT = "Layout";
/** ParentView组件标识 */
public final static String PARENT_VIEW = "ParentView";
/** InnerLink组件标识 */
public final static String INNER_LINK = "InnerLink";
/** 校验是否唯一的返回标识 */
public final static boolean UNIQUE = true;
public final static boolean NOT_UNIQUE = false;
/**
* 用户名长度限制
*/
public static final int USERNAME_MIN_LENGTH = 2;
public static final int USERNAME_MAX_LENGTH = 20;
/**
* 密码长度限制
*/
public static final int PASSWORD_MIN_LENGTH = 5;
public static final int PASSWORD_MAX_LENGTH = 20;
}

View File

@@ -0,0 +1,54 @@
package org.lingniu.sdk.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.lingniu.sdk.model.token.AccessTokenInfo;
import org.lingniu.sdk.service.TokenService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class IdpAuthenticationFilter extends OncePerRequestFilter {
private final TokenService tokenService;
public IdpAuthenticationFilter(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
AccessTokenInfo accessTokenInfo = null;
// 验证令牌
if (tokenService.validateAccessToken(request)) {
accessTokenInfo = tokenService.getAccessTokenInfo(request);
}else{
accessTokenInfo = tokenService.refreshToken(request, response);
}
if(accessTokenInfo!=null){
// 创建认证对象
OAuth2AuthenticationToken authentication =
new OAuth2AuthenticationToken(
tokenService.convertPrincipal(accessTokenInfo),null,accessTokenInfo.getClientRegistrationId()
);
// 设置认证信息到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// 令牌验证失败,记录日志但不中断请求
logger.error("token 验证失败", e);
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,61 @@
package org.lingniu.sdk.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.lingniu.sdk.model.base.CommonResult;
import org.lingniu.sdk.model.token.TokenInfo;
import org.lingniu.sdk.model.user.UserInfo;
import org.lingniu.sdk.service.TokenService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private final TokenService tokenService;
private final ObjectMapper objectMapper;
public LoginSuccessHandler(TokenService tokenService, ObjectMapper objectMapper) {
this.tokenService = tokenService;
this.objectMapper = objectMapper;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken)authentication;
DefaultOidcUser principal = (DefaultOidcUser)oAuth2AuthenticationToken.getPrincipal();
Map<String, Object> claims = principal.getUserInfo().getClaims();
String clientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId();
String s = objectMapper.writeValueAsString(claims);
// 生成token
TokenInfo token = tokenService.createToken(principal.getName());
token.getAccessTokenInfo().setAdditionalInfo(s);
token.getAccessTokenInfo().setClientRegistrationId(clientRegistrationId);
token.getRefreshTokenInfo().setClientRegistrationId(clientRegistrationId);
// 保存token
tokenService.storeTokenInfo(token);
// 将短token放入响应头
tokenService.setAccessTokenHeader(response,token.getAccessTokenInfo().getTokenValue());
// 设置Refresh Token到HttpOnly Cookie
tokenService.setRefreshTokenCookie(response, token);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
objectMapper.writeValue(response.getWriter(), CommonResult.success(CommonResult.success(claims)));
}
}

View File

@@ -0,0 +1,36 @@
package org.lingniu.sdk.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.lingniu.sdk.model.base.CommonResult;
import org.lingniu.sdk.service.TokenService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class LogoutIdpSuccessHandler implements LogoutSuccessHandler{
private final ObjectMapper objectMapper;
private final TokenService tokenService;
public LogoutIdpSuccessHandler(ObjectMapper objectMapper, TokenService tokenService) {
this.objectMapper = objectMapper;
this.tokenService = tokenService;
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
tokenService.clearToken(request);
tokenService.clearRefreshTokenCookie(response);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
objectMapper.writeValue(response.getWriter(), CommonResult.success("success"));
}
}

View File

@@ -0,0 +1,30 @@
package org.lingniu.sdk.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.lingniu.sdk.model.base.CommonResult;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
@Component
public class RedirectHandler implements RedirectStrategy {
private final ObjectMapper objectMapper;
public RedirectHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
objectMapper.writeValue(response.getWriter(), CommonResult.success(Map.of("redirect_url",url)));
}
}

View File

@@ -0,0 +1,78 @@
package org.lingniu.sdk.model.base;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.http.HttpStatus;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
/**
* 通用返回
*
* @param <T> 数据泛型
*/
@Data
public class CommonResult<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 错误码
*
*/
private Integer code;
/**
* 错误提示,用户可阅读
*
*/
private String msg;
/**
* 返回数据
*/
private T data;
/**
* 将传入的 result 对象,转换成另外一个泛型结果的对象
*
* 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
*
* @param result 传入的 result 对象
* @param <T> 返回的泛型
* @return 新的 CommonResult 对象
*/
public static <T> CommonResult<T> error(CommonResult<?> result) {
return error(result.getCode(), result.getMsg());
}
public static <T> CommonResult<T> error(Integer code, String message) {
CommonResult<T> result = new CommonResult<>();
result.code = code;
result.msg = message;
return result;
}
public static <T> CommonResult<T> success(T data) {
CommonResult<T> result = new CommonResult<>();
result.code = HttpStatus.OK.value();
result.data = data;
result.msg = "";
return result;
}
public static boolean isSuccess(Integer code) {
return Objects.equals(code, HttpStatus.OK.value());
}
@JsonIgnore // 避免 jackson 序列化
public boolean isSuccess() {
return isSuccess(code);
}
@JsonIgnore // 避免 jackson 序列化
public boolean isError() {
return !isSuccess();
}
}

View File

@@ -0,0 +1,152 @@
package org.lingniu.sdk.model.token;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
/**
* Access Token 信息
* 存储在 Redis String 结构中
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AccessTokenInfo {
private String tokenValue;
/**
* 用户名
*/
private String username;
/**
* 颁发时间
*/
private Instant issuedAt;
/**
* 过期时间
*/
private Instant expiresAt;
/**
* 关联的刷新Token ID
*/
private String refreshTokenId;
/**
* JWT ID如果是JWT token
*/
private String jti;
/**
* 附加数据JSON格式
*/
private String additionalInfo;
private String clientRegistrationId;
/**
* 检查Token是否过期
*/
public boolean isExpired() {
return expiresAt != null && Instant.now().isAfter(expiresAt);
}
/**
* 检查Token是否有效
*/
public boolean isValid() {
return !isExpired();
}
/**
* 获取剩余有效时间(秒)
*/
public long getRemainingSeconds() {
if (expiresAt == null) {
return 0;
}
Instant now = Instant.now();
if (now.isAfter(expiresAt)) {
return 0;
}
return expiresAt.getEpochSecond() - now.getEpochSecond();
}
/**
* 获取Token使用时长
*/
public long getUsedSeconds() {
if (issuedAt == null) {
return 0;
}
Instant end = Instant.now();
return end.getEpochSecond() - issuedAt.getEpochSecond();
}
/**
* 转换为Map便于Redis存储
*/
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("username", username);
map.put("tokenValue",tokenValue);
map.put("issuedAt", issuedAt != null ? issuedAt.toString() : null);
map.put("expiresAt", expiresAt != null ? expiresAt.toString() : null);
map.put("refreshTokenId", refreshTokenId);
map.put("jti", jti);
map.put("clientRegistrationId",clientRegistrationId);
map.put("additionalInfo", additionalInfo);
return map;
}
/**
* 从Map创建AccessTokenInfo
*/
public static AccessTokenInfo fromMap(Map<String, Object> map) {
if (map == null || map.isEmpty()) {
return null;
}
AccessTokenInfo.AccessTokenInfoBuilder builder = AccessTokenInfo.builder();
builder.username((String) map.get("username"));
builder.tokenValue((String) map.get("tokenValue"));
// 处理时间字段
String issuedAtStr = (String) map.get("issuedAt");
if (issuedAtStr != null) {
builder.issuedAt(Instant.parse(issuedAtStr));
}
String expiresAtStr = (String) map.get("expiresAt");
if (expiresAtStr != null) {
builder.expiresAt(Instant.parse(expiresAtStr));
}
builder.refreshTokenId((String) map.get("refreshTokenId"));
builder.jti((String) map.get("jti"));
builder.clientRegistrationId((String) map.get("clientRegistrationId"));
builder.additionalInfo((String) map.get("additionalInfo"));
return builder.build();
}
/**
* 简化的用户信息(用于接口返回)
*/
public Map<String, Object> toSimpleInfo() {
Map<String, Object> info = new HashMap<>();
info.put("username", username);
info.put("expiresAt", expiresAt != null ? expiresAt.toEpochMilli() : null);
info.put("issuedAt", issuedAt != null ? issuedAt.toEpochMilli() : null);
info.put("valid", isValid());
return info;
}
}

View File

@@ -0,0 +1,226 @@
package org.lingniu.sdk.model.token;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
/**
* Refresh Token 信息
* 存储在 Redis Hash 结构中
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RefreshTokenInfo {
private String tokenValue;
/**
* 用户名
*/
private String username;
/**
* 创建时间
*/
private Instant createdAt;
/**
* 最后使用时间
*/
private Instant lastUsedAt;
/**
* 过期时间
*/
private Instant expiresAt;
/**
* 对应accessToken
*/
private String accessToken;
/**
* 关联的Access Token数量用于统计
*/
private int accessTokenCount;
/**
* 使用次数
*/
private int usageCount;
/**
* 附加数据JSON格式
*/
private String additionalInfo;
private String clientRegistrationId;
/**
* 检查Refresh Token是否过期
*/
public boolean isExpired() {
return expiresAt != null && Instant.now().isAfter(expiresAt);
}
/**
* 检查Refresh Token是否有效
*/
public boolean isValid() {
return !isExpired();
}
/**
* 获取剩余有效时间(秒)
*/
public long getRemainingSeconds() {
if (expiresAt == null) {
return 0;
}
Instant now = Instant.now();
if (now.isAfter(expiresAt)) {
return 0;
}
return expiresAt.getEpochSecond() - now.getEpochSecond();
}
/**
* 获取活跃天数(创建到现在)
*/
public long getActiveDays() {
if (createdAt == null) {
return 0;
}
Instant end = Instant.now();
long seconds = end.getEpochSecond() - createdAt.getEpochSecond();
return seconds / (24 * 3600);
}
/**
* 获取闲置天数(最后使用到现在)
*/
public long getIdleDays() {
if (lastUsedAt == null) {
return getActiveDays();
}
Instant now = Instant.now();
long seconds = now.getEpochSecond() - lastUsedAt.getEpochSecond();
return seconds / (24 * 3600);
}
/**
* 增加使用计数
*/
public void incrementUsage() {
this.usageCount++;
this.lastUsedAt = Instant.now();
}
public void incrementAccessTokenUsage(String accessToken) {
this.accessTokenCount++;
this.accessToken = accessToken;
}
/**
* 转换为Map便于Redis存储
*/
public Map<String, String> toMap() {
Map<String, String> hash = new HashMap<>();
hash.put("username", username != null ? username : "");
hash.put("tokenValue", tokenValue != null ? tokenValue : "");
hash.put("accessToken", accessToken != null ? accessToken : "");
hash.put("createdAt", createdAt != null ? createdAt.toString() : "");
hash.put("lastUsedAt", lastUsedAt != null ? lastUsedAt.toString() : "");
hash.put("expiresAt", expiresAt != null ? expiresAt.toString() : "");
hash.put("accessTokenCount", Integer.toString(accessTokenCount));
hash.put("usageCount", Integer.toString(usageCount));
hash.put("clientRegistrationId", clientRegistrationId);
hash.put("additionalInfo", additionalInfo != null ? additionalInfo : "");
return hash;
}
public Map<String, String> toUpdateMap() {
Map<String, String> hash = new HashMap<>();
hash.put("lastUsedAt", lastUsedAt != null ? lastUsedAt.toString() : "");
hash.put("accessTokenCount", Integer.toString(accessTokenCount));
hash.put("accessToken", accessToken != null ? accessToken : "");
hash.put("usageCount", Integer.toString(usageCount));
return hash;
}
/**
* 从Redis Hash创建RefreshTokenInfo
*/
public static RefreshTokenInfo fromMap(Map<String, Object> hash) {
if (hash == null || hash.isEmpty()) {
return null;
}
RefreshTokenInfoBuilder builder = RefreshTokenInfo.builder();
builder.username((String) hash.getOrDefault("username", ""));
builder.accessToken((String) hash.getOrDefault("accessToken", ""));
builder.tokenValue((String) hash.getOrDefault("tokenValue", ""));
builder.clientRegistrationId((String) hash.getOrDefault("clientRegistrationId", ""));
// 处理时间字段
String createdAtStr = (String)hash.get("createdAt");
if (createdAtStr != null && !createdAtStr.isEmpty()) {
try {
builder.createdAt(Instant.parse(createdAtStr));
} catch (Exception e) {
// 解析失败,使用当前时间
builder.createdAt(Instant.now());
}
}
String lastUsedAtStr = (String)hash.get("lastUsedAt");
if (lastUsedAtStr != null && !lastUsedAtStr.isEmpty()) {
try {
builder.lastUsedAt(Instant.parse(lastUsedAtStr));
} catch (Exception e) {
// 解析失败,忽略
}
}
String expiresAtStr = (String)hash.get("expiresAt");
if (expiresAtStr != null && !expiresAtStr.isEmpty()) {
try {
builder.expiresAt(Instant.parse(expiresAtStr));
} catch (Exception e) {
// 解析失败,忽略
}
}
// 处理数值字段
String accessTokenCountStr = (String)hash.get("accessTokenCount");
if (accessTokenCountStr != null && !accessTokenCountStr.isEmpty()) {
try {
builder.accessTokenCount(Integer.parseInt(accessTokenCountStr));
} catch (NumberFormatException e) {
builder.accessTokenCount(0);
}
}
String usageCountStr = (String)hash.get("usageCount");
if (usageCountStr != null && !usageCountStr.isEmpty()) {
try {
builder.usageCount(Integer.parseInt(usageCountStr));
} catch (NumberFormatException e) {
builder.usageCount(0);
}
}
return builder.build();
}
}

View File

@@ -0,0 +1,22 @@
package org.lingniu.sdk.model.token;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
// TokenInfo.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private AccessTokenInfo accessTokenInfo;
private RefreshTokenInfo refreshTokenInfo;
}

View File

@@ -0,0 +1,23 @@
package org.lingniu.sdk.model.user;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
public class DataPermission {
/** 允许全部*/
private boolean allowAll;
/**仅自己*/
private boolean onlySelf;
/**部门列表*/
private Set<String> deptList;
/**地区*/
private Set<String> areas;
}

View File

@@ -0,0 +1,36 @@
package org.lingniu.sdk.model.user;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
public class UserDept implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 部门ID */
private Long deptId;
/** 父部门ID */
private Long parentId;
/** 祖级列表 */
private String ancestors;
/** 部门名称 */
private String deptName;
/** 显示顺序 */
private Integer orderNum;
/** 负责人 */
private String leader;
/** 部门状态:0正常,1停用 */
private String status;
}

View File

@@ -0,0 +1,78 @@
package org.lingniu.sdk.model.user;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
public class UserInfo implements OAuth2User, Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* userid
*/
private Long userId;
/**
* 用户账号
*/
private String username;
/**
* 用户名
*/
private String nickName;
/**
* 性别
*/
private String sex;
/**
* 当前部门
*/
private Long currentDeptId;
/**
* 用户部门列表
*/
private List<UserDept> userDepts;
/**
* 用户岗位列表
*/
private List<UserPost> userPosts;
/**
* 用户数据权限
*/
private DataPermission dataPermission;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 角色列表
*/
private Set<String> roles;
@Override
public String getName() {
return username;
}
@Override
public Map<String, Object> getAttributes() {
return Map.of();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
}

View File

@@ -0,0 +1,31 @@
package org.lingniu.sdk.model.user;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
public class UserPost implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 岗位序号 */
private Long postId;
/** 岗位编码 */
private String postCode;
/** 岗位名称 */
private String postName;
/** 岗位排序 */
private Integer postSort;
/** 状态0正常 1停用 */
private String status;
}

View File

@@ -0,0 +1,89 @@
package org.lingniu.sdk.service;
import lombok.extern.slf4j.Slf4j;
import org.lingniu.sdk.common.redis.RedisCache;
import org.lingniu.sdk.constant.CacheConstants;
import org.lingniu.sdk.model.token.AccessTokenInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Map;
@Component
@Slf4j
public class RedisAccessTokenService {
@Autowired
private RedisCache redisCache;
private final long ACCESS_TOKEN_EXPIRE = 3600; // 1小时
/**
* 存储Access Token到Redis
*/
public void storeAccessToken(AccessTokenInfo tokenInfo) {
String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, tokenInfo.getTokenValue());
try {
redisCache.setCacheMap(key,tokenInfo.toMap());
Instant expiresAt = tokenInfo.getExpiresAt();
long expire = ACCESS_TOKEN_EXPIRE;
if(expiresAt!=null){
expire = expiresAt.getEpochSecond() - Instant.now().getEpochSecond();
}
redisCache.expire(key,expire);
} catch (Exception e) {
log.error("存储Access Token失败", e);
}
}
/**
* 验证Access Token
*/
public boolean validateAccessToken(String token) {
String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token);
if(!redisCache.hasKey(key)){
return false;
}
AccessTokenInfo accessTokenInfo = getAccessTokenInfo(token);
if(accessTokenInfo==null){
return false;
}
return accessTokenInfo.isValid();
}
/**
* 删除Access Token
*/
public boolean removeAccessToken(String token) {
String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token);
return redisCache.deleteObject(key);
}
/**
* 获取Access Token信息
*/
public AccessTokenInfo getAccessTokenInfo(String token) {
String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, token);
Map<String, Object> cacheMap = redisCache.getCacheMap(key);
if(cacheMap!=null){
return AccessTokenInfo.fromMap(cacheMap);
}
return null;
}
/**
* 作废 删除
* @param tokenInfo
*/
public void revokeAccessToken(AccessTokenInfo tokenInfo) {
if(tokenInfo==null){
return;
}
String key = String.format(CacheConstants.ACCESS_TOKEN_KEY, tokenInfo.getTokenValue());
redisCache.deleteObject(key);
}
}

View File

@@ -0,0 +1,248 @@
package org.lingniu.sdk.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.lingniu.sdk.constant.CacheConstants;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Component
public class RedisOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository {
private final RedisTemplate<String, Object> redisTemplate;
private final ClientRegistrationRepository clientRegistrationRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
public RedisOAuth2AuthorizedClientRepository(
RedisTemplate<String, Object> redisTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
this.redisTemplate = redisTemplate;
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(
String clientRegistrationId,
Authentication principal,
HttpServletRequest request) {
if (principal == null || !principal.isAuthenticated()) {
return null;
}
String principalName = principal.getName();
String key = buildClientKey(principalName, clientRegistrationId);
try {
Object data = redisTemplate.opsForValue().get(key);
if (data == null) {
return null;
}
// 反序列化
Map<String, Object> clientData = objectMapper.convertValue(data, new TypeReference<Map<String, Object>>() {});
// 重建 ClientRegistration
ClientRegistration clientRegistration = clientRegistrationRepository
.findByRegistrationId(clientRegistrationId);
if (clientRegistration == null) {
return null;
}
// 重建 OAuth2AccessToken
OAuth2AccessToken accessToken = rebuildAccessToken(
(Map<String, Object>) clientData.get("accessToken")
);
// 重建 OAuth2RefreshToken如果有
OAuth2RefreshToken refreshToken = null;
if (clientData.containsKey("refreshToken")) {
refreshToken = rebuildRefreshToken(
(Map<String, Object>) clientData.get("refreshToken")
);
}
// 重建 OAuth2AuthorizedClient
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
clientRegistration,
principalName,
accessToken,
refreshToken
);
@SuppressWarnings("unchecked")
T result = (T) authorizedClient;
return result;
} catch (Exception e) {
// 如果反序列化失败,删除损坏的数据
redisTemplate.delete(key);
return null;
}
}
@Override
public void saveAuthorizedClient(
OAuth2AuthorizedClient authorizedClient,
Authentication principal,
HttpServletRequest request,
HttpServletResponse response) {
if (principal == null || !principal.isAuthenticated()) {
return;
}
String principalName = principal.getName();
String clientRegistrationId = authorizedClient.getClientRegistration().getRegistrationId();
String key = buildClientKey(principalName, clientRegistrationId);
try {
// 序列化 OAuth2AuthorizedClient
Map<String, Object> clientData = new HashMap<>();
// 存储 ClientRegistrationId
clientData.put("clientRegistrationId", clientRegistrationId);
clientData.put("principalName", principalName);
// 序列化 AccessToken
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
Map<String, Object> accessTokenData = new HashMap<>();
accessTokenData.put("tokenValue", accessToken.getTokenValue());
accessTokenData.put("tokenType", accessToken.getTokenType().getValue());
accessTokenData.put("issuedAt", accessToken.getIssuedAt().toString());
accessTokenData.put("expiresAt", accessToken.getExpiresAt().toString());
accessTokenData.put("scopes", new ArrayList<>(accessToken.getScopes()));
clientData.put("accessToken", accessTokenData);
// 序列化 RefreshToken如果有
OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();
if (refreshToken != null) {
Map<String, Object> refreshTokenData = new HashMap<>();
refreshTokenData.put("tokenValue", refreshToken.getTokenValue());
if (refreshToken.getIssuedAt() != null) {
refreshTokenData.put("issuedAt", refreshToken.getIssuedAt().toString());
}
if (refreshToken.getExpiresAt() != null) {
refreshTokenData.put("expiresAt", refreshToken.getExpiresAt().toString());
}
clientData.put("refreshToken", refreshTokenData);
}
// 存储到 Redis
redisTemplate.opsForValue().set(key, clientData);
// 设置过期时间(根据 AccessToken 的过期时间)
Duration expiresIn = Duration.between(Instant.now(), accessToken.getExpiresAt());
if (!expiresIn.isNegative()) {
redisTemplate.expire(key, 7, TimeUnit.DAYS);
}
} catch (Exception e) {
throw new RuntimeException("Failed to save OAuth2AuthorizedClient to Redis", e);
}
}
@Override
public void removeAuthorizedClient(
String clientRegistrationId,
Authentication principal,
HttpServletRequest request,
HttpServletResponse response) {
if (principal == null) {
return;
}
String principalName = principal.getName();
String key = buildClientKey(principalName, clientRegistrationId);
// 删除客户端数据
redisTemplate.delete(key);
}
private String buildClientKey(String principalName, String clientRegistrationId) {
return CacheConstants.OAUTH2_CLIENT_KEY_PREFIX + principalName + ":" + clientRegistrationId;
}
private OAuth2AccessToken rebuildAccessToken(Map<String, Object> data) {
String tokenValue = (String) data.get("tokenValue");
OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER;
if (data.containsKey("tokenType")) {
String typeStr = (String) data.get("tokenType");
tokenType = new OAuth2AccessToken.TokenType(typeStr);
}
Instant issuedAt = Instant.parse((String) data.get("issuedAt"));
Instant expiresAt = Instant.parse((String) data.get("expiresAt"));
@SuppressWarnings("unchecked")
Set<String> scopes = new HashSet<>((List<String>) data.get("scopes"));
return new OAuth2AccessToken(tokenType, tokenValue, issuedAt, expiresAt, scopes);
}
private OAuth2RefreshToken rebuildRefreshToken(Map<String, Object> data) {
String tokenValue = (String) data.get("tokenValue");
Instant issuedAt = data.containsKey("issuedAt") ?
Instant.parse((String) data.get("issuedAt")) : null;
Instant expiresAt = data.containsKey("expiresAt") ?
Instant.parse((String) data.get("expiresAt")) : null;
return new OAuth2RefreshToken(tokenValue, issuedAt, expiresAt);
}
/**
* 获取用户的所有客户端
*/
public List<OAuth2AuthorizedClient> findByPrincipalName(String principalName) {
String pattern = CacheConstants.OAUTH2_CLIENT_KEY_PREFIX + principalName + ":*";
Set<String> keys = redisTemplate.keys(pattern);
List<OAuth2AuthorizedClient> clients = new ArrayList<>();
for (String key : keys) {
String clientRegistrationId = extractClientRegistrationId(key);
// 这里需要 principal简化处理
// 实际使用时可能需要调整
}
return clients;
}
/**
* 清理过期的客户端
*/
public void cleanupExpiredClients() {
// 可以通过 Redis 的过期策略自动清理
// 也可以手动扫描并删除过期的 token
String pattern = CacheConstants.OAUTH2_CLIENT_KEY_PREFIX + "*";
Set<String> keys = redisTemplate.keys(pattern);
for (String key : keys) {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if (ttl != null && ttl <= 0) {
redisTemplate.delete(key);
}
}
}
private String extractClientRegistrationId(String key) {
return key.substring(key.lastIndexOf(":") + 1);
}
}

View File

@@ -0,0 +1,50 @@
package org.lingniu.sdk.service;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.stereotype.Component;
import java.util.Collections;
@Component
public class RedisOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {
private final RedisOAuth2AuthorizedClientRepository repository;
public RedisOAuth2AuthorizedClientService(RedisOAuth2AuthorizedClientRepository repository) {
this.repository = repository;
}
@Override
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(
String clientRegistrationId, String principalName) {
// 从 Redis 加载
Authentication authentication = createAuthentication(principalName);
return repository.loadAuthorizedClient(clientRegistrationId, authentication, null);
}
@Override
public void saveAuthorizedClient(
OAuth2AuthorizedClient authorizedClient, Authentication principal) {
repository.saveAuthorizedClient(authorizedClient, principal, null, null);
}
@Override
public void removeAuthorizedClient(String clientRegistrationId, String principalName) {
Authentication authentication = createAuthentication(principalName);
repository.removeAuthorizedClient(clientRegistrationId, authentication, null, null);
}
private Authentication createAuthentication(String principalName) {
return new UsernamePasswordAuthenticationToken(
principalName,
null,
Collections.emptyList()
);
}
}

View File

@@ -0,0 +1,81 @@
package org.lingniu.sdk.service;
import lombok.extern.slf4j.Slf4j;
import org.lingniu.sdk.common.redis.RedisCache;
import org.lingniu.sdk.constant.CacheConstants;
import org.lingniu.sdk.model.token.RefreshTokenInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Map;
@Component
@Slf4j
public class RedisRefreshTokenService {
@Autowired
private RedisCache redisCache;
private final long REFRESH_TOKEN_EXPIRE = 30 * 24 * 3600L; // 30天
/**
* 存储Refresh Token到Redis Hash
*/
public void storeRefreshToken(RefreshTokenInfo tokenInfo) {
if(tokenInfo==null){
return;
}
String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue());
redisCache.setCacheMap(key,tokenInfo.toMap());
Instant expiresAt = tokenInfo.getExpiresAt();
long expire = REFRESH_TOKEN_EXPIRE;
if(expiresAt!=null){
expire = expiresAt.getEpochSecond() - Instant.now().getEpochSecond();
}
redisCache.expire(key,expire);
// 维护用户会话列表
String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, tokenInfo.getUsername());
redisCache.setCacheSet(userSessionsKey,tokenInfo.getTokenValue());
redisCache.expire(userSessionsKey,expire);
}
/**
* 获取Refresh Token信息
*/
public RefreshTokenInfo getRefreshTokenInfo(String token) {
String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, token);
Map<String, Object> cacheMap = redisCache.getCacheMap(key);
if(cacheMap!=null){
return RefreshTokenInfo.fromMap(cacheMap);
}
return null;
}
/**
* 更新Refresh Token最后使用时间
*/
public void updateRefreshToken(RefreshTokenInfo tokenInfo) {
if(tokenInfo==null){
return;
}
String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue());
redisCache.setCacheMap(key,tokenInfo.toUpdateMap());
}
/**
* 作废 删除
* @param tokenInfo
*/
public void revokeRefreshToken(RefreshTokenInfo tokenInfo) {
if(tokenInfo==null){
return;
}
String key = String.format(CacheConstants.REFRESH_TOKEN_KEY, tokenInfo.getTokenValue());
String userSessionsKey = String.format(CacheConstants.USER_SESSIONS, tokenInfo.getUsername());
redisCache.deleteCacheSetValue(userSessionsKey,tokenInfo.getTokenValue());
redisCache.deleteObject(key);
}
}

View File

@@ -0,0 +1,240 @@
package org.lingniu.sdk.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.lingniu.sdk.model.token.AccessTokenInfo;
import org.lingniu.sdk.model.token.RefreshTokenInfo;
import org.lingniu.sdk.model.token.TokenInfo;
import org.lingniu.sdk.model.user.UserInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@Component
@Slf4j
public class TokenService {
private final ObjectMapper objectMapper;
// 令牌有效期默认30分钟单位分钟
@Value("${token.accessToken.expireTime:30}")
private int accessTokenExpireTime;
// 刷新令牌有效期(默认24小时) 单位小时
@Value("${token.refreshToken.expireTime:168}")
private int refreshTokenExpireTime;
@Value("${token.header:Authorization}")
private String tokenHeader;
private final RedisRefreshTokenService refreshTokenService;
private final RedisAccessTokenService accessTokenService;
private final RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService;
public TokenService(RedisRefreshTokenService refreshTokenService, RedisAccessTokenService accessTokenService, RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService, ObjectMapper objectMapper) {
this.refreshTokenService = refreshTokenService;
this.accessTokenService = accessTokenService;
this.redisOAuth2AuthorizedClientService = redisOAuth2AuthorizedClientService;
this.objectMapper = objectMapper;
}
public TokenInfo createToken(String username) throws JsonProcessingException {
String accessToken = UUID.randomUUID().toString().replace("-", "");
String refreshToken = UUID.randomUUID().toString().replace("-", "");
Instant issuedAt = Instant.now();
Instant accessExpiresAt = issuedAt.plusSeconds(accessTokenExpireTime * 60L);
Instant refreshExpiresAt = issuedAt.plus(refreshTokenExpireTime, ChronoUnit.HOURS);
AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder()
.tokenValue(accessToken)
.username(username)
.issuedAt(issuedAt)
.expiresAt(accessExpiresAt)
.refreshTokenId(refreshToken)
.build();
RefreshTokenInfo refreshTokenInfo = RefreshTokenInfo.builder()
.tokenValue(refreshToken)
.username(username)
.accessToken(accessToken)
.createdAt(issuedAt)
.lastUsedAt(refreshExpiresAt)
.accessTokenCount(1)
.usageCount(0)
.build();
return new TokenInfo(accessTokenInfo,refreshTokenInfo);
}
public void storeTokenInfo(TokenInfo tokenInfo){
accessTokenService.storeAccessToken(tokenInfo.getAccessTokenInfo());
refreshTokenService.storeRefreshToken(tokenInfo.getRefreshTokenInfo());
}
public void setAccessTokenHeader(HttpServletResponse response,String accessToken){
response.addHeader(tokenHeader,accessToken);
}
public void setRefreshTokenCookie(HttpServletResponse response, TokenInfo tokenInfo) {
String refreshToken = tokenInfo.getRefreshTokenInfo().getTokenValue();
// Cookie cookie = new Cookie("refresh_token", refreshToken);
// cookie.setHttpOnly(true);
// cookie.setSecure(true); // 生产环境设为true
// cookie.setPath("/");
// cookie.setMaxAge(refreshTokenExpireTime * 60 * 60);
// cookie.setDomain(".lingniu.com"); // 设置域名
// 添加SameSite属性
response.addHeader("Set-Cookie",
String.format("app_refresh_token=%s; HttpOnly; Secure; Path=/; Max-Age=%d; SameSite=Strict",
refreshToken, refreshTokenExpireTime * 60 * 60));
}
public String getCookieRefreshToken(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
return Arrays.stream(cookies)
.filter(cookie -> "app_refresh_token".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
public boolean validateAccessToken(HttpServletRequest request){
String accessToken = request.getHeader(tokenHeader);
return accessTokenService.validateAccessToken(accessToken);
}
public AccessTokenInfo getAccessTokenInfo(HttpServletRequest request){
String accessToken = request.getHeader(tokenHeader);
return accessTokenService.getAccessTokenInfo(accessToken);
}
public RefreshTokenInfo getRefreshTokenInfo(HttpServletRequest request){
String accessToken = getCookieRefreshToken(request);
return refreshTokenService.getRefreshTokenInfo(accessToken);
}
public AccessTokenInfo refreshToken(HttpServletRequest request,HttpServletResponse response) throws IOException {
String accessToken = request.getHeader(tokenHeader);
String cookieRefreshToken = getCookieRefreshToken(request);
RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken);
if(refreshTokenInfo == null || !refreshTokenInfo.isValid()){
log.error("token 已刷新");
return null;
}
if(refreshTokenInfo.getAccessToken()!=null && !refreshTokenInfo.getAccessToken().equals(accessToken)){
log.error("token 已刷新");
}
String clientRegistrationId = refreshTokenInfo.getClientRegistrationId();
String username = refreshTokenInfo.getUsername();
OAuth2AuthorizedClient oAuth2AuthorizedClient = redisOAuth2AuthorizedClientService.loadAuthorizedClient(clientRegistrationId, username);
if(oAuth2AuthorizedClient==null){
log.error("idp client is expire");
return null;
}
if(hasTokenExpired(oAuth2AuthorizedClient.getAccessToken())){
RefreshTokenOAuth2AuthorizedClientProvider refreshTokenOAuth2AuthorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
OAuth2AuthorizationContext oAuth2AuthorizationContext = OAuth2AuthorizationContext.withAuthorizedClient(oAuth2AuthorizedClient).principal(createAuthentication(username)).build();
oAuth2AuthorizedClient = refreshTokenOAuth2AuthorizedClientProvider.authorize(oAuth2AuthorizationContext);
redisOAuth2AuthorizedClientService.saveAuthorizedClient(oAuth2AuthorizedClient,createAuthentication(username));
}
if(oAuth2AuthorizedClient==null){
log.error("idp client is expire");
return null;
}
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
OAuth2UserRequest oAuth2UserRequest = new OAuth2UserRequest(oAuth2AuthorizedClient.getClientRegistration(),oAuth2AuthorizedClient.getAccessToken());
OAuth2User oAuth2User = defaultOAuth2UserService.loadUser(oAuth2UserRequest);
String s = objectMapper.writeValueAsString(oAuth2User.getAttributes());
String accessTokenNew = UUID.randomUUID().toString().replace("-", "");
Instant issuedAt = Instant.now();
Instant accessExpiresAt = issuedAt.plusSeconds(accessTokenExpireTime * 60L);
AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder()
.tokenValue(accessTokenNew)
.username(refreshTokenInfo.getUsername())
.username(refreshTokenInfo.getUsername())
.issuedAt(issuedAt)
.expiresAt(accessExpiresAt)
.additionalInfo(s)
.clientRegistrationId(refreshTokenInfo.getClientRegistrationId())
.refreshTokenId(refreshTokenInfo.getTokenValue())
.build();
accessTokenService.storeAccessToken(accessTokenInfo);
refreshTokenInfo.incrementUsage();
refreshTokenInfo.incrementAccessTokenUsage(accessTokenNew);
refreshTokenService.updateRefreshToken(refreshTokenInfo);
setAccessTokenHeader(response,accessTokenNew);
return accessTokenInfo;
}
public UserInfo convertPrincipal(AccessTokenInfo accessTokenInfo) throws JsonProcessingException {
return objectMapper.convertValue(objectMapper.readValue(accessTokenInfo.getAdditionalInfo(), Map.class), UserInfo.class);
}
public void revokeToken(HttpServletRequest request,HttpServletResponse response){
String accessToken = request.getHeader(tokenHeader);
String cookieRefreshToken = getCookieRefreshToken(request);
AccessTokenInfo accessTokenInfo = accessTokenService.getAccessTokenInfo(accessToken);
RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken);
Instant now = Instant.now();
if(accessTokenInfo!=null){
accessTokenService.revokeAccessToken(accessTokenInfo);
}
if(refreshTokenInfo!=null){
refreshTokenService.revokeRefreshToken(refreshTokenInfo);
}
clearRefreshTokenCookie(response);
}
public void clearToken(HttpServletRequest request){
RefreshTokenInfo refreshTokenInfo = getRefreshTokenInfo(request);
AccessTokenInfo accessTokenInfo = getAccessTokenInfo(request);
accessTokenService.revokeAccessToken(accessTokenInfo);
refreshTokenService.revokeRefreshToken(refreshTokenInfo);
if(refreshTokenInfo!=null){
AccessTokenInfo accessTokenInfo1 = accessTokenService.getAccessTokenInfo(refreshTokenInfo.getAccessToken());
accessTokenService.revokeAccessToken(accessTokenInfo1);
}
// redisOAuth2AuthorizedClientService.removeAuthorizedClient(refreshTokenInfo.getClientRegistrationId(),refreshTokenInfo.getUsername());
}
public void clearRefreshTokenCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("refresh_token", null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
private final Duration clockSkew = Duration.ofSeconds(60);
private final Clock clock = Clock.systemUTC();
private boolean hasTokenExpired(OAuth2Token token) {
return this.clock.instant().isAfter(Objects.requireNonNull(token.getExpiresAt()).minus(this.clockSkew));
}
private Authentication createAuthentication(String principalName) {
return new UsernamePasswordAuthenticationToken(
principalName,
null,
Collections.emptyList()
);
}
}

View File

@@ -0,0 +1,111 @@
package org.lingniu.sdk.utils;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class HttpClientUtils {
private static final HttpClient DEFAULT_CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.version(HttpClient.Version.HTTP_2)
.build();
// GET请求
public static String get(String url, Map<String, String> headers) throws Exception {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET();
if (headers != null) {
headers.forEach(builder::header);
}
HttpRequest request = builder.build();
HttpResponse<String> response = DEFAULT_CLIENT.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() >= 200 && response.statusCode() < 300) {
return response.body();
} else {
throw new RuntimeException("HTTP Error: " + response.statusCode());
}
}
// POST请求
public static String post(String url, String body, Map<String, String> headers)
throws Exception {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.POST(HttpRequest.BodyPublishers.ofString(body));
if (headers != null) {
headers.forEach(builder::header);
}
HttpRequest request = builder.build();
HttpResponse<String> response = DEFAULT_CLIENT.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() >= 200 && response.statusCode() < 300) {
return response.body();
} else {
throw new RuntimeException("HTTP Error: " + response.statusCode());
}
}
// POST JSON请求
public static String postJson(String url, String json) throws Exception {
return post(url, json, Map.of(
"Content-Type", "application/json",
"Accept", "application/json"
));
}
// 异步GET请求
public static CompletableFuture<String> getAsync(String url) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
return DEFAULT_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
if (response.statusCode() >= 200 && response.statusCode() < 300) {
return response.body();
} else {
throw new RuntimeException("HTTP Error: " + response.statusCode());
}
});
}
// 测试示例
public static void main(String[] args) {
try {
// 同步GET
String response = get(
"https://jsonplaceholder.typicode.com/posts/1",
Map.of("User-Agent", "Java Client")
);
System.out.println("GET Response: " + response);
// 异步GET
getAsync("https://jsonplaceholder.typicode.com/posts/2")
.thenAccept(r -> System.out.println("Async Response: " + r))
.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,41 @@
package org.lingniu.sdk.web;
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.lingniu.sdk.model.base.CommonResult;
import org.lingniu.sdk.model.user.UserInfo;
import org.lingniu.sdk.utils.HttpClientUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RequestMapping("/idp")
@RestController
public class UserController {
private final OAuth2ClientProperties oAuth2ClientProperties;
private final ObjectMapper objectMapper;
public UserController(OAuth2ClientProperties oAuth2ClientProperties, ObjectMapper objectMapper) {
this.oAuth2ClientProperties = oAuth2ClientProperties;
this.objectMapper = objectMapper;
}
@GetMapping("/routes")
public CommonResult<Object> getUserMenu(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) throws Exception {
OAuth2AccessToken.TokenType tokenType = oAuth2AuthorizedClient.getAccessToken().getTokenType();
String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue();
String s = HttpClientUtils.get(oAuth2ClientProperties.getProvider().get("idp").getUserInfoUri().replace("userinfo","idp/getRouters"),
Map.of("Authorization",tokenType.getValue() + " " + tokenValue,"Accept","application/json"));
return CommonResult.success(objectMapper.readValue(s,Map.class).get("data"));
}
}

View File

@@ -0,0 +1,5 @@
#Generated by Maven
#Sun Feb 08 09:46:13 CST 2026
groupId=org.lingniu
artifactId=oauth2-login-sdk
version=1.0-SNAPSHOT

View File

@@ -0,0 +1,30 @@
org\lingniu\sdk\model\token\AccessTokenInfo.class
org\lingniu\sdk\service\RedisRefreshTokenService.class
org\lingniu\sdk\constant\UserConstants.class
org\lingniu\sdk\model\token\TokenInfo.class
org\lingniu\sdk\common\redis\RedisCache.class
org\lingniu\sdk\model\base\CommonResult.class
org\lingniu\sdk\model\user\UserPost.class
org\lingniu\sdk\service\RedisOAuth2AuthorizedClientService.class
org\lingniu\sdk\constant\CacheConstants.class
org\lingniu\sdk\service\RedisOAuth2AuthorizedClientRepository$1.class
org\lingniu\sdk\service\RedisAccessTokenService.class
org\lingniu\sdk\model\user\DataPermission.class
org\lingniu\sdk\service\RedisOAuth2AuthorizedClientRepository.class
org\lingniu\sdk\web\UserController.class
org\lingniu\sdk\model\user\UserInfo.class
org\lingniu\sdk\utils\HttpClientUtils.class
org\lingniu\sdk\handler\LoginSuccessHandler.class
org\lingniu\sdk\model\token\TokenInfo$TokenInfoBuilder.class
org\lingniu\sdk\constant\Constants.class
org\lingniu\sdk\service\TokenService.class
org\lingniu\sdk\model\user\UserDept.class
org\lingniu\sdk\model\token\AccessTokenInfo$AccessTokenInfoBuilder.class
org\lingniu\sdk\model\token\RefreshTokenInfo$RefreshTokenInfoBuilder.class
org\lingniu\sdk\config\RedisConfig.class
org\lingniu\sdk\config\JacksonConfiguration.class
org\lingniu\sdk\filter\IdpAuthenticationFilter.class
org\lingniu\sdk\common\serializer\FastJson2JsonRedisSerializer.class
org\lingniu\sdk\handler\LogoutIdpSuccessHandler.class
org\lingniu\sdk\model\token\RefreshTokenInfo.class
org\lingniu\sdk\config\SecurityConfig.class

View File

@@ -0,0 +1,26 @@
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\constant\CacheConstants.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\DataPermission.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\TokenService.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\token\TokenInfo.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\utils\HttpClientUtils.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\UserInfo.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisOAuth2AuthorizedClientService.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\constant\Constants.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisAccessTokenService.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\config\JacksonConfiguration.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\config\RedisConfig.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\base\CommonResult.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\UserDept.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\config\SecurityConfig.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\filter\IdpAuthenticationFilter.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\user\UserPost.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\common\redis\RedisCache.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\token\RefreshTokenInfo.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\web\UserController.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisOAuth2AuthorizedClientRepository.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\handler\LogoutIdpSuccessHandler.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\service\RedisRefreshTokenService.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\constant\UserConstants.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\handler\LoginSuccessHandler.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\model\token\AccessTokenInfo.java
D:\privateProjects\lingniu-platform\backend\sdk\oauth2-login-sdk\src\main\java\org\lingniu\sdk\common\serializer\FastJson2JsonRedisSerializer.java