Commit 243a95c0 by zhangxingmin

微服务核心组件配置

parent de57dff1
### 通用开发环境 ###
# IDE 配置
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 编辑器临时文件
*~
~$*
*.tmp
*.bak
*.swp
### 操作系统文件 ###
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
# Windows
Thumbs.db
ehthumbs.db
[Dd]esktop.ini
$RECYCLE.BIN/
# Linux
.directory
.trash-*
### 编程语言相关 ###
# Java
*.class
*.jar
*.war
*.ear
*.log
target/
build/
out/
bin/
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
*.pyc
env/
venv/
.python-version
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnp/
.pnp.js
# C/C++
*.o
*.ko
*.obj
*.exe
*.dll
*.so
*.dylib
# Rust
/target/
**/*.rs.bk
### 构建系统 ###
# Maven
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
# Gradle
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
# Android
*.apk
*.ap_
*.dex
*.class
gen/
bin/
### 日志文件 ###
*.log
logs/
*.logs
### 测试文件 ###
coverage/
.nyc_output/
test-results/
### 系统文件 ###
*.cab
*.msi
*.msix
*.msm
*.msp
### 文档文件 ###
*.pdf
*.doc
*.docx
*.xls
*.xlsx
*.ppt
*.pptx
### 压缩文件 ###
*.zip
*.tar.gz
*.7z
*.rar
*.gz
### 自定义规则 ###
# 项目特定文件
.env
config.local.yml
secrets.ini
# 临时文件
temp/
tmp/
dump.rdb
# 大文件
*.large
*.h5
*.dat
# 排除特定文件
**/pom.properties
**/inputFiles.lst
**/createdFiles.lst
...@@ -19,6 +19,12 @@ ...@@ -19,6 +19,12 @@
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> </dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Boot Starter --> <!-- Spring Boot Starter -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
...@@ -26,6 +32,10 @@ ...@@ -26,6 +32,10 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
...@@ -64,6 +74,22 @@ ...@@ -64,6 +74,22 @@
<artifactId>spring-cloud-starter-bootstrap</artifactId> <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
<!-- MyBatis Plus 扩展功能 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
</dependency>
<!-- 阿里数据库连接池 --> <!-- 阿里数据库连接池 -->
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
......
...@@ -7,7 +7,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder; ...@@ -7,7 +7,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.yd") @SpringBootApplication(scanBasePackages = "com.yd")
@MapperScan("com.yd.**.mapper") @MapperScan("com.yd.**.dao")
@EnableFeignClients(basePackages = "com.yd") @EnableFeignClients(basePackages = "com.yd")
public class AuthApplication { public class AuthApplication {
......
package com.yd.auth.core.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
@Configuration
public class AuthMybatisPlusConfig {
/**
* 分页插件配置(必须)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 使用正确的枚举值 DbType.MYSQL
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* 自动填充字段配置(如创建时间、更新时间)
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
};
}
}
package com.yd.auth.core.config;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定义密码加密器,实现自定义的密码比对逻辑
*/
@Component
public class CustomPasswordEncoder implements PasswordEncoder {
/**
* 加密方法:这里简化处理,直接返回原始密码(实际项目中建议加密)
* @param rawPassword 原始密码(用户输入的明文)
* @return 加密后的密码(这里返回明文,仅作示例)
*/
@Override
public String encode(CharSequence rawPassword) {
// 实际业务中,建议在这里实现你的加密逻辑(如加盐MD5、SHA等)
return rawPassword.toString();
}
/**
* 密码比对方法:自定义比对逻辑
* @param rawPassword 前端传参的密码(用户输入的明文)
* @param encodedPassword 数据库中存储的密码(可能是加密后的)
* @return 比对结果(true表示一致,false表示不一致)
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
// 自定义比对逻辑:例如直接比较两个字符串是否相等
// 注意:实际项目中需根据加密规则解密后比对,或加密输入密码后比对
return rawPassword.toString().equals(encodedPassword);
}
}
...@@ -33,9 +33,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { ...@@ -33,9 +33,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
this.authenticationFilter = authenticationFilter; this.authenticationFilter = authenticationFilter;
} }
//密码校验器
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // return new BCryptPasswordEncoder();
// 返回自定义的加密器实例
return new CustomPasswordEncoder();
} }
@Bean @Bean
......
package com.yd.auth.core.dto; package com.yd.auth.core.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data; import lombok.Data;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
...@@ -23,7 +24,7 @@ public class AuthUserDto implements UserDetails { ...@@ -23,7 +24,7 @@ public class AuthUserDto implements UserDetails {
/** /**
* 用户唯一标识(业务ID),用于业务层面的用户识别 * 用户唯一标识(业务ID),用于业务层面的用户识别
*/ */
private String userUid; private String userBizId;
/** /**
* 登录账号(实现UserDetails接口必需) * 登录账号(实现UserDetails接口必需)
...@@ -35,6 +36,11 @@ public class AuthUserDto implements UserDetails { ...@@ -35,6 +36,11 @@ public class AuthUserDto implements UserDetails {
*/ */
private String password; private String password;
/**
* 是否超级管理员(0:否 1:是)
*/
private Integer isSuperAdmin;
// ============== Spring Security账户状态属性 ============== // ============== Spring Security账户状态属性 ==============
/** /**
* 账户是否启用(默认true启用) * 账户是否启用(默认true启用)
......
...@@ -5,6 +5,7 @@ import com.yd.user.service.model.SysUser; ...@@ -5,6 +5,7 @@ import com.yd.user.service.model.SysUser;
import com.yd.user.service.service.ISysUserService; import com.yd.user.service.service.ISysUserService;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
...@@ -23,6 +24,7 @@ import java.util.Objects; ...@@ -23,6 +24,7 @@ import java.util.Objects;
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired @Autowired
@Qualifier("sysUserServiceImpl")
private ISysUserService iSysUserService; private ISysUserService iSysUserService;
private final JwtTokenProvider tokenProvider; private final JwtTokenProvider tokenProvider;
...@@ -42,10 +44,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { ...@@ -42,10 +44,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 2. 检查令牌是否存在且有效 // 2. 检查令牌是否存在且有效
if (token != null && tokenProvider.validateToken(token)) { if (token != null && tokenProvider.validateToken(token)) {
// 3. 从JWT令牌中提取用户唯一标识(业务ID) // 3. 从JWT令牌中提取用户唯一标识(业务ID)
String userUid = tokenProvider.getUserUidFromToken(token); String userBizId = tokenProvider.getUserBizIdFromToken(token);
// 4. 根据用户唯一标识查询用户详细信息(包括权限信息) // 4. 根据用户唯一标识查询用户详细信息(包括权限信息)
UserDetails userDetails = queryUserDetails(userUid); UserDetails userDetails = queryUserDetails(userBizId);
// 5. 创建Spring Security认证对象 // 5. 创建Spring Security认证对象
// - userDetails: 包含用户身份和权限信息 // - userDetails: 包含用户身份和权限信息
...@@ -64,19 +66,19 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { ...@@ -64,19 +66,19 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
/** /**
* 查询用户信息返回spring security认证对象UserDetails * 查询用户信息返回spring security认证对象UserDetails
* @param userUid * @param userBizId
* @return spring security认证对象UserDetails * @return spring security认证对象UserDetails
*/ */
public UserDetails queryUserDetails(String userUid) { public UserDetails queryUserDetails(String userBizId) {
AuthUserDto authUserDto = null; AuthUserDto authUserDto = null;
SysUser sysUser = iSysUserService.queryOne(userUid); SysUser sysUser = iSysUserService.queryOne(userBizId);
if (Objects.isNull(sysUser)) { if (Objects.isNull(sysUser)) {
throw new UsernameNotFoundException("用户不存在:"); throw new UsernameNotFoundException("用户不存在:");
} }
authUserDto = new AuthUserDto(); authUserDto = new AuthUserDto();
BeanUtils.copyProperties(sysUser,authUserDto); BeanUtils.copyProperties(sysUser,authUserDto);
authUserDto.setUsername(sysUser.getUserName());
// 查询用户角色 TODO // 查询用户角色 TODO
// authUserDto.setRoles(roles); // authUserDto.setRoles(roles);
...@@ -89,6 +91,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { ...@@ -89,6 +91,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); return bearerToken.substring(7);
} }
return null; return bearerToken;
} }
} }
package com.yd.auth.core.security; package com.yd.auth.core.security;
import com.yd.auth.core.dto.AuthUserDto;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
...@@ -11,83 +14,132 @@ import org.springframework.stereotype.Component; ...@@ -11,83 +14,132 @@ import org.springframework.stereotype.Component;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.Base64;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* JWT令牌提供者
* 修复了密钥长度不足的问题,并优化了异常处理
*/
@Component @Component
public class JwtTokenProvider { public class JwtTokenProvider {
@Value("${jwt.secret:1}") // 使用512位长度的密钥(通过配置文件设置)
@Value("${jwt.secret}")
private String jwtSecret; private String jwtSecret;
@Value("${jwt.expiration:1}") @Value("${jwt.expiration}") // 默认24小时(单位毫秒)
private int jwtExpiration; private int jwtExpiration;
/**
* 生成JWT令牌
*/
public String generateToken(Authentication authentication) { public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); if (authentication == null || authentication.getPrincipal() == null) {
throw new JwtAuthenticationException("无法从空认证信息生成令牌");
return Jwts.builder() }
.setSubject(userDetails.getUsername())
.claim("roles", getRoles(userDetails.getAuthorities())) AuthUserDto authUserDto = (AuthUserDto) authentication.getPrincipal();
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration * 1000L)) try {
.signWith(getSigningKey(), SignatureAlgorithm.HS512) return Jwts.builder()
.compact(); .setSubject(authUserDto.getUserBizId())
.claim("roles", getRoles(authUserDto.getAuthorities()))
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
} catch (Exception e) {
throw new JwtAuthenticationException("生成JWT令牌失败: " + e.getMessage());
}
} }
/**
* 提取角色信息
*/
private Collection<String> getRoles(Collection<? extends GrantedAuthority> authorities) { private Collection<String> getRoles(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream() return authorities.stream()
.map(GrantedAuthority::getAuthority) .map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public String getUserUidFromToken(String token) { /**
return Jwts.parserBuilder() * 从令牌中获取用户名
.setSigningKey(getSigningKey()) */
.build() public String getUsernameFromToken(String token) {
.parseClaimsJws(token) try {
.getBody() return Jwts.parserBuilder()
.getSubject(); .setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
} catch (Exception e) {
throw new JwtAuthenticationException("从令牌获取用户名失败: " + e.getMessage());
}
} }
public String getUsernameFromToken(String token) { /**
return Jwts.parserBuilder() * 从令牌中获取用户业务id
.setSigningKey(getSigningKey()) */
.build() public String getUserBizIdFromToken(String token) {
.parseClaimsJws(token) try {
.getBody() return Jwts.parserBuilder()
.getSubject(); .setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
} catch (Exception e) {
throw new JwtAuthenticationException("从令牌获取用户名失败: " + e.getMessage());
}
} }
/**
* 从令牌中获取认证信息
*/
public Authentication getAuthentication(String token) { public Authentication getAuthentication(String token) {
if (validateToken(token)) { try {
String username = getUsernameFromToken(token); if (validateToken(token)) {
// 这里应该从数据库加载用户信息,但为了性能通常只加载基本信息 String username = getUsernameFromToken(token);
// 实际项目中可以在这里加载用户权限 return new UsernamePasswordAuthenticationToken(
return new UsernamePasswordAuthenticationToken( username,
username, null,
null, getAuthoritiesFromToken(token)
// 从token中提取权限 );
getAuthoritiesFromToken(token) }
); return null;
} catch (Exception e) {
throw new JwtAuthenticationException("获取认证信息失败: " + e.getMessage());
} }
return null;
} }
/**
* 从令牌中获取权限信息
*/
private Collection<? extends GrantedAuthority> getAuthoritiesFromToken(String token) { private Collection<? extends GrantedAuthority> getAuthoritiesFromToken(String token) {
Claims claims = Jwts.parserBuilder() try {
.setSigningKey(getSigningKey()) Claims claims = Jwts.parserBuilder()
.build() .setSigningKey(getSigningKey())
.parseClaimsJws(token) .build()
.getBody(); .parseClaimsJws(token)
.getBody();
@SuppressWarnings("unchecked")
Collection<String> roles = (Collection<String>) claims.get("roles"); @SuppressWarnings("unchecked")
Collection<String> roles = (Collection<String>) claims.get("roles");
return roles.stream()
.map(role -> (GrantedAuthority) () -> role) return roles.stream()
.collect(Collectors.toList()); .map(role -> (GrantedAuthority) () -> role)
.collect(Collectors.toList());
} catch (Exception e) {
throw new JwtAuthenticationException("从令牌获取权限信息失败: " + e.getMessage());
}
} }
/**
* 验证令牌有效性
*/
public boolean validateToken(String token) { public boolean validateToken(String token) {
try { try {
Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token); Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
...@@ -101,21 +153,77 @@ public class JwtTokenProvider { ...@@ -101,21 +153,77 @@ public class JwtTokenProvider {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new JwtAuthenticationException("JWT令牌无效"); throw new JwtAuthenticationException("JWT令牌无效");
} catch (Exception e) { } catch (Exception e) {
throw new JwtAuthenticationException("无效的JWT令牌"); throw new JwtAuthenticationException("无效的JWT令牌: " + e.getMessage());
} }
} }
/**
* 获取签名密钥
*/
private SecretKey getSigningKey() { private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtSecret.getBytes()); try {
if (jwtSecret == null || jwtSecret.trim().isEmpty()) {
throw new JwtAuthenticationException("JWT密钥未配置");
}
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
} catch (Exception e) {
throw new JwtAuthenticationException("获取签名密钥失败: " + e.getMessage());
}
}
/**
* 生成符合HS512要求的密钥(用于生成配置文件中的密钥)
*/
public static String generateSecureKey() {
try {
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
return Base64.getEncoder().encodeToString(key.getEncoded());
} catch (Exception e) {
throw new RuntimeException("生成安全密钥失败", e);
}
} }
public int getJwtExpiration() { public int getJwtExpiration() {
return jwtExpiration; return jwtExpiration;
} }
/**
* JWT认证异常
*/
public static class JwtAuthenticationException extends RuntimeException { public static class JwtAuthenticationException extends RuntimeException {
public JwtAuthenticationException(String message) { public JwtAuthenticationException(String message) {
super(message); super(message);
} }
public JwtAuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}
// 解析 JWT 声明
public Claims parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 获取 JWT 头部信息
public Map<String, Object> getHeaders(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getHeader();
}
public static void main(String[] args) {
// 生成一个符合HS512要求的密钥
System.out.println("请将以下生成的JWT密钥复制到配置文件中(application.yml或application.properties):");
System.out.println("jwt.secret: " + generateSecureKey());
System.out.println("jwt.expiration: 86400000 # 24小时");
} }
} }
...@@ -4,16 +4,27 @@ import com.yd.auth.core.request.LoginRequest; ...@@ -4,16 +4,27 @@ import com.yd.auth.core.request.LoginRequest;
import com.yd.auth.core.response.LoginResponse; import com.yd.auth.core.response.LoginResponse;
import com.yd.auth.core.security.JwtTokenProvider; import com.yd.auth.core.security.JwtTokenProvider;
import com.yd.auth.core.service.AuthService; import com.yd.auth.core.service.AuthService;
import com.yd.common.utils.SM3Util;
import com.yd.user.service.model.SysUser;
import com.yd.user.service.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Objects;
@Service @Service
public class AuthServiceImpl implements AuthService { public class AuthServiceImpl implements AuthService {
@Autowired
@Qualifier("sysUserServiceImpl")
private ISysUserService iSysUserService;
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
...@@ -33,12 +44,24 @@ public class AuthServiceImpl implements AuthService { ...@@ -33,12 +44,24 @@ public class AuthServiceImpl implements AuthService {
@Override @Override
public LoginResponse login(LoginRequest loginRequest) { public LoginResponse login(LoginRequest loginRequest) {
// 通过Feign调用yd-user-service服务获取用户信息
SysUser sysUser = iSysUserService.queryOneByName(loginRequest.getUsername());
if (Objects.isNull(sysUser)) {
throw new UsernameNotFoundException("用户不存在");
}
//密码:SM3国密算法
//生成盐值
String salt = sysUser.getPasswordSalt();
//生成加密密码:带盐的SM3哈希
String hashedPassword = SM3Util.hashWithSalt(loginRequest.getPassword(), salt);
// 1. 创建未认证的Authentication对象 // 1. 创建未认证的Authentication对象
// - 第一个参数:用户名(作为身份主体) // - 第一个参数:用户名(作为身份主体)
// - 第二个参数:原始密码(作为凭证) // - 第二个参数:原始密码(作为凭证)
Authentication authentication = new UsernamePasswordAuthenticationToken( Authentication authentication = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getUsername(),
loginRequest.getPassword() hashedPassword
); );
// 2. 执行Spring Security认证流程(核心步骤) // 2. 执行Spring Security认证流程(核心步骤)
......
...@@ -5,6 +5,7 @@ import com.yd.user.service.model.SysUser; ...@@ -5,6 +5,7 @@ import com.yd.user.service.model.SysUser;
import com.yd.user.service.service.ISysUserService; import com.yd.user.service.service.ISysUserService;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
...@@ -20,6 +21,7 @@ import java.util.Objects; ...@@ -20,6 +21,7 @@ import java.util.Objects;
public class AuthUserDetailsService implements UserDetailsService { public class AuthUserDetailsService implements UserDetailsService {
@Autowired @Autowired
@Qualifier("sysUserServiceImpl")
private ISysUserService iSysUserService; private ISysUserService iSysUserService;
@Override @Override
...@@ -33,7 +35,7 @@ public class AuthUserDetailsService implements UserDetailsService { ...@@ -33,7 +35,7 @@ public class AuthUserDetailsService implements UserDetailsService {
} }
authUserDto = new AuthUserDto(); authUserDto = new AuthUserDto();
BeanUtils.copyProperties(sysUser,authUserDto); BeanUtils.copyProperties(sysUser,authUserDto);
authUserDto.setUsername(sysUser.getUserName());
// 查询用户角色 TODO // 查询用户角色 TODO
// authUserDto.setRoles(roles); // authUserDto.setRoles(roles);
......
package com.yd.auth.core.utils;
import com.yd.auth.core.dto.AuthUserDto;
import com.yd.common.exception.BusinessException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class SecurityUtil {
/**
* 获取当前登录用户信息
* @return
*/
public static AuthUserDto getCurrentLoginUser() {
// 从 SecurityContext 获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new BusinessException("用户未登录!");
}
if (!(authentication.getPrincipal() instanceof AuthUserDto)) {
throw new BusinessException("用户未登录!");
}
AuthUserDto currentUser = (AuthUserDto) authentication.getPrincipal();
return currentUser;
}
}
...@@ -61,6 +61,7 @@ spring: ...@@ -61,6 +61,7 @@ spring:
namespace: ${spring.cloud.nacos.config.namespace} namespace: ${spring.cloud.nacos.config.namespace}
# nacos的ip地址和端口 # nacos的ip地址和端口
server-addr: ${spring.cloud.nacos.config.server-addr} server-addr: ${spring.cloud.nacos.config.server-addr}
group: YD_GROUP
--- ---
spring: spring:
profiles: prod profiles: prod
......
#Generated by Maven #Generated by Maven
#Mon Jul 28 18:02:31 CST 2025 #Fri Aug 22 10:56:43 CST 2025
version=1.0-SNAPSHOT version=1.0-SNAPSHOT
groupId=com.yd groupId=com.yd
artifactId=yd-auth-core artifactId=yd-auth-core
...@@ -37,6 +37,10 @@ ...@@ -37,6 +37,10 @@
<artifactId>spring-cloud-starter-openfeign</artifactId> <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
</dependency> </dependency>
......
...@@ -25,4 +25,18 @@ public class ServerNameConstants { ...@@ -25,4 +25,18 @@ public class ServerNameConstants {
*/ */
public static String ydScrmApi="yd-scrm-api"; public static String ydScrmApi="yd-scrm-api";
/**
* yd-oss-api OSS文件服务接口模块服务名称
*/
public static String ydOssApi="yd-oss-api";
/**
* yd-insurance-base-api 银盾保险基础数据服务接口模块服务名称
*/
public static String ydInsuranceBaseApi="yd-insurance-base-api";
/**
* yd-csf-api 香港保险服务接口模块服务名称
*/
public static String ydCsfApi="yd-csf-api";
} }
...@@ -5,12 +5,31 @@ package com.yd.common.enums; ...@@ -5,12 +5,31 @@ package com.yd.common.enums;
*/ */
public enum CommonEnum { public enum CommonEnum {
//业务唯一标识id的类型 //业务唯一标识id的类型枚举
UID_TYPE_TENANT("tenant","租户"), UID_TYPE_TENANT("tenant","租户"),
UID_TYPE_PROJECT("project","项目"), UID_TYPE_PROJECT("project","项目"),
UID_TYPE_USER("user","用户"), UID_TYPE_USER("user","用户"),
UID_TYPE_ROLE("role","角色"), UID_TYPE_ROLE("role","角色"),
UID_TYPE_MENU("menu","菜单"), UID_TYPE_MENU("menu","菜单"),
UID_TYPE_DEPT("dept","部门"),
UID_TYPE_INSURANCE_PRODUCT("insurance_product","保险产品"),
UID_TYPE_INSURANCE_PRODUCT_PLAN("insurance_product_plan","保险产品计划"),
UID_TYPE_INSURANCE_ADDITIONAL_PRODUCT("insurance_additional_product","保险附加产品"),
UID_TYPE_OSS_FILE("oss_file","OSS文件元数据"),
//作用域枚举
SCOPE_SYS("1","系统级(全局)"),
SCOPE_TENANT("2","租户级"),
SCOPE_PROJECT("3","项目级"),
//作用域对应的数据范围枚举
DATA_SCOPE_PUBLIC("0","公开"),
DATA_SCOPE_PRIVATE("1","私有"),
//操作来源枚举
OPR_SOURCE_ADD("1","添加"),
OPR_SOURCE_EDIT("2","编辑"),
; ;
......
package com.yd.common.enums;
/**
* 菜单类型枚举
*/
public enum MenuTypeEnum {
//菜单类型枚举
ML("1","目录"),
CD("2","菜单"),
AN("3","按钮"),
;
//编码
private String code;
//名称
private String name;
//构造函数
MenuTypeEnum(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}
...@@ -12,6 +12,14 @@ public enum ResultCode { ...@@ -12,6 +12,14 @@ public enum ResultCode {
FAIL(500,"操作失败","") , FAIL(500,"操作失败","") ,
NULL_ERROR(201,"数据不存在","") , NULL_ERROR(201,"数据不存在","") ,
TENANT_NAME_EXISTS(202,"租户名称已存在","") , TENANT_NAME_EXISTS(202,"租户名称已存在","") ,
PROJECT_NAME_EXISTS(203,"项目名称已存在","") ,
ROLE_NAME_EXISTS(204,"角色名称已存在","") ,
MENU_NAME_EXISTS(205,"菜单名称已存在","") ,
DEPT_NAME_EXISTS(206,"部门名称已存在","") ,
PRODUCT_NAME_EXISTS(207,"保险产品名称已存在","") ,
PLAN_NAME_EXISTS(208,"保险产品计划名称已存在","") ,
ADDITIONAL_PRODUCT_NAME_EXISTS(209,"保险附加产品名称已存在","") ,
USER_NOT_EXISTS(300,"用户不存在","") ,
PARAM_CHECK_ERROR(4001,"参数校验异常",""), PARAM_CHECK_ERROR(4001,"参数校验异常",""),
......
package com.yd.common.exception; package com.yd.common.exception;
import com.yd.common.enums.ResultCode;
import lombok.Getter; import lombok.Getter;
/** /**
......
package com.yd.common.utils;
import java.util.List;
public class Menu {
private Long id;
private Long parentId;
private String name;
private Integer orderNum;
private String path;
private String icon;
private List<Menu> children;
// 构造方法
public Menu(Long id, Long parentId, String name, Integer orderNum, String path, String icon) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.orderNum = orderNum;
this.path = path;
this.icon = icon;
}
// getter 和 setter 方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getParentId() { return parentId; }
public void setParentId(Long parentId) { this.parentId = parentId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getOrderNum() { return orderNum; }
public void setOrderNum(Integer orderNum) { this.orderNum = orderNum; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
public String getIcon() { return icon; }
public void setIcon(String icon) { this.icon = icon; }
public List<Menu> getChildren() { return children; }
public void setChildren(List<Menu> children) { this.children = children; }
@Override
public String toString() {
return "Menu{" +
"id=" + id +
", name='" + name + '\'' +
", children=" + (children != null ? children.size() : 0) +
'}';
}
}
...@@ -36,7 +36,7 @@ public class RandomStringGenerator { ...@@ -36,7 +36,7 @@ public class RandomStringGenerator {
* @param uidType 业务标识类型 * @param uidType 业务标识类型
* @return 业务标识类型+32位随机字符串 * @return 业务标识类型+32位随机字符串
*/ */
public static String generateUid32(String uidType) { public static String generateBizId32(String uidType) {
return uidType + "_" + generate(32,true); return uidType + "_" + generate(32,true);
} }
...@@ -45,7 +45,7 @@ public class RandomStringGenerator { ...@@ -45,7 +45,7 @@ public class RandomStringGenerator {
* @param uidType 业务标识类型 * @param uidType 业务标识类型
* @return 业务标识类型+16位随机字符串 * @return 业务标识类型+16位随机字符串
*/ */
public static String generateUid16(String uidType) { public static String generateBizId16(String uidType) {
return uidType + "_" + generate(16,true); return uidType + "_" + generate(16,true);
} }
...@@ -58,6 +58,6 @@ public class RandomStringGenerator { ...@@ -58,6 +58,6 @@ public class RandomStringGenerator {
String random32 = generate(32, false); String random32 = generate(32, false);
System.out.println("32位: " + random32); System.out.println("32位: " + random32);
System.out.println(generateUid16(CommonEnum.UID_TYPE_USER.getCode())); System.out.println(generateBizId16(CommonEnum.UID_TYPE_USER.getCode()));
} }
} }
package com.yd.common.utils;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.util.encoders.Hex;
import java.security.*;
/**
* SM2国密算法工具类
*/
public class SM2Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 生成SM2密钥对
public static KeyPair generateKeyPair() throws Exception {
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
kpg.initialize(ecSpec, new SecureRandom());
return kpg.generateKeyPair();
}
// SM2加密
public static String encrypt(String publicKeyHex, String data) throws Exception {
ECPublicKeyParameters publicKey = getPublicKeyFromHex(publicKeyHex);
SM2Engine engine = new SM2Engine();
engine.init(true, new ParametersWithRandom(publicKey, new SecureRandom()));
byte[] dataBytes = data.getBytes();
byte[] encrypted = engine.processBlock(dataBytes, 0, dataBytes.length);
return Hex.toHexString(encrypted);
}
// SM2解密
public static String decrypt(String privateKeyHex, String encryptedData) throws Exception {
ECPrivateKeyParameters privateKey = getPrivateKeyFromHex(privateKeyHex);
SM2Engine engine = new SM2Engine();
engine.init(false, privateKey);
byte[] encryptedBytes = Hex.decode(encryptedData);
byte[] decrypted = engine.processBlock(encryptedBytes, 0, encryptedBytes.length);
return new String(decrypted);
}
// 从十六进制字符串获取公钥
private static ECPublicKeyParameters getPublicKeyFromHex(String publicKeyHex) {
// 实现细节省略
return null;
}
// 从十六进制字符串获取私钥
private static ECPrivateKeyParameters getPrivateKeyFromHex(String privateKeyHex) {
// 实现细节省略
return null;
}
}
package com.yd.common.utils;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex;
import java.security.SecureRandom;
import java.util.Base64;
/**
* SM3国密算法工具类
*/
public class SM3Util {
// 生成盐值
public static String generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
// SM3哈希计算
public static String hash(String data) {
SM3Digest digest = new SM3Digest();
byte[] dataBytes = data.getBytes();
digest.update(dataBytes, 0, dataBytes.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
// 带盐的SM3哈希
public static String hashWithSalt(String data, String salt) {
return hash(data + salt);
}
// 验证密码
public static boolean verifyPassword(String inputPassword,
String storedHash, String salt) {
String hashedInput = hashWithSalt(inputPassword, salt);
return hashedInput.equals(storedHash);
}
public static void main(String[] args) {
// 1. 处理密码 - 使用SM3加盐哈希
String password = "123456";
String salt = SM3Util.generateSalt();
// String salt = "hdcgRnNrvxNShm/VuNRqfA==";
String hashedPassword = SM3Util.hashWithSalt(password, salt);
//05c6ff03f779dd0fab34fb67c6c0be1c8cfb9a7cc241e86c23a8d9c05a8ea939
System.out.println(hashedPassword);
}
}
package com.yd.common.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TreeMenuExample {
public static void main(String[] args) {
// 1. 创建菜单列表(模拟数据库查询结果)
List<Menu> menuList = new ArrayList<>();
menuList.add(new Menu(1L, 0L, "系统管理", 1, "/system", "system"));
menuList.add(new Menu(2L, 1L, "用户管理", 1, "/system/user", "user"));
menuList.add(new Menu(3L, 1L, "角色管理", 2, "/system/role", "peoples"));
menuList.add(new Menu(4L, 0L, "监控管理", 2, "/monitor", "monitor"));
menuList.add(new Menu(5L, 4L, "在线用户", 1, "/monitor/online", "online"));
menuList.add(new Menu(6L, 4L, "系统日志", 2, "/monitor/log", "log"));
menuList.add(new Menu(7L, 2L, "用户新增", 1, null, null));
menuList.add(new Menu(8L, 2L, "用户编辑", 2, null, null));
// 2. 构建树形结构
List<Menu> treeMenu = TreeUtils.buildTree(
menuList,
Menu::getId, // 获取节点ID
Menu::getParentId, // 获取父节点ID
(parent, children) -> parent.setChildren(children), // 设置子节点
0L // 根节点的父ID值
);
// 3. 打印树形结构
// printTree(treeMenu, 0);
// 4. 创建树形JSON
List<Map<String, Object>> treeJson = TreeUtils.createTreeJson(
treeMenu,
Menu::getChildren,
menu -> {
Map<String, Object> map = new HashMap<>();
map.put("id", menu.getId());
map.put("name", menu.getName());
map.put("path", menu.getPath());
map.put("icon", menu.getIcon());
return map;
},
"/"
);
System.out.println("\n树形JSON结构:");
System.out.println(treeJson);
}
// 递归打印树形结构
private static void printTree(List<Menu> menus, int level) {
if (menus == null) return;
for (Menu menu : menus) {
for (int i = 0; i < level; i++) {
System.out.print(" ");
}
System.out.println("├─ " + menu.getName() + " (ID: " + menu.getId() + ")");
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
printTree(menu.getChildren(), level + 1);
}
}
}
}
package com.yd.common.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 树形工具类 - 用于将扁平列表转换为树形结构
*/
public class TreeUtils {
/**
* 将扁平列表转换为树形结构
*
* @param list 原始数据列表
* @param idGetter 获取节点ID的方法
* @param parentIdGetter 获取父节点ID的方法
* @param childrenSetter 设置子节点的方法
* @param rootParentId 根节点的父ID值(通常为0或null)
* @param <T> 节点类型
* @param <R> ID类型
* @return 树形结构列表
*/
public static <T, R> List<T> buildTree(
List<T> list,
Function<T, R> idGetter,
Function<T, R> parentIdGetter,
ChildrenSetter<T> childrenSetter,
R rootParentId) {
// 1. 创建节点映射表(ID -> 节点)
Map<R, T> nodeMap = list.stream()
.collect(Collectors.toMap(idGetter, Function.identity()));
// 2. 创建结果列表(根节点列表)
List<T> rootNodes = new ArrayList<>();
// 3. 遍历所有节点,构建父子关系
for (T node : list) {
R parentId = parentIdGetter.apply(node);
// 根节点处理
if (Objects.equals(parentId, rootParentId)) {
rootNodes.add(node);
continue;
}
// 非根节点处理
T parentNode = nodeMap.get(parentId);
if (parentNode != null) {
List<T> children = childrenSetter.getChildren(parentNode);
if (children == null) {
children = new ArrayList<>();
childrenSetter.setChildren(parentNode, children);
}
children.add(node);
}
}
// 4. 对树进行排序(可选)
sortTree(rootNodes, childrenSetter);
return rootNodes;
}
/**
* 递归排序树形结构(按orderNum排序)
*
* @param nodes 当前层节点列表
* @param childrenSetter 子节点访问器
* @param <T> 节点类型
*/
public static <T> void sortTree(List<T> nodes, ChildrenSetter<T> childrenSetter) {
if (nodes == null || nodes.isEmpty()) {
return;
}
// 按orderNum排序(如果没有orderNum属性,需要自定义排序逻辑)
nodes.sort((a, b) -> {
try {
// 使用反射获取orderNum属性
java.lang.reflect.Method getOrderNum = a.getClass().getMethod("getOrderNum");
Integer orderNumA = (Integer) getOrderNum.invoke(a);
Integer orderNumB = (Integer) getOrderNum.invoke(b);
return Integer.compare(orderNumA == null ? 0 : orderNumA,
orderNumB == null ? 0 : orderNumB);
} catch (Exception e) {
// 如果没有orderNum属性,则按ID排序
try {
java.lang.reflect.Method getId = a.getClass().getMethod("getId");
Comparable idA = (Comparable) getId.invoke(a);
Comparable idB = (Comparable) getId.invoke(b);
return idA.compareTo(idB);
} catch (Exception ex) {
return 0;
}
}
});
// 递归排序子节点
for (T node : nodes) {
List<T> children = childrenSetter.getChildren(node);
if (children != null && !children.isEmpty()) {
sortTree(children, childrenSetter);
}
}
}
/**
* 从树形结构中查找指定节点
*
* @param tree 树形结构
* @param targetId 目标ID
* @param idGetter ID获取器
* @param childrenGetter 子节点获取器
* @param <T> 节点类型
* @param <R> ID类型
* @return 找到的节点,未找到返回null
*/
public static <T, R> T findNodeInTree(
List<T> tree,
R targetId,
Function<T, R> idGetter,
Function<T, List<T>> childrenGetter) {
for (T node : tree) {
// 检查当前节点
if (Objects.equals(idGetter.apply(node), targetId)) {
return node;
}
// 递归检查子节点
List<T> children = childrenGetter.apply(node);
if (children != null && !children.isEmpty()) {
T found = findNodeInTree(children, targetId, idGetter, childrenGetter);
if (found != null) {
return found;
}
}
}
return null;
}
/**
* 将树形结构扁平化为列表
*
* @param tree 树形结构
* @param childrenGetter 子节点获取器
* @param <T> 节点类型
* @return 扁平化列表
*/
public static <T> List<T> flattenTree(
List<T> tree,
Function<T, List<T>> childrenGetter) {
List<T> result = new ArrayList<>();
flattenTree(tree, childrenGetter, result);
return result;
}
private static <T> void flattenTree(
List<T> nodes,
Function<T, List<T>> childrenGetter,
List<T> result) {
if (nodes == null) return;
for (T node : nodes) {
result.add(node);
List<T> children = childrenGetter.apply(node);
if (children != null && !children.isEmpty()) {
flattenTree(children, childrenGetter, result);
}
}
}
/**
* 子节点设置器接口
*
* @param <T> 节点类型
*/
@FunctionalInterface
public interface ChildrenSetter<T> {
/**
* 设置子节点列表
*
* @param parent 父节点
* @param children 子节点列表
*/
void setChildren(T parent, List<T> children);
/**
* 获取子节点列表
*
* @param parent 父节点
* @return 子节点列表
*/
default List<T> getChildren(T parent) {
try {
// 使用反射获取children属性
java.lang.reflect.Method getChildren = parent.getClass().getMethod("getChildren");
return (List<T>) getChildren.invoke(parent);
} catch (Exception e) {
return null;
}
}
}
/**
* 创建树形菜单的JSON结构(带层级路径)
*
* @param tree 树形结构
* @param childrenGetter 子节点获取器
* @param mapper 节点转换器
* @param <T> 原始节点类型
* @param <R> 结果类型
* @return 树形JSON结构
*/
public static <T, R> List<Map<String, Object>> createTreeJson(
List<T> tree,
Function<T, List<T>> childrenGetter,
Function<T, R> mapper,
String pathSeparator) {
List<Map<String, Object>> result = new ArrayList<>();
createTreeJson(tree, childrenGetter, mapper, result, "", pathSeparator);
return result;
}
private static <T, R> void createTreeJson(
List<T> nodes,
Function<T, List<T>> childrenGetter,
Function<T, R> mapper,
List<Map<String, Object>> result,
String parentPath,
String pathSeparator) {
if (nodes == null) return;
for (T node : nodes) {
Map<String, Object> nodeMap = new HashMap<>();
// 添加节点数据
nodeMap.put("data", mapper.apply(node));
// 创建当前节点路径
try {
java.lang.reflect.Method getId = node.getClass().getMethod("getId");
Object id = getId.invoke(node);
String currentPath = parentPath.isEmpty() ? id.toString() :
parentPath + pathSeparator + id;
nodeMap.put("path", currentPath);
} catch (Exception e) {
nodeMap.put("path", parentPath);
}
// 处理子节点
List<T> children = childrenGetter.apply(node);
if (children != null && !children.isEmpty()) {
List<Map<String, Object>> childrenJson = new ArrayList<>();
createTreeJson(children, childrenGetter, mapper, childrenJson,
(String) nodeMap.get("path"), pathSeparator);
nodeMap.put("children", childrenJson);
}
result.add(nodeMap);
}
}
}
#Generated by Maven #Generated by Maven
#Mon Jul 28 18:02:10 CST 2025 #Tue Aug 26 16:56:01 CST 2025
version=1.0-SNAPSHOT version=1.0-SNAPSHOT
groupId=com.yd groupId=com.yd
artifactId=yd-common artifactId=yd-common
#Generated by Maven #Generated by Maven
#Mon Jul 28 18:02:38 CST 2025 #Fri Aug 22 10:56:50 CST 2025
version=1.0-SNAPSHOT version=1.0-SNAPSHOT
groupId=com.yd groupId=com.yd
artifactId=yd-feign artifactId=yd-feign
...@@ -34,5 +34,9 @@ ...@@ -34,5 +34,9 @@
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
...@@ -4,6 +4,7 @@ import com.yd.common.enums.ResultCode; ...@@ -4,6 +4,7 @@ import com.yd.common.enums.ResultCode;
import com.yd.common.exception.BusinessException; import com.yd.common.exception.BusinessException;
import com.yd.common.result.Result; import com.yd.common.result.Result;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
...@@ -62,6 +63,14 @@ public class GlobalExceptionHandler { ...@@ -62,6 +63,14 @@ public class GlobalExceptionHandler {
return Result.fail(ResultCode.PARAM_CHECK_ERROR.getCode(),message); return Result.fail(ResultCode.PARAM_CHECK_ERROR.getCode(),message);
} }
// 专门捕获 BadCredentialsException,返回“密码错误”的自定义响应
@ExceptionHandler(BadCredentialsException.class)
public Result<?> handleBadCredentialsException(BadCredentialsException e) {
log.error("认证失败:{}", e.getMessage());
// 返回自定义的错误码和“密码错误”提示(而非模糊的“用户名或密码错误”)
return Result.fail("认证失败:密码错误,请重新输入");
}
/** /**
* 处理其他异常 * 处理其他异常
*/ */
......
#Generated by Maven #Generated by Maven
#Mon Jul 28 18:02:41 CST 2025 #Fri Aug 22 10:56:35 CST 2025
version=1.0-SNAPSHOT version=1.0-SNAPSHOT
groupId=com.yd groupId=com.yd
artifactId=yd-framework artifactId=yd-framework
...@@ -18,26 +18,56 @@ ...@@ -18,26 +18,56 @@
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId> <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> </dependency>
<!-- SpringCloud Openfeign --> <!-- SpringCloud Alibaba Nacos Config -->
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.yd</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>yd-auth-core</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.yd</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>yd-common</artifactId> <artifactId>spring-boot-starter</artifactId>
</dependency>
<!--对于bootstrap.properties/bootstrap.yaml配置文件(我们合起来成为Bootstrap配置文件)的支持,需要导入如下的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
</dependency> </dependency>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.yd</groupId>-->
<!-- <artifactId>yd-auth-core</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.yd</groupId>
<artifactId>yd-common</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
...@@ -3,19 +3,18 @@ package com.yd.gateway; ...@@ -3,19 +3,18 @@ package com.yd.gateway;
import com.yd.common.constant.ServerNameConstants; import com.yd.common.constant.ServerNameConstants;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
/** /**
* 网关启动程序 * 网关启动程序
*/ */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @SpringBootApplication(scanBasePackages = "com.yd")
@EnableFeignClients(basePackages = "com.yd.gateway.**") @EnableFeignClients(basePackages = "com.yd")
public class GatewayApplication { public class GatewayApplication {
public static void main(String[] args) { public static void main(String[] args) {
new SpringApplicationBuilder(GatewayApplication.class) new SpringApplicationBuilder(GatewayApplication.class)
.properties("spring.config.name:bootstrap", "config/bootstrap.yml") // .properties("spring.config.name:bootstrap", "config/bootstrap.yml")
.properties("spring.application.name="+ ServerNameConstants.ydGateway) .properties("spring.application.name="+ ServerNameConstants.ydGateway)
.build().run(args); .build().run(args);
System.out.println("(♥◠‿◠)ノ゙ yd-gateway网关启动成功 ლ(´ڡ`ლ)゙ "); System.out.println("(♥◠‿◠)ノ゙ yd-gateway网关启动成功 ლ(´ڡ`ლ)゙ ");
......
package com.yd.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
package com.yd.gateway.config;
import com.yd.gateway.exception.JwtAuthException;
import com.yd.gateway.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 网关JWT认证过滤器:只做轻量校验(验证token有效性),不查询业务数据
*/
@Configuration
public class GatewayJwtAuthFilterConfig {
@Autowired
private JwtTokenUtil jwtTokenUtil;
/**
* 注册全局过滤器,优先级高于路由过滤器(确保先校验再路由)
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级,在路由前执行
public GlobalFilter jwtAuthFilter() {
return (exchange, chain) -> {
// 1. 获取请求路径,跳过白名单(如登录、Swagger等)
String path = exchange.getRequest().getPath().toString();
if (isWhitelist(path)) {
// 白名单路径直接放行
return chain.filter(exchange);
}
// 2. 从请求头获取token
String token = resolveToken(exchange);
if (token == null) {
// 无token,返回401未授权
return setUnauthorizedResponse(exchange, "未提供令牌(Authorization头缺失或格式错误)");
}
// 3. 验证token有效性(只做签名、过期等基础校验,不查用户)
try {
jwtTokenUtil.validateToken(token);
// token有效,继续路由到下游服务
return chain.filter(exchange);
} catch (JwtAuthException e) {
// token无效(如过期、签名错误),返回401
return setUnauthorizedResponse(exchange, "令牌无效:" + e.getMessage());
}
};
}
/**
* 解析请求头中的token(Authorization: Bearer <token>)
*/
private String resolveToken(ServerWebExchange exchange) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7).trim(); // 去除"Bearer "前缀
}
return null;
}
/**
* 判断路径是否在白名单中(无需认证)
*/
private boolean isWhitelist(String path) {
// 白名单路径:与SecurityWebFilterChain中的配置保持一致
return path.startsWith("/auth/")
|| path.startsWith("/swagger-ui/")
|| path.startsWith("/v3/api-docs/");
}
/**
* 设置401响应
*/
private Mono<Void> setUnauthorizedResponse(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().add("Content-Type", "application/json;charset=UTF-8");
// 构建错误响应体
String json = "{\"status\":401,\"error\":\"Unauthorized\",\"message\":\"" + message + "\"}";
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(json.getBytes())));
}
}
package com.yd.gateway.config; //package com.yd.gateway.config;
//
import com.yd.auth.core.security.JwtTokenProvider; //import com.yd.auth.core.security.JwtTokenProvider;
import com.yd.auth.core.security.JwtTokenProvider.JwtAuthenticationException; //import com.yd.auth.core.security.JwtTokenProvider.JwtAuthenticationException;
import org.springframework.context.annotation.Bean; //import io.jsonwebtoken.Claims;
import org.springframework.context.annotation.Configuration; //import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; //import org.springframework.context.annotation.Bean;
import org.springframework.security.config.web.server.ServerHttpSecurity; //import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.Jwt; //import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain; //import org.springframework.security.config.web.server.ServerHttpSecurity;
import reactor.core.publisher.Mono; //import org.springframework.security.oauth2.jwt.Jwt;
//import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration //import reactor.core.publisher.Mono;
@EnableWebFluxSecurity //
public class GatewaySecurityConfig { //@Configuration
//@EnableWebFluxSecurity
private final JwtTokenProvider jwtTokenProvider; //public class GatewaySecurityConfig {
//
public GatewaySecurityConfig(JwtTokenProvider jwtTokenProvider) { //
this.jwtTokenProvider = jwtTokenProvider; //// private final JwtTokenProvider jwtTokenProvider;
} ////
//// public GatewaySecurityConfig(JwtTokenProvider jwtTokenProvider) {
@Bean //// this.jwtTokenProvider = jwtTokenProvider;
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { //// }
http //
.csrf().disable() // @Autowired
.authorizeExchange(exchanges -> exchanges // private JwtTokenProvider jwtTokenProvider;
.pathMatchers( //
"/auth/**", // @Bean
"/swagger-ui/**", // public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
"/v3/api-docs/**" // http
).permitAll() // 放行认证和Swagger路径 // .csrf().disable()
.anyExchange().authenticated() // 其他请求需要认证 // .authorizeExchange(exchanges -> exchanges
) // .pathMatchers(
.oauth2ResourceServer(oauth2 -> oauth2 // "/auth/**",
.jwt(jwt -> jwt // "/swagger-ui/**",
.jwtDecoder(token -> { // "/v3/api-docs/**"
try { // ).permitAll() // 放行认证和Swagger路径
// 验证令牌有效性 // .anyExchange().authenticated() // 其他请求需要认证
jwtTokenProvider.validateToken(token); // )
// 将字符串转换为 Jwt 对象 // .oauth2ResourceServer(oauth2 -> oauth2
Jwt jwtObject = Jwt.withTokenValue(token).build(); // .jwt(jwt -> jwt
return Mono.just(jwtObject); // .jwtDecoder(token -> {
} catch (JwtAuthenticationException e) { // try {
return Mono.error(e); // // 验证令牌有效性
} // jwtTokenProvider.validateToken(token);
}) //
) // // 完整解析 JWT 令牌
); // Claims claims = jwtTokenProvider.parseClaims(token);
return http.build(); //
} // // 构建完整 JWT 对象
} // return Mono.just(new Jwt(
// token,
// claims.getIssuedAt().toInstant(),
// claims.getExpiration().toInstant(),
// jwtTokenProvider.getHeaders(token), // 获取头部信息
// claims
// ));
// } catch (JwtAuthenticationException e) {
// return Mono.error(e);
// }
// })
// )
// );
// return http.build();
// }
//}
//package com.yd.gateway.config;
//
//import org.springframework.cloud.gateway.filter.GlobalFilter;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import reactor.core.publisher.Mono;
//
//@Configuration
//public class RouteDebugFilter {
// @Bean
// public GlobalFilter debugFilter() {
// return (exchange, chain) -> {
// System.out.println("请求路径: " + exchange.getRequest().getPath());
// System.out.println("转发URL: " + exchange.getRequiredAttribute("GATEWAY_REQUEST_URL_ATTR"));
// return chain.filter(exchange);
// };
// }
//}
package com.yd.gateway.exception;
/**
* JWT认证异常
*/
public class JwtAuthException extends RuntimeException {
public JwtAuthException(String message) {
super(message);
}
public JwtAuthException(String message, Throwable cause) {
super(message, cause);
}
}
package com.yd.gateway.utils;
import com.yd.gateway.exception.JwtAuthException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
/**
* JWT令牌提供者
* 修复了密钥长度不足的问题,并优化了异常处理
*/
@Component
public class JwtTokenUtil {
// 使用512位长度的密钥(通过配置文件设置)
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}") // 默认24小时(单位毫秒)
private int jwtExpiration;
/**
* 验证令牌有效性
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
throw new JwtAuthException("无效的JWT签名");
} catch (ExpiredJwtException e) {
throw new JwtAuthException("JWT令牌已过期");
} catch (UnsupportedJwtException e) {
throw new JwtAuthException("不支持的JWT令牌");
} catch (IllegalArgumentException e) {
throw new JwtAuthException("JWT令牌无效");
} catch (Exception e) {
throw new JwtAuthException("无效的JWT令牌: " + e.getMessage());
}
}
/**
* 获取签名密钥
*/
private SecretKey getSigningKey() {
try {
if (jwtSecret == null || jwtSecret.trim().isEmpty()) {
throw new JwtAuthException("JWT密钥未配置");
}
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
} catch (Exception e) {
throw new JwtAuthException("获取签名密钥失败: " + e.getMessage());
}
}
}
${AnsiColor.GREEN}
_ _ _ __ __ ____ _ _ ____ _ __ __
| | (_)_ __ | | _\ \ / /__ / ___| |__ __ _| |_ / ___| __ _| |_ __\ \ / /_ _ _ _
| | | | '_ \| |/ /\ \ /\ / / _ \ | | '_ \ / _` | __| | _ / _` | __/ _ \ \ /\ / / _` | | | |
| |___| | | | | < \ V V / __/ |___| | | | (_| | |_| |_| | (_| | || __/\ V V / (_| | |_| |
|_____|_|_| |_|_|\_\ \_/\_/ \___|\____|_| |_|\__,_|\__|\____|\__,_|\__\___| \_/\_/ \__,_|\__, |
|___/
${AnsiColor.BRIGHT_WHITE}
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
\ No newline at end of file
spring:
profiles:
active: test
# active: '@spring.profiles.active@'
---
spring:
profiles: dev
main:
allow-bean-definition-overriding: true
allow-circular-references: true
cloud:
nacos:
# 配置中心
config:
# 命名空间id(此处不用public,因public初始化的空间, id为空) 4e237601-cea8-414d-b7b9-d7adc8cbcf95
namespace: 22f9d61e-9011-4d45-88cb-24f9857e3eec
# nacos的ip地址和端口 120.79.64.17:10848
server-addr: 127.0.0.1:8848
# 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
file-extension: yml
config-retry-time: 300000
# 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
shared-configs[0]:
dataId: linkwe-common.yml
refresh: true
# 发布到注册中心 (如果没有使用可以不配)
discovery:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace: ${spring.cloud.nacos.config.namespace}
# nacos的ip地址和端口
server-addr: ${spring.cloud.nacos.config.server-addr}
---
spring:
profiles: test
main:
allow-bean-definition-overriding: true
allow-circular-references: true
web-application-type: reactive # 强制使用响应式模式
cloud:
nacos:
# 配置中心
config:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace: c1e4cbcf-d8b7-4da9-a75a-7b75890fc390
# nacos的ip地址和端口
server-addr: 139.224.145.34:8848
# 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
file-extension: yml
config-retry-time: 300000
# 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
shared-configs[0]:
dataId: yd-common.yml
group: YD_GROUP
refresh: true
extension-configs: # 扩展配置
- data-id: yd-gateway.yml
group: YD_GROUP
refresh: true
# 发布到注册中心 (如果没有使用可以不配)
discovery:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace: ${spring.cloud.nacos.config.namespace}
# nacos的ip地址和端口
server-addr: ${spring.cloud.nacos.config.server-addr}
group: YD_GROUP
---
spring:
profiles: prod
main:
allow-bean-definition-overriding: true
allow-circular-references: true
cloud:
nacos:
# 配置中心
config:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace: ewscrm
# nacos的ip地址和端口
server-addr: 127.0.0.1:8848
# 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
file-extension: yml
config-retry-time: 300000
# 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
shared-configs[0]:
dataId: yd-common.yml
refresh: true
group: YD_GROUP
# 发布到注册中心 (如果没有使用可以不配)
discovery:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace: ${spring.cloud.nacos.config.namespace}
# nacos的ip地址和端口
server-addr: ${spring.cloud.nacos.config.server-addr}
#Generated by Maven #Generated by Maven
#Mon Jul 28 18:02:36 CST 2025 #Fri Aug 22 10:56:47 CST 2025
version=1.0-SNAPSHOT version=1.0-SNAPSHOT
groupId=com.yd groupId=com.yd
artifactId=yd-gateway artifactId=yd-gateway
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