service auth

This commit is contained in:
2025-09-28 15:50:59 +08:00
parent 0190655c53
commit faa7411ecc
19 changed files with 1662 additions and 8 deletions

View File

@@ -12,10 +12,132 @@
<groupId>org.xyzh</groupId>
<artifactId>auth</artifactId>
<version>${school-news.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
</properties>
<dependencies>
<!-- 公共模块依赖 -->
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>common-core</artifactId>
<version>${school-news.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>common-dto</artifactId>
<version>${school-news.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>common-exception</artifactId>
<version>${school-news.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>common-redis</artifactId>
<version>${school-news.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>common-util</artifactId>
<version>${school-news.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>api-auth</artifactId>
<version>${school-news.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>api-system</artifactId>
<version>${school-news.version}</version>
</dependency>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除默认的logback依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<!-- 排除默认的logback依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Log4j2 日志依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 配置处理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<!-- 排除默认的logback依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,7 +0,0 @@
package org.xyzh;
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}

View File

@@ -0,0 +1,153 @@
package org.xyzh.auth.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.ArrayList;
/**
* @description AuthProperties.java文件描述 认证配置属性
* @filename AuthProperties.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
@ConfigurationProperties(prefix = "school-news.auth")
public class AuthProperties {
/**
* @description 免登录白名单路径
* @author yslg
* @since 2025-09-28
*/
private List<String> whiteList = new ArrayList<>();
/**
* @description JWT密钥
* @author yslg
* @since 2025-09-28
*/
private String jwtSecret = "schoolNewsDefaultSecretKeyForJWT2025";
/**
* @description JWT过期时间
* @author yslg
* @since 2025-09-28
*/
private Long jwtExpiration = 86400L;
/**
* @description 最大登录失败次数
* @author yslg
* @since 2025-09-28
*/
private Integer maxLoginAttempts = 5;
/**
* @description 账户锁定时间(分钟)
* @author yslg
* @since 2025-09-28
*/
private Integer lockoutDuration = 30;
/**
* @description 获取白名单
* @return List<String> 白名单路径列表
* @author yslg
* @since 2025-09-28
*/
public List<String> getWhiteList() {
return whiteList;
}
/**
* @description 设置白名单
* @param whiteList 白名单路径列表
* @author yslg
* @since 2025-09-28
*/
public void setWhiteList(List<String> whiteList) {
this.whiteList = whiteList;
}
/**
* @description 获取JWT密钥
* @return String JWT密钥
* @author yslg
* @since 2025-09-28
*/
public String getJwtSecret() {
return jwtSecret;
}
/**
* @description 设置JWT密钥
* @param jwtSecret JWT密钥
* @author yslg
* @since 2025-09-28
*/
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
/**
* @description 获取JWT过期时间
* @return Long JWT过期时间
* @author yslg
* @since 2025-09-28
*/
public Long getJwtExpiration() {
return jwtExpiration;
}
/**
* @description 设置JWT过期时间
* @param jwtExpiration JWT过期时间
* @author yslg
* @since 2025-09-28
*/
public void setJwtExpiration(Long jwtExpiration) {
this.jwtExpiration = jwtExpiration;
}
/**
* @description 获取最大登录失败次数
* @return Integer 最大登录失败次数
* @author yslg
* @since 2025-09-28
*/
public Integer getMaxLoginAttempts() {
return maxLoginAttempts;
}
/**
* @description 设置最大登录失败次数
* @param maxLoginAttempts 最大登录失败次数
* @author yslg
* @since 2025-09-28
*/
public void setMaxLoginAttempts(Integer maxLoginAttempts) {
this.maxLoginAttempts = maxLoginAttempts;
}
/**
* @description 获取账户锁定时间
* @return Integer 账户锁定时间(分钟)
* @author yslg
* @since 2025-09-28
*/
public Integer getLockoutDuration() {
return lockoutDuration;
}
/**
* @description 设置账户锁定时间
* @param lockoutDuration 账户锁定时间(分钟)
* @author yslg
* @since 2025-09-28
*/
public void setLockoutDuration(Integer lockoutDuration) {
this.lockoutDuration = lockoutDuration;
}
}

View File

@@ -0,0 +1,74 @@
package org.xyzh.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.xyzh.auth.filter.JwtAuthenticationFilter;
/**
* @description SecurityConfig.java文件描述 Spring Security配置
* @filename SecurityConfig.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private AuthProperties authProperties;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* @description 密码编码器
* @return PasswordEncoder 密码编码器
* @author yslg
* @since 2025-09-28
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* @description 安全过滤器链配置
* @param http HTTP安全配置
* @return SecurityFilterChain 安全过滤器链
* @author yslg
* @since 2025-09-28
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 白名单路径转换为数组
String[] whiteListArray = authProperties.getWhiteList().toArray(new String[0]);
http
// 禁用CSRF
.csrf(csrf -> csrf.disable())
// 无状态session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置授权规则
.authorizeHttpRequests(authz -> authz
.requestMatchers(whiteListArray).permitAll()
.requestMatchers("/auth/login", "/auth/logout", "/auth/captcha").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/favicon.ico", "/error").permitAll()
.anyRequest().authenticated()
)
// 添加JWT过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@@ -0,0 +1,100 @@
package org.xyzh.auth.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.auth.login.LoginService;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.utils.IDUtils;
/**
* @description AuthController.java文件描述 认证控制器
* @filename AuthController.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private LoginService loginService;
/**
* @description 用户登录
* @param loginParam 登录参数
* @return ResultDomain<String> 登录结果
* @author yslg
* @since 2025-09-28
*/
@PostMapping("/login")
public ResultDomain<String> login(@RequestBody LoginParam loginParam) {
return loginService.login(loginParam);
}
/**
* @description 用户退出登录
* @param loginDomain 登录域对象
* @return ResultDomain<String> 退出结果
* @author yslg
* @since 2025-09-28
*/
@PostMapping("/logout")
public ResultDomain<String> logout(@RequestBody LoginDomain loginDomain) {
return loginService.logout(loginDomain);
}
/**
* @description 获取验证码
* @return ResultDomain<String> 验证码
* @author yslg
* @since 2025-09-28
*/
@GetMapping("/captcha")
public ResultDomain<String> getCaptcha() {
// TODO: 实现验证码生成逻辑
ResultDomain<String> result = new ResultDomain<>();
// 生成验证码会话ID用于验证时匹配
String captchaId = IDUtils.generateID();
String captchaData = captchaId + ":captcha-placeholder"; // 格式: ID:验证码内容
result.success("验证码获取成功", captchaData);
return result;
}
/**
* @description 刷新令牌
* @param token 原令牌
* @return ResultDomain<String> 新令牌
* @author yslg
* @since 2025-09-28
*/
@PostMapping("/refresh")
public ResultDomain<String> refreshToken(@RequestHeader("Authorization") String token) {
// TODO: 实现令牌刷新逻辑
ResultDomain<String> result = new ResultDomain<>();
// 为新令牌生成唯一ID
String newTokenId = IDUtils.generateID();
String newToken = "new-token-" + newTokenId; // 临时占位符
result.success("令牌刷新成功", newToken);
return result;
}
/**
* @description 健康检查
* @return ResultDomain<String> 健康状态
* @author yslg
* @since 2025-09-28
*/
@GetMapping("/health")
public ResultDomain<String> health() {
ResultDomain<String> result = new ResultDomain<>();
result.success("认证服务运行正常", "OK");
return result;
}
}

View File

@@ -0,0 +1,98 @@
package org.xyzh.auth.domain;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.permission.TbSysPermission;
import org.xyzh.common.core.enums.UserStatus;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @description UserPrincipal.java文件描述 用户主体类
* @filename UserPrincipal.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
public class UserPrincipal implements UserDetails {
private TbSysUser user;
private List<TbSysDeptRole> roles;
private List<TbSysPermission> permissions;
private UserPrincipal(TbSysUser user, List<TbSysDeptRole> roles, List<TbSysPermission> permissions) {
this.user = user;
this.roles = roles;
this.permissions = permissions;
}
public static UserPrincipal create(TbSysUser user, List<TbSysDeptRole> roles, List<TbSysPermission> permissions) {
return new UserPrincipal(user, roles, permissions);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 角色权限
List<GrantedAuthority> roleAuthorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getID()))
.collect(Collectors.toList());
// 功能权限
List<GrantedAuthority> permissionAuthorities = permissions.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getCode()))
.collect(Collectors.toList());
return Stream.concat(roleAuthorities.stream(), permissionAuthorities.stream())
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
UserStatus status = UserStatus.fromCode(String.valueOf(user.getStatus()));
return status != UserStatus.LOCKED;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
UserStatus status = UserStatus.fromCode(String.valueOf(user.getStatus()));
return status == UserStatus.NORMAL;
}
public TbSysUser getUser() {
return user;
}
public List<TbSysDeptRole> getRoles() {
return roles;
}
public List<TbSysPermission> getPermissions() {
return permissions;
}
}

View File

@@ -0,0 +1,102 @@
package org.xyzh.auth.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.xyzh.auth.service.UserDetailsServiceImpl;
import org.xyzh.auth.util.JwtTokenUtil;
import org.xyzh.auth.config.AuthProperties;
import java.io.IOException;
/**
* @description JwtAuthenticationFilter.java文件描述 JWT认证过滤器
* @filename JwtAuthenticationFilter.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthProperties authProperties;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
// 检查是否在白名单中
if (isWhitelisted(requestURI)) {
filterChain.doFilter(request, response);
return;
}
String token = getTokenFromRequest(request);
if (StringUtils.hasText(token)) {
try {
String userId = jwtTokenUtil.getUserIdFromToken(token);
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
if (jwtTokenUtil.validateToken(token, userId)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
logger.error("JWT token validation failed: " + e.getMessage());
}
}
filterChain.doFilter(request, response);
}
/**
* @description 从请求中获取Token
* @param request HTTP请求
* @return String Token
* @author yslg
* @since 2025-09-28
*/
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
/**
* @description 检查请求路径是否在白名单中
* @param requestURI 请求URI
* @return boolean 是否在白名单中
* @author yslg
* @since 2025-09-28
*/
private boolean isWhitelisted(String requestURI) {
return authProperties.getWhiteList().stream()
.anyMatch(whitePath -> requestURI.matches(whitePath.replace("*", ".*")));
}
}

View File

@@ -0,0 +1,69 @@
package org.xyzh.auth.service;
import org.springframework.stereotype.Service;
import org.xyzh.common.dto.log.TbSysLoginLog;
import org.xyzh.common.utils.IDUtils;
/**
* @description LoginLogService.java文件描述 登录日志服务
* @filename LoginLogService.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Service
public class LoginLogService {
/**
* @description 保存登录日志
* @param loginLog 登录日志
* @author yslg
* @since 2025-09-28
*/
public void saveLoginLog(TbSysLoginLog loginLog) {
// 确保登录日志有ID如果没有则生成一个
if (loginLog.getID() == null || loginLog.getID().isEmpty()) {
loginLog.setID(IDUtils.generateID());
}
// TODO: 实现登录日志保存逻辑
// 这里应该调用数据层保存日志
System.out.println("保存登录日志: " + loginLog);
}
/**
* @description 根据用户ID查询登录日志
* @param userId 用户ID
* @return List<TbSysLoginLog> 登录日志列表
* @author yslg
* @since 2025-09-28
*/
public java.util.List<TbSysLoginLog> findLoginLogsByUserId(String userId) {
// TODO: 实现根据用户ID查询登录日志的逻辑
return new java.util.ArrayList<>();
}
/**
* @description 查询登录失败次数
* @param userId 用户ID
* @param timeRange 时间范围(分钟)
* @return int 失败次数
* @author yslg
* @since 2025-09-28
*/
public int countFailedLoginAttempts(String userId, int timeRange) {
// TODO: 实现查询指定时间范围内的登录失败次数
return 0;
}
/**
* @description 清除登录失败记录
* @param userId 用户ID
* @author yslg
* @since 2025-09-28
*/
public void clearFailedLoginAttempts(String userId) {
// TODO: 实现清除登录失败记录的逻辑
System.out.println("清除用户 " + userId + " 的登录失败记录");
}
}

View File

@@ -0,0 +1,228 @@
package org.xyzh.auth.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.xyzh.api.auth.login.LoginService;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.auth.strategy.LoginStrategyFactory;
import org.xyzh.auth.util.JwtTokenUtil;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.enums.UserStatus;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.dto.log.TbSysLoginLog;
import org.xyzh.common.exception.auth.AuthException;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.api.system.user.UserService;
import org.xyzh.api.system.role.RoleService;
import org.xyzh.api.system.permission.PermissionService;
import java.util.Date;
import java.util.ArrayList;
/**
* @description LoginServiceImpl.java文件描述 登录服务实现
* @filename LoginServiceImpl.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private LoginStrategyFactory loginStrategyFactory;
@Autowired
private UserService userService;
@Autowired(required = false)
private RoleService roleService;
@Autowired(required = false)
private PermissionService permissionService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private LoginLogService loginLogService;
@Override
public ResultDomain<String> login(LoginParam loginParam) {
ResultDomain<String> result = new ResultDomain<>();
try {
// 自动检测登录类型
String loginType = detectLoginType(loginParam);
loginParam.setLoginType(loginType);
// 获取对应的登录策略
LoginStrategy strategy = loginStrategyFactory.getStrategy(loginType);
// 验证登录参数
if (!strategy.validate(loginParam)) {
result.fail("登录参数不正确");
return result;
}
// 查找用户
TbSysUser user = strategy.findUser(loginParam);
if (user == null) {
result.fail("用户不存在");
logLoginAttempt(loginParam, null, false, "用户不存在");
return result;
}
// 检查用户状态
UserStatus userStatus = UserStatus.fromCode(String.valueOf(user.getStatus()));
if (userStatus != UserStatus.NORMAL) {
result.fail("用户状态异常: " + userStatus.getName());
logLoginAttempt(loginParam, user, false, "用户状态异常: " + userStatus.getName());
return result;
}
// 验证密码
if (!strategy.verifyPassword(loginParam.getPassword(), user.getPassword())) {
result.fail("密码错误");
logLoginAttempt(loginParam, user, false, "密码错误");
return result;
}
// 构建登录域对象
LoginDomain loginDomain = buildLoginDomain(user, loginType);
// 生成JWT令牌
String token = jwtTokenUtil.generateToken(loginDomain);
// 记录成功登录日志
logLoginAttempt(loginParam, user, true, "登录成功");
result.success("登录成功", (String)null);
result.setData(token);
} catch (AuthException e) {
result.fail(e.getMessage());
} catch (Exception e) {
result.fail("登录失败: " + e.getMessage());
}
return result;
}
@Override
public ResultDomain<String> logout(LoginDomain loginDomain) {
ResultDomain<String> result = new ResultDomain<>();
try {
// TODO: 将token加入黑名单或从Redis中删除
// 这里可以实现token黑名单机制
result.success("退出登录成功", (String)null);
} catch (Exception e) {
result.fail("退出登录失败: " + e.getMessage());
}
return result;
}
/**
* @description 自动检测登录类型
* @param loginParam 登录参数
* @return String 登录类型
* @author yslg
* @since 2025-09-28
*/
private String detectLoginType(LoginParam loginParam) {
if (StringUtils.hasText(loginParam.getLoginType())) {
return loginParam.getLoginType();
}
if (StringUtils.hasText(loginParam.getEmail())) {
return "email";
}
if (StringUtils.hasText(loginParam.getUsername())) {
return "username";
}
if (StringUtils.hasText(loginParam.getPhone())) {
return "phone";
}
if (StringUtils.hasText(loginParam.getWechatID())) {
return "wechat";
}
throw new AuthException("INVALID_LOGIN_TYPE", "无法确定登录类型");
}
/**
* @description 构建登录域对象
* @param user 用户对象
* @param loginType 登录类型
* @return LoginDomain 登录域对象
* @author yslg
* @since 2025-09-28
*/
private LoginDomain buildLoginDomain(TbSysUser user, String loginType) {
LoginDomain loginDomain = new LoginDomain();
loginDomain.setUser(user);
loginDomain.setLoginType(loginType);
loginDomain.setLoginTime(new Date());
// 获取用户角色和权限(如果服务可用)
if (roleService != null) {
try {
// TODO: 需要在RoleService中实现findRolesByUserId方法
// loginDomain.setRoles(roleService.findRolesByUserId(user.getID()));
loginDomain.setRoles(new ArrayList<>());
} catch (Exception e) {
loginDomain.setRoles(new ArrayList<>());
}
} else {
loginDomain.setRoles(new ArrayList<>());
}
if (permissionService != null) {
try {
// TODO: 需要在PermissionService中实现findPermissionsByUserId方法
// loginDomain.setPermissions(permissionService.findPermissionsByUserId(user.getID()));
loginDomain.setPermissions(new ArrayList<>());
} catch (Exception e) {
loginDomain.setPermissions(new ArrayList<>());
}
} else {
loginDomain.setPermissions(new ArrayList<>());
}
return loginDomain;
}
/**
* @description 记录登录日志
* @param loginParam 登录参数
* @param user 用户对象
* @param success 是否成功
* @param message 消息
* @author yslg
* @since 2025-09-28
*/
private void logLoginAttempt(LoginParam loginParam, TbSysUser user, boolean success, String message) {
TbSysLoginLog loginLog = new TbSysLoginLog();
// 使用IDUtils生成登录日志ID
loginLog.setID(IDUtils.generateID());
if (user != null) {
loginLog.setUserID(user.getID());
loginLog.setUsername(user.getUsername());
}
// 注意:实际生产中不应记录密码
// loginLog.setPassword(loginParam.getPassword());
loginLog.setStatus(success ? 1 : 0);
loginLog.setMessage(message);
loginLog.setLoginTime(new Date().toString());
loginLogService.saveLoginLog(loginLog);
}
}

View File

@@ -0,0 +1,95 @@
package org.xyzh.auth.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.xyzh.auth.domain.UserPrincipal;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.permission.TbSysPermission;
import org.xyzh.api.system.user.UserService;
import org.xyzh.api.system.role.RoleService;
import org.xyzh.api.system.permission.PermissionService;
import java.util.List;
import java.util.ArrayList;
/**
* @description UserDetailsServiceImpl.java文件描述 用户详情服务实现
* @filename UserDetailsServiceImpl.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UserService userService;
@Autowired(required = false)
private RoleService roleService;
@Autowired(required = false)
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TbSysUser filter = new TbSysUser();
filter.setUsername(username);
TbSysUser user = userService.find(filter);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
return loadUserByUserId(user.getID());
}
/**
* @description 根据用户ID加载用户详情
* @param userId 用户ID
* @return UserDetails 用户详情
* @author yslg
* @since 2025-09-28
*/
public UserDetails loadUserByUserId(String userId) {
TbSysUser filter = new TbSysUser();
filter.setID(userId);
TbSysUser user = userService.find(filter);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + userId);
}
// 获取用户角色(如果角色服务可用)
List<TbSysDeptRole> roles = new ArrayList<>();
if (roleService != null) {
try {
// TODO: 需要在RoleService中实现findRolesByUserId方法
// roles = roleService.findRolesByUserId(userId);
} catch (Exception e) {
logger.warn("无法获取用户角色: " + e.getMessage());
}
}
// 获取用户权限(如果权限服务可用)
List<TbSysPermission> permissions = new ArrayList<>();
if (permissionService != null) {
try {
// TODO: 需要在PermissionService中实现findPermissionsByUserId方法
// permissions = permissionService.findPermissionsByUserId(userId);
} catch (Exception e) {
logger.warn("无法获取用户权限: " + e.getMessage());
}
}
return UserPrincipal.create(user, roles, permissions);
}
}

View File

@@ -0,0 +1,50 @@
package org.xyzh.auth.strategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
/**
* @description LoginStrategy.java文件描述 登录策略接口
* @filename LoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
public interface LoginStrategy {
/**
* @description 支持的登录类型
* @return String 登录类型
* @author yslg
* @since 2025-09-28
*/
String getLoginType();
/**
* @description 验证登录参数
* @param loginParam 登录参数
* @return boolean 是否有效
* @author yslg
* @since 2025-09-28
*/
boolean validate(LoginParam loginParam);
/**
* @description 根据登录参数查找用户
* @param loginParam 登录参数
* @return TbSysUser 用户对象
* @author yslg
* @since 2025-09-28
*/
TbSysUser findUser(LoginParam loginParam);
/**
* @description 验证密码
* @param inputPassword 输入密码
* @param storedPassword 存储密码
* @return boolean 是否匹配
* @author yslg
* @since 2025-09-28
*/
boolean verifyPassword(String inputPassword, String storedPassword);
}

View File

@@ -0,0 +1,51 @@
package org.xyzh.auth.strategy;
import org.springframework.stereotype.Component;
import org.xyzh.common.exception.auth.AuthException;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @description LoginStrategyFactory.java文件描述 登录策略工厂
* @filename LoginStrategyFactory.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class LoginStrategyFactory {
private final Map<String, LoginStrategy> strategies;
public LoginStrategyFactory(List<LoginStrategy> loginStrategies) {
this.strategies = loginStrategies.stream()
.collect(Collectors.toMap(LoginStrategy::getLoginType, Function.identity()));
}
/**
* @description 获取登录策略
* @param loginType 登录类型
* @return LoginStrategy 登录策略
* @author yslg
* @since 2025-09-28
*/
public LoginStrategy getStrategy(String loginType) {
LoginStrategy strategy = strategies.get(loginType);
if (strategy == null) {
throw new AuthException("UNSUPPORTED_LOGIN_TYPE", "不支持的登录类型: " + loginType);
}
return strategy;
}
/**
* @description 获取所有支持的登录类型
* @return Set<String> 登录类型集合
* @author yslg
* @since 2025-09-28
*/
public java.util.Set<String> getSupportedLoginTypes() {
return strategies.keySet();
}
}

View File

@@ -0,0 +1,49 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
/**
* @description EmailLoginStrategy.java文件描述 邮箱登录策略
* @filename EmailLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class EmailLoginStrategy implements LoginStrategy {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "email";
}
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getEmail() != null && !loginParam.getEmail().trim().isEmpty()
&& loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty();
}
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setEmail(loginParam.getEmail());
return userService.find(filter);
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
return passwordEncoder.matches(inputPassword, storedPassword);
}
}

View File

@@ -0,0 +1,53 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
/**
* @description PhoneLoginStrategy.java文件描述 手机号登录策略
* @filename PhoneLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class PhoneLoginStrategy implements LoginStrategy {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "phone";
}
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getPhone() != null && !loginParam.getPhone().trim().isEmpty()
&& (loginParam.getPassword() != null || loginParam.getCaptcha() != null);
}
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setPhone(loginParam.getPhone());
return userService.find(filter);
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
// 手机号登录可能使用验证码,如果有验证码则跳过密码验证
if (inputPassword == null) {
return true; // 假设验证码已经在其他地方验证过了
}
return passwordEncoder.matches(inputPassword, storedPassword);
}
}

View File

@@ -0,0 +1,49 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
/**
* @description UsernameLoginStrategy.java文件描述 用户名登录策略
* @filename UsernameLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class UsernameLoginStrategy implements LoginStrategy {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "username";
}
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getUsername() != null && !loginParam.getUsername().trim().isEmpty()
&& loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty();
}
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setUsername(loginParam.getUsername());
return userService.find(filter);
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
return passwordEncoder.matches(inputPassword, storedPassword);
}
}

View File

@@ -0,0 +1,47 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
/**
* @description WechatLoginStrategy.java文件描述 微信登录策略
* @filename WechatLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class WechatLoginStrategy implements LoginStrategy {
@Autowired
private UserService userService;
@Override
public String getLoginType() {
return "wechat";
}
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getWechatID() != null && !loginParam.getWechatID().trim().isEmpty();
}
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setWechatID(loginParam.getWechatID());
return userService.find(filter);
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
// 微信登录通常不需要密码验证,通过微信授权码验证
// 这里可以添加微信授权验证逻辑
return true;
}
}

View File

@@ -0,0 +1,151 @@
package org.xyzh.auth.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.utils.IDUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @description JwtTokenUtil.java文件描述 JWT工具类
* @filename JwtTokenUtil.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class JwtTokenUtil {
@Value("${school-news.auth.jwt-secret:schoolNewsDefaultSecretKeyForJWT2025}")
private String secret;
@Value("${school-news.auth.jwt-expiration:86400}")
private Long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* @description 生成JWT令牌
* @param loginDomain 登录域对象
* @return String JWT令牌
* @author yslg
* @since 2025-09-28
*/
public String generateToken(LoginDomain loginDomain) {
Map<String, Object> claims = new HashMap<>();
TbSysUser user = loginDomain.getUser();
claims.put("userId", user.getID());
claims.put("username", user.getUsername());
claims.put("email", user.getEmail());
claims.put("loginType", loginDomain.getLoginType());
claims.put("ipAddress", loginDomain.getIpAddress());
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getID())
.setId(IDUtils.generateID()) // 使用IDUtils生成JWT ID
.setIssuedAt(new Date())
.setExpiration(generateExpirationDate())
.signWith(getSigningKey())
.compact();
}
/**
* @description 从令牌中获取用户ID
* @param token JWT令牌
* @return String 用户ID
* @author yslg
* @since 2025-09-28
*/
public String getUserIdFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* @description 从令牌中获取过期时间
* @param token JWT令牌
* @return Date 过期时间
* @author yslg
* @since 2025-09-28
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* @description 从令牌中获取指定信息
* @param token JWT令牌
* @param claimsResolver 信息解析器
* @return T 指定信息
* @author yslg
* @since 2025-09-28
*/
public <T> T getClaimFromToken(String token, java.util.function.Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* @description 从令牌中获取所有信息
* @param token JWT令牌
* @return Claims 所有信息
* @author yslg
* @since 2025-09-28
*/
public Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* @description 验证令牌
* @param token JWT令牌
* @param userId 用户ID
* @return boolean 是否有效
* @author yslg
* @since 2025-09-28
*/
public boolean validateToken(String token, String userId) {
try {
final String tokenUserId = getUserIdFromToken(token);
return (userId.equals(tokenUserId) && !isTokenExpired(token));
} catch (Exception e) {
return false;
}
}
/**
* @description 检查令牌是否过期
* @param token JWT令牌
* @return boolean 是否过期
* @author yslg
* @since 2025-09-28
*/
public boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* @description 生成过期时间
* @return Date 过期时间
* @author yslg
* @since 2025-09-28
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}

View File

@@ -0,0 +1,63 @@
server:
port: 8081
spring:
application:
name: school-news-auth
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/school_news?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 20
minimum-idle: 5
# 认证配置
school-news:
auth:
# JWT配置
jwt-secret: schoolNewsSecretKeyForJWT2025SecureEnough
jwt-expiration: 86400 # 24小时
# 安全配置
max-login-attempts: 5
lockout-duration: 30 # 锁定30分钟
# 免登录白名单
white-list:
- "/auth/login"
- "/auth/logout"
- "/auth/captcha"
- "/auth/health"
- "/actuator/**"
- "/swagger-ui/**"
- "/v3/api-docs/**"
- "/favicon.ico"
- "/error"
- "/public/**"
- "/static/**"
# 日志配置使用log4j2配置文件
logging:
config: classpath:log4j2-spring.xml
# 管理端点配置
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
# 文档配置
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status这个用于设置log4j2自身内部的信息输出可以不设置当设置成trace时你会看到log4j2内部各种详细输出-->
<!--monitorIntervalLog4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="WARN" monitorInterval="30">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--变量配置-->
<Properties>
<!-- 格式化输出:%date表示日期%thread表示线程名%-5level级别从左显示5个字符宽度 %msg日志消息%n是换行符-->
<!-- %logger{36} 表示 Logger 名字最长36个字符 -->
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
<!-- 定义日志存储的路径 -->
<property name="FILE_PATH" value="./logs" />
<property name="FILE_NAME" value="school-news-auth" />
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="${LOG_PATTERN}"/>
<!--控制台只输出level及其以上级别的信息onMatch其他的直接拒绝onMismatch-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
</console>
<!--文件会打印出所有信息这个log每次运行程序会自动清空由append属性决定适合临时测试用-->
<File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">
<PatternLayout pattern="${LOG_PATTERN}"/>
</File>
<!-- 这个会打印出所有的info及以下级别的信息每次大小超过size则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息onMatch其他的直接拒绝onMismatch-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>
<!-- 这个会打印出所有的warn及以下级别的信息每次大小超过size则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息onMatch其他的直接拒绝onMismatch-->
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>
<!-- 这个会打印出所有的error及以下级别的信息每次大小超过size则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息onMatch其他的直接拒绝onMismatch-->
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>
</appenders>
<!--Logger节点用来单独指定日志的形式比如要为指定包下的class指定不同的日志级别等。-->
<!--然后定义loggers只有定义了logger并引入的appenderappender才会生效-->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<logger name="org.mybatis" level="info" additivity="false">
<AppenderRef ref="Console"/>
</logger>
<!--监控系统信息-->
<!--若是additivity设为false则 子Logger 只会在自己的appender里输出不会在 父Logger 的appender里输出。-->
<Logger name="org.springframework" level="info" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<!-- 项目包日志配置 -->
<Logger name="org.xyzh" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="Filelog"/>
<AppenderRef ref="RollingFileInfo"/>
<AppenderRef ref="RollingFileWarn"/>
<AppenderRef ref="RollingFileError"/>
</Logger>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="Filelog"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
</loggers>
</configuration>