commit 280de668f0da9f11ca4391e70f8e2c1c5d9c7f74 Author: 孙小云 Date: Fri Jan 9 14:21:23 2026 +0800 Initial commit: ruoyi-auth module diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..320350f --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs +hs_err_pid* + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Gradle +.gradle +build/ + +# IntelliJ IDEA +.idea/ +*.iws +*.iml +*.ipr +out/ + +# Eclipse +.classpath +.project +.settings/ +bin/ + +# NetBeans +nbproject/private/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +# VS Code +.vscode/ + +# macOS +.DS_Store + +# Application specific +logs/ +temp/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6342e0a --- /dev/null +++ b/pom.xml @@ -0,0 +1,74 @@ + + + com.ruoyi + ruoyi + 3.6.7 + + 4.0.0 + + ruoyi-auth + + + ruoyi-auth认证授权中心 + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.ruoyi + ruoyi-common-security + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/src/main/java/com/ruoyi/auth/RuoYiAuthApplication.java b/src/main/java/com/ruoyi/auth/RuoYiAuthApplication.java new file mode 100644 index 0000000..1e3a087 --- /dev/null +++ b/src/main/java/com/ruoyi/auth/RuoYiAuthApplication.java @@ -0,0 +1,31 @@ +package com.ruoyi.auth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import com.ruoyi.common.security.annotation.EnableRyFeignClients; + +/** + * 认证授权中心 + * + * @author ruoyi + */ +@EnableRyFeignClients +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class }) +public class RuoYiAuthApplication +{ + public static void main(String[] args) + { + SpringApplication.run(RuoYiAuthApplication.class, args); + System.out.println("(♥◠‿◠)ノ゙ 认证授权中心启动成功 ლ(´ڡ`ლ)゙ \n" + + " .-------. ____ __ \n" + + " | _ _ \\ \\ \\ / / \n" + + " | ( ' ) | \\ _. / ' \n" + + " |(_ o _) / _( )_ .' \n" + + " | (_,_).' __ ___(_ o _)' \n" + + " | |\\ \\ | || |(_,_)' \n" + + " | | \\ `' /| `-' / \n" + + " | | \\ / \\ / \n" + + " ''-' `'-' `-..-' "); + } +} diff --git a/src/main/java/com/ruoyi/auth/controller/TokenController.java b/src/main/java/com/ruoyi/auth/controller/TokenController.java new file mode 100644 index 0000000..30dbda4 --- /dev/null +++ b/src/main/java/com/ruoyi/auth/controller/TokenController.java @@ -0,0 +1,78 @@ +package com.ruoyi.auth.controller; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.auth.form.LoginBody; +import com.ruoyi.auth.form.RegisterBody; +import com.ruoyi.auth.service.SysLoginService; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.utils.JwtUtils; +import com.ruoyi.common.core.utils.StringUtils; +import com.ruoyi.common.security.auth.AuthUtil; +import com.ruoyi.common.security.service.TokenService; +import com.ruoyi.common.security.utils.SecurityUtils; +import com.ruoyi.system.api.model.LoginUser; + +/** + * token 控制 + * + * @author ruoyi + */ +@RestController +public class TokenController +{ + @Autowired + private TokenService tokenService; + + @Autowired + private SysLoginService sysLoginService; + + @PostMapping("login") + public R login(@RequestBody LoginBody form) + { + // 用户登录 + LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword()); + // 获取登录token + return R.ok(tokenService.createToken(userInfo)); + } + + @DeleteMapping("logout") + public R logout(HttpServletRequest request) + { + String token = SecurityUtils.getToken(request); + if (StringUtils.isNotEmpty(token)) + { + String username = JwtUtils.getUserName(token); + // 删除用户缓存记录 + AuthUtil.logoutByToken(token); + // 记录用户退出日志 + sysLoginService.logout(username); + } + return R.ok(); + } + + @PostMapping("refresh") + public R refresh(HttpServletRequest request) + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + // 刷新令牌有效期 + tokenService.refreshToken(loginUser); + return R.ok(); + } + return R.ok(); + } + + @PostMapping("register") + public R register(@RequestBody RegisterBody registerBody) + { + // 用户注册 + sysLoginService.register(registerBody.getUsername(), registerBody.getPassword()); + return R.ok(); + } +} diff --git a/src/main/java/com/ruoyi/auth/form/LoginBody.java b/src/main/java/com/ruoyi/auth/form/LoginBody.java new file mode 100644 index 0000000..b12fb31 --- /dev/null +++ b/src/main/java/com/ruoyi/auth/form/LoginBody.java @@ -0,0 +1,39 @@ +package com.ruoyi.auth.form; + +/** + * 用户登录对象 + * + * @author ruoyi + */ +public class LoginBody +{ + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } +} diff --git a/src/main/java/com/ruoyi/auth/form/RegisterBody.java b/src/main/java/com/ruoyi/auth/form/RegisterBody.java new file mode 100644 index 0000000..9741507 --- /dev/null +++ b/src/main/java/com/ruoyi/auth/form/RegisterBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.auth.form; + +/** + * 用户注册对象 + * + * @author ruoyi + */ +public class RegisterBody extends LoginBody +{ + +} diff --git a/src/main/java/com/ruoyi/auth/service/SysLoginService.java b/src/main/java/com/ruoyi/auth/service/SysLoginService.java new file mode 100644 index 0000000..9ba6046 --- /dev/null +++ b/src/main/java/com/ruoyi/auth/service/SysLoginService.java @@ -0,0 +1,156 @@ +package com.ruoyi.auth.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.core.constant.CacheConstants; +import com.ruoyi.common.core.constant.Constants; +import com.ruoyi.common.core.constant.SecurityConstants; +import com.ruoyi.common.core.constant.UserConstants; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.enums.UserStatus; +import com.ruoyi.common.core.exception.ServiceException; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.core.utils.DateUtils; +import com.ruoyi.common.core.utils.StringUtils; +import com.ruoyi.common.core.utils.ip.IpUtils; +import com.ruoyi.common.redis.service.RedisService; +import com.ruoyi.common.security.utils.SecurityUtils; +import com.ruoyi.system.api.RemoteUserService; +import com.ruoyi.system.api.domain.SysUser; +import com.ruoyi.system.api.model.LoginUser; + +/** + * 登录校验方法 + * + * @author ruoyi + */ +@Component +public class SysLoginService +{ + @Autowired + private RemoteUserService remoteUserService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysRecordLogService recordLogService; + + @Autowired + private RedisService redisService; + + /** + * 登录 + */ + public LoginUser login(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isAnyBlank(username, password)) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写"); + throw new ServiceException("用户/密码必须填写"); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围"); + throw new ServiceException("用户密码不在指定范围"); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围"); + throw new ServiceException("用户名不在指定范围"); + } + // IP黑名单校验 + String blackStr = Convert.toStr(redisService.getCacheObject(CacheConstants.SYS_LOGIN_BLACKIPLIST)); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "很遗憾,访问IP已被列入系统黑名单"); + throw new ServiceException("很遗憾,访问IP已被列入系统黑名单"); + } + // 查询用户信息 + R userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); + + if (R.FAIL == userResult.getCode()) + { + throw new ServiceException(userResult.getMsg()); + } + + LoginUser userInfo = userResult.getData(); + SysUser user = userResult.getData().getSysUser(); + if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除"); + throw new ServiceException("对不起,您的账号:" + username + " 已被删除"); + } + if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员"); + throw new ServiceException("对不起,您的账号:" + username + " 已停用"); + } + passwordService.validate(user, password); + recordLogService.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功"); + recordLoginInfo(user.getUserId()); + return userInfo; + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + // 更新用户登录IP + sysUser.setLoginIp(IpUtils.getIpAddr()); + // 更新用户登录时间 + sysUser.setLoginDate(DateUtils.getNowDate()); + remoteUserService.recordUserLogin(sysUser, SecurityConstants.INNER); + } + + public void logout(String loginName) + { + recordLogService.recordLogininfor(loginName, Constants.LOGOUT, "退出成功"); + } + + /** + * 注册 + */ + public void register(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isAnyBlank(username, password)) + { + throw new ServiceException("用户/密码必须填写"); + } + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + throw new ServiceException("账户长度必须在2到20个字符之间"); + } + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + throw new ServiceException("密码长度必须在5到20个字符之间"); + } + + // 注册用户信息 + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + sysUser.setNickName(username); + sysUser.setPwdUpdateDate(DateUtils.getNowDate()); + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + R registerResult = remoteUserService.registerUserInfo(sysUser, SecurityConstants.INNER); + + if (R.FAIL == registerResult.getCode()) + { + throw new ServiceException(registerResult.getMsg()); + } + recordLogService.recordLogininfor(username, Constants.REGISTER, "注册成功"); + } +} diff --git a/src/main/java/com/ruoyi/auth/service/SysPasswordService.java b/src/main/java/com/ruoyi/auth/service/SysPasswordService.java new file mode 100644 index 0000000..eb18ea8 --- /dev/null +++ b/src/main/java/com/ruoyi/auth/service/SysPasswordService.java @@ -0,0 +1,85 @@ +package com.ruoyi.auth.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.core.constant.CacheConstants; +import com.ruoyi.common.core.constant.Constants; +import com.ruoyi.common.core.exception.ServiceException; +import com.ruoyi.common.redis.service.RedisService; +import com.ruoyi.common.security.utils.SecurityUtils; +import com.ruoyi.system.api.domain.SysUser; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisService redisService; + + private int maxRetryCount = CacheConstants.PASSWORD_MAX_RETRY_COUNT; + + private Long lockTime = CacheConstants.PASSWORD_LOCK_TIME; + + @Autowired + private SysRecordLogService recordLogService; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user, String password) + { + String username = user.getUserName(); + + Integer retryCount = redisService.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime); + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL,errMsg); + throw new ServiceException(errMsg); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, String.format("密码输入错误%s次", retryCount)); + redisService.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new ServiceException("用户不存在/密码错误"); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisService.hasKey(getCacheKey(loginName))) + { + redisService.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/src/main/java/com/ruoyi/auth/service/SysRecordLogService.java b/src/main/java/com/ruoyi/auth/service/SysRecordLogService.java new file mode 100644 index 0000000..7ca0f00 --- /dev/null +++ b/src/main/java/com/ruoyi/auth/service/SysRecordLogService.java @@ -0,0 +1,48 @@ +package com.ruoyi.auth.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.core.constant.Constants; +import com.ruoyi.common.core.constant.SecurityConstants; +import com.ruoyi.common.core.utils.StringUtils; +import com.ruoyi.common.core.utils.ip.IpUtils; +import com.ruoyi.system.api.RemoteLogService; +import com.ruoyi.system.api.domain.SysLogininfor; + +/** + * 记录日志方法 + * + * @author ruoyi + */ +@Component +public class SysRecordLogService +{ + @Autowired + private RemoteLogService remoteLogService; + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息内容 + * @return + */ + public void recordLogininfor(String username, String status, String message) + { + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(IpUtils.getIpAddr()); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.LOGIN_SUCCESS_STATUS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.LOGIN_FAIL_STATUS); + } + remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER); + } +} diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..97c5c27 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ _ _ + (_) | | | | + _ __ _ _ ___ _ _ _ ______ __ _ _ _ | |_ | |__ +| '__|| | | | / _ \ | | | || ||______| / _` || | | || __|| '_ \ +| | | |_| || (_) || |_| || | | (_| || |_| || |_ | | | | +|_| \__,_| \___/ \__, ||_| \__,_| \__,_| \__||_| |_| + __/ | + |___/ \ No newline at end of file diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..105cbdd --- /dev/null +++ b/src/main/resources/bootstrap.yml @@ -0,0 +1,25 @@ +# Tomcat +server: + port: 9200 + +# Spring +spring: + application: + # 应用名称 + name: ruoyi-auth + profiles: + # 环境配置 + active: prod + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: ruoyi-nacos:8848 + config: + # 配置中心地址 + server-addr: ruoyi-nacos:8848 + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..098a610 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file