登录成功

This commit is contained in:
2025-10-06 16:20:05 +08:00
parent a3e8687b31
commit a58f316703
54 changed files with 17818 additions and 622 deletions

View File

@@ -90,7 +90,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>

View File

@@ -8,6 +8,8 @@ import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.utils.IDUtils;
import jakarta.servlet.http.HttpServletRequest;
/**
* @description AuthController.java文件描述 认证控制器
* @filename AuthController.java
@@ -25,13 +27,13 @@ public class AuthController {
/**
* @description 用户登录
* @param loginParam 登录参数
* @return ResultDomain<String> 登录结果
* @return ResultDomain<LoginDomain> 登录结果
* @author yslg
* @since 2025-09-28
*/
@PostMapping("/login")
public ResultDomain<String> login(@RequestBody LoginParam loginParam) {
return loginService.login(loginParam);
public ResultDomain<LoginDomain> login(@RequestBody LoginParam loginParam, HttpServletRequest request) {
return loginService.login(loginParam, request);
}
/**

View File

@@ -0,0 +1,48 @@
package org.xyzh.auth.security;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.xyzh.auth.util.JwtTokenUtil;
import org.xyzh.common.core.security.TokenParser;
/**
* @description JwtTokenParser.java文件描述 JWT令牌解析器实现类
* @filename JwtTokenParser.java
* @author yslg
* @copyright xyzh
* @since 2025-10-06
*/
@Component
public class JwtTokenParser implements TokenParser {
private final JwtTokenUtil jwtTokenUtil;
/**
* Spring会自动注入无需@Autowired注解
*/
public JwtTokenParser(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
public String getUserIdFromToken(String token) {
return jwtTokenUtil.getUserIdFromToken(token);
}
@Override
public Claims getAllClaimsFromToken(String token) {
return jwtTokenUtil.getAllClaimsFromToken(token);
}
@Override
public boolean validateToken(String token, String userId) {
return jwtTokenUtil.validateToken(token, userId);
}
@Override
public boolean isTokenExpired(String token) {
return jwtTokenUtil.isTokenExpired(token);
}
}

View File

@@ -11,15 +11,25 @@ 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.permission.TbSysPermission;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.dto.log.TbSysLoginLog;
import org.xyzh.common.dto.menu.TbSysMenu;
import org.xyzh.common.exception.auth.AuthException;
import org.xyzh.common.utils.IDUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.xyzh.api.system.user.UserService;
import org.xyzh.api.system.role.RoleService;
import org.xyzh.api.system.permission.PermissionService;
import org.xyzh.common.redis.service.RedisService;
import org.xyzh.api.system.menu.MenuService;
import org.xyzh.common.vo.DeptRoleVO;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
/**
@@ -38,12 +48,18 @@ public class LoginServiceImpl implements LoginService {
@Autowired
private UserService userService;
@Autowired(required = false)
@Autowired
private RoleService roleService;
@Autowired(required = false)
@Autowired
private PermissionService permissionService;
@Autowired
private MenuService menuService;
@Autowired
private RedisService redisService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@@ -51,9 +67,9 @@ public class LoginServiceImpl implements LoginService {
private LoginLogService loginLogService;
@Override
public ResultDomain<String> login(LoginParam loginParam) {
ResultDomain<String> result = new ResultDomain<>();
public ResultDomain<LoginDomain> login(LoginParam loginParam, HttpServletRequest request) {
ResultDomain<LoginDomain> result = new ResultDomain<>();
String ipAddress = request.getRemoteAddr();
try {
// 自动检测登录类型
String loginType = detectLoginType(loginParam);
@@ -84,24 +100,40 @@ public class LoginServiceImpl implements LoginService {
return result;
}
// 验证密码
if (!strategy.verifyPassword(loginParam.getPassword(), user.getPassword())) {
result.fail("密码错误");
logLoginAttempt(loginParam, user, false, "密码错误");
return result;
if (loginType.equals("password")) {
// 验证凭据(密码或验证码)
if (!strategy.verifyCredential(loginParam.getPassword(), user.getPassword())) {
result.fail("密码错误");
logLoginAttempt(loginParam, user, false, "密码错误");
return result;
}
}
else {
// 验证凭据(验证码)
String storedCaptcha = (String) redisService.get("captcha:" + loginParam.getPhone());
if (!strategy.verifyCredential(loginParam.getCaptcha(), storedCaptcha)) {
result.fail("验证码错误");
logLoginAttempt(loginParam, user, false, "验证码错误");
return result;
}
// 验证码使用后删除
redisService.delete("captcha:" + loginParam.getPhone());
}
// 构建登录域对象
LoginDomain loginDomain = buildLoginDomain(user, loginType);
LoginDomain loginDomain = buildLoginDomain(user, loginType, ipAddress);
// 生成JWT令牌
String token = jwtTokenUtil.generateToken(loginDomain);
loginDomain.setToken(jwtTokenUtil.generateToken(loginDomain));
// 将LoginDomain存储到Redis中
String redisKey = "login:token:" + user.getID();
redisService.set(redisKey, loginDomain, 24 * 60 * 60, TimeUnit.SECONDS);
// 记录成功登录日志
logLoginAttempt(loginParam, user, true, "登录成功");
result.success("登录成功", (String)null);
result.setData(token);
result.success("登录成功", loginDomain);
result.setData(loginDomain);
} catch (AuthException e) {
result.fail(e.getMessage());
@@ -139,7 +171,9 @@ public class LoginServiceImpl implements LoginService {
if (StringUtils.hasText(loginParam.getLoginType())) {
return loginParam.getLoginType();
}
if (StringUtils.hasText(loginParam.getPassword())) {
return "password";
}
if (StringUtils.hasText(loginParam.getEmail())) {
return "email";
}
@@ -164,36 +198,48 @@ public class LoginServiceImpl implements LoginService {
* @author yslg
* @since 2025-09-28
*/
private LoginDomain buildLoginDomain(TbSysUser user, String loginType) {
private LoginDomain buildLoginDomain(TbSysUser user, String loginType, String ipAddress) {
LoginDomain loginDomain = new LoginDomain();
loginDomain.setUser(user);
loginDomain.setLoginType(loginType);
loginDomain.setLoginTime(new Date());
loginDomain.setIpAddress(ipAddress);
// 获取用户角色和权限(如果服务可用)
if (roleService != null) {
try {
// TODO: 需要在RoleService中实现findRolesByUserId方法
// loginDomain.setRoles(roleService.findRolesByUserId(user.getID()));
loginDomain.setRoles(new ArrayList<>());
} catch (Exception e) {
try {
ResultDomain<DeptRoleVO> resultDomain = roleService.getDeptRolesByUserId(user.getID());
if (resultDomain.isSuccess()) {
List<DeptRoleVO> roles = resultDomain.getDataList();
loginDomain.setRoles(roles);
} else {
loginDomain.setRoles(new ArrayList<>());
}
} else {
} catch (Exception e) {
loginDomain.setRoles(new ArrayList<>());
}
if (permissionService != null) {
try {
// TODO: 需要在PermissionService中实现findPermissionsByUserId方法
// loginDomain.setPermissions(permissionService.findPermissionsByUserId(user.getID()));
loginDomain.setPermissions(new ArrayList<>());
} catch (Exception e) {
try {
ResultDomain<TbSysPermission> resultDomain = permissionService.getPermissionsByUserId(user.getID());
if (resultDomain.isSuccess()) {
List<TbSysPermission> permissions = resultDomain.getDataList();
loginDomain.setPermissions(permissions);
} else {
loginDomain.setPermissions(new ArrayList<>());
}
} else {
} catch (Exception e) {
loginDomain.setPermissions(new ArrayList<>());
}
try {
ResultDomain<TbSysMenu> resultDomain = menuService.getMenusByUserId(user.getID());
if (resultDomain.isSuccess()) {
List<TbSysMenu> menus = resultDomain.getDataList();
loginDomain.setMenus(menus);
} else {
loginDomain.setMenus(new ArrayList<>());
}
} catch (Exception e) {
loginDomain.setMenus(new ArrayList<>());
}
return loginDomain;
}

View File

@@ -39,12 +39,12 @@ public interface LoginStrategy {
TbSysUser findUser(LoginParam loginParam);
/**
* @description 验证密码
* @param inputPassword 输入密码
* @param storedPassword 存储密码
* @description 验证凭据(密码或验证码)
* @param inputCredential 输入凭据(密码或验证码)
* @param storedCredential 存储凭据(密码或验证码)
* @return boolean 是否匹配
* @author yslg
* @since 2025-09-28
*/
boolean verifyPassword(String inputPassword, String storedPassword);
boolean verifyCredential(String inputCredential, String storedCredential);
}

View File

@@ -43,7 +43,7 @@ public class EmailLoginStrategy implements LoginStrategy {
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
return passwordEncoder.matches(inputPassword, storedPassword);
public boolean verifyCredential(String inputCredential, String storedCredential) {
return passwordEncoder.matches(inputCredential, storedCredential);
}
}

View File

@@ -0,0 +1,70 @@
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.common.utils.NonUtils;
import org.xyzh.api.system.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @description PasswordLoginStrategy.java文件描述 密码登录策略
* @filename PasswordLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@Component
public class PasswordLoginStrategy implements LoginStrategy {
private static final Logger logger = LoggerFactory.getLogger(PasswordLoginStrategy.class);
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "password";
}
@Override
public boolean validate(LoginParam loginParam) {
if (NonUtils.isEmpty(loginParam.getPassword())) {
return false;
}
if (NonUtils.isEmpty(loginParam.getUsername()) && NonUtils.isEmpty(loginParam.getEmail()) && NonUtils.isEmpty(loginParam.getPhone())) {
return false;
}
return true;
}
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
if (NonUtils.isNotEmpty(loginParam.getUsername())) {
filter.setUsername(loginParam.getUsername());
}
if (NonUtils.isNotEmpty(loginParam.getEmail())) {
filter.setEmail(loginParam.getEmail());
}
if (NonUtils.isNotEmpty(loginParam.getPhone())) {
filter.setPhone(loginParam.getPhone());
}
return userService.getUserByFilter(filter).getData();
}
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
logger.info(passwordEncoder.encode(inputCredential));
return passwordEncoder.matches(inputCredential, storedCredential);
}
}

View File

@@ -43,11 +43,11 @@ public class PhoneLoginStrategy implements LoginStrategy {
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 手机号登录可能使用验证码,如果有验证码则跳过密码验证
if (inputPassword == null) {
if (inputCredential == null) {
return true; // 假设验证码已经在其他地方验证过了
}
return passwordEncoder.matches(inputPassword, storedPassword);
return passwordEncoder.matches(inputCredential, storedCredential);
}
}

View File

@@ -43,7 +43,7 @@ public class UsernameLoginStrategy implements LoginStrategy {
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
return passwordEncoder.matches(inputPassword, storedPassword);
public boolean verifyCredential(String inputCredential, String storedCredential) {
return passwordEncoder.matches(inputCredential, storedCredential);
}
}

View File

@@ -39,7 +39,7 @@ public class WechatLoginStrategy implements LoginStrategy {
}
@Override
public boolean verifyPassword(String inputPassword, String storedPassword) {
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 微信登录通常不需要密码验证,通过微信授权码验证
// 这里可以添加微信授权验证逻辑
return true;

View File

@@ -5,6 +5,21 @@ spring:
application:
name: school-news-auth
# Redis配置
data:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 5000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/school_news?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

View File

@@ -12,27 +12,29 @@
<!-- 定义日志存储的路径 -->
<property name="FILE_PATH" value="./logs" />
<property name="FILE_NAME" value="school-news-auth" />
<property name="file.encoding" value="UTF-8" />
<property name="console.encoding" value="UTF-8" />
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="${LOG_PATTERN}"/>
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<!--控制台只输出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}"/>
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
</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">
<RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/${FILE_NAME}-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}"/>
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<Policies>
<!--interval属性用来指定多久滚动一次默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
@@ -43,10 +45,10 @@
</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">
<RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/${FILE_NAME}-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}"/>
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<Policies>
<!--interval属性用来指定多久滚动一次默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
@@ -57,10 +59,10 @@
</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">
<RollingFile name="RollingFileError" fileName="${FILE_PATH}/${FILE_NAME}-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}"/>
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<Policies>
<!--interval属性用来指定多久滚动一次默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>