今天咱们就扒一扒34k星开源项目「芋道源码」的登录模块,从核心代码到底层原理,一步步拆明白它是怎么实现"安全又优雅"的登录流程的。
为什么选它
- 34.1k Star,社区体量足够,遇到问题能搜到现成讨论。
- 官方把 RBAC、多租户、工作流、支付、短信、商城等模块做成可插拔 starter,不必自己补业务场景,直接调试即可。
- 同一套后端接口同时供给 Vue3 管理端 + UniApp 小程序,前端部分可一次性验证 PC 与移动端,减少重复对接。
技术栈:Spring Boot、MyBatis Plus、Vue3、Element Plus、UniApp。
一、登录核心流程:3步走通账号密码登录 🔑
芋道的登录主方法把复杂流程拆成了清晰的三步,就像组装乐高一样,每一步职责明确,既好维护又方便调试。先看整体骨架:
java
/**
* Auth Service 实现类
* @author 芋道源码
*/
@Service
@Slf4j
public class AdminAuthServiceImpl implements AdminAuthService {
/**
* 登录主方法:完整处理账号密码登录流程
* @param reqVO 登录请求(含用户名、密码、验证码)
* @return 登录响应(含Token等核心信息)
*/
@Override
@DataPermission(enable = false)
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
// 步骤1:先验验证码,防机器暴力破解 🛡️
validateCaptcha(reqVO);
// 步骤2:账号密码校验,核心认证环节
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
// 步骤3:生成Token+记日志,给前端返回结果
return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
}
}
下面咱们逐个拆解,看看每个环节里都藏着哪些细节。
1. 步骤1:验证码校验,第一道防护墙 🛡️
验证码的作用不用多说------挡住机器人批量试密码。但芋道的实现很灵活,支持通过配置开关控制是否启用,方便测试环境跳过验证。
java
/**
* 验证码功能开关:从配置文件读取,默认开启
* @Value:Spring注解,读配置项yudao.captcha.enable,没配置就用true
* @Setter:Lombok生成setter,方便测试时临时关验证码
*/
@Value("${yudao.captcha.enable:true}")
@Setter
private Boolean captchaEnable;
// 验证码校验核心方法(测试可见)
@VisibleForTesting
void validateCaptcha(AuthLoginReqVO reqVO) {
ResponseModel response = doValidateCaptcha(reqVO);
// 验证码不对?直接抛异常+记失败日志
if (!response.isSuccess()) {
createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);
throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());
}
}
// 实际执行校验的私有方法
private ResponseModel doValidateCaptcha(CaptchaVerificationReqVO reqVO) {
// 开关关了就直接通过,太贴心了有没有 😭
if (!captchaEnable) {
return ResponseModel.success();
}
// 校验请求参数合法性,再调用验证码服务验证
ValidationUtils.validate(validator, reqVO, CaptchaVerificationReqVO.CodeEnableGroup.class);
CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
return captchaService.verification(captchaVO);
}
小细节:用@VisibleForTesting注解让测试类能访问这个方法,方便写单元测试;开关变量加@Setter,测试时不用改配置就能关验证码,开发体验拉满!
2. 步骤2:账号密码认证,核心校验环节 🔍
这一步是登录的核心------要确认"你是谁""你有权限登录吗"。芋道在这里做了三层校验,还加了安全细节(比如统一错误提示,防止泄露用户是否存在)。
java
// 注入依赖:密码编码器(Spring Security提供)和用户服务
private final PasswordEncoder passwordEncoder;
private final AdminUserService userService;
// 构造器注入:推荐方式,避免循环依赖问题
public AdminAuthServiceImpl(PasswordEncoder passwordEncoder, AdminUserService userService) {
this.passwordEncoder = passwordEncoder;
this.userService = userService;
}
/**
* 核心认证方法:校验用户存在性、密码正确性、账号状态
*/
@Override
public AdminUserDO authenticate(String username, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
// 校验1:用户存在吗?查不到就记日志抛异常
AdminUserDO user = userService.getUserByUsername(username);
if (user == null) {
createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
// 统一提示"凭证错误",不泄露"用户不存在",安全细节拉满 🔒
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验2:密码对吗?用Spring Security的编码器比对
if (!isPasswordMatch(password, user.getPassword())) {
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验3:账号被禁用了吗?
if (CommonStatusEnum.isDisable(user.getStatus())) {
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
throw exception(AUTH_LOGIN_USER_DISABLED);
}
// 所有校验通过,返回用户信息
return user;
}
// 密码比对:明文密码 vs 数据库加密密码
@Override
public boolean isPasswordMatch(String rawPassword, String encodedPassword) {
// 编码器内部会用加密时的盐值重新加密明文,再对比
return passwordEncoder.matches(rawPassword, encodedPassword);
}
3. 步骤3:生成Token,登录成功的"通行证" 🎫
验证通过后,就得给用户发"通行证"------Token。同时还要记录登录日志,方便后续排查问题。这一步的核心是调用Token服务生成凭证。
java
/**
* 登录成功后:生成Token+记日志+构造响应
*/
private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
// 记录成功日志,运营排查问题全靠它
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
// 调用OAuth2服务生成访问令牌
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
// 转换为前端需要的响应格式
return AuthConvert.INSTANCE.convert(accessTokenDO);
}
二、Token生成深扒:OAuth2.0简化模式实战 🚀
Token是用户登录后的"身份凭证",芋道用了OAuth2.0的简化模式实现,核心是生成"访问令牌+刷新令牌"双凭证,既安全又灵活。整个流程分5步:
① 接收参数 → ② 校验客户端 → ③ 创建刷新令牌 → ④ 创建访问令牌 → ⑤ 返回结果
1. 入口方法:串联Token生成全流程
java
// 位于OAuth2TokenServiceImpl.java,事务注解保证原子性
@Override
@Transactional(rollbackFor = Exception.class)
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType,
String clientId, List<String> scopes) {
// ② 校验客户端:必须存在、启用、权限合法(核心校验)
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
// ③ 创建刷新令牌(有效期长,用于换访问令牌)
OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
// ④ 创建访问令牌(有效期短,用于接口调用)
return createOAuth2AccessToken(refreshTokenDO, clientDO);
}
2. 客户端校验:确保请求来源合法 🔐
客户端就像"请求的身份证",必须校验它是不是系统认可的。这里还用了缓存加速,避免每次都查数据库。
java
// 位于OAuth2ClientServiceImpl.java,客户端校验核心方法
public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
String grantType, Collection<String> scopes, String redirectUri) {
// 先查缓存,没命中再查DB(缓存逻辑后面讲)
OAuth2ClientDO client = getOAuth2ClientFromCache(clientId);
if (client == null) throw exception(OAUTH2_CLIENT_NOT_EXISTS); // 客户端不存在
if (CommonStatusEnum.isDisable(client.getStatus())) throw exception(OAUTH2_CLIENT_DISABLE); // 客户端已禁用
if (!client.getScopes().containsAll(scopes)) throw exception(OAUTH2_CLIENT_SCOPE_OVER); // 权限越界
// 其余4项校验省略...
return client;
}
3. 双令牌生成:安全与便捷的平衡 🎯
为什么要搞"访问令牌+刷新令牌"?因为访问令牌有效期短(比如2小时),泄露风险低;刷新令牌有效期长(比如7天),用来在访问令牌过期后"免登录续期",用户体验好。

刷新令牌生成
java
private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType,
OAuth2ClientDO client, List<String> scopes) {
OAuth2RefreshTokenDO entity = new OAuth2RefreshTokenDO();
entity.setId(generateId()); // 雪花ID,唯一标识
entity.setRefreshToken(randomUUID());// 随机串,安全不易猜
entity.setUserId(userId); // 关联用户ID
entity.setUserType(userType); // 用户类型
entity.setClientId(client.getClientId()); // 关联客户端
entity.setScopes(scopes); // 权限范围
// 过期时间:从客户端配置读取(比如7天)
entity.setExpiresTime(LocalDateTime.now()
.plusSeconds(client.getRefreshTokenValiditySeconds()));
oauth2RefreshTokenMapper.insert(entity); // 存DB+缓存
return entity;
}
访问令牌生成
java
private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshToken,
OAuth2ClientDO client) {
OAuth2AccessTokenDO entity = new OAuth2AccessTokenDO();
entity.setId(generateId());
entity.setAccessToken(randomUUID()); // 访问令牌随机串
entity.setRefreshTokenId(refreshToken.getId()); // 关联刷新令牌
entity.setClientId(client.getClientId());
// 过期时间:客户端配置(比如2小时)
entity.setExpiresTime(LocalDateTime.now()
.plusSeconds(client.getAccessTokenValiditySeconds()));
oauth2AccessTokenMapper.insert(entity); // 存DB+缓存
return entity; // 最终返回给前端
}
三、相关知识点 🛠️
除了核心流程,异常处理、缓存、依赖注入这些"底层基建",才是让登录功能稳定又好维护的关键。
1. 异常处理:用户和开发都省心 😌
登录过程中会遇到各种错误:密码错、验证码错、服务器崩了... 芋道用"自定义异常+全局拦截"让错误处理更优雅。
为什么要自定义异常?
JDK自带的异常(比如NullPointerException)分不清是"用户操作错"还是"系统出故障"。芋道把异常分成两类:
- ServiceException(业务异常) :用户能理解的错误,比如"密码错误",带错误码和提示,前端直接展示给用户。
- ServerException(系统异常) :服务器故障,比如数据库连不上,包装原始异常方便排查,给用户看"系统繁忙"即可。
全局异常拦截:一次配置,处处生效
用Spring的@RestControllerAdvice注解做全局拦截,不用在每个方法里写try-catch,代码超干净!
java
@RestControllerAdvice // 全局异常处理器
public class GlobalExceptionHandler {
// 处理业务异常(比如密码错误)
@ExceptionHandler(ServiceException.class)
public CommonResult<?> handleBiz(ServiceException ex) {
log.warn("业务异常: {}", ex.getMessage()); // 只记警告,不打堆栈
return CommonResult.error(ex.getCode(), ex.getMessage());
}
// 处理系统异常(比如DB连接失败)
@ExceptionHandler(ServerException.class)
public CommonResult<?> handleSys(ServerException ex) {
log.error("系统异常", ex); // 打完整堆栈,方便排查
return CommonResult.error(ex.getCode(), "系统繁忙,请稍后再试");
}
// 兜底处理其他未捕获异常
@ExceptionHandler(Exception.class)
public CommonResult<?> handleOther(Exception ex) {
log.error("未捕获异常", ex);
return CommonResult.error(500999, "系统异常");
}
}
前端永远收到结构一致的响应:{code, msg, data},不用处理各种乱七八糟的错误格式,前后端协作效率翻倍!
2. 缓存:让登录更快,DB压力更小 ⚡
客户端信息、用户信息这些高频访问的数据,每次都查DB太慢了。芋道用Redis做缓存,一行注解就实现"查缓存→没命中查DB→存缓存"的全流程。
第一步:加依赖+配Redis
yaml
# application.yml配置
spring:
cache:
type: redis # 用Redis做缓存
redis:
time-to-live: 1800s # 默认30分钟过期
cache-null-values: false # 不缓存null,防穿透
redis:
host: localhost
port: 6379
password: # 无密码留空
第二步:加个注解,缓存自动生效
java
// 查客户端信息的方法,加@Cacheable注解即可
@Override
@Cacheable(cacheNames = RedisKeyConstants.OAUTH_CLIENT, key = "#clientId",
unless = "#result == null") // 返回null不缓存,防穿透
public OAuth2ClientDO getOAuth2ClientFromCache(String clientId) {
// 第一次调用:查DB;后续调用:直接从Redis拿,方法体都不执行
return oauth2ClientMapper.selectById(clientId);
}
缓存执行流程,一目了然
- 第一次调用getOAuth2ClientFromCache("default") → 查Redis没命中 → 查DB → 结果存Redis(设30分钟过期)→ 返回数据。
- 第二次调用 → 查Redis命中 → 直接返回数据 → 方法体不执行(没SQL输出)。
- 返回null时 → unless条件生效 → 不存缓存,避免缓存穿透攻击。
3. Spring依赖注入:不用自己new对象
登录功能里用到了UserService、PasswordEncoder等各种服务,要是每次都自己new,代码会乱成一团。Spring的依赖注入帮我们搞定了"对象创建+管理+赋值"的全流程。
常用注入方式
java
@Service
public class DemoService {
// 1. @Resource:按名字注入(适合多实现)
@Resource(name = "customValidator")
private Validator validator;
// 2. @Autowired:按类型注入(适合唯一实现)
@Autowired
private MailService mailService;
// 3. @Autowired+@Qualifier:按名字+类型(多实现时精准匹配)
@Autowired
@Qualifier("smsSender")
private Sender sender;
// 4. 构造器注入:推荐!避免循环依赖,代码更健壮
private final UserDao userDao;
public DemoService(UserDao userDao) {
this.userDao = userDao;
}
}
注入核心原理
Spring启动时会扫描带@Service、@Component等注解的类,用反射创建对象(这就是Bean),然后把这些Bean"塞"到需要的地方。简单说:你只管要,Spring负责给,不用自己new!
4. YAML配置:环境切换超灵活 🔄
开发环境要关验证码,生产环境要开;开发环境连本地Redis,生产环境连云Redis------这些都靠YAML配置实现,不用改代码就能切换环境。
主配置+环境配置,优雅切换
yaml
# 主配置 application.yaml
spring:
profiles:
active: local # 激活本地环境配置
yudao:
captcha:
enable: true # 全局默认开验证码
yaml
# 本地环境配置 application-local.yaml
# 覆盖主配置,本地关验证码
yudao:
captcha:
enable: false
# 本地Redis配置
spring:
redis:
host: localhost
port: 6379
kotlin
// 代码里用@Value读配置,自动适配当前环境
@Value("${yudao.captcha.enable:true}")
private Boolean captchaEnable; // 本地环境是false,生产环境是true
四、总结:芋道登录功能的优秀设计思路 🎯
把芋道的登录功能拆完,不得不说它的设计太值得学习了:
- 流程清晰:登录拆成"验验证码→认证账号→生成Token",每个步骤职责单一,好维护。
- 安全优先:统一错误提示防信息泄露、双Token机制降低风险、密码加密存储。
- 开发友好:验证码开关、缓存注解、全局异常处理,各种细节提升开发效率。
- 可扩展性强:基于OAuth2.0,后续加第三方登录(微信、QQ)也方便。
登录功能虽然小,但藏着系统设计的大逻辑。希望这篇拆解能帮你吃透芋道的实战思路,下次自己写登录功能时,也能做到"安全、优雅、好维护"! 🚀