This commit is contained in:
2025-04-19 17:31:40 +08:00
parent 6230c36cf2
commit e731ef8bcd
146 changed files with 6286 additions and 2 deletions

View File

@@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-module-sso</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tashow-module-sso-biz</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
system 模块下,我们放通用业务,支撑上层的核心业务。
例如说:用户、部门、权限、数据字典等等
</description>
<dependencies>
<!-- Spring Cloud 基础 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-env</artifactId>
</dependency>
<!-- 依赖服务 -->
<!-- <dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-module-system-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-module-infra-api</artifactId>
<version>${revision}</version>
</dependency>-->
<!-- 业务组件 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-permission</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-redis</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-rpc</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Job 定时任务相关 -->
<!-- <dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-job</artifactId>
</dependency>-->
<!-- 消息队列相关
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-mq</artifactId>
</dependency> -->
<!-- 服务保障相关 TODO 芋艿:暂时去掉 -->
<!-- <dependency>-->
<!-- <groupId>com.tashow.cloud</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-protection</artifactId>-->
<!-- </dependency>-->
<!-- 工具类相关 -->
<!-- <dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-excel</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-monitor</artifactId>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>com.xingyuv</groupId>
<artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
</dependency>
<dependency>
<groupId>com.xingyuv</groupId>
<artifactId>spring-boot-starter-captcha-plus</artifactId> <!-- 验证码,一般用于登录使用 -->
</dependency>
<dependency>
<groupId>org.dromara.hutool</groupId>
<artifactId>hutool-extra</artifactId> <!-- 邮件 -->
</dependency>
<!-- Sa-Token 权限认证Reactor响应式集成在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>1.42.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.42.0</version>
</dependency>
<!-- Sa-Token 插件整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.42.0</version>
</dependency>
<!-- Sa-Token OAuth2.0 模块 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-oauth2</artifactId>
<version>1.42.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.42.0</version>
</dependency>
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.26</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 视图引擎(在前后端不分离模式下提供视图支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,16 @@
package com.tashow.cloud;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TashowModuleSsoBizApplication {
public static void main(String[] args) {
SpringApplication.run(TashowModuleSsoBizApplication.class, args);
System.out.println("\nSa-Token-OAuth2 Server端启动成功配置如下");
System.out.println(SaOAuth2Manager.getServerConfig());
}
}

View File

@@ -0,0 +1,27 @@
package com.tashow.cloud.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis Plus配置类
*/
@Configuration
@MapperScan("com.tashow.cloud.mapper")
public class MybatisPlusConfig {
/**
* 配置分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@@ -0,0 +1,21 @@
package com.tashow.cloud.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* SSO模块安全配置类
*/
@Configuration
public class SsoSecurityConfig {
/**
* 配置密码编码器
* 使用BCrypt强哈希算法
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,378 @@
package com.tashow.cloud.controller;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;
import cn.dev33.satoken.oauth2.consts.GrantType;
import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;
import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
import com.tashow.cloud.model.SystemUser;
import com.tashow.cloud.service.SystemUserService;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.HashMap;
import java.util.Map;
/**
* Sa-Token OAuth2 Server端 控制器
*/
@RestController
public class SaOAuth2ServerController {
@Autowired
private SystemUserService userService;
// OAuth2-Server 端:处理所有 OAuth2 相关请求
@RequestMapping("/oauth2/*")
@PermitAll
public Object request() {
System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2ServerProcessor.instance.dister();
}
// Sa-Token OAuth2 定制化配置
@Autowired
@PermitAll
public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
// 添加 client 信息
oauth2Server.addClient(
new SaClientModel()
.setClientId("1001") // client id
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥
.addAllowRedirectUris("*") // 所有允许授权的 url
.addContractScopes("openid", "userid", "userinfo") // 所有签约的权限
.addAllowGrantTypes( // 所有允许的授权模式
GrantType.authorization_code, // 授权码式
GrantType.implicit, // 隐式式
GrantType.refresh_token, // 刷新令牌
GrantType.password, // 密码式
GrantType.client_credentials // 客户端模式
)
);
// 可以添加更多 client 信息,只要保持 clientId 唯一就行了
// oauth2Server.addClient(...)
// 配置未登录时返回的View
oauth2Server.notLoginView = () -> {
String msg = "当前会话在OAuth-Server端尚未登录请先访问"
+ "<a href='/oauth2/login-page' target='_blank'>登录页面</a>"
+ "或 <a href='/sso/register-page' target='_blank'>注册用户</a>"
+ ",进行登录之后,刷新页面开始授权";
return msg;
};
// 配置:登录处理函数
oauth2Server.doLoginHandle = (name, pwd) -> {
// 从数据库查询用户
SystemUser user = userService.getUserByUsername(name);
// 验证用户是否存在且密码是否正确
if(user != null && userService.validatePassword(pwd, user.getPassword())) {
// 用户状态检查
if(user.getStatus() != 0) {
return SaResult.error("账号已被禁用");
}
// 登录成功记录用户IP
StpUtil.login(user.getId());
// 获取当前的HTTP请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ip = "unknown";
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
ip = getClientIp(request);
}
userService.updateLoginInfo(user.getId(), ip);
return SaResult.ok();
}
return SaResult.error("账号名或密码错误");
};
// 配置:确认授权时返回的 view
oauth2Server.confirmView = (clientId, scopes) -> {
String scopeStr = SaFoxUtil.convertListToString(scopes);
String yesCode =
"fetch('/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "', {method: 'POST'})" +
".then(res => res.json())" +
".then(res => location.reload())";
String res = "<p>应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?</p>"
+ "<p>" +
" <button onclick=\"" + yesCode + "\">同意</button>" +
" <button onclick='history.back()'>拒绝</button>" +
"</p>";
return res;
};
}
/**
* 获取客户端真实IP地址
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况第一个IP为客户端真实IP
if (ip != null && ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
return ip;
}
/**
* 登录页面接口 - 返回登录页面HTML
*/
@GetMapping("/oauth2/login-page")
@PermitAll
public Object loginPage() {
String html = "<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>OAuth2登录</title>\n" +
" <style>\n" +
" body {\n" +
" font-family: Arial, sans-serif;\n" +
" display: flex;\n" +
" justify-content: center;\n" +
" align-items: center;\n" +
" height: 100vh;\n" +
" margin: 0;\n" +
" background-color: #f5f5f5;\n" +
" }\n" +
" .login-container {\n" +
" background-color: white;\n" +
" padding: 30px;\n" +
" border-radius: 8px;\n" +
" box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n" +
" width: 350px;\n" +
" }\n" +
" h2 {\n" +
" text-align: center;\n" +
" margin-bottom: 20px;\n" +
" }\n" +
" .form-group {\n" +
" margin-bottom: 15px;\n" +
" }\n" +
" label {\n" +
" display: block;\n" +
" margin-bottom: 5px;\n" +
" font-weight: bold;\n" +
" }\n" +
" input {\n" +
" width: 100%;\n" +
" padding: 10px;\n" +
" border: 1px solid #ddd;\n" +
" border-radius: 4px;\n" +
" box-sizing: border-box;\n" +
" }\n" +
" button {\n" +
" width: 100%;\n" +
" padding: 10px;\n" +
" background-color: #007bff;\n" +
" color: white;\n" +
" border: none;\n" +
" border-radius: 4px;\n" +
" cursor: pointer;\n" +
" font-size: 16px;\n" +
" }\n" +
" button:hover {\n" +
" background-color: #0069d9;\n" +
" }\n" +
" .error-message {\n" +
" color: red;\n" +
" margin-top: 15px;\n" +
" text-align: center;\n" +
" display: none;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"login-container\">\n" +
" <h2>用户登录</h2>\n" +
" <form id=\"loginForm\">\n" +
" <div class=\"form-group\">\n" +
" <label for=\"username\">用户名</label>\n" +
" <input type=\"text\" id=\"username\" name=\"username\" required>\n" +
" </div>\n" +
" <div class=\"form-group\">\n" +
" <label for=\"password\">密码</label>\n" +
" <input type=\"password\" id=\"password\" name=\"password\" required>\n" +
" </div>\n" +
" <button type=\"submit\">登录</button>\n" +
" </form>\n" +
" <div id=\"errorMessage\" class=\"error-message\"></div>\n" +
" </div>\n" +
" <script>\n" +
" document.getElementById('loginForm').addEventListener('submit', function(e) {\n" +
" e.preventDefault();\n" +
" \n" +
" const username = document.getElementById('username').value;\n" +
" const password = document.getElementById('password').value;\n" +
" \n" +
" fetch('/oauth2/doLogin?name=' + encodeURIComponent(username) + '&pwd=' + encodeURIComponent(password), {\n" +
" method: 'POST'\n" +
" })\n" +
" .then(response => response.json())\n" +
" .then(data => {\n" +
" if (data.code === 200) {\n" +
" window.location.href = '/oauth2/login-success';\n" +
" } else {\n" +
" const errorMessage = document.getElementById('errorMessage');\n" +
" errorMessage.textContent = data.msg || '登录失败';\n" +
" errorMessage.style.display = 'block';\n" +
" }\n" +
" })\n" +
" .catch(error => {\n" +
" console.error('登录请求失败:', error);\n" +
" const errorMessage = document.getElementById('errorMessage');\n" +
" errorMessage.textContent = '网络错误,请稍后重试';\n" +
" errorMessage.style.display = 'block';\n" +
" });\n" +
" });\n" +
" </script>\n" +
"</body>\n" +
"</html>";
return html;
}
/**
* 登录接口 - 处理表单提交
*/
@PostMapping("/oauth2/login")
@PermitAll
public Object login(
@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletRequest request) {
// 查询用户
SystemUser user = userService.getUserByUsername(username);
// 验证用户
if (user == null || !userService.validatePassword(password, user.getPassword())) {
return SaResult.error("账号或密码错误");
}
// 检查用户状态
if (user.getStatus() != 0) {
return SaResult.error("账号已被禁用");
}
// 登录
StpUtil.login(user.getId());
// 更新登录信息
String ip = getClientIp(request);
userService.updateLoginInfo(user.getId(), ip);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("tokenValue", StpUtil.getTokenValue());
result.put("tokenName", StpUtil.getTokenName());
return SaResult.data(result);
}
/**
* 登录成功页面 - 当未指定回调地址时的默认页面
*/
@GetMapping("/oauth2/login-success")
@PermitAll
public Object loginSuccess() {
String html = "<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>授权成功</title>\n" +
" <style>\n" +
" body {\n" +
" font-family: Arial, sans-serif;\n" +
" display: flex;\n" +
" justify-content: center;\n" +
" align-items: center;\n" +
" height: 100vh;\n" +
" margin: 0;\n" +
" background-color: #f5f5f5;\n" +
" flex-direction: column;\n" +
" }\n" +
" .success-container {\n" +
" background-color: white;\n" +
" padding: 30px;\n" +
" border-radius: 8px;\n" +
" box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n" +
" width: 400px;\n" +
" text-align: center;\n" +
" }\n" +
" h2 {\n" +
" color: #28a745;\n" +
" margin-bottom: 20px;\n" +
" }\n" +
" p {\n" +
" margin: 10px 0;\n" +
" color: #333;\n" +
" }\n" +
" .userinfo {\n" +
" margin-top: 20px;\n" +
" text-align: left;\n" +
" padding: 15px;\n" +
" background-color: #f8f9fa;\n" +
" border-radius: 5px;\n" +
" }\n" +
" .button {\n" +
" display: inline-block;\n" +
" margin-top: 20px;\n" +
" padding: 10px 20px;\n" +
" background-color: #007bff;\n" +
" color: white;\n" +
" border: none;\n" +
" border-radius: 4px;\n" +
" text-decoration: none;\n" +
" cursor: pointer;\n" +
" }\n" +
" .button:hover {\n" +
" background-color: #0069d9;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"success-container\">\n" +
" <h2>OAuth2授权成功</h2>\n" +
" <p>您已成功登录并授权给应用</p>\n" +
" <div class=\"userinfo\">\n" +
" <p>当前登录状态:已登录</p>\n" +
" <p>令牌信息已生成</p>\n" +
" </div>\n" +
" <a href=\"javascript:void(0)\" onclick=\"window.close()\" class=\"button\">关闭窗口</a>\n" +
" </div>\n" +
"</body>\n" +
"</html>";
return html;
}
}

View File

@@ -0,0 +1,654 @@
package com.tashow.cloud.controller;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.sso.config.SaSsoServerConfig;
import cn.dev33.satoken.sso.processor.SaSsoServerProcessor;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.dtflys.forest.Forest;
import com.tashow.cloud.model.SystemUser;
import com.tashow.cloud.service.SystemUserService;
import com.tashow.cloud.service.impl.SystemUserServiceImpl;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Sa-Token-SSO Server端 Controller
*/
@RestController
public class SsoServerController {
@Autowired
private SystemUserService userService;
@Autowired
private SystemUserServiceImpl userServiceImpl;
/**
* SSO-Server端处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口)
*/
@RequestMapping("/sso/*")
@PermitAll
public Object ssoRequest() {
return SaSsoServerProcessor.instance.dister();
}
/**
* 配置SSO相关参数
*/
@Autowired
@PermitAll
private void configSso(SaSsoServerConfig ssoServer) {
// 配置未登录时返回的View
ssoServer.notLoginView = () -> {
String msg = "当前会话在SSO-Server端尚未登录请访问"
+ "<a href='/sso/login-page' target='_blank'>登录页面</a>"
+ "或 <a href='/sso/register-page' target='_blank'>注册用户</a>"
+ ",登录成功后刷新页面开始授权";
return msg;
};
// 配置:登录处理函数
ssoServer.doLoginHandle = (name, pwd) -> {
// 从数据库查询用户
SystemUser user = userService.getUserByUsername(name);
// 验证用户是否存在且密码是否正确
if(user != null && userService.validatePassword(pwd, user.getPassword())) {
// 用户状态检查
if(user.getStatus() != 0) {
return SaResult.error("账号已被禁用");
}
// 登录成功记录用户IP
StpUtil.login(user.getId());
// 获取当前的HTTP请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ip = "unknown";
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
ip = getClientIp(request);
}
userService.updateLoginInfo(user.getId(), ip);
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("账号或密码错误");
};
// 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
ssoServer.sendHttp = url -> {
try {
System.out.println("------ 发起请求:" + url);
String resStr = Forest.get(url).executeAsString();
System.out.println("------ 请求结果:" + resStr);
return resStr;
} catch (Exception e) {
e.printStackTrace();
return null;
}
};
}
/**
* 获取客户端真实IP地址
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况第一个IP为客户端真实IP
if (ip != null && ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
return ip;
}
/**
* 单点登录检查 - 自定义接口,方便前端调用
*/
@GetMapping("/sso/checkLogin")
@PermitAll
public Object ssoCheckLogin() {
Map<String, Object> result = new HashMap<>();
result.put("isLogin", StpUtil.isLogin());
if (StpUtil.isLogin()) {
result.put("loginId", StpUtil.getLoginId());
result.put("tokenValue", StpUtil.getTokenValue());
}
return result;
}
/**
* 登录页面接口 - 返回登录页面HTML
*/
@GetMapping("/sso/login-page")
@PermitAll
public Object loginPage() {
String html = "<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>单点登录</title>\n" +
" <style>\n" +
" body {\n" +
" font-family: Arial, sans-serif;\n" +
" display: flex;\n" +
" justify-content: center;\n" +
" align-items: center;\n" +
" height: 100vh;\n" +
" margin: 0;\n" +
" background-color: #f5f5f5;\n" +
" }\n" +
" .login-container {\n" +
" background-color: white;\n" +
" padding: 30px;\n" +
" border-radius: 8px;\n" +
" box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n" +
" width: 350px;\n" +
" }\n" +
" h2 {\n" +
" text-align: center;\n" +
" margin-bottom: 20px;\n" +
" }\n" +
" .form-group {\n" +
" margin-bottom: 15px;\n" +
" }\n" +
" label {\n" +
" display: block;\n" +
" margin-bottom: 5px;\n" +
" font-weight: bold;\n" +
" }\n" +
" input {\n" +
" width: 100%;\n" +
" padding: 10px;\n" +
" border: 1px solid #ddd;\n" +
" border-radius: 4px;\n" +
" box-sizing: border-box;\n" +
" }\n" +
" button {\n" +
" width: 100%;\n" +
" padding: 10px;\n" +
" background-color: #007bff;\n" +
" color: white;\n" +
" border: none;\n" +
" border-radius: 4px;\n" +
" cursor: pointer;\n" +
" font-size: 16px;\n" +
" }\n" +
" button:hover {\n" +
" background-color: #0069d9;\n" +
" }\n" +
" .error-message {\n" +
" color: red;\n" +
" margin-top: 15px;\n" +
" text-align: center;\n" +
" display: none;\n" +
" }\n" +
" .register-link {\n" +
" display: block;\n" +
" text-align: center;\n" +
" margin-top: 20px;\n" +
" color: #28a745;\n" +
" text-decoration: none;\n" +
" }\n" +
" .register-link:hover {\n" +
" text-decoration: underline;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"login-container\">\n" +
" <h2>用户登录</h2>\n" +
" <form id=\"loginForm\">\n" +
" <div class=\"form-group\">\n" +
" <label for=\"username\">用户名</label>\n" +
" <input type=\"text\" id=\"username\" name=\"username\" required>\n" +
" </div>\n" +
" <div class=\"form-group\">\n" +
" <label for=\"password\">密码</label>\n" +
" <input type=\"password\" id=\"password\" name=\"password\" required>\n" +
" </div>\n" +
" <button type=\"submit\">登录</button>\n" +
" </form>\n" +
" <div id=\"errorMessage\" class=\"error-message\"></div>\n" +
" <a href=\"/sso/register-page\" class=\"register-link\">没有账号?点击注册</a>\n" +
" </div>\n" +
" <script>\n" +
" document.getElementById('loginForm').addEventListener('submit', function(e) {\n" +
" e.preventDefault();\n" +
" \n" +
" const username = document.getElementById('username').value;\n" +
" const password = document.getElementById('password').value;\n" +
" \n" +
" fetch('/sso/login', {\n" +
" method: 'POST',\n" +
" headers: {\n" +
" 'Content-Type': 'application/x-www-form-urlencoded',\n" +
" },\n" +
" body: 'username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password)\n" +
" })\n" +
" .then(response => response.json())\n" +
" .then(data => {\n" +
" if (data.code === 200) {\n" +
" window.location.href = '/sso/login-success';\n" +
" } else {\n" +
" const errorMessage = document.getElementById('errorMessage');\n" +
" errorMessage.textContent = data.msg || '登录失败';\n" +
" errorMessage.style.display = 'block';\n" +
" }\n" +
" })\n" +
" .catch(error => {\n" +
" console.error('登录请求失败:', error);\n" +
" const errorMessage = document.getElementById('errorMessage');\n" +
" errorMessage.textContent = '网络错误,请稍后重试';\n" +
" errorMessage.style.display = 'block';\n" +
" });\n" +
" });\n" +
" </script>\n" +
"</body>\n" +
"</html>";
return html;
}
/**
* 登录接口 - 前端表单登录请求
*/
@PostMapping("/sso/login")
@PermitAll
public Object login(
@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletRequest request) {
// 查询用户
SystemUser user = userService.getUserByUsername(username);
// 验证用户
if (user == null || !userService.validatePassword(password, user.getPassword())) {
return SaResult.error("账号或密码错误");
}
// 检查用户状态
if (user.getStatus() != 0) {
return SaResult.error("账号已被禁用");
}
// 登录
StpUtil.login(user.getId());
// 更新登录信息
String ip = getClientIp(request);
userService.updateLoginInfo(user.getId(), ip);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("tokenValue", StpUtil.getTokenValue());
result.put("tokenName", StpUtil.getTokenName());
return SaResult.data(result);
}
/**
* 注销登录
*/
@GetMapping("/sso/logout")
@PermitAll
public Object logout() {
if (StpUtil.isLogin()) {
StpUtil.logout();
return SaResult.ok("注销成功");
}
return SaResult.error("当前会话未登录");
}
/**
* 查询用户信息 - 自定义接口,用于获取当前登录用户信息
*/
@GetMapping("/sso/userinfo")
@PermitAll
public Object ssoUserInfo() {
if (!StpUtil.isLogin()) {
return SaResult.error("用户未登录");
}
Long userId = Long.valueOf(StpUtil.getLoginId().toString());
SystemUser user = userService.getUserById(userId);
if (user == null) {
return SaResult.error("用户不存在");
}
// 获取用户角色信息
List<String> roleCodes = userServiceImpl.getUserRoleCodes(userId);
List<String> roleNames = userServiceImpl.getUserRoleNames(userId);
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", user.getId());
userInfo.put("username", user.getUsername());
userInfo.put("nickname", user.getNickname());
userInfo.put("email", user.getEmail());
userInfo.put("mobile", user.getMobile());
userInfo.put("avatar", user.getAvatar());
userInfo.put("roleCodes", roleCodes);
userInfo.put("roleNames", roleNames);
return SaResult.data(userInfo);
}
/**
* 登录成功页面 - 当未指定回调地址时的默认页面
*/
@GetMapping("/sso/login-success")
@PermitAll
public Object loginSuccess() {
String html = "<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>登录成功</title>\n" +
" <style>\n" +
" body {\n" +
" font-family: Arial, sans-serif;\n" +
" display: flex;\n" +
" justify-content: center;\n" +
" align-items: center;\n" +
" height: 100vh;\n" +
" margin: 0;\n" +
" background-color: #f5f5f5;\n" +
" flex-direction: column;\n" +
" }\n" +
" .success-container {\n" +
" background-color: white;\n" +
" padding: 30px;\n" +
" border-radius: 8px;\n" +
" box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n" +
" width: 400px;\n" +
" text-align: center;\n" +
" }\n" +
" h2 {\n" +
" color: #28a745;\n" +
" margin-bottom: 20px;\n" +
" }\n" +
" p {\n" +
" margin: 10px 0;\n" +
" color: #333;\n" +
" }\n" +
" .userinfo {\n" +
" margin-top: 20px;\n" +
" text-align: left;\n" +
" padding: 15px;\n" +
" background-color: #f8f9fa;\n" +
" border-radius: 5px;\n" +
" }\n" +
" .button {\n" +
" display: inline-block;\n" +
" margin-top: 20px;\n" +
" padding: 10px 20px;\n" +
" background-color: #007bff;\n" +
" color: white;\n" +
" border: none;\n" +
" border-radius: 4px;\n" +
" text-decoration: none;\n" +
" cursor: pointer;\n" +
" }\n" +
" .button:hover {\n" +
" background-color: #0069d9;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"success-container\">\n" +
" <h2>登录成功</h2>\n" +
" <p>您已成功登录到单点登录系统</p>\n" +
" <div class=\"userinfo\" id=\"userinfo\">\n" +
" <p>正在加载用户信息...</p>\n" +
" </div>\n" +
" <a href=\"/sso/logout\" class=\"button\">退出登录</a>\n" +
" </div>\n" +
" <script>\n" +
" // 获取用户信息\n" +
" fetch('/sso/userinfo')\n" +
" .then(response => response.json())\n" +
" .then(data => {\n" +
" if (data.code === 200 && data.data) {\n" +
" const user = data.data;\n" +
" let html = '<p><strong>用户ID</strong>' + user.id + '</p>';\n" +
" html += '<p><strong>用户名:</strong>' + user.username + '</p>';\n" +
" html += '<p><strong>昵称:</strong>' + user.nickname + '</p>';\n" +
" if (user.email) {\n" +
" html += '<p><strong>邮箱:</strong>' + user.email + '</p>';\n" +
" }\n" +
" if (user.mobile) {\n" +
" html += '<p><strong>手机:</strong>' + user.mobile + '</p>';\n" +
" }\n" +
" if (user.roleNames && user.roleNames.length > 0) {\n" +
" html += '<p><strong>角色:</strong>' + user.roleNames.join(', ') + '</p>';\n" +
" }\n" +
" document.getElementById('userinfo').innerHTML = html;\n" +
" } else {\n" +
" document.getElementById('userinfo').innerHTML = '<p>无法获取用户信息</p>';\n" +
" }\n" +
" })\n" +
" .catch(error => {\n" +
" console.error('获取用户信息失败:', error);\n" +
" document.getElementById('userinfo').innerHTML = '<p>获取用户信息时发生错误</p>';\n" +
" });\n" +
" </script>\n" +
"</body>\n" +
"</html>";
return html;
}
/**
* 注册页面 - 返回注册页面HTML
*/
@GetMapping("/sso/register-page")
@PermitAll
public Object registerPage() {
String html = "<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>用户注册</title>\n" +
" <style>\n" +
" body {\n" +
" font-family: Arial, sans-serif;\n" +
" display: flex;\n" +
" justify-content: center;\n" +
" align-items: center;\n" +
" height: 100vh;\n" +
" margin: 0;\n" +
" background-color: #f5f5f5;\n" +
" }\n" +
" .register-container {\n" +
" background-color: white;\n" +
" padding: 30px;\n" +
" border-radius: 8px;\n" +
" box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n" +
" width: 400px;\n" +
" }\n" +
" h2 {\n" +
" text-align: center;\n" +
" margin-bottom: 20px;\n" +
" }\n" +
" .form-group {\n" +
" margin-bottom: 15px;\n" +
" }\n" +
" label {\n" +
" display: block;\n" +
" margin-bottom: 5px;\n" +
" font-weight: bold;\n" +
" }\n" +
" input {\n" +
" width: 100%;\n" +
" padding: 10px;\n" +
" border: 1px solid #ddd;\n" +
" border-radius: 4px;\n" +
" box-sizing: border-box;\n" +
" }\n" +
" button {\n" +
" width: 100%;\n" +
" padding: 10px;\n" +
" background-color: #28a745;\n" +
" color: white;\n" +
" border: none;\n" +
" border-radius: 4px;\n" +
" cursor: pointer;\n" +
" font-size: 16px;\n" +
" }\n" +
" button:hover {\n" +
" background-color: #218838;\n" +
" }\n" +
" .error-message {\n" +
" color: red;\n" +
" margin-top: 15px;\n" +
" text-align: center;\n" +
" display: none;\n" +
" }\n" +
" .success-message {\n" +
" color: green;\n" +
" margin-top: 15px;\n" +
" text-align: center;\n" +
" display: none;\n" +
" }\n" +
" .login-link {\n" +
" display: block;\n" +
" text-align: center;\n" +
" margin-top: 20px;\n" +
" color: #007bff;\n" +
" text-decoration: none;\n" +
" }\n" +
" .login-link:hover {\n" +
" text-decoration: underline;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"register-container\">\n" +
" <h2>用户注册</h2>\n" +
" <form id=\"registerForm\">\n" +
" <div class=\"form-group\">\n" +
" <label for=\"username\">用户名 *</label>\n" +
" <input type=\"text\" id=\"username\" name=\"username\" required>\n" +
" </div>\n" +
" <div class=\"form-group\">\n" +
" <label for=\"password\">密码 *</label>\n" +
" <input type=\"password\" id=\"password\" name=\"password\" required>\n" +
" </div>\n" +
" <div class=\"form-group\">\n" +
" <label for=\"confirmPassword\">确认密码 *</label>\n" +
" <input type=\"password\" id=\"confirmPassword\" name=\"confirmPassword\" required>\n" +
" </div>\n" +
" <div class=\"form-group\">\n" +
" <label for=\"nickname\">昵称 *</label>\n" +
" <input type=\"text\" id=\"nickname\" name=\"nickname\" required>\n" +
" </div>\n" +
" <div class=\"form-group\">\n" +
" <label for=\"email\">邮箱</label>\n" +
" <input type=\"email\" id=\"email\" name=\"email\">\n" +
" </div>\n" +
" <div class=\"form-group\">\n" +
" <label for=\"mobile\">手机号</label>\n" +
" <input type=\"tel\" id=\"mobile\" name=\"mobile\">\n" +
" </div>\n" +
" <button type=\"submit\">注册</button>\n" +
" </form>\n" +
" <div id=\"errorMessage\" class=\"error-message\"></div>\n" +
" <div id=\"successMessage\" class=\"success-message\"></div>\n" +
" <a href=\"/sso/login-page\" class=\"login-link\">已有账号?点击登录</a>\n" +
" </div>\n" +
" <script>\n" +
" document.getElementById('registerForm').addEventListener('submit', function(e) {\n" +
" e.preventDefault();\n" +
" \n" +
" const username = document.getElementById('username').value;\n" +
" const password = document.getElementById('password').value;\n" +
" const confirmPassword = document.getElementById('confirmPassword').value;\n" +
" const nickname = document.getElementById('nickname').value;\n" +
" const email = document.getElementById('email').value;\n" +
" const mobile = document.getElementById('mobile').value;\n" +
" \n" +
" // 简单前端验证\n" +
" if (password !== confirmPassword) {\n" +
" const errorMessage = document.getElementById('errorMessage');\n" +
" errorMessage.textContent = '两次输入的密码不一致';\n" +
" errorMessage.style.display = 'block';\n" +
" return;\n" +
" }\n" +
" \n" +
" // 构建表单数据\n" +
" const formData = new URLSearchParams();\n" +
" formData.append('username', username);\n" +
" formData.append('password', password);\n" +
" formData.append('nickname', nickname);\n" +
" if (email) formData.append('email', email);\n" +
" if (mobile) formData.append('mobile', mobile);\n" +
" \n" +
" // 发送注册请求\n" +
" fetch('/user/register', {\n" +
" method: 'POST',\n" +
" headers: {\n" +
" 'Content-Type': 'application/x-www-form-urlencoded',\n" +
" },\n" +
" body: formData\n" +
" })\n" +
" .then(response => response.json())\n" +
" .then(data => {\n" +
" if (data.code === 200) {\n" +
" const successMessage = document.getElementById('successMessage');\n" +
" successMessage.textContent = '注册成功3秒后跳转到登录页面...';\n" +
" successMessage.style.display = 'block';\n" +
" \n" +
" // 清空表单\n" +
" document.getElementById('registerForm').reset();\n" +
" \n" +
" // 3秒后跳转到登录页面\n" +
" setTimeout(() => {\n" +
" window.location.href = '/sso/login-page';\n" +
" }, 3000);\n" +
" } else {\n" +
" const errorMessage = document.getElementById('errorMessage');\n" +
" errorMessage.textContent = data.msg || '注册失败,请稍后重试';\n" +
" errorMessage.style.display = 'block';\n" +
" }\n" +
" })\n" +
" .catch(error => {\n" +
" console.error('注册请求失败:', error);\n" +
" const errorMessage = document.getElementById('errorMessage');\n" +
" errorMessage.textContent = '网络错误,请稍后重试';\n" +
" errorMessage.style.display = 'block';\n" +
" });\n" +
" });\n" +
" </script>\n" +
"</body>\n" +
"</html>";
return html;
}
}

View File

@@ -0,0 +1,49 @@
package com.tashow.cloud.framework.core;
import com.xingyuv.captcha.service.CaptchaCacheService;
import lombok.Setter;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 基于 Redis 实现验证码的存储
*
* @author 星语
*/
@Setter
public class RedisCaptchaServiceImpl implements CaptchaCacheService {
private StringRedisTemplate stringRedisTemplate;
@Override
public String type() {
return "redis";
}
@Override
public void set(String key, String value, long expiresInSeconds) {
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
}
@Override
public boolean exists(String key) {
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
}
@Override
public void delete(String key) {
stringRedisTemplate.delete(key);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public Long increment(String key, long val) {
return stringRedisTemplate.opsForValue().increment(key,val);
}
}

View File

@@ -0,0 +1,46 @@
package com.tashow.cloud.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tashow.cloud.model.SystemUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 系统用户Mapper接口
*/
@Mapper
public interface SystemUserMapper extends BaseMapper<SystemUser> {
/**
* 获取用户角色ID列表
*
* @param userId 用户ID
* @return 角色ID列表
*/
@Select("SELECT role_id FROM system_user_role WHERE user_id = #{userId} AND deleted = 0")
List<Long> selectUserRoleIds(@Param("userId") Long userId);
/**
* 获取用户角色名称列表
*
* @param userId 用户ID
* @return 角色名称列表
*/
@Select("SELECT r.name FROM system_role r " +
"JOIN system_user_role ur ON r.id = ur.role_id " +
"WHERE ur.user_id = #{userId} AND r.deleted = 0 AND ur.deleted = 0 AND r.status = 0")
List<String> selectUserRoleNames(@Param("userId") Long userId);
/**
* 获取用户角色编码列表
*
* @param userId 用户ID
* @return 角色编码列表
*/
@Select("SELECT r.code FROM system_role r " +
"JOIN system_user_role ur ON r.id = ur.role_id " +
"WHERE ur.user_id = #{userId} AND r.deleted = 0 AND ur.deleted = 0 AND r.status = 0")
List<String> selectUserRoleCodes(@Param("userId") Long userId);
}

View File

@@ -0,0 +1,110 @@
package com.tashow.cloud.model;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统用户实体类
*/
@Data
public class SystemUser {
/**
* 用户ID
*/
private Long id;
/**
* 用户账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 用户昵称
*/
private String nickname;
/**
* 备注
*/
private String remark;
/**
* 部门ID
*/
private Long deptId;
/**
* 岗位编号数组
*/
private String postIds;
/**
* 用户邮箱
*/
private String email;
/**
* 手机号码
*/
private String mobile;
/**
* 用户性别
*/
private Integer sex;
/**
* 头像地址
*/
private String avatar;
/**
* 帐号状态0正常 1停用
*/
private Integer status;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private LocalDateTime loginDate;
/**
* 创建者
*/
private String creator;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新者
*/
private String updater;
/**
* a更新时间
*/
private LocalDateTime updateTime;
/**
* 是否删除
*/
private Boolean deleted;
/**
* 租户编号
*/
private Long tenantId;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* 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.tashow.cloud.security.config;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* 自定义过滤器
*/
@Component
public class MyFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
System.out.println("进入自定义过滤器");
try {
// 先 set 上下文,再调用 Sa-Token 同步 API并在 finally 里清除上下文
SaReactorSyncHolder.setContext(exchange);
System.out.println(StpUtil.isLogin());
}
finally {
SaReactorSyncHolder.clearContext();
}
return chain.filter(exchange);
}
}

View File

@@ -0,0 +1,41 @@
package com.tashow.cloud.security.config;
import com.tashow.cloud.security.security.config.AuthorizeRequestsCustomizer;
import com.tashow.cloud.systemapi.enums.ApiConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
/**
* System 模块的 Security 配置
*/
@Configuration(proxyBeanMethods = false, value = "systemSecurityConfiguration")
public class SecurityConfiguration {
@Bean("systemAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() {
@Override
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
// TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案
// Swagger 接口文档
registry.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/webjars/**").permitAll()
.requestMatchers("/swagger-ui").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
;
// Druid 监控
registry.requestMatchers("/druid/**").permitAll();
// Spring Boot Actuator 的安全配置
registry.requestMatchers("/actuator").permitAll()
.requestMatchers("/actuator/**").permitAll();
// RPC 服务的安全配置
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
}
};
}
}

View File

@@ -0,0 +1,58 @@
package com.tashow.cloud.service;
import com.tashow.cloud.model.SystemUser;
/**
* 系统用户服务接口
*/
public interface SystemUserService {
/**
* 根据用户名查询用户
*
* @param username 用户名
* @return 用户信息
*/
SystemUser getUserByUsername(String username);
/**
* 验证用户密码
*
* @param rawPassword 原始密码
* @param encodedPassword 编码后的密码
* @return 是否匹配
*/
boolean validatePassword(String rawPassword, String encodedPassword);
/**
* 根据用户ID获取用户信息
*
* @param userId 用户ID
* @return 用户信息
*/
SystemUser getUserById(Long userId);
/**
* 更新用户登录信息
*
* @param userId 用户ID
* @param ip 登录IP
*/
void updateLoginInfo(Long userId, String ip);
/**
* 注册新用户
*
* @param user 用户信息
* @return 是否注册成功
*/
boolean registerUser(SystemUser user);
/**
* 加密密码
*
* @param rawPassword 原始密码
* @return 加密后的密码
*/
String encodePassword(String rawPassword);
}

View File

@@ -0,0 +1,104 @@
package com.tashow.cloud.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tashow.cloud.mapper.SystemUserMapper;
import com.tashow.cloud.model.SystemUser;
import com.tashow.cloud.service.SystemUserService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
/**
* 系统用户服务实现类
*/
@Service
public class SystemUserServiceImpl extends ServiceImpl<SystemUserMapper, SystemUser> implements SystemUserService {
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Override
public SystemUser getUserByUsername(String username) {
return getOne(new LambdaQueryWrapper<SystemUser>()
.eq(SystemUser::getUsername, username)
.eq(SystemUser::getStatus, 0)
.eq(SystemUser::getDeleted, false));
}
@Override
public boolean validatePassword(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
@Override
public SystemUser getUserById(Long userId) {
return getById(userId);
}
@Override
public void updateLoginInfo(Long userId, String ip) {
SystemUser user = new SystemUser();
user.setId(userId);
user.setLoginIp(ip);
LocalDateTime now = LocalDateTime.now();
user.setLoginDate(now);
user.setUpdateTime(now);
updateById(user);
}
@Override
public boolean registerUser(SystemUser user) {
// 设置默认值
LocalDateTime now = LocalDateTime.now();
user.setCreateTime(now);
user.setUpdateTime(now);
user.setLoginDate(now);
user.setCreator("system");
user.setUpdater("system");
if (user.getAvatar() == null || user.getAvatar().isEmpty()) {
user.setAvatar("https://sa-token.cc/logo.png"); // 设置默认头像
}
user.setTenantId(0L); // 默认租户ID
user.setDeleted(false); // 设置为未删除
return save(user);
}
@Override
public String encodePassword(String rawPassword) {
return passwordEncoder.encode(rawPassword);
}
/**
* 获取用户角色ID列表
*
* @param userId 用户ID
* @return 角色ID列表
*/
public List<Long> getUserRoleIds(Long userId) {
return baseMapper.selectUserRoleIds(userId);
}
/**
* 获取用户角色名称列表
*
* @param userId 用户ID
* @return 角色名称列表
*/
public List<String> getUserRoleNames(Long userId) {
return baseMapper.selectUserRoleNames(userId);
}
/**
* 获取用户角色编码列表
*
* @param userId 用户ID
* @return 角色编码列表
*/
public List<String> getUserRoleCodes(Long userId) {
return baseMapper.selectUserRoleCodes(userId);
}
}

View File

@@ -0,0 +1 @@
com.tashow.cloud.framework.core.RedisCaptchaServiceImpl

View File

@@ -0,0 +1,66 @@
--- #################### 注册中心 + 配置中心相关配置 ####################
spring:
cloud:
nacos:
server-addr: 43.139.42.137:8848 # Nacos 服务器地址
username: nacos # Nacos 账号
password: nacos # Nacos 密码
discovery: # 【配置中心】配置项
namespace: 5c8b8fe6-9a89-4ae3-975e-ef3bf560ff82 # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: 5c8b8fe6-9a89-4ae3-975e-ef3bf560ff82 # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
# 数据源配置项
autoconfigure:
exclude:
- de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 1 # 初始连接数
min-idle: 1 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: master
datasource:
master:
url: jdbc:mysql://43.139.42.137:8406/tashow-platform?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
username: tashow-platform
password: tashow123,
# slave: # 模拟从库,可根据自己需要修改
# lazy: true # 开启懒加载,保证启动速度
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
# username: root
# password: 123456

View File

@@ -0,0 +1,236 @@
server:
port: 48082
spring:
application:
name: sso-server
profiles:
active: local
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
# Servlet 配置
servlet:
# 文件上传相关配置项
multipart:
max-file-size: 16MB # 单个文件大小
max-request-size: 32MB # 设置总上传的文件大小
# Jackson 配置项
jackson:
serialization:
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
fail-on-empty-beans: false # 允许序列化无属性的 Bean
# Cache 配置项
cache:
type: REDIS
redis:
time-to-live: 1h # 设置过期时间为 1 小时
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
--- #################### 接口文档配置 ####################
springdoc:
api-docs:
enabled: true # 1. 是否开启 Swagger 接文档的元数据
path: /v3/api-docs
swagger-ui:
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
path: /swagger-ui
default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档
knife4j:
enable: true # 2.2 是否开启 Swagger 文档的 Knife4j UI 界面
setting:
language: zh_cn
# MyBatis Plus 的配置项
mybatis-plus:
configuration:
map-underscore-to-camel-case: true #虽然默认为 true ,但是还是显示去指定下。
global-config:
db-config:
id-type: NONE # "智能"模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
banner: false # 关闭控制台的 Banner 打印
type-aliases-package: ${tashow.info.base-package}.dal.dataobject
encryptor:
password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成
mybatis-plus-join:
banner: false # 关闭控制台的 Banner 打印
# Spring Data Redis 配置
spring:
data:
redis:
repositories:
enabled: false # 项目未使用到 Spring Data Redis 的 Repository所以直接禁用保证启动速度
# VO 转换(数据翻译)相关
easy-trans:
is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口
--- #################### RPC 远程调用相关配置 ####################
--- #################### 消息队列相关 ####################
spring:
# Kafka 配置项,对应 KafkaProperties 配置类
kafka:
# Kafka Producer 配置项
producer:
acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
retries: 3 # 发送失败时,重试发送的次数
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
# Kafka Consumer 配置项
consumer:
auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: '*'
# Kafka Consumer Listener 监听器配置
listener:
missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
--- #################### 定时任务相关配置 ####################
xxl:
job:
executor:
appname: ${spring.application.name} # 执行器 AppName
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
accessToken: default_token # 执行器通讯TOKEN
--- #################### 验证码相关配置 ####################
aj:
captcha:
jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
cache-type: redis # 缓存 local/redis...
cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存
timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行
type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
water-mark: 他秀 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 UnicodeLinux 可能需要转 unicode
interference-options: 0 # 滑动干扰项(0/1/2)
req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false
req-get-lock-limit: 5 # 验证失败5次get接口锁定
req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔
req-get-minute-limit: 30 # get 接口一分钟内请求数限制
req-check-minute-limit: 60 # check 接口一分钟内请求数限制
req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制
--- #################### 芋道相关配置 ####################
tashow:
info:
version: 1.0.0
base-package: com.tashow.cloud.system
web:
admin-ui:
url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址
xss:
enable: false
exclude-urls: # 如下 url仅仅是为了演示去掉配置也没关系
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
swagger:
title: 管理后台
description: 提供管理员管理的所有功能
version: ${tashow.info.version}
tenant: # 多租户相关配置项
enable: true
ignore-urls:
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
- /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号
- /admin-api/system/captcha/get-image # 获取图片验证码,和租户无关
- /admin-api/system/captcha/get # 获取图片验证码,和租户无关
- /admin-api/system/captcha/check # 校验图片验证码,和租户无关
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
- /rpc-api/system/tenant/valid # 防止递归。避免调用 /rpc-api/system/tenant/valid 接口时,又去触发 /rpc-api/system/tenant/valid 去校验
- /rpc-api/system/tenant/id-list # 获得租户列表的时候,无需传递租户编号
- /rpc-api/system/oauth2/token/check # 访问令牌校验时,无需传递租户编号;主要解决上传文件的场景,前端不会传递 tenant-id
ignore-tables:
- system_tenant
- system_tenant_package
- system_dict_data
- system_dict_type
- system_error_code
- system_menu
- system_sms_channel
- system_sms_template
- system_sms_log
- system_sensitive_word
- system_oauth2_client
- system_mail_account
- system_mail_template
- system_mail_log
- system_notify_template
ignore-caches:
- user_role_ids
- permission_menu_ids
- oauth_client
- notify_template
- mail_account
- mail_template
- sms_template
sms-code: # 短信验证码相关的配置项
expire-times: 10m
send-frequency: 1m
send-maximum-quantity-per-day: 10
begin-code: 9999 # 这里配置 9999 的原因是,测试方便。
end-code: 9999 # 这里配置 9999 的原因是,测试方便。
debug: false
sa-token:
# ------- SSO-模式一相关配置 (非模式一不需要配置)
# cookie:
# 配置 Cookie 作用域
# domain: stp.com
# ------- SSO-模式二相关配置
sso-server:
# Ticket有效期 (单位: 秒),默认五分钟
ticket-timeout: 300
# 所有允许的授权回调地址
allow-url: "*"
home-route: "/sso/login-success"
# ------- SSO-模式三相关配置 下面的配置在使用SSO模式三时打开
# 是否打开模式三
is-http: true
# OAuth2相关配置
oauth2:
is-code: true
is-implicit: true
is-password: true
is-client: true
is-refresh: true
# 授权码有效期 (单位: 秒)
code-timeout: 300
# Access-Token有效期 (单位: 秒)
access-token-timeout: 7200
# Refresh-Token有效期 (单位: 秒)
refresh-token-timeout: 604800
# 客户端模式 Access-Token 有效期 (单位: 秒)
client-token-timeout: 7200
# 登录后的默认跳转页面
home-route: "/oauth2/login-success"
sign:
# API 接口调用秘钥
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器文档有步骤说明

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,76 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 变量 yudao.info.base-package基础业务包 -->
<springProperty scope="context" name="tashow.info.base-package" source="tashow.info.base-package"/>
<!-- 格式化输出:%d 表示日期,%X{tid} SkWalking 链路追踪编号,%thread 表示线程名,%-5level级别从左显示 5 个字符宽度,%msg日志消息%n是换行符 -->
<property name="PATTERN_DEFAULT" value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} | %highlight(${LOG_LEVEL_PATTERN:-%5p} ${PID:- }) | %boldYellow(%thread [%tid]) %boldGreen(%-40.40logger{39}) | %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 控制台 Appender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">     
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
</appender>
<!-- 文件 Appender -->
<!-- 参考 Spring Boot 的 file-appender.xml 编写 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
<!-- 日志文件名 -->
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 滚动后的日志文件名 -->
<fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志。默认的,如果队列的 80% 已满,则会丢弃 TRACT、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- SkyWalking GRPC 日志收集实现日志中心。注意SkyWalking 8.4.0 版本开始支持 -->
<appender name="GRPC" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
</appender>
<!-- 本地环境 -->
<springProfile name="local">
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC"/>
<appender-ref ref="GRPC"/>
</root>
</springProfile>
</configuration>