Commit 581a2e6e authored by 林洋洋's avatar 林洋洋

项目初始化

parent 35642d38
Pipeline #322 failed with stages
root = true
[*.{groovy,java,kt,xml}]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
### gradle ###
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
### STS ###
.settings/
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
bin/
### IntelliJ IDEA ###
!.idea/icon.png
.idea
*.iws
*.iml
*.ipr
rebel.xml
### NetBeans ###
nbproject/private/
build/
nbbuild/
nbdist/
.nb-gradle/
### maven ###
target/
*.war
*.ear
*.zip
*.tar
*.tar.gz
*.versionsBackup
### vscode ###
.vscode
### logs ###
/logs/
*.log
### temp ignore ###
*.cache
*.diff
*.patch
*.tmp
*.java~
*.properties~
*.xml~
### system ignore ###
.DS_Store
Thumbs.db
Servers
.metadata
.flattened-pom.xml
This diff is collapsed.
<p align="center">
<img src="https://img.shields.io/badge/Pig-3.9-success.svg" alt="Build Status">
<img src="https://img.shields.io/badge/Spring%20Cloud-2025-blue.svg" alt="Coverage Status">
<img src="https://img.shields.io/badge/Spring%20Boot-3.5-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Vue-3.5-blue.svg" alt="Downloads">
<img src="https://img.shields.io/github/license/pig-mesh/pig"/>
<img src="https://gitcode.com/pig-mesh/pig/star/badge.svg"/>
</p>
## 系统说明
- 基于 Spring Cloud 、Spring Boot、 OAuth2 的 RBAC **企业快速开发平台**, 同时支持微服务架构和单体架构
- 提供对 Spring Authorization Server 生产级实践,支持多种安全授权模式
- 提供对常见容器化方案支持 Kubernetes、Rancher2 、Kubesphere、EDAS、SAE 支持
#### 使用文档
PIG 提供了详尽的部署文档 👉 [wiki.pig4cloud.com](https://wiki.pig4cloud.com),涵盖开发环境配置、服务端启动、前端运行等关键步骤。
重要的事情说三遍:
- 🔥 [ 配套文档 wiki.pig4cloud.com](https://wiki.pig4cloud.com)
- 🔥 [ 配套文档 wiki.pig4cloud.com](https://wiki.pig4cloud.com)
- 🔥 [ 配套文档 wiki.pig4cloud.com](https://wiki.pig4cloud.com)
#### 其他产品
- 👉🏻 [PIGX 在线体验](http://home.pig4cloud.com:38081)
- 👉🏻 [自研BPMN工作流引擎](http://home.pig4cloud.com:38082)
- 👉🏻 [大模型 RAG 知识库](http://home.pig4cloud.com:38083)
## 微信群 [禁广告]
<img src='https://minio.pigx.vip/oss/202412/1735262426.png' alt='1735262426'/>
## 快速开始
#### Docker 快速体验
```shell
# 可用内存大于4G
curl -o docker-compose.yaml https://try.pig4cloud.com
# 等待5分钟
docker compose up
```
### 核心依赖
| 依赖 | 版本 |
|-----------------------------|------|
| Spring Boot | 3.5 |
| Spring Cloud | 2025 |
| Spring Cloud Alibaba | 2023 |
| Spring Authorization Server | 1.5 |
| Mybatis Plus | 3.5 |
| Vue | 3.5 |
| Element Plus | 2.7 |
### 模块说明
```lua
pig-ui -- https://gitee.com/log4j/pig-ui
pig
├── pig-boot -- 单体模式启动器[9999]
├── pig-auth -- 授权服务提供[3000]
└── pig-common -- 系统公共模块
├── pig-common-bom -- 全局依赖管理控制
├── pig-common-core -- 公共工具类核心包
├── pig-common-datasource -- 动态数据源包
├── pig-common-log -- 日志服务
├── pig-common-oss -- 文件上传工具类
├── pig-common-mybatis -- mybatis 扩展封装
├── pig-common-seata -- 分布式事务
├── pig-common-security -- 安全工具类
├── pig-common-swagger -- 接口文档
├── pig-common-feign -- feign 扩展封装
└── pig-common-xss -- xss 安全封装
├── pig-register -- Nacos Server[8848]
├── pig-gateway -- Spring Cloud Gateway网关[9999]
└── pig-upms -- 通用用户权限管理模块
└── pig-upms-api -- 通用用户权限管理系统公共api模块
└── pig-upms-biz -- 通用用户权限管理系统业务处理模块[4000]
└── pig-visual
└── pig-monitor -- 服务监控 [5001]
├── pig-codegen -- 图形化代码生成 [5002]
└── pig-quartz -- 定时任务管理台 [5007]
```
## 免费公开课
<table>
<tr>
<td><a href="https://www.bilibili.com/video/av45084065" target="_blank"><img src="https://foruda.gitee.com/images/1731647304254897555/88a9c2fa_441246.jpeg"></a></td>
<td><a href="https://www.bilibili.com/video/av77344954" target="_blank"><img src="https://foruda.gitee.com/images/1731647324953921510/39689640_441246.jpeg"></a></td>
</tr>
<tr>
<td><a href="https://www.bilibili.com/video/BV1J5411476V" target="_blank"><img src="https://foruda.gitee.com/images/1731647357502030768/7f31f392_441246.jpeg"></a></td>
<td><a href="https://www.bilibili.com/video/BV14p4y197K5" target="_blank"><img src="https://foruda.gitee.com/images/1731647375444479120/2b8fd494_441246.jpeg"></a></td>
</tr>
</table>
## 开源共建
### 开源协议
pig 开源软件遵循 [Apache 2.0 协议](https://www.apache.org/licenses/LICENSE-2.0.html)
允许商业使用,但务必保留类作者、Copyright 信息。
![](https://foruda.gitee.com/images/1731647419204307063/91217172_441246.jpeg)
### 其他说明
1. 欢迎提交 [PR](https://dwz.cn/2KURd5Vf),注意对应提交对应 `dev` 分支
代码规范 [spring-javaformat](https://github.com/spring-io/spring-javaformat)
<details>
<summary>代码规范说明</summary>
1. 由于 <a href="https://github.com/spring-io/spring-javaformat" target="_blank">spring-javaformat</a>
强制所有代码按照指定格式排版,未按此要求提交的代码将不能通过合并(打包)
2. 如果使用 IntelliJ IDEA
开发,请安装自动格式化软件 <a href="https://repo1.maven.org/maven2/io/spring/javaformat/spring-javaformat-intellij-idea-plugin/" target="_blank">
spring-javaformat-intellij-idea-plugin</a>
3. 其他开发工具,请参考 <a href="https://github.com/spring-io/spring-javaformat" target="_blank">
spring-javaformat</a>
说明,或`提交代码前`在项目根目录运行下列命令(需要开发者电脑支持`mvn`命令)进行代码格式化
```
mvn spring-javaformat:apply
```
</details>
2. 欢迎提交 [issue](https://gitee.com/log4j/pig/issues),请写清楚遇到问题的原因、开发环境、复显步骤。
FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/mysql-server:8.0.32
MAINTAINER lengleng(wangiegie@gmail.com)
ENV TZ=Asia/Shanghai
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY ./pig.sql /docker-entrypoint-initdb.d
COPY ./pig_config.sql /docker-entrypoint-initdb.d
EXPOSE 3306
This diff is collapsed.
This diff is collapsed.
services:
pig-mysql:
build:
context: ./db
environment:
MYSQL_ROOT_HOST: "%"
MYSQL_ROOT_PASSWORD: root
restart: always
container_name: pig-mysql
image: pig-mysql
ports:
- 33306:3306
networks:
- spring_cloud_default
pig-redis:
image: registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/redis
ports:
- 36379:6379
restart: always
container_name: pig-redis
hostname: pig-redis
networks:
- spring_cloud_default
pig-register:
build:
context: ./pig-register
restart: always
ports:
- 8848:8848
- 9848:9848
- 8080:8080
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
container_name: pig-register
hostname: pig-register
image: pig-register
networks:
- spring_cloud_default
pig-gateway:
build:
context: ./pig-gateway
restart: always
ports:
- 9999:9999
container_name: pig-gateway
hostname: pig-gateway
image: pig-gateway
environment:
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-auth:
build:
context: ./pig-auth
restart: always
container_name: pig-auth
hostname: pig-auth
image: pig-auth
environment:
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-upms:
build:
context: ./pig-upms/pig-upms-biz
restart: always
container_name: pig-upms
hostname: pig-upms
image: pig-upms
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-monitor:
build:
context: ./pig-visual/pig-monitor
restart: always
ports:
- 5001:5001
container_name: pig-monitor
hostname: pig-monitor
image: pig-monitor
environment:
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-codegen:
build:
context: ./pig-visual/pig-codegen
restart: always
container_name: pig-codegen
hostname: pig-codegen
image: pig-codegen
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-quartz:
build:
context: ./pig-visual/pig-quartz
restart: always
image: pig-quartz
container_name: pig-quartz
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
networks:
spring_cloud_default:
name: spring_cloud_default
driver: bridge
FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:21-anolis
WORKDIR /pig-auth
ARG JAR_FILE=target/pig-auth.jar
COPY ${JAR_FILE} app.jar
EXPOSE 3000
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java $JAVA_OPTS -jar app.jar
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
~
~ 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
~
~ http://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.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pig</artifactId>
<version>${revision}</version>
</parent>
<artifactId>pig-auth</artifactId>
<packaging>jar</packaging>
<description>pig 认证授权中心,基于 spring security oAuth2</description>
<dependencies>
<!--注册中心客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--配置中心客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--断路器依赖-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pig-common-feign</artifactId>
</dependency>
<!--upms api、model 模块-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pig-upms-api</artifactId>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pig-common-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--freemarker 授权码模式渲染-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--undertow容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- log -->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pig-common-log</artifactId>
</dependency>
<!-- 调用验证码核心模块 -->
<dependency>
<groupId>com.pig4cloud.plugin</groupId>
<artifactId>captcha-core</artifactId>
<version>${captcha.version}</version>
</dependency>
<!-- 加解密依赖 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>
<id>boot</id>
</profile>
<profile>
<id>cloud</id>
<activation>
<!-- 默认环境 -->
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig.auth;
import com.pig4cloud.pig.common.feign.annotation.EnablePigFeignClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 认证授权中心应用启动类
*
* @author lengleng
* @date 2025/05/30
*/
@EnablePigFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class PigAuthApplication {
public static void main(String[] args) {
SpringApplication.run(PigAuthApplication.class, args);
}
}
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig.auth.config;
import com.pig4cloud.pig.auth.support.CustomeOAuth2AccessTokenGenerator;
import com.pig4cloud.pig.auth.support.core.CustomeOAuth2TokenCustomizer;
import com.pig4cloud.pig.auth.support.core.FormIdentityLoginConfigurer;
import com.pig4cloud.pig.auth.support.core.PigDaoAuthenticationProvider;
import com.pig4cloud.pig.auth.support.filter.PasswordDecoderFilter;
import com.pig4cloud.pig.auth.support.filter.ValidateCodeFilter;
import com.pig4cloud.pig.auth.support.handler.PigAuthenticationFailureEventHandler;
import com.pig4cloud.pig.auth.support.handler.PigAuthenticationSuccessEventHandler;
import com.pig4cloud.pig.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationConverter;
import com.pig4cloud.pig.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationProvider;
import com.pig4cloud.pig.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationConverter;
import com.pig4cloud.pig.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationProvider;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.Arrays;
/**
* 认证服务器配置类
*
* @author lengleng
* @date 2025/05/30
*/
@Configuration
@RequiredArgsConstructor
public class AuthorizationServerConfiguration {
private final OAuth2AuthorizationService authorizationService;
private final PasswordDecoderFilter passwordDecoderFilter;
private final ValidateCodeFilter validateCodeFilter;
/**
* Authorization Server 配置,仅对 /oauth2/** 的请求有效
* @param http http
* @return {@link SecurityFilterChain }
* @throws Exception 异常
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServer(HttpSecurity http) throws Exception {
// 配置授权服务器的安全策略,只有/oauth2/**的请求才会走如下的配置
http.securityMatcher("/oauth2/**");
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
// 增加验证码过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 增加密码解密过滤器
http.addFilterBefore(passwordDecoderFilter, UsernamePasswordAuthenticationFilter.class);
http.with(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
.accessTokenResponseHandler(new PigAuthenticationSuccessEventHandler()) // 登录成功处理器
.errorResponseHandler(new PigAuthenticationFailureEventHandler());// 登录失败处理器
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(new PigAuthenticationFailureEventHandler()))// 处理客户端认证异常
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)), Customizer.withDefaults())
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated());
// 设置 Token 存储的策略
http.with(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
.authorizationServerSettings(
AuthorizationServerSettings.builder().issuer(SecurityConstants.PROJECT_LICENSE).build()),
Customizer.withDefaults());
// 设置授权码模式登录页面
http.with(new FormIdentityLoginConfigurer(), Customizer.withDefaults());
DefaultSecurityFilterChain securityFilterChain = http.build();
// 注入自定义授权模式实现
addCustomOAuth2GrantAuthenticationProvider(http);
return securityFilterChain;
}
/**
* 令牌生成规则实现 </br>
* client:username:uuid
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator oAuth2TokenGenerator() {
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
// 注入Token 增加关联用户信息
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
}
/**
* request -> xToken 注入请求转换器
* @return DelegatingAuthenticationConverter
*/
@Bean
public AuthenticationConverter accessTokenRequestConverter() {
return new DelegatingAuthenticationConverter(Arrays.asList(
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(), new OAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
/**
* 注入授权模式实现提供方
* <p>
* 1. 密码模式 </br>
* 2. 短信登录 </br>
*/
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
// 处理 UsernamePasswordAuthenticationToken
http.authenticationProvider(new PigDaoAuthenticationProvider());
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
}
}
package com.pig4cloud.pig.auth.endpoint;
import cn.hutool.core.lang.Validator;
import com.pig4cloud.captcha.ArithmeticCaptcha;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.RedisUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 验证码相关的接口
*
* @author lengleng
* @date 2022/6/27
*/
@RestController
@RequestMapping("/code")
@RequiredArgsConstructor
public class ImageCodeEndpoint {
private static final Integer DEFAULT_IMAGE_WIDTH = 100;
private static final Integer DEFAULT_IMAGE_HEIGHT = 40;
/**
* 创建图形验证码并输出到响应流
* @param randomStr 随机字符串,用于缓存验证码
* @param response HTTP响应对象,用于输出验证码图片
*/
@SneakyThrows
@GetMapping("/image")
public void image(String randomStr, HttpServletResponse response) {
ArithmeticCaptcha captcha = new ArithmeticCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
if (Validator.isMobile(randomStr)) {
return;
}
String result = captcha.text();
RedisUtils.set(CacheConstants.DEFAULT_CODE_KEY + randomStr, result, SecurityConstants.CODE_TIME,
TimeUnit.SECONDS);
// 转换流信息写出
captcha.out(response.getOutputStream());
}
}
package com.pig4cloud.pig.auth.support;
import org.springframework.lang.Nullable;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.Serial;
import java.time.Instant;
import java.util.*;
/**
* 自定义OAuth2访问令牌生成器
*
* @author lengleng
* @date 2025/05/30
*/
public class CustomeOAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
private final StringKeyGenerator accessTokenGenerator = new Base64StringKeyGenerator(
Base64.getUrlEncoder().withoutPadding(), 96);
/**
* 生成OAuth2访问令牌
* @param context OAuth2令牌上下文
* @return 生成的访问令牌,如果令牌类型不是ACCESS_TOKEN或格式不是REFERENCE则返回null
* @see OAuth2TokenContext
* @see OAuth2AccessToken
*/
@Nullable
@Override
public OAuth2AccessToken generate(OAuth2TokenContext context) {
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || !OAuth2TokenFormat.REFERENCE
.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
return null;
}
String issuer = null;
if (context.getAuthorizationServerContext() != null) {
issuer = context.getAuthorizationServerContext().getIssuer();
}
RegisteredClient registeredClient = context.getRegisteredClient();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
// @formatter:off
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(context.getPrincipal().getName())
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.notBefore(issuedAt)
.id(UUID.randomUUID().toString());
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
}
// @formatter:on
if (this.accessTokenCustomizer != null) {
// @formatter:off
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
.registeredClient(context.getRegisteredClient())
.principal(context.getPrincipal())
.authorizationServerContext(context.getAuthorizationServerContext())
.authorizedScopes(context.getAuthorizedScopes())
.tokenType(context.getTokenType())
.authorizationGrantType(context.getAuthorizationGrantType());
if (context.getAuthorization() != null) {
accessTokenContextBuilder.authorization(context.getAuthorization());
}
if (context.getAuthorizationGrant() != null) {
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
}
// @formatter:on
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
this.accessTokenCustomizer.customize(accessTokenContext);
}
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
return new CustomeOAuth2AccessTokenGenerator.OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
this.accessTokenGenerator.generateKey(), accessTokenClaimsSet.getIssuedAt(),
accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());
}
/**
* 设置用于定制{@link OAuth2AccessToken}的{@link OAuth2TokenClaimsContext#getClaims()}的{@link OAuth2TokenCustomizer}
* @param accessTokenCustomizer
* 用于定制{@code OAuth2AccessToken}声明的{@link OAuth2TokenCustomizer}
* @throws IllegalArgumentException 当accessTokenCustomizer为null时抛出
*/
public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
this.accessTokenCustomizer = accessTokenCustomizer;
}
/**
* OAuth2访问令牌声明类,继承自OAuth2AccessToken并实现ClaimAccessor接口
*
* @author lengleng
* @date 2025/05/30
*/
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
@Serial
private static final long serialVersionUID = 1L;
private final Map<String, Object> claims;
/**
* 构造OAuth2访问令牌声明
* @param tokenType 令牌类型
* @param tokenValue 令牌值
* @param issuedAt 颁发时间
* @param expiresAt 过期时间
* @param scopes 权限范围集合
* @param claims 声明信息映射
*/
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
Set<String> scopes, Map<String, Object> claims) {
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
this.claims = claims;
}
/**
* 获取claims集合
* @return claims键值对集合
*/
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
}
}
package com.pig4cloud.pig.auth.support.base;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* OAuth2资源所有者基础认证转换器抽象类
*
* @param <T> 继承自OAuth2ResourceOwnerBaseAuthenticationToken的泛型类型
* @author lengleng
* @date 2025/05/30
*/
public abstract class OAuth2ResourceOwnerBaseAuthenticationConverter<T extends OAuth2ResourceOwnerBaseAuthenticationToken>
implements AuthenticationConverter {
/**
* 是否支持此convert
* @param grantType 授权类型
* @return
*/
public abstract boolean support(String grantType);
/**
* 校验参数
* @param request 请求
*/
public void checkParams(HttpServletRequest request) {
}
/**
* 构建具体类型的token
* @param clientPrincipal 客户端认证信息
* @param requestedScopes 请求的作用域集合
* @param additionalParameters 附加参数映射
* @return 构建完成的token对象
*/
public abstract T buildToken(Authentication clientPrincipal, Set<String> requestedScopes,
Map<String, Object> additionalParameters);
/**
* 将HttpServletRequest转换为Authentication对象
* @param request HTTP请求对象
* @return 认证信息对象
* @throws OAuth2AuthenticationException 当请求参数不合法或客户端未认证时抛出异常
*/
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!support(grantType)) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// 校验个性化参数
checkParams(request);
// 获取当前已经认证的客户端信息
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
if (clientPrincipal == null) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 扩展信息
Map<String, Object> additionalParameters = parameters.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)
&& !e.getKey().equals(OAuth2ParameterNames.SCOPE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
// 创建token
return buildToken(clientPrincipal, requestedScopes, additionalParameters);
}
}
package com.pig4cloud.pig.auth.support.base;
import lombok.Getter;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
import java.io.Serial;
import java.util.*;
/**
* OAuth2资源所有者基础认证令牌抽象类
*
* @author lengleng
* @date 2025/05/30
*/
public abstract class OAuth2ResourceOwnerBaseAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 1L;
@Getter
private final AuthorizationGrantType authorizationGrantType;
@Getter
private final Authentication clientPrincipal;
@Getter
private final Set<String> scopes;
@Getter
private final Map<String, Object> additionalParameters;
public OAuth2ResourceOwnerBaseAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, @Nullable Set<String> scopes,
@Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
this.authorizationGrantType = authorizationGrantType;
this.clientPrincipal = clientPrincipal;
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
this.additionalParameters = Collections.unmodifiableMap(
additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap());
}
/**
* 扩展模式一般不需要密码
*/
@Override
public Object getCredentials() {
return "";
}
/**
* 获取用户名
*/
@Override
public Object getPrincipal() {
return this.clientPrincipal;
}
}
/**
* 自定义认证模式接入的抽象实现
*/
package com.pig4cloud.pig.auth.support.base;
package com.pig4cloud.pig.auth.support.core;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.service.PigUser;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
/**
* OAuth2 Token 自定义增强实现类
*
* @author lengleng
* @date 2025/05/30
*/
public class CustomeOAuth2TokenCustomizer implements OAuth2TokenCustomizer<OAuth2TokenClaimsContext> {
/**
* 自定义OAuth 2.0 Token属性
* @param context 包含OAuth 2.0 Token属性的上下文
*/
@Override
public void customize(OAuth2TokenClaimsContext context) {
OAuth2TokenClaimsSet.Builder claims = context.getClaims();
claims.claim(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PROJECT_LICENSE);
String clientId = context.getAuthorizationGrant().getName();
claims.claim(SecurityConstants.CLIENT_ID, clientId);
// 客户端模式不返回具体用户信息
if (SecurityConstants.CLIENT_CREDENTIALS.equals(context.getAuthorizationGrantType().getValue())) {
return;
}
PigUser pigUser = (PigUser) context.getPrincipal().getPrincipal();
claims.claim(SecurityConstants.DETAILS_USER, pigUser);
claims.claim(SecurityConstants.DETAILS_USER_ID, pigUser.getId());
claims.claim(SecurityConstants.USERNAME, pigUser.getUsername());
}
}
package com.pig4cloud.pig.auth.support.core;
import com.pig4cloud.pig.auth.support.handler.FormAuthenticationFailureHandler;
import com.pig4cloud.pig.auth.support.handler.SsoLogoutSuccessHandler;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
/**
* 基于授权码模式的统一认证登录配置类,适用于Spring Security和SAS
*
* @author lengleng
* @date 2025/05/30
*/
public final class FormIdentityLoginConfigurer
extends AbstractHttpConfigurer<FormIdentityLoginConfigurer, HttpSecurity> {
@Override
public void init(HttpSecurity http) throws Exception {
http.formLogin(formLogin -> {
formLogin.loginPage("/token/login");
formLogin.loginProcessingUrl("/oauth2/form");
formLogin.failureHandler(new FormAuthenticationFailureHandler());
})
.logout(logout -> logout.logoutUrl("/oauth2/logout")
.logoutSuccessHandler(new SsoLogoutSuccessHandler())
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)) // SSO登出成功处理
.csrf(AbstractHttpConfigurer::disable);
}
}
package com.pig4cloud.pig.auth.support.core;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.pig4cloud.pig.common.core.util.WebUtils;
import com.pig4cloud.pig.common.security.service.PigUserDetailsService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.SneakyThrows;
import org.springframework.core.Ordered;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
import org.springframework.util.Assert;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
/**
* 基于DAO的认证提供者实现,用于处理用户名密码认证
*
* @author lengleng
* @date 2025/05/30
*/
public class PigDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* 用户未找到时用于PasswordEncoder#matches(CharSequence, String)的明文密码,避免SEC-2056问题
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private final static BasicAuthenticationConverter basicConvert = new BasicAuthenticationConverter();
/**
* 密码编码器
*/
private PasswordEncoder passwordEncoder;
/**
* 用户未找到时的加密密码,用于避免SEC-2056问题,某些密码编码器在密码格式无效时会短路处理
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public PigDaoAuthenticationProvider() {
setMessageSource(SpringUtil.getBean("securityMessageSource"));
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* 执行额外的身份验证检查
* @param userDetails 用户详细信息
* @param authentication 身份验证令牌
* @throws AuthenticationException 身份验证失败时抛出异常
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 只有密码模式需要校验密码
String grantType = WebUtils.getRequest().get().getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!StrUtil.equals(AuthorizationGrantType.PASSWORD.getValue(), grantType)) {
return;
}
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
/**
* 根据用户名检索用户详情
* @param username 用户名
* @param authentication 认证令牌
* @return 用户详情信息
* @throws InternalAuthenticationServiceException
* 当无法获取请求、未注册UserDetailsService或加载用户失败时抛出
* @throws UsernameNotFoundException 当用户名不存在时抛出
*/
@SneakyThrows
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
prepareTimingAttackProtection();
HttpServletRequest request = WebUtils.getRequest()
.orElseThrow(
(Supplier<Throwable>) () -> new InternalAuthenticationServiceException("web request is empty"));
String grantType = WebUtils.getRequest().get().getParameter(OAuth2ParameterNames.GRANT_TYPE);
String clientId = WebUtils.getRequest().get().getParameter(OAuth2ParameterNames.CLIENT_ID);
if (StrUtil.isBlank(clientId)) {
clientId = basicConvert.convert(request).getName();
}
Map<String, PigUserDetailsService> userDetailsServiceMap = SpringUtil
.getBeansOfType(PigUserDetailsService.class);
String finalClientId = clientId;
Optional<PigUserDetailsService> optional = userDetailsServiceMap.values()
.stream()
.filter(service -> service.support(finalClientId, grantType))
.max(Comparator.comparingInt(Ordered::getOrder));
if (optional.isEmpty()) {
throw new InternalAuthenticationServiceException("UserDetailsService error , not register");
}
try {
UserDetails loadedUser = optional.get().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
/**
* 创建认证成功后的Authentication对象
* @param principal 认证主体
* @param authentication 认证信息
* @param user 用户详情
* @return 认证成功后的Authentication对象
*/
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
/**
* 准备定时攻击保护,如果未找到用户编码密码为空则进行编码
*/
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
/**
* 防止时序攻击的缓解措施
* @param authentication 用户名密码认证令牌
*/
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
/**
* 设置用于编码和验证密码的PasswordEncoder实例
* @param passwordEncoder 密码编码器实例,不能为null
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return this.passwordEncoder;
}
/**
* 设置用户详情服务
* @param userDetailsService 用户详情服务
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
/**
* 设置用户详情密码服务
* @param userDetailsPasswordService 用户详情密码服务
*/
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
package com.pig4cloud.pig.auth.support.filter;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 安全认证配置属性类
*
* <p>
* 用于配置网关安全相关属性
* </p>
*
* @author lengleng
* @date 2025/05/30
* @since 2020/10/4
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties("security")
public class AuthSecurityConfigProperties {
/**
* 是否是微服务架构
*/
private boolean isMicro;
/**
* 网关解密登录前端密码 秘钥
*/
private String encodeKey;
/**
* 网关不需要校验验证码的客户端
*/
private List<String> ignoreClients;
}
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig.auth.support.filter;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.servlet.RepeatBodyRequestWrapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.util.Map;
/**
* 密码解密过滤器:用于处理登录请求中的密码解密
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PasswordDecoderFilter extends OncePerRequestFilter {
private final AuthSecurityConfigProperties authSecurityConfigProperties;
private static final String PASSWORD = "password";
private static final String KEY_ALGORITHM = "AES";
static {
// 关闭hutool 强制关闭Bouncy Castle库的依赖
SecureUtil.disableBouncyCastle();
}
/**
* 过滤器内部处理逻辑,用于处理登录请求中的密码解密
* @param request HTTP请求对象
* @param response HTTP响应对象
* @param chain 过滤器链
* @throws ServletException 如果发生servlet相关异常
* @throws IOException 如果发生I/O异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 不是登录请求,直接向下执行
if (!StrUtil.containsAnyIgnoreCase(request.getRequestURI(), SecurityConstants.OAUTH_TOKEN_URL)) {
chain.doFilter(request, response);
return;
}
// 将请求流转换为可多次读取的请求流
RepeatBodyRequestWrapper requestWrapper = new RepeatBodyRequestWrapper(request);
Map<String, String[]> parameterMap = requestWrapper.getParameterMap();
// 构建前端对应解密AES 因子
AES aes = new AES(Mode.CFB, Padding.NoPadding,
new SecretKeySpec(authSecurityConfigProperties.getEncodeKey().getBytes(), KEY_ALGORITHM),
new IvParameterSpec(authSecurityConfigProperties.getEncodeKey().getBytes()));
parameterMap.forEach((k, v) -> {
String[] values = parameterMap.get(k);
if (!PASSWORD.equals(k) || ArrayUtil.isEmpty(values)) {
return;
}
// 解密密码
String decryptPassword = aes.decryptStr(values[0]);
parameterMap.put(k, new String[] { decryptPassword });
});
chain.doFilter(requestWrapper, response);
}
}
package com.pig4cloud.pig.auth.support.filter;
/**
* 登录前处理器
*
* @author lengleng
* @date 2024/4/3
*/
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.exception.ValidateCodeException;
import com.pig4cloud.pig.common.core.util.RedisUtils;
import com.pig4cloud.pig.common.core.util.WebUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Optional;
/**
* 验证码过滤器:用于处理登录请求中的验证码校验
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
private final AuthSecurityConfigProperties authSecurityConfigProperties;
/**
* 过滤器内部处理逻辑,用于验证码校验
* @param request HTTP请求
* @param response HTTP响应
* @param filterChain 过滤器链
* @throws ServletException Servlet异常
* @throws IOException IO异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestUrl = request.getServletPath();
// 不是登录URL 请求直接跳过
if (!SecurityConstants.OAUTH_TOKEN_URL.equals(requestUrl)) {
filterChain.doFilter(request, response);
return;
}
// 如果登录URL 但是刷新token的请求,直接向下执行
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
filterChain.doFilter(request, response);
return;
}
// 如果是密码模式 && 客户端不需要校验验证码
boolean isIgnoreClient = authSecurityConfigProperties.getIgnoreClients().contains(WebUtils.getClientId());
if (StrUtil.equalsAnyIgnoreCase(grantType, SecurityConstants.PASSWORD, SecurityConstants.CLIENT_CREDENTIALS,
SecurityConstants.AUTHORIZATION_CODE) && isIgnoreClient) {
filterChain.doFilter(request, response);
return;
}
// 校验验证码 1. 客户端开启验证码 2. 短信模式
try {
checkCode();
filterChain.doFilter(request, response);
}
catch (ValidateCodeException validateCodeException) {
throw new OAuth2AuthenticationException(validateCodeException.getMessage());
}
}
/**
* 校验验证码
*/
private void checkCode() throws ValidateCodeException {
Optional<HttpServletRequest> request = WebUtils.getRequest();
String code = request.get().getParameter("code");
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException("验证码不能为空");
}
String randomStr = request.get().getParameter("randomStr");
// https://gitee.com/log4j/pig/issues/IWA0D
String mobile = request.get().getParameter("mobile");
if (StrUtil.isNotBlank(mobile)) {
randomStr = mobile;
}
String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
if (!RedisUtils.hasKey(key)) {
throw new ValidateCodeException("验证码不合法");
}
String saveCode = RedisUtils.get(key);
if (StrUtil.isBlank(saveCode)) {
RedisUtils.delete(key);
throw new ValidateCodeException("验证码不合法");
}
if (!StrUtil.equals(saveCode, code)) {
RedisUtils.delete(key);
throw new ValidateCodeException("验证码不合法");
}
}
}
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.HttpUtil;
import com.pig4cloud.pig.common.core.util.WebUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.io.IOException;
/**
* 表单登录失败处理逻辑
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
public class FormAuthenticationFailureHandler implements AuthenticationFailureHandler {
/**
* 当认证失败时调用
* @param request 认证尝试发生的请求
* @param response 响应对象
* @param exception 拒绝认证时抛出的异常
*/
@Override
@SneakyThrows
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
log.debug("表单登录失败:{}", exception.getLocalizedMessage());
// 获取当前请求的context-path
String contextPath = request.getContextPath();
// 构建重定向URL,加入context-path
String url = HttpUtil.encodeParams(
String.format("%s/token/login?error=%s", contextPath, exception.getMessage()),
CharsetUtil.CHARSET_UTF_8);
try {
WebUtils.getResponse().sendRedirect(url);
}
catch (IOException e) {
log.error("重定向失败", e);
}
}
}
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.log.event.SysLogEvent;
import com.pig4cloud.pig.common.log.util.LogTypeEnum;
import com.pig4cloud.pig.common.log.util.SysLogUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.io.IOException;
/**
* 认证失败处理器:处理用户认证失败事件并记录日志
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
public class PigAuthenticationFailureEventHandler implements AuthenticationFailureHandler {
private final MappingJackson2HttpMessageConverter errorHttpResponseConverter = new MappingJackson2HttpMessageConverter();
/**
* 当认证失败时调用
* @param request 认证请求
* @param response 认证响应
* @param exception 认证失败的异常
*/
@Override
@SneakyThrows
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
String username = request.getParameter(OAuth2ParameterNames.USERNAME);
log.info("用户:{} 登录失败,异常:{}", username, exception.getLocalizedMessage());
SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle("登录失败");
logVo.setLogType(LogTypeEnum.ERROR.getType());
logVo.setException(exception.getLocalizedMessage());
// 发送异步日志事件
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
if (StrUtil.isNotBlank(startTimeStr)) {
Long startTime = Long.parseLong(startTimeStr);
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
}
logVo.setCreateBy(username);
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
// 写出错误信息
sendErrorResponse(request, response, exception);
}
/**
* 发送错误响应
* @param request HTTP请求
* @param response HTTP响应
* @param exception 认证异常
* @throws IOException 写入响应时发生IO异常
*/
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
String errorMessage;
if (exception instanceof OAuth2AuthenticationException) {
OAuth2AuthenticationException authorizationException = (OAuth2AuthenticationException) exception;
errorMessage = StrUtil.isBlank(authorizationException.getError().getDescription())
? authorizationException.getError().getErrorCode()
: authorizationException.getError().getDescription();
}
else {
errorMessage = exception.getLocalizedMessage();
}
this.errorHttpResponseConverter.write(R.failed(errorMessage), MediaType.APPLICATION_JSON, httpResponse);
}
}
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.log.event.SysLogEvent;
import com.pig4cloud.pig.common.log.util.SysLogUtils;
import com.pig4cloud.pig.common.security.component.PigCustomOAuth2AccessTokenResponseHttpMessageConverter;
import com.pig4cloud.pig.common.security.service.PigUser;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* 处理认证成功事件的处理器
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
public class PigAuthenticationSuccessEventHandler implements AuthenticationSuccessHandler {
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new PigCustomOAuth2AccessTokenResponseHttpMessageConverter();
/**
* 用户认证成功时调用
* @param request 触发认证成功的请求
* @param response 响应对象
* @param authentication 认证过程中创建的认证对象
*/
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
Map<String, Object> map = accessTokenAuthentication.getAdditionalParameters();
if (MapUtil.isNotEmpty(map)) {
// 发送异步日志事件
PigUser userInfo = (PigUser) map.get(SecurityConstants.DETAILS_USER);
log.info("用户:{} 登录成功", userInfo.getName());
SecurityContextHolder.getContext().setAuthentication(accessTokenAuthentication);
SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle("登录成功");
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
if (StrUtil.isNotBlank(startTimeStr)) {
Long startTime = Long.parseLong(startTimeStr);
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
}
logVo.setCreateBy(userInfo.getName());
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
// 输出token
sendAccessTokenResponse(request, response, authentication);
}
/**
* 发送访问令牌响应
* @param request HTTP请求
* @param response HTTP响应
* @param authentication 认证信息
* @throws IOException 写入响应时可能抛出IO异常
*/
private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (!CollectionUtils.isEmpty(additionalParameters)) {
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
// 无状态 注意删除 context 上下文的信息
SecurityContextHolder.clearContext();
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}
}
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.core.util.WebUtils;
import com.pig4cloud.pig.common.log.event.SysLogEvent;
import com.pig4cloud.pig.common.log.util.SysLogUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;
/**
* 处理用户退出成功事件处理器
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
@Component
public class PigLogoutSuccessEventHandler implements ApplicationListener<LogoutSuccessEvent> {
/**
* 处理登出成功事件
* @param event 登出成功事件
*/
@Override
public void onApplicationEvent(LogoutSuccessEvent event) {
Authentication authentication = (Authentication) event.getSource();
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
handle(authentication);
}
}
/**
* 处理退出成功方法
* <p>
* 获取到登录的authentication 对象
* @param authentication 登录对象
*/
public void handle(Authentication authentication) {
log.info("用户:{} 退出成功", authentication.getPrincipal());
SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle("退出成功");
// 设置对应的token
WebUtils.getRequest().ifPresent(request -> {
logVo.setParams(request.getHeader(HttpHeaders.AUTHORIZATION));
// 计算请求耗时
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
if (StrUtil.isNotBlank(startTimeStr)) {
Long startTime = Long.parseLong(startTimeStr);
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
}
});
// 这边设置ServiceId
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
logVo.setServiceId(authentication.getCredentials().toString());
}
logVo.setCreateBy(authentication.getName());
// 发送异步日志事件
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
}
package com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import java.io.IOException;
/**
* SSO 登出成功处理器,根据客户端传入的跳转地址进行重定向
*
* @author lengleng
* @date 2025/05/30
*/
public class SsoLogoutSuccessHandler implements LogoutSuccessHandler {
private static final String REDIRECT_URL = "redirect_url";
/**
* 登出成功处理逻辑
* @param request HTTP请求
* @param response HTTP响应
* @param authentication 认证信息
* @throws IOException 重定向失败时抛出IO异常
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
if (response == null) {
return;
}
// 获取请求参数中是否包含 回调地址
String redirectUrl = request.getParameter(REDIRECT_URL);
if (StrUtil.isNotBlank(redirectUrl)) {
response.sendRedirect(redirectUrl);
}
else if (StrUtil.isNotBlank(request.getHeader(HttpHeaders.REFERER))) {
// 默认跳转referer 地址
String referer = request.getHeader(HttpHeaders.REFERER);
response.sendRedirect(referer);
}
}
}
package com.pig4cloud.pig.auth.support.password;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.Set;
/**
* OAuth2 资源所有者密码认证转换器
*
* @author lengleng
* @author jumuning
* @date 2025/05/30
*/
public class OAuth2ResourceOwnerPasswordAuthenticationConverter
extends OAuth2ResourceOwnerBaseAuthenticationConverter<OAuth2ResourceOwnerPasswordAuthenticationToken> {
/**
* 支持密码模式
* @param grantType 授权类型
*/
@Override
public boolean support(String grantType) {
return AuthorizationGrantType.PASSWORD.getValue().equals(grantType);
}
/**
* 构建OAuth2资源所有者密码认证令牌
* @param clientPrincipal 客户端主体认证信息
* @param requestedScopes 请求的作用域集合
* @param additionalParameters 附加参数映射
* @return 构建完成的OAuth2资源所有者密码认证令牌
*/
@Override
public OAuth2ResourceOwnerPasswordAuthenticationToken buildToken(Authentication clientPrincipal,
Set requestedScopes, Map additionalParameters) {
return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal,
requestedScopes, additionalParameters);
}
/**
* 校验扩展参数 密码模式密码必须不为空
* @param request 参数列表
*/
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// username (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// password (REQUIRED)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
}
}
package com.pig4cloud.pig.auth.support.password;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import java.util.Map;
/**
* OAuth2 资源所有者密码认证提供者
*
* @author lengleng
* @author jumuning
* @date 2025/05/30
* @since 0.2.3
*/
public class OAuth2ResourceOwnerPasswordAuthenticationProvider
extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerPasswordAuthenticationToken> {
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerPasswordAuthenticationProvider.class);
/**
* 使用提供的参数构造一个OAuth2ResourceOwnerPasswordAuthenticationProvider
* @param authenticationManager 认证管理器
* @param authorizationService 授权服务
* @param tokenGenerator 令牌生成器
* @since 0.2.3
*/
public OAuth2ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
super(authenticationManager, authorizationService, tokenGenerator);
}
/**
* 构建用户名密码认证令牌
* @param reqParameters 请求参数映射,包含用户名和密码
* @return 用户名密码认证令牌
*/
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String username = (String) reqParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) reqParameters.get(OAuth2ParameterNames.PASSWORD);
return new UsernamePasswordAuthenticationToken(username, password);
}
/**
* 判断是否支持指定的认证类型
* @param authentication 待验证的认证类型
* @return 如果支持该认证类型则返回true,否则返回false
*/
@Override
public boolean supports(Class<?> authentication) {
boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication);
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
return supports;
}
/**
* 检查客户端是否支持密码授权模式
* @param registeredClient 已注册的客户端
* @throws OAuth2AuthenticationException 当客户端不支持密码授权模式时抛出异常
*/
@Override
public void checkClient(RegisteredClient registeredClient) {
assert registeredClient != null;
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
}
}
package com.pig4cloud.pig.auth.support.password;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import java.io.Serial;
import java.util.Map;
import java.util.Set;
/**
* OAuth2资源所有者密码认证令牌
*
* @author lengleng
* @author jumuning
* @date 2025/05/30
*/
public class OAuth2ResourceOwnerPasswordAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造OAuth2资源所有者密码认证令牌
* @param authorizationGrantType 授权类型
* @param clientPrincipal 客户端认证主体
* @param scopes 权限范围集合
* @param additionalParameters 附加参数映射
*/
public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
}
}
/**
* 密码模式
*/
package com.pig4cloud.pig.auth.support.password;
package com.pig4cloud.pig.auth.support.sms;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.Set;
/**
* @author lengleng
* @date 2022-05-31
*
* 短信登录转换器
*/
public class OAuth2ResourceOwnerSmsAuthenticationConverter
extends OAuth2ResourceOwnerBaseAuthenticationConverter<OAuth2ResourceOwnerSmsAuthenticationToken> {
/**
* 是否支持此convert
* @param grantType 授权类型
* @return
*/
@Override
public boolean support(String grantType) {
return SecurityConstants.MOBILE.equals(grantType);
}
@Override
public OAuth2ResourceOwnerSmsAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes,
Map additionalParameters) {
return new OAuth2ResourceOwnerSmsAuthenticationToken(new AuthorizationGrantType(SecurityConstants.MOBILE),
clientPrincipal, requestedScopes, additionalParameters);
}
/**
* 校验扩展参数 密码模式密码必须不为空
* @param request 参数列表
*/
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// PHONE (REQUIRED)
String phone = parameters.getFirst(SecurityConstants.SMS_PARAMETER_NAME);
if (!StringUtils.hasText(phone) || parameters.get(SecurityConstants.SMS_PARAMETER_NAME).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, SecurityConstants.SMS_PARAMETER_NAME,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
}
}
package com.pig4cloud.pig.auth.support.sms;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import java.util.Map;
/**
* @author lengleng
* @date date
*
* 短信登录的核心处理
*/
public class OAuth2ResourceOwnerSmsAuthenticationProvider
extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerSmsAuthenticationToken> {
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerSmsAuthenticationProvider.class);
/**
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
* provided parameters.
* @param authenticationManager
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2ResourceOwnerSmsAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
super(authenticationManager, authorizationService, tokenGenerator);
}
@Override
public boolean supports(Class<?> authentication) {
boolean supports = OAuth2ResourceOwnerSmsAuthenticationToken.class.isAssignableFrom(authentication);
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
return supports;
}
@Override
public void checkClient(RegisteredClient registeredClient) {
assert registeredClient != null;
if (!registeredClient.getAuthorizationGrantTypes()
.contains(new AuthorizationGrantType(SecurityConstants.MOBILE))) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
}
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String phone = (String) reqParameters.get(SecurityConstants.SMS_PARAMETER_NAME);
return new UsernamePasswordAuthenticationToken(phone, null);
}
}
package com.pig4cloud.pig.auth.support.sms;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import java.util.Map;
import java.util.Set;
/**
* @author lengleng
* @description 短信登录token信息
*/
public class OAuth2ResourceOwnerSmsAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
public OAuth2ResourceOwnerSmsAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
}
}
/**
* 短信模式
*/
package com.pig4cloud.pig.auth.support.sms;
server:
port: 3000
spring:
application:
name: @artifactId@
cloud:
nacos:
username: @nacos.username@
password: @nacos.password@
discovery:
server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
config:
import:
- nacos:application-@profiles.active@.yml
- nacos:${spring.application.name}-@profiles.active@.yml
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
~
~ 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
~
~ http://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.
-->
<configuration debug="false" scan="false">
<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/>
<property name="log.path" value="logs/${spring.application.name}"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="FILE_LOG_PATTERN" value=":%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %t %c{1}: %m%n"/>
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
class="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
class="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- Console log output -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- Log file debug output -->
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- Log file error output -->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!--nacos 心跳 INFO 屏蔽-->
<logger name="com.alibaba.nacos" level="OFF">
<appender-ref ref="error"/>
</logger>
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 -->
<root level="DEBUG">
<appender-ref ref="console"/>
<appender-ref ref="debug"/>
<appender-ref ref="error"/>
</root>
</configuration>
<#assign content>
<div class="mb-7">
<h3 class="font-semibold text-2xl text-gray-800 dark:text-gray-200 text-center transition-colors">应用授权确认</h3>
<div class="mt-4 flex items-center justify-center text-sm">
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-800 rounded-full transition-all">
<div class="text-gray-700 dark:text-gray-300">
<#if principalName=="anonymousUser">
<span class="text-gray-500 dark:text-gray-400">未登录用户</span>
<#else>
<a href="https://pig4cloud.com" class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 font-medium transition-colors">
${principalName}
</a>
</#if>
</div>
</div>
</div>
</div>
<form id='confirmationForm' name='confirmationForm' action="${request.contextPath}/oauth2/authorize" method='post'>
<input type="hidden" name="client_id" value="${clientId}">
<input type="hidden" name="state" value="${state}">
<div class="space-y-6">
<div class="mb-4">
<p class="text-gray-700 dark:text-gray-300 mb-3 transition-colors">将获得以下权限:</p>
<div class="space-y-3 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 transition-all">
<#list scopeList as scope>
<div class="flex items-center">
<input type="checkbox" checked="checked" name="scope" value="${scope}"
class="h-4 w-4 text-purple-600 focus:ring-purple-500 dark:focus:ring-purple-400 border-gray-300 dark:border-gray-600 rounded transition-colors">
<label class="ml-3 text-gray-600 dark:text-gray-400 transition-colors">${scope}</label>
</div>
</#list>
</div>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mb-4 transition-colors">
授权后表明你已同意
<a href="#" class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 transition-colors">服务协议</a>
</div>
<div>
<button type="submit" id="write-email-btn"
class="w-full flex justify-center bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600 text-gray-100 p-3 rounded-lg tracking-wide font-semibold cursor-pointer transition-all duration-300 transform hover:scale-[1.02]">
确认授权
</button>
</div>
</div>
</form>
</#assign>
<#include "layout/base.ftl">
<!doctype html>
<html class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><#if title??>${title}<#else>Pig 统一身份认证</#if></title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#6366f1'
}
}
}
}
</script>
<style>
.dark-mode-toggle {
transition: all 0.3s ease;
}
.dark-mode-toggle:hover {
transform: scale(1.1);
}
.animate-spin-slow {
animation: spin 2s linear infinite;
}
</style>
<#if extraHead??>${extraHead}</#if>
</head>
<body class="min-h-screen">
<!-- 暗黑模式切换按钮 -->
<div class="fixed top-6 right-6 z-50">
<button id="theme-toggle" class="dark-mode-toggle p-3 rounded-xl bg-purple-100/90 dark:bg-gray-800/90 text-purple-600 dark:text-yellow-300 hover:bg-purple-200 dark:hover:bg-gray-700 focus:outline-none transition-all duration-300 shadow-lg">
<!-- 月亮图标 -->
<svg id="moon-icon" class="w-6 h-6 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
<!-- 太阳图标 -->
<svg id="sun-icon" class="w-6 h-6 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</button>
</div>
<!-- 背景渐变 -->
<div class="bg-purple-900 fixed top-0 left-0 bg-gradient-to-b from-gray-900 via-gray-900 to-purple-800 w-full h-full -z-10">
</div>
<div class="relative min-h-screen flex flex-col md:flex-row items-center justify-center px-4 py-12 md:px-8">
<!-- 左侧说明文本 -->
<div class="flex-1 max-w-xl mb-8 md:mb-0 md:mr-12 z-10">
<div class="hidden md:block">
<h2 class="text-3xl font-bold text-white mb-6">统一身份认证平台</h2>
<p class="text-gray-300 text-lg leading-relaxed opacity-90">
为企业提供一套集中式的账号、权限、认证、审计工具,帮助企业打通身份数据孤岛,实现"一个账号、一次认证、多点通行"的效果,强化企业安全体系的同时,提升组织管理效率,助力企业数字化升级转型。
</p>
</div>
</div>
<!-- 右侧内容区 -->
<div class="flex-1 w-full max-w-md z-10">
<div class="p-8 md:p-10 bg-white dark:bg-gray-900 rounded-2xl shadow-2xl backdrop-blur-sm transition-all duration-300">
<#if content??>${content}</#if>
<!-- 版权信息 -->
<div class="mt-8 pt-6 text-center text-gray-400 dark:text-gray-500 text-sm border-t border-gray-100 dark:border-gray-800">
<span>
Copyright © 2021-2025
<a href="#" class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 transition-colors">PIGCLOUD</a>
</span>
</div>
</div>
</div>
</div>
<!-- 底部波浪装饰 -->
<svg class="fixed bottom-0 left-0 w-full transition-colors -z-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
<path class="fill-white dark:fill-gray-900 transition-colors"
d="M0,0L40,42.7C80,85,160,171,240,197.3C320,224,400,192,480,154.7C560,117,640,75,720,74.7C800,75,880,117,960,154.7C1040,192,1120,224,1200,213.3C1280,203,1360,149,1400,122.7L1440,96L1440,320L1400,320C1360,320,1280,320,1200,320C1120,320,1040,320,960,320C880,320,800,320,720,320C640,320,560,320,480,320C400,320,320,320,240,320C160,320,80,320,40,320L0,320Z">
</path>
</svg>
<script>
// 主题切换功能
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;
// 检查本地存储中的主题设置
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
// 切换主题
themeToggle.addEventListener('click', () => {
html.classList.toggle('dark');
// 保存主题设置到本地存储
if (html.classList.contains('dark')) {
localStorage.theme = 'dark';
} else {
localStorage.theme = 'light';
}
// 添加点击动画效果
themeToggle.classList.add('animate-spin-slow');
setTimeout(() => {
themeToggle.classList.remove('animate-spin-slow');
}, 300);
});
</script>
</body>
</html>
<#assign content>
<div class="mb-8 text-center">
<svg class="w-16 h-16 mx-auto mb-4 text-purple-600 dark:text-purple-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 14.5V16.5M7 10.0288C7.47142 10.0288 7.86284 9.63734 7.86284 9.16592C7.86284 8.69449 7.47142 8.30307 7 8.30307C6.52858 8.30307 6.13716 8.69449 6.13716 9.16592C6.13716 9.63734 6.52858 10.0288 7 10.0288ZM17 10.0288C17.4714 10.0288 17.8628 9.63734 17.8628 9.16592C17.8628 8.69449 17.4714 8.30307 17 8.30307C16.5286 8.30307 16.1372 8.69449 16.1372 9.16592C16.1372 9.63734 16.5286 10.0288 17 10.0288ZM12 12.5C13.6569 12.5 15 11.1569 15 9.5C15 7.84315 13.6569 6.5 12 6.5C10.3431 6.5 9 7.84315 9 9.5C9 11.1569 10.3431 12.5 12 12.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="currentColor" stroke-width="1.5"/>
<path d="M17.6972 19.7C16.0993 18.0307 14.125 17 12 17C9.87499 17 7.90072 18.0307 6.30283 19.7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">安全便捷的企业级认证服务</p>
</div>
<form class="form-signin" action="${request.contextPath}/oauth2/form" method="post">
<input type="hidden" name="client_id" value="pig">
<input type="hidden" name="grant_type" value="password">
<div class="space-y-6">
<div class="">
<input class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors"
type="text" placeholder="账号" name="username" required>
</div>
<div class="relative">
<input placeholder="密码" type="password" name="password" required
class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors">
</div>
<#if error??>
<div class="relative text-center">
<span class="text-red-600 dark:text-red-400">${error}</span>
</div>
</#if>
<div>
<button type="submit"
class="w-full flex justify-center bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600 text-gray-100 p-3 rounded-lg tracking-wide font-semibold cursor-pointer transition-all duration-300 transform hover:scale-[1.02]">
登 录
</button>
</div>
</div>
</form>
</#assign>
<#include "layout/base.ftl">
FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:21-anolis
WORKDIR /pig-boot
ARG JAR_FILE=target/pig-boot.jar
COPY ${JAR_FILE} app.jar
EXPOSE 9999
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx1024m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java $JAVA_OPTS -jar app.jar
This diff is collapsed.
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* 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
*
* http://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 com.pig4cloud.pig;
import com.pig4cloud.pig.common.security.annotation.EnablePigResourceServer;
import com.pig4cloud.pig.common.swagger.annotation.EnablePigDoc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 单体版本启动器,运行此模块即可启动整个系统
*
* @author lengleng
* @date 2025/05/30
*/
@EnablePigDoc(value = "admin", isMicro = false)
@EnablePigResourceServer
@SpringBootApplication
public class PigBootApplication {
public static void main(String[] args) {
SpringApplication.run(PigBootApplication.class, args);
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
com.pig4cloud.pig.common.core.config.JacksonConfiguration
com.pig4cloud.pig.common.core.config.RedisTemplateConfiguration
com.pig4cloud.pig.common.core.config.RestTemplateConfiguration
com.pig4cloud.pig.common.core.util.SpringContextHolder
com.pig4cloud.pig.common.core.config.WebMvcConfiguration
${AnsiColor.BRIGHT_YELLOW}
::::::::: ::::::::::: ::::::::
:+: :+: :+: :+: :+:
+:+ +:+ +:+ +:+
+#++:++#+ +#+ :#:
+#+ +#+ +#+ +#+#
#+# #+# #+# #+#
### ########### ########
www.pig4cloud.com
Pig Microservice Architecture
${AnsiColor.DEFAULT}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment