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

View File

@@ -0,0 +1,533 @@
## 安装
```bash
npm install unified-login-sdk --save
# 或
yarn add unified-login-sdk
```
## 快速开始
### 基本使用
```typescript
import unifiedLoginSDK from 'unified-login-sdk';
// 初始化配置
unifiedLoginSDK.init({
clientId: 'your-client-id',
authorizationEndpoint: 'https://auth.example.com/authorize',
tokenEndpoint: 'https://auth.example.com/token',
userInfoEndpoint: 'https://auth.example.com/userinfo',
redirectUri: 'https://your-app.example.com/callback',
storageType: 'localStorage',
autoRefreshToken: true,
tenantId: 'your-tenant-id' // 可选会自动添加到请求头中的tenant-id字段
});
// 登录
document.getElementById('login-btn')?.addEventListener('click', () => {
unifiedLoginSDK.login();
});
// 处理回调
if (unifiedLoginSDK.isAuthenticated()) {
// 已登录,获取用户信息
unifiedLoginSDK.getUserInfo().then(userInfo => {
console.log('User info:', userInfo);
});
} else if (unifiedLoginSDK.isCallback()) {
// 处理授权回调
unifiedLoginSDK.handleCallback().then(userInfo => {
console.log('Login successful:', userInfo);
// 跳转到首页
window.location.href = '/';
}).catch(error => {
console.error('Login failed:', error);
});
}
// 退出登录
document.getElementById('logout-btn')?.addEventListener('click', () => {
unifiedLoginSDK.logout().then(() => {
console.log('Logout successful');
window.location.href = '/login';
});
});
```
## 核心功能
### 初始化配置
```typescript
unifiedLoginSDK.init({
clientId: 'your-client-id',
clientSecret: 'your-client-secret', // 可选,某些场景下需要
authorizationEndpoint: 'https://auth.example.com/authorize',
tokenEndpoint: 'https://auth.example.com/token',
userInfoEndpoint: 'https://auth.example.com/userinfo',
redirectUri: 'https://your-app.example.com/callback',
storageType: 'localStorage', // 可选默认localStorage
autoRefreshToken: true, // 可选默认true
permissionsEndpoint: 'https://auth.example.com/permissions' // 可选,权限端点
});
```
### 登录流程
1. 调用`login()`方法跳转到授权页面
2. 用户在授权页面登录并授权
3. 授权服务器重定向到配置的`redirectUri`
4. 调用`handleCallback()`方法处理授权回调,获取用户信息
### Token管理
```typescript
// 获取访问令牌
const accessToken = unifiedLoginSDK.getAccessToken();
// 刷新令牌
unifiedLoginSDK.refreshToken().then(() => {
console.log('Token refreshed');
}).catch(error => {
console.error('Failed to refresh token:', error);
});
// 检查是否已认证
const isAuthenticated = unifiedLoginSDK.isAuthenticated();
```
### 用户信息管理
```typescript
// 获取用户信息
unifiedLoginSDK.getUserInfo().then(userInfo => {
console.log('User info:', userInfo);
});
// 获取用户权限列表
unifiedLoginSDK.getPermissions().then(permissions => {
console.log('Permissions:', permissions);
});
```
### 事件监听
```typescript
// 监听登录事件
unifiedLoginSDK.on('login', () => {
console.log('User logged in');
});
// 监听退出事件
unifiedLoginSDK.on('logout', () => {
console.log('User logged out');
});
// 监听Token过期事件
unifiedLoginSDK.on('tokenExpired', () => {
console.log('Token expired');
// 可以在这里执行自定义逻辑,如跳转到登录页
unifiedLoginSDK.login();
});
// 移除事件监听
const handleLogin = () => console.log('User logged in');
unifiedLoginSDK.on('login', handleLogin);
unifiedLoginSDK.off('login', handleLogin);
```
## 框架集成
### Vue 2
```javascript
// main.js
import Vue from 'vue';
import { createVuePlugin } from 'unified-login-sdk';
import App from './App.vue';
import router from './router';
// 创建Vue插件
const vuePlugin = createVuePlugin('localStorage');
// 安装插件
Vue.use(vuePlugin, {
config: {
clientId: 'your-client-id',
authorizationEndpoint: 'https://auth.example.com/authorize',
tokenEndpoint: 'https://auth.example.com/token',
userInfoEndpoint: 'https://auth.example.com/userinfo',
redirectUri: 'https://your-app.example.com/callback'
}
});
new Vue({
router,
render: h => h(App)
}).$mount('#app');
```
在组件中使用:
```vue
<template>
<div>
<div v-if="$auth.isAuthenticated()">
<h1>Welcome, {{ userInfo?.name }}</h1>
<button @click="logout">Logout</button>
</div>
<div v-else>
<button @click="login">Login</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
userInfo: null
};
},
mounted() {
if (this.$auth.isAuthenticated()) {
this.getUserInfo();
} else if (this.$auth.isCallback()) {
this.handleCallback();
}
},
methods: {
login() {
this.$auth.login();
},
async logout() {
await this.$auth.logout();
window.location.href = '/';
},
async getUserInfo() {
this.userInfo = await this.$auth.getUserInfo();
},
async handleCallback() {
try {
this.userInfo = await this.$auth.handleCallback();
window.location.href = '/';
} catch (error) {
console.error('Login failed:', error);
}
}
}
};
</script>
```
### Vue 3
```javascript
// main.js
import { createApp } from 'vue';
import { createVuePlugin } from 'unified-login-sdk';
import App from './App.vue';
import router from './router';
// 创建Vue插件
const vuePlugin = createVuePlugin('localStorage');
const app = createApp(App);
// 安装插件
app.use(vuePlugin, {
config: {
clientId: 'your-client-id',
authorizationEndpoint: 'https://auth.example.com/authorize',
tokenEndpoint: 'https://auth.example.com/token',
userInfoEndpoint: 'https://auth.example.com/userinfo',
redirectUri: 'https://your-app.example.com/callback'
}
});
app.use(router);
app.mount('#app');
```
在组件中使用Composition API
```vue
<template>
<div>
<div v-if="isAuthenticated">
<h1>Welcome, {{ userInfo?.name }}</h1>
<button @click="logout">Logout</button>
</div>
<div v-else>
<button @click="login">Login</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, inject } from 'vue';
// 注入SDK实例
const auth = inject('unifiedLogin');
const userInfo = ref(null);
const isAuthenticated = ref(auth.isAuthenticated());
onMounted(() => {
if (isAuthenticated.value) {
getUserInfo();
} else if (auth.isCallback()) {
handleCallback();
}
});
const login = () => {
auth.login();
};
const logout = async () => {
await auth.logout();
window.location.href = '/';
};
const getUserInfo = async () => {
userInfo.value = await auth.getUserInfo();
};
const handleCallback = async () => {
try {
userInfo.value = await auth.handleCallback();
isAuthenticated.value = true;
window.location.href = '/';
} catch (error) {
console.error('Login failed:', error);
}
};
</script>
```
```
## API参考
### 初始化
```typescript
init(config: SDKConfig): void
```
初始化SDK配置。
### 登录
```typescript
login(redirectUri?: string): void
```
触发登录流程,可选参数`redirectUri`可覆盖初始化时的配置。
### 退出登录
```typescript
logout(): Promise<void>
```
退出登录清除本地存储的Token和用户信息。
### 处理授权回调
```typescript
handleCallback(): Promise<UserInfo>
```
处理授权回调,获取用户信息。
### 获取用户信息
```typescript
getUserInfo(): Promise<UserInfo>
```
获取用户基本信息。
### 获取用户权限列表
```typescript
getPermissions(): Promise<string[]>
```
获取用户权限列表。
### 检查是否已认证
```typescript
isAuthenticated(): boolean
```
检查用户是否已认证。
### 获取访问令牌
```typescript
getAccessToken(): string | null
```
获取访问令牌。
### 刷新访问令牌
```typescript
refreshToken(): Promise<void>
```
刷新访问令牌。
### 事件监听
```typescript
on(event: 'login' | 'logout' | 'tokenExpired', callback: Function): void
```
监听登录、退出或Token过期事件。
### 移除事件监听
```typescript
off(event: 'login' | 'logout' | 'tokenExpired', callback: Function): void
```
移除事件监听。
## 配置选项
| 选项 | 类型 | 必填 | 默认值 | 描述 |
|------|------|------|--------|------|
| clientId | string | 是 | - | 客户端ID |
| clientSecret | string | 否 | - | 客户端密钥,某些场景下需要 |
| authorizationEndpoint | string | 是 | - | 授权端点URL |
| tokenEndpoint | string | 是 | - | Token端点URL |
| userInfoEndpoint | string | 是 | - | 用户信息端点URL |
| redirectUri | string | 是 | - | 重定向URL |
| storageType | 'localStorage' 'sessionStorage' 'cookie' | 否 | 'localStorage' | Token存储类型 |
| autoRefreshToken | boolean | 否 | true | 是否自动刷新Token |
| permissionsEndpoint | string | 否 | - | 权限端点URL |
| stateLength | number | 否 | 32 | 状态参数长度 |
| tenantId | string | 否 | - | 租户ID会自动添加到请求头中的tenant-id字段 |
## 事件处理
| 事件 | 描述 |
|------|------|
| login | 用户登录成功时触发 |
| logout | 用户退出登录时触发 |
| tokenExpired | Token过期时触发 |
## 路由守卫
### Vue路由守卫
```javascript
// router/index.js
import VueRouter from 'vue-router';
import { Auth } from 'unified-login-sdk';
import { Storage } from 'unified-login-sdk';
import { RouterGuard } from 'unified-login-sdk';
const storage = new Storage('localStorage');
const auth = new Auth(storage);
const routerGuard = new RouterGuard(auth);
const router = new VueRouter({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/protected',
name: 'Protected',
component: Protected,
meta: {
auth: {
requiresAuth: true,
requiredPermissions: ['read:protected']
}
}
}
]
});
// 添加路由守卫
router.beforeEach(routerGuard.createVueGuard());
export default router;
```
## 错误处理
### 网络错误处理
```typescript
try {
await unifiedLoginSDK.getUserInfo();
} catch (error) {
if (error.name === 'HttpError') {
// 处理HTTP错误
console.error('HTTP Error:', error.status, error.message);
if (error.status === 401) {
// 未授权,跳转到登录页
unifiedLoginSDK.login();
} else if (error.status === 403) {
// 权限不足
window.location.href = '/403';
}
} else {
// 处理其他错误
console.error('Error:', error.message);
}
}
```
### Token失效处理
```typescript
// 监听Token过期事件
unifiedLoginSDK.on('tokenExpired', () => {
console.log('Token expired');
// 跳转到登录页
unifiedLoginSDK.login();
});
```
## 最佳实践
1. **配置安全存储**根据项目需求选择合适的存储类型敏感信息建议使用cookie并设置secure和httpOnly标志。
2. **合理设置Token过期时间**根据项目安全性要求设置合适的Token过期时间建议access token过期时间较短refresh token过期时间较长。
3. **使用路由守卫保护敏感路由**:对需要登录或特定权限的路由使用路由守卫进行保护。
4. **处理网络错误**在调用SDK方法时使用try-catch捕获并处理可能的错误。
5. **监听Token过期事件**及时处理Token过期情况避免用户体验下降。
6. **不要直接暴露clientSecret**clientSecret应该只在后端使用前端SDK尽量避免使用clientSecret。
7. **使用HTTPS**确保所有与授权服务器的通信都使用HTTPS避免Token被窃取。
8. **定期清理存储**:在用户退出登录时,确保清理所有相关存储的信息。
## 浏览器兼容性
- Chrome (推荐)
- Firefox
- Safari
- Edge
## 许可证
MIT License

View File

@@ -0,0 +1,101 @@
/**
* 认证核心逻辑
* 实现OAuth2授权码模式的完整流程
*/
import { EventType, RouterInfo, SDKConfig, UserInfo } from '../types';
import { Storage } from '../utils/storage';
/**
* 认证核心类
*/
export declare class Auth {
private config;
private tokenManager;
private httpClient;
private storage;
private eventHandlers;
private userInfoCache;
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage: Storage);
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config: SDKConfig): void;
getToken(): string | null;
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
login(redirectUri?: string): Promise<void>;
/**
* 退出登录
*/
logout(): Promise<void>;
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
handleCallback(): Promise<void>;
getRoutes(): Promise<RouterInfo>;
/**
* 获取用户信息
* @returns UserInfo 用户信息
*/
getUserInfo(): UserInfo;
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
hasRole(role: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
hasAllRoles(roles: string[]): Promise<boolean>;
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
hasPermission(permission: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
hasAllPermissions(permissions: string[]): Promise<boolean>;
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated(): boolean;
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event: EventType, callback: Function): void;
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event: EventType, callback: Function): void;
/**
* 触发事件
* @param event 事件类型
* @param data 事件数据
*/
private emit;
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback(): boolean;
}
//# sourceMappingURL=auth.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAC,MAAM,UAAU,CAAC;AAGpE,OAAO,EAAC,OAAO,EAAC,MAAM,kBAAkB,CAAC;AAGzC;;GAEG;AACH,qBAAa,IAAI;IACf,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,aAAa,CAInB;IACF,OAAO,CAAC,aAAa,CAAyB;IAE9C;;;OAGG;gBACS,OAAO,EAAE,OAAO;IAQ5B;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAM7B,QAAQ,IAAG,MAAM,GAAG,IAAI;IAIxB;;;OAGG;IACG,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAchD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAe7B;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC/B,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IAYtC;;;OAGG;IACF,WAAW,IAAI,QAAQ;IAOxB;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBxD;;;;OAIG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAWpD;;;;OAIG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBpE;;;;OAIG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAYhE;;;OAGG;IACH,eAAe,IAAI,OAAO;IAK1B;;;;OAIG;IACH,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI9C;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI/C;;;;OAIG;IACH,OAAO,CAAC,IAAI;IAUZ;;;OAGG;IACH,UAAU,IAAI,OAAO;CAGtB"}

View File

@@ -0,0 +1,241 @@
/**
* 认证核心逻辑
* 实现OAuth2授权码模式的完整流程
*/
import { TokenManager } from './token';
import { HttpClient } from './http';
import { buildQueryParams, isCallbackUrl, parseQueryParams } from '../utils/url';
/**
* 认证核心类
*/
export class Auth {
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage) {
this.config = null;
this.eventHandlers = {
login: [],
logout: [],
tokenExpired: []
};
this.userInfoCache = null;
this.storage = storage;
// 先创建HttpClient初始时tokenManager为undefined
this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null);
// 然后创建TokenManager
this.tokenManager = new TokenManager(storage);
}
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config) {
this.config = config;
// 设置租户ID到HTTP客户端
this.httpClient.setTenantId(config.tenantId);
}
getToken() {
return this.tokenManager.getToken();
}
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
async login(redirectUri) {
if (!this.config) {
throw new Error('SDK not initialized');
}
const registrationId = this.config.registrationId || 'idp';
const basepath = this.config.basepath || '';
const path = `${basepath}/oauth2/authorization/${registrationId}`;
const tokenResponse = await this.httpClient.get(path, { needAuth: false });
const redirect = tokenResponse.data.redirect_url;
const params = parseQueryParams(redirect);
this.storage.set(params.state, window.location.href);
window.location.href = redirect;
}
/**
* 退出登录
*/
async logout() {
if (!this.config) {
throw new Error('SDK not initialized');
}
// 清除本地存储的Token和用户信息
this.tokenManager.clearToken();
this.userInfoCache = null;
this.storage.remove('userInfo');
const basepath = this.config.basepath || '';
await this.httpClient.post(`${basepath}/logout`, null, { needAuth: true });
// 触发退出事件
this.emit('logout');
window.location.href = this.config.idpLogoutUrl + '?redirect=' + this.config.homePage;
}
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
async handleCallback() {
if (!this.config) {
throw new Error('SDK not initialized');
}
const params = parseQueryParams();
// 检查是否有错误
if (params.error) {
throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`);
}
// 检查是否有授权码
if (!params.code) {
throw new Error('Authorization code not found');
}
const registrationId = this.config.registrationId || 'idp';
const basepath = this.config.basepath || '';
const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`;
const tokenResponse = await this.httpClient.get(callback, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
needAuth: false
});
// 触发登录事件
this.emit('login');
this.storage.set('userInfo', tokenResponse.data.data);
this.tokenManager.saveToken(tokenResponse.headers['authorization'] || tokenResponse.headers['Authorization']);
let url = this.config.homePage;
if (params.state) {
url = this.storage.get(params.state) || url;
}
window.location.href = url;
}
async getRoutes() {
if (!this.config) {
throw new Error('SDK not initialized');
}
const basepath = this.config.basepath || '';
const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`, { needAuth: true });
if (tokenResponse.status === 401) {
await this.logout();
}
return tokenResponse.data.data;
}
/**
* 获取用户信息
* @returns UserInfo 用户信息
*/
getUserInfo() {
return this.storage.get("userInfo");
}
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
async hasRole(role) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const roleCodes = userInfo.roles || [];
if (Array.isArray(role)) {
// 检查是否有任一角色
return role.some(r => roleCodes.includes(r));
}
// 检查是否有单个角色
return roleCodes.includes(role);
}
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
async hasAllRoles(roles) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const roleCodes = userInfo.roles || [];
// 检查是否有所有角色
return roles.every(r => roleCodes.includes(r));
}
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
async hasPermission(permission) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const permissions = userInfo.permissions || [];
if (Array.isArray(permission)) {
// 检查是否有任一权限
return permission.some(p => permissions.includes(p));
}
// 检查是否有单个权限
return permissions.includes(permission);
}
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
async hasAllPermissions(permissions) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const userPermissions = userInfo.permissions || [];
// 检查是否有所有权限
return permissions.every(p => userPermissions.includes(p));
}
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated() {
// 检查Token是否存在且未过期
return !!this.tokenManager.getToken();
}
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event, callback) {
this.eventHandlers[event].push(callback);
}
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event, callback) {
this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback);
}
/**
* 触发事件
* @param event 事件类型
* @param data 事件数据
*/
emit(event, data) {
this.eventHandlers[event].forEach(handler => {
try {
handler(data);
}
catch (error) {
console.error(`Error in ${event} event handler:`, error);
}
});
}
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback() {
return isCallbackUrl();
}
}
//# sourceMappingURL=auth.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,155 @@
/**
* HTTP客户端
* 用于与后端API进行通信
*/
/**
* HTTP请求方法类型
*/
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
/**
* HTTP请求选项
*/
export interface HttpRequestOptions {
/** 请求方法 */
method: HttpMethod;
/** 请求URL */
url: string;
/** 请求头 */
headers?: Record<string, string>;
/** 请求体 */
body?: any;
/** 是否需要认证 */
needAuth?: boolean;
}
/**
* HTTP响应类型
*/
export interface HttpResponse<T = any> {
/** 状态码 */
status: number;
/** 状态文本 */
statusText: string;
/** 响应体 */
data: T;
/** 响应头 */
headers: Record<string, string>;
}
/**
* HTTP错误类型
*/
export declare class HttpError extends Error {
/** 状态码 */
status: number;
/** 状态文本 */
statusText: string;
/** 错误数据 */
data: any;
/**
* 构造函数
* @param message 错误信息
* @param status 状态码
* @param statusText 状态文本
* @param data 错误数据
*/
constructor(message: string, status: number, statusText: string, data: any);
}
/**
* HTTP客户端类
*/
export declare class HttpClient {
private tokenGetter?;
private tenantId?;
/**
* 构造函数
* @param logout
* @param tokenGetter Token获取函数
*/
constructor(tokenGetter?: () => string | null);
/**
* 设置Token获取函数
* @param tokenGetter Token获取函数
*/
setTokenGetter(tokenGetter: () => string | null): void;
/**
* 设置租户ID
* @param tenantId 租户ID
*/
setTenantId(tenantId?: string): void;
/**
* 发送HTTP请求
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>>;
/**
* 处理响应数据
* @param response 响应对象
* @param responseData 响应数据
* @returns HttpResponse<T> 处理后的响应
*/
private handleResponse;
/**
* 检查是否为业务响应结构
* @param responseData 响应数据
* @returns boolean 是否为业务响应结构
*/
private isBusinessResponse;
/**
* 获取错误信息
* @param responseData 响应数据
* @returns string 错误信息
*/
private getErrorMessage;
/**
* GET请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
get<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>>;
/**
* POST请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
post<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>>;
/**
* PUT请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
put<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>>;
/**
* DELETE请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
delete<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>>;
/**
* PATCH请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
patch<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>>;
/**
* 解析响应体
* @param response 响应对象
* @returns Promise<any> 解析后的响应体
*/
private parseResponse;
/**
* 解析响应头
* @param headers 响应头对象
* @returns Record<string, string> 解析后的响应头
*/
private parseHeaders;
}
export {};
//# sourceMappingURL=http.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,KAAK,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU;IACV,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU;IACV,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,aAAa;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,GAAG;IACnC,UAAU;IACV,MAAM,EAAE,MAAM,CAAC;IACf,WAAW;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU;IACV,IAAI,EAAE,CAAC,CAAC;IACR,UAAU;IACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,UAAU;IACH,MAAM,EAAE,MAAM,CAAC;IACtB,WAAW;IACJ,UAAU,EAAE,MAAM,CAAC;IAC1B,WAAW;IACJ,IAAI,EAAE,GAAG,CAAC;IAEjB;;;;;;OAMG;gBACS,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;CAO3E;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,WAAW,CAAC,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAAC,CAAS;IAE1B;;;;OAIG;gBACS,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI;IAK7C;;;OAGG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI;IAItD;;;OAGG;IACH,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAIpC;;;;OAIG;IACG,OAAO,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IA0F7E;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAmCtB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAQ1B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAUvB;;;;;OAKG;IACG,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAQ/G;;;;;;OAMG;IACG,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IASrI;;;;;;OAMG;IACG,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IASpI;;;;;OAKG;IACG,MAAM,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAQlH;;;;;;OAMG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAStI;;;;OAIG;YACW,aAAa;IAY3B;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAOrB"}

View File

@@ -0,0 +1,274 @@
/**
* HTTP客户端
* 用于与后端API进行通信
*/
/**
* HTTP错误类型
*/
export class HttpError extends Error {
/**
* 构造函数
* @param message 错误信息
* @param status 状态码
* @param statusText 状态文本
* @param data 错误数据
*/
constructor(message, status, statusText, data) {
super(message);
this.name = 'HttpError';
this.status = status;
this.statusText = statusText;
this.data = data;
}
}
/**
* HTTP客户端类
*/
export class HttpClient {
/**
* 构造函数
* @param logout
* @param tokenGetter Token获取函数
*/
constructor(tokenGetter) {
this.tokenGetter = tokenGetter;
}
/**
* 设置Token获取函数
* @param tokenGetter Token获取函数
*/
setTokenGetter(tokenGetter) {
this.tokenGetter = tokenGetter;
}
/**
* 设置租户ID
* @param tenantId 租户ID
*/
setTenantId(tenantId) {
this.tenantId = tenantId;
}
/**
* 发送HTTP请求
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async request(options) {
const { method, url, headers = {}, body, needAuth = true } = options;
// 构建请求头
const requestHeaders = {
'Content-Type': 'application/json',
...headers
};
// 添加认证头
const addAuthHeader = () => {
if (needAuth && this.tokenGetter) {
const token = this.tokenGetter();
if (token) {
requestHeaders.Authorization = `${token}`;
}
}
};
// 添加租户ID头
if (this.tenantId) {
requestHeaders['tenant-id'] = this.tenantId;
}
addAuthHeader();
// 构建请求配置
const fetchOptions = {
method,
headers: requestHeaders,
credentials: 'include' // 包含cookie
};
// 添加请求体
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
}
try {
// 发送请求
const response = await fetch(url, fetchOptions);
const responseData = await this.parseResponse(response);
// 检查响应状态
if (!response.ok) {
// 如果是401错误尝试刷新Token并重试
if (response.status === 401) {
return {
status: response.status,
statusText: response.statusText,
data: '',
headers: this.parseHeaders(response.headers)
};
}
// 其他错误,直接抛出
const errorMsg = this.getErrorMessage(responseData);
throw new HttpError(errorMsg, response.status, response.statusText, responseData);
}
// 处理成功响应的业务逻辑
return this.handleResponse(response, responseData);
}
catch (error) {
if (error instanceof HttpError) {
throw error;
}
// 网络错误或其他错误
throw new HttpError(error instanceof Error ? error.message : 'Network Error', 0, 'Network Error', null);
}
}
/**
* 处理响应数据
* @param response 响应对象
* @param responseData 响应数据
* @returns HttpResponse<T> 处理后的响应
*/
handleResponse(response, responseData) {
// 检查是否为业务响应结构
if (this.isBusinessResponse(responseData)) {
// 业务响应结构:{ code, msg, data }
const { code, msg, data } = responseData;
// 检查业务状态码
if (code !== 0 && code !== 200 && code !== '0' && code !== '200') {
// 业务错误抛出HttpError
throw new HttpError(msg || `Business Error: ${code}`, response.status, response.statusText, responseData);
}
// 业务成功返回data字段作为实际数据
return {
status: response.status,
statusText: response.statusText,
data: data,
headers: this.parseHeaders(response.headers)
};
}
// 非业务响应结构,直接返回原始数据
return {
status: response.status,
statusText: response.statusText,
data: responseData,
headers: this.parseHeaders(response.headers)
};
}
/**
* 检查是否为业务响应结构
* @param responseData 响应数据
* @returns boolean 是否为业务响应结构
*/
isBusinessResponse(responseData) {
return typeof responseData === 'object' &&
responseData !== null &&
('code' in responseData) &&
('msg' in responseData) &&
('data' in responseData);
}
/**
* 获取错误信息
* @param responseData 响应数据
* @returns string 错误信息
*/
getErrorMessage(responseData) {
// 如果是业务响应结构
if (this.isBusinessResponse(responseData)) {
return responseData.msg || `Business Error: ${responseData.code}`;
}
// 其他错误结构
return responseData.message || responseData.error || `HTTP Error`;
}
/**
* GET请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async get(url, options) {
return this.request({
method: 'GET',
url,
...options
});
}
/**
* POST请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async post(url, body, options) {
return this.request({
method: 'POST',
url,
body,
...options
});
}
/**
* PUT请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async put(url, body, options) {
return this.request({
method: 'PUT',
url,
body,
...options
});
}
/**
* DELETE请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async delete(url, options) {
return this.request({
method: 'DELETE',
url,
...options
});
}
/**
* PATCH请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async patch(url, body, options) {
return this.request({
method: 'PATCH',
url,
body,
...options
});
}
/**
* 解析响应体
* @param response 响应对象
* @returns Promise<any> 解析后的响应体
*/
async parseResponse(response) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
else if (contentType.includes('text/')) {
return response.text();
}
else {
return response.blob();
}
}
/**
* 解析响应头
* @param headers 响应头对象
* @returns Record<string, string> 解析后的响应头
*/
parseHeaders(headers) {
const result = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
}
//# sourceMappingURL=http.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
/**
* Token管理模块
* 负责Token的存储、获取、刷新和过期处理
*/
import { Storage } from '../utils/storage';
/**
* Token管理类
*/
export declare class TokenManager {
private storage;
/**
* 构造函数
* @param storage 存储实例
* @param httpClient HTTP客户端实例
*/
constructor(storage: Storage);
/**
* 存储Token信息
* @param tokenInfo Token信息
*/
saveToken(tokenInfo: string): void;
/**
* 获取Token信息
* @returns TokenInfo | null Token信息
*/
getToken(): string | null;
/**
* 清除Token信息
*/
clearToken(): void;
}
//# sourceMappingURL=token.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/core/token.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE3C;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAU;IAEzB;;;;OAIG;gBACS,OAAO,EAAE,OAAO;IAI5B;;;OAGG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIlC;;;OAGG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI;IAIzB;;OAEG;IACH,UAAU,IAAI,IAAI;CAGnB"}

View File

@@ -0,0 +1,38 @@
/**
* Token管理模块
* 负责Token的存储、获取、刷新和过期处理
*/
/**
* Token管理类
*/
export class TokenManager {
/**
* 构造函数
* @param storage 存储实例
* @param httpClient HTTP客户端实例
*/
constructor(storage) {
this.storage = storage;
}
/**
* 存储Token信息
* @param tokenInfo Token信息
*/
saveToken(tokenInfo) {
this.storage.set('token', tokenInfo);
}
/**
* 获取Token信息
* @returns TokenInfo | null Token信息
*/
getToken() {
return this.storage.get('token');
}
/**
* 清除Token信息
*/
clearToken() {
this.storage.remove('token');
}
}
//# sourceMappingURL=token.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/core/token.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,OAAO,YAAY;IAGvB;;;;OAIG;IACH,YAAY,OAAgB;QAC1B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,SAAiB;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,UAAU;QACR,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF"}

View File

@@ -0,0 +1,55 @@
/**
* 路由守卫模块
* 提供基于权限的路由拦截和未登录自动跳转登录页功能
*/
import { Auth } from '../core/auth';
/**
* 路由守卫选项
*/
export interface RouterGuardOptions {
/**
* 是否需要登录
*/
requiresAuth?: boolean;
/**
* 需要的权限列表
*/
requiredPermissions?: string[];
/**
* 登录后重定向的URL
*/
redirectUri?: string;
/**
* 权限不足时重定向的URL
*/
unauthorizedRedirectUri?: string;
}
/**
* 路由守卫类
*/
export declare class RouterGuard {
private auth;
/**
* 构造函数
* @param auth 认证实例
*/
constructor(auth: Auth);
/**
* 检查路由权限
* @param options 路由守卫选项
* @returns Promise<boolean> 是否通过权限检查
*/
check(options: RouterGuardOptions): Promise<boolean>;
/**
* 创建Vue路由守卫
* @returns 路由守卫函数
*/
createVueGuard(): (to: any, from: any, next: any) => Promise<void>;
/**
* 检查当前用户是否有权限访问资源
* @param permissions 需要的权限列表
* @returns Promise<boolean> 是否拥有权限
*/
hasPermission(permissions: string | string[]): Promise<boolean>;
}
//# sourceMappingURL=router.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/guards/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAO;IAEnB;;;OAGG;gBACS,IAAI,EAAE,IAAI;IAItB;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC1D;;;OAGG;IACH,cAAc,KACE,IAAI,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG;IAgB7C;;;;OAIG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAoBtE"}

View File

@@ -0,0 +1,89 @@
/**
* 路由守卫模块
* 提供基于权限的路由拦截和未登录自动跳转登录页功能
*/
/**
* 路由守卫类
*/
export class RouterGuard {
/**
* 构造函数
* @param auth 认证实例
*/
constructor(auth) {
this.auth = auth;
}
/**
* 检查路由权限
* @param options 路由守卫选项
* @returns Promise<boolean> 是否通过权限检查
*/
async check(options) {
const { requiresAuth = true, requiredPermissions = [] } = options;
// 检查是否需要登录
if (requiresAuth) {
// 检查是否已认证
if (!this.auth.isAuthenticated()) {
// 未认证,跳转到登录页
this.auth.login(options.redirectUri);
return false;
}
// 检查是否需要权限
if (requiredPermissions.length > 0) {
// 获取用户权限
const userPermissions = [''];
// 检查是否拥有所有需要的权限
const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission));
if (!hasPermission) {
// 权限不足,跳转到权限不足页
if (options.unauthorizedRedirectUri) {
window.location.href = options.unauthorizedRedirectUri;
}
return false;
}
}
}
return true;
}
/**
* 创建Vue路由守卫
* @returns 路由守卫函数
*/
createVueGuard() {
return async (to, from, next) => {
var _a;
// 从路由元信息中获取守卫选项
const options = ((_a = to.meta) === null || _a === void 0 ? void 0 : _a.auth) || {};
try {
const allowed = await this.check(options);
if (allowed) {
next();
}
}
catch (error) {
console.error('Route guard error:', error);
next(false);
}
};
}
/**
* 检查当前用户是否有权限访问资源
* @param permissions 需要的权限列表
* @returns Promise<boolean> 是否拥有权限
*/
async hasPermission(permissions) {
if (!permissions) {
return true;
}
const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];
// 检查是否已认证
if (!this.auth.isAuthenticated()) {
return false;
}
// 获取用户权限
const userPermissions = [''];
// 检查是否拥有所有需要的权限
return requiredPermissions.every(permission => userPermissions.includes(permission));
}
}
//# sourceMappingURL=router.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/guards/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA0BH;;GAEG;AACH,MAAM,OAAO,WAAW;IAGtB;;;OAGG;IACH,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,OAA2B;QACrC,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,mBAAmB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;QAElE,WAAW;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,UAAU;YACV,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;gBACjC,aAAa;gBACb,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;gBACrC,OAAO,KAAK,CAAC;YACf,CAAC;YAED,WAAW;YACX,IAAI,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACnC,SAAS;gBACT,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC,CAAC;gBAE7B,gBAAgB;gBAChB,MAAM,aAAa,GAAG,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAC3D,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC,CAAC;gBAEF,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,gBAAgB;oBAChB,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC;wBACpC,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,uBAAuB,CAAC;oBACzD,CAAC;oBACD,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,OAAO,KAAK,EAAE,EAAO,EAAE,IAAS,EAAE,IAAS,EAAE,EAAE;;YAC7C,gBAAgB;YAChB,MAAM,OAAO,GAAuB,CAAA,MAAA,EAAE,CAAC,IAAI,0CAAE,IAAI,KAAI,EAAE,CAAC;YAExD,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC1C,IAAI,OAAO,EAAE,CAAC;oBACZ,IAAI,EAAE,CAAC;gBACT,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;gBAC3C,IAAI,CAAC,KAAK,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa,CAAC,WAA8B;QAChD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;QAErF,UAAU;QACV,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;QAED,SAAS;QACT,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC,CAAA;QAE5B,gBAAgB;QAChB,OAAO,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAC5C,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC,CAAC;IACJ,CAAC;CACF"}

View File

@@ -0,0 +1,20 @@
/**
* 统一登录SDK入口文件
* 支持OAuth2授权码模式提供完整的Token管理和用户信息管理功能
*/
export { Auth } from './core/auth';
export { TokenManager } from './core/token';
export { HttpClient, HttpError } from './core/http';
export { Storage } from './utils/storage';
export { RouterGuard, RouterGuardOptions } from './guards/router';
export { generateRandomString, parseQueryParams, buildQueryParams, generateAuthorizationUrl, isCallbackUrl } from './utils/url';
export * from './types';
export { VuePlugin, createVuePlugin } from './plugins/vue';
import { UnifiedLoginSDK } from './types';
/**
* 默认导出的SDK实例
*/
export declare const unifiedLoginSDK: UnifiedLoginSDK;
export default unifiedLoginSDK;
export declare const version = "1.0.0";
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,wBAAwB,EACxB,aAAa,EACd,MAAM,aAAa,CAAC;AAGrB,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAa,eAAe,EAAE,MAAM,SAAS,CAAC;AAUrD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,eA8C7B,CAAC;AAGF,eAAe,eAAe,CAAC;AAG/B,eAAO,MAAM,OAAO,UAAU,CAAC"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
/**
* Vue插件模块
* 提供Vue应用中使用统一登录SDK的能力
*/
import { Auth } from '../core/auth';
import { SDKConfig } from '../types';
import { Storage } from '../utils/storage';
import { RouterGuard } from '../guards/router';
/**
* Vue插件选项
*/
export interface VuePluginOptions {
/**
* SDK配置
*/
config: SDKConfig;
/**
* 插件名称,默认'unifiedLogin'
*/
pluginName?: string;
}
/**
* Vue插件类
*/
export declare class VuePlugin {
private auth;
private routerGuard;
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage: Storage);
/**
* 安装Vue插件
* @param app Vue构造函数或Vue 3应用实例
* @param options 插件选项
*/
install(app: any, options: VuePluginOptions): void;
/**
* 获取认证实例
* @returns Auth 认证实例
*/
getAuth(): Auth;
/**
* 获取路由守卫实例
* @returns RouterGuard 路由守卫实例
*/
getRouterGuard(): RouterGuard;
}
/**
* 创建Vue插件实例
* @param storageType 存储类型
* @returns VuePlugin Vue插件实例
*/
export declare function createVuePlugin(storageType?: 'localStorage' | 'sessionStorage' | 'cookie'): VuePlugin;
//# sourceMappingURL=vue.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"vue.d.ts","sourceRoot":"","sources":["../../src/plugins/vue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,MAAM,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,WAAW,CAAc;IAEjC;;;OAGG;gBACS,OAAO,EAAE,OAAO;IAK5B;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAkDlD;;;OAGG;IACH,OAAO,IAAI,IAAI;IAIf;;;OAGG;IACH,cAAc,IAAI,WAAW;CAG9B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,WAAW,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAGrG"}

View File

@@ -0,0 +1,93 @@
/**
* Vue插件模块
* 提供Vue应用中使用统一登录SDK的能力
*/
import { Auth } from '../core/auth';
import { Storage } from '../utils/storage';
import { RouterGuard } from '../guards/router';
/**
* Vue插件类
*/
export class VuePlugin {
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage) {
this.auth = new Auth(storage);
this.routerGuard = new RouterGuard(this.auth);
}
/**
* 安装Vue插件
* @param app Vue构造函数或Vue 3应用实例
* @param options 插件选项
*/
install(app, options) {
const { config, pluginName = 'unifiedLogin' } = options;
// 初始化SDK
this.auth.init(config);
// 判断是Vue 2还是Vue 3
const isVue3 = typeof app.config !== 'undefined';
if (isVue3) {
// Vue 3
// 在全局属性上挂载SDK实例
app.config.globalProperties[`${pluginName}`] = this.auth;
app.config.globalProperties.$auth = this.auth; // 兼容简写
// 提供Vue组件内的注入
app.provide(pluginName, this.auth);
app.provide('auth', this.auth); // 兼容简写
// 处理路由守卫
app.mixin({
beforeCreate() {
// 如果是根组件,添加路由守卫
if (this.$options.router) {
const router = this.$options.router;
// 添加全局前置守卫
router.beforeEach(this.routerGuard.createVueGuard());
}
}
});
}
else {
// Vue 2
// 在Vue实例上挂载SDK实例
app.prototype[`${pluginName}`] = this.auth;
app.prototype.$auth = this.auth; // 兼容简写
// 全局混入
app.mixin({
beforeCreate() {
// 如果是根组件,添加路由守卫
if (this.$options.router) {
const router = this.$options.router;
// 添加全局前置守卫
router.beforeEach(this.routerGuard.createVueGuard());
}
}
});
}
}
/**
* 获取认证实例
* @returns Auth 认证实例
*/
getAuth() {
return this.auth;
}
/**
* 获取路由守卫实例
* @returns RouterGuard 路由守卫实例
*/
getRouterGuard() {
return this.routerGuard;
}
}
/**
* 创建Vue插件实例
* @param storageType 存储类型
* @returns VuePlugin Vue插件实例
*/
export function createVuePlugin(storageType) {
const storage = new Storage(storageType);
return new VuePlugin(storage);
}
//# sourceMappingURL=vue.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"vue.js","sourceRoot":"","sources":["../../src/plugins/vue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAgB/C;;GAEG;AACH,MAAM,OAAO,SAAS;IAIpB;;;OAGG;IACH,YAAY,OAAgB;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAQ,EAAE,OAAyB;QACzC,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC;QAExD,SAAS;QACT,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAEvB,kBAAkB;QAClB,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,WAAW,CAAC;QAEjD,IAAI,MAAM,EAAE,CAAC;YACX,QAAQ;YACR,gBAAgB;YAChB,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YACzD,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO;YAEtD,cAAc;YACd,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACnC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;YAEvC,SAAS;YACT,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY;oBACV,gBAAgB;oBAChB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;wBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBACpC,WAAW;wBACX,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC,CAAC;oBACvD,CAAC;gBACH,CAAC;aACF,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ;YACR,iBAAiB;YACjB,GAAG,CAAC,SAAS,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YAC3C,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO;YAExC,OAAO;YACP,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY;oBACV,gBAAgB;oBAChB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;wBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBACpC,WAAW;wBACX,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC,CAAC;oBACvD,CAAC;gBACH,CAAC;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,WAA0D;IACxF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC"}

View File

@@ -0,0 +1,39 @@
/**
* SDK配置选项
*/
export interface SDKConfig {
/** 客户端ID */
clientId: string;
/** 注册id **/
registrationId: string;
/** 后端basepath路径*/
basepath: string;
/** 存储类型默认localStorage */
storageType?: 'localStorage' | 'sessionStorage' | 'cookie';
idpLogoutUrl: string;
homePage: string;
/** 租户ID可选 */
tenantId?: string;
}
/**
* Token信息
*/
export interface TokenInfo {
/** 访问令牌 */
accessToken: string;
/** 刷新令牌 */
refreshToken: string;
/** 令牌类型默认Bearer */
tokenType?: string;
/** 访问令牌过期时间(秒) */
expiresIn: number;
/** 刷新令牌过期时间(秒) */
refreshExpiresIn?: number;
/** 令牌颁发时间戳 */
issuedAt: number;
}
/**
* 事件类型
*/
export type EventType = 'login' | 'logout' | 'tokenExpired';
//# sourceMappingURL=config.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,YAAY;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC;IAC3D,YAAY,EAAC,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,WAAW;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,CAAC"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=config.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,80 @@
export * from './config';
export * from './user';
/**
* 统一登录SDK接口
*/
export interface UnifiedLoginSDK {
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config: import('./config').SDKConfig): void;
getToken(): string | null;
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
login(redirectUri?: string): Promise<void>;
/**
* 退出登录
*/
logout(): Promise<void>;
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
handleCallback(): Promise<void>;
getRoutes(): Promise<import('./user').RouterInfo>;
/**
* 获取用户信息
* @returns Promise<UserInfo> 用户信息
*/
getUserInfo(): import('./user').UserInfo;
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated(): boolean;
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
hasRole(role: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
hasAllRoles(roles: string[]): Promise<boolean>;
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
hasPermission(permission: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
hasAllPermissions(permissions: string[]): Promise<boolean>;
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event: import('./config').EventType, callback: Function): void;
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event: import('./config').EventType, callback: Function): void;
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback(): boolean;
}
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AAEvB;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAEjD,QAAQ,IAAG,MAAM,GAAC,IAAI,CAAA;IACtB;;;OAGG;IACH,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3C;;OAEG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;OAGG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,IAAI,OAAO,CAAC,OAAO,QAAQ,EAAE,UAAU,CAAC,CAAC;IAElD;;;OAGG;IACH,WAAW,IAAI,OAAO,QAAQ,EAAE,QAAQ,CAAC;IAEzC;;;OAGG;IACH,eAAe,IAAI,OAAO,CAAC;IAE3B;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEnD;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/C;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/D;;;;OAIG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE3D;;;;OAIG;IACH,EAAE,CAAC,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAElE;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAEnE;;;OAGG;IACH,UAAU,IAAI,OAAO,CAAC;CACvB"}

View File

@@ -0,0 +1,3 @@
export * from './config';
export * from './user';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC"}

Some files were not shown because too many files have changed in this diff Show More