若依框架 SpringSecurity :用户登录源码全拆解(附图解)
本文详细分析了若依框架中基于SpringSecurity的登录认证流程。系统首先进行验证码校验,从Redis获取并比对验证码。然后进行用户名密码的前置校验,包括长度限制和IP黑名单检查。核心认证过程通过AuthenticationManager触发,调用UserDetailsService加载用户信息并验证密码。认证成功后生成JWT令牌,并记录用户登录信息。文章还深入探讨了配置管理策略(数据库动态
一、前置知识
Spring Security 的配置类解析在下面这篇博客里面。
若依框架 SpringSecurity :基础配置源码全拆解(附图解)-CSDN博客
二、controller层
admin包下的SysLoginController

/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 用户登录对象
*
* @author ruoyi
*/
public class LoginBody
{
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
/**
* 验证码
*/
private String code;
/**
* 唯一标识
*/
private String uuid;
//省略getter,setter
}
2.1 AjaxResult
/**
* 操作消息提醒
*
* @author ruoyi
*/
public class AjaxResult extends HashMap<String, Object>
-
继承自 HashMap,天然具备键值对存储能力。可以非常灵活地扩展返回字段,除了内置的
code、msg、data,还能随时添加其他业务需要的字段。就像login这个方法里面就往里面put了token - 它定义了三个静态常量作为标准响应字段,统一了前后端交互的协议:
CODE_TAG = "code":状态码,用于标识请求的处理结果(如成功、失败、权限不足等)。MSG_TAG = "msg":提示信息,用于返回给用户的文本说明。DATA_TAG = "data":数据对象,用于返回接口的业务数据。
三、登录总体流程
进入SysLoginService
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String code, String uuid)
{
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
- 前置校验:先验证验证码格式和账号状态。
- 身份验证:用 Spring Security 进行用户名和密码的验证。
- 异常处理:验证失败时异步记录失败日志,并抛出对应异常。
- 成功收尾:验证成功后异步记录成功日志,更新用户登录信息,并生成返回 JWT 令牌。
四、验证码校验
/**
* 校验验证码
*
* @param username 用户名
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public void validateCaptcha(String username, String code, String uuid)
{
boolean captchaEnabled = configService.selectCaptchaEnabled();
if (captchaEnabled)
{
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
String captcha = redisCache.getCacheObject(verifyKey);
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
redisCache.deleteObject(verifyKey);
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}
}
}
- 先判断:检查系统是否开启验证码,未开启则直接跳过校验。
- 取缓存:通过 uuid 从 Redis 获取已生成的验证码。
- 验有效性:缓存中无验证码(过期 / 不存在)则抛异常。
- 删缓存:无论后续校验结果如何,先删除验证码确保一次性使用。
- 比内容:忽略大小写对比用户输入与缓存验证码,不匹配则抛异常。
4.1读取系统配置
打开SysConfigServiceImpl
/**
* 获取验证码开关
*
* @return true开启,false关闭
*/
@Override
public boolean selectCaptchaEnabled()
{
String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled");
if (StringUtils.isEmpty(captchaEnabled))
{
return true;
}
return Convert.toBool(captchaEnabled);
}
/**
* 根据键名查询参数配置信息
*
* @param configKey 参数key
* @return 参数键值
*/
@Override
public String selectConfigByKey(String configKey)
{
String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey)));
if (StringUtils.isNotEmpty(configValue))
{
return configValue;
}
SysConfig config = new SysConfig();
config.setConfigKey(configKey);
SysConfig retConfig = configMapper.selectConfig(config);
if (StringUtils.isNotNull(retConfig))
{
redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());
return retConfig.getConfigValue();
}
return StringUtils.EMPTY;
}
- 先查 Redis 缓存:有值直接返回,提升效率。
- 缓存无值查数据库:从
sys_config表查询对应配置。 - 查成存入缓存:数据库查到结果后,写入 Redis 供后续使用。
- 兜底返回空串:无查询结果时返回空字符串,避免空指针异常。
若依数据库中的配置表

思考:为什么采用配置表而不是读取配置文件?
- 配置文件(如
application.yml)是静态的,修改后必须重启服务才能生效。数据库存储的配置可以在后台管理界面直接修改,即时生效,无需重启,适合线上业务不中断的场景。 - 在微服务或集群部署中,多台服务器的配置文件需要手动同步,容易出错。数据库作为唯一的配置源,所有服务实例都从同一处读取,天然保证了配置的一致性。
- 配置文件适合存储简单的键值对,但对于复杂的业务配置(如多租户、多环境的差异化配置),难以结构化管理。数据库可以通过表结构、索引等方式,高效存储和查询复杂的配置数据。
4.2 加载系统配置
那我们顺便来看一下这些配置是怎么加载到数据库中的。
定位SysConfigServiceImpl
/**
* 项目启动时,初始化参数到缓存
*/
@PostConstruct
public void init()
{
loadingConfigCache();
}
/**
* 加载参数缓存数据
*/
@Override
public void loadingConfigCache()
{
List<SysConfig> configsList = configMapper.selectConfigList(new SysConfig());
for (SysConfig config : configsList)
{
redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
}
}
- 项目启动阶段:Spring 容器初始化这个类,完成依赖注入。
- 自动触发初始化:
@PostConstruct注解触发init()方法。 - 加载配置数据:
init()调用loadingConfigCache(),从数据库查询所有系统配置,这里查询条件直接new了一个,所有属性都是null,那么条件查询回来的都是全部的配置。 - 写入缓存:遍历配置列表,将每个配置项以键值对的形式存入 Redis。
4.3 生成验证码
校验验证码的逻辑中,是直接从redis中读取的,那我们来追踪一下这个验证码是什么时候生成的。定位到CaptchaController
/**
* 验证码操作处理
*
* @author ruoyi
*/
@RestController
public class CaptchaController
{
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
@Autowired
private RedisCache redisCache;
@Autowired
private ISysConfigService configService;
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = configService.selectCaptchaEnabled();
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled)
{
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
}
- 先判断系统是否开启验证码,未开启直接返回结果。(这里的逻辑和我们校验验证码的逻辑一样)
- 唯一标识:生成
uuid,构造 Redis 缓存键。 - 生成验证码:从配置属性类中读取验证码的类型。
- 返回结果:将答案存入 Redis 并设置过期时间,图片转 Base64 后,与
uuid一起返回给前端。
思考:
- 为什么要生成验证码时检验一次配置,校验验证码的时候检验一次配置,不麻烦吗?
- 因为要考虑实际环境的动态性,假如生成验证码之后校验验证码之前关闭了验证码功能,那这个还需要校验吗?而且配置信息在redis中存放,获取是比较快的。
- 验证码生成的uuid和我们用户的唯一标识uuid是同一个吗?
- 不是。用户的uuid是在创建令牌时生成的,并且我们验证码的uuid在校验成功后就删除了,不会保留这么长时间,所以就算我们把验证码关了,也不会影响到我们用户的uuid。
- 那我们登录时的LoginBody中的uuid就是我们验证码生成的uuid,用来校验的,而我们用户的uuid是在登录后的。
- 为什么是否生成验证码是由数据库来管理,而验证码的类型却放在配置属性类里面来读取?
-
是否生成验证码
- 变更频率高:这是一个运营级开关,管理员需要在后台随时启用或禁用,比如在系统维护、活动高峰时快速调整。
- 影响范围小:只控制验证码的 “有无”,不改变验证码的生成逻辑,不需要重启服务,实时生效即可。
- 用户体验优先:通过数据库存储,可以做到 “秒级开关”,避免因为修改配置文件而重启服务,影响用户访问。
-
验证码类型(math/char)
- 变更频率极低:这是技术选型,比如决定用算术验证码还是字符验证码,一旦确定就很少变动。
- 影响范围大:它会直接改变验证码的生成逻辑,更换类型通常需要验证兼容性,所以适合在发布时统一调整。
- 性能与稳定性优先:放在配置文件里,项目启动时就加载到内存,避免了每次生成验证码都去查数据库的性能损耗。
-
如果所有配置都放数据库,高频访问的基础配置每次都查库,增加数据库 IO 压力,拖慢系统响应。 所有配置都放配置属性类,灵活性就不会那么强。
-
五、登录前置校验
// 登录前置校验
loginPreCheck(username, password);
/**
* 登录前置校验
* @param username 用户名
* @param password 用户密码
*/
public void loginPreCheck(String username, String password)
{
// 用户名或密码为空 错误
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
throw new UserNotExistsException();
}
// 密码如果不在指定范围内 错误
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
// 用户名不在指定范围内 错误
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
// IP黑名单校验
String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
throw new BlackListException();
}
}
这里针对用户信息进行了2次校验,防止有些绕开前端直接发送请求(比如我们开发时通过Swagger、Apifox发送请求)
5.1 后端二次校验场景
- 用户认证 / 授权相关:登录、注册、修改密码、刷新令牌、绑定手机号。
- 数据增删改操作:新增用户、修改订单、删除数据、提交表单(尤其是涉及金额、权限、个人信息的)。
- 涉及资源 / 资金相关:支付、提现、兑换、文件上传(校验文件类型、大小、内容)。
- 高频访问 / 易被攻击的接口:验证码获取、登录接口、短信发送接口(防止刷取)。
5.2 可简化 / 无需后端二次校验(非核心场景)
- 纯查询类请求(无敏感数据、无参数风险):
- 查询公开信息(如网站公告、商品列表(非敏感字段筛选)、帮助文档)。
- 无参数的简单查询(如查询当前系统时间、查询自己的公开个人信息(已通过登录授权))。
- 参数已被上游统一校验:
- 比如请求参数已经被网关、拦截器做了统一格式校验(如 ID 必须是数字、分页参数必须合法),后端业务层可简化重复校验。
六、用户认证总体流程
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
- 声明
Authentication对象并置空,构建UsernamePasswordAuthenticationToken(用户名 + 密码的认证令牌)。将令牌存入AuthenticationContextHolder,绑定当前线程的认证上下文。 - 调用
authenticationManager.authenticate(...)触发认证:底层会调用UserDetailsServiceImpl.loadUserByUsername查用户,对比密码完成认证,返回认证后的Authentication对象。 - 异常处理(分两类)异步记录登录失败日志。
- 执行
AuthenticationContextHolder.clearContext(),清空当前线程的认证上下文,避免内存泄漏。 - 认证成功后:异步记录登录成功日志→ 从认证对象获取
LoginUser用户信息 → 调用recordLoginInfo记录用户登录信息。
6.1 AuthenticationContextHolder和SecurityContextHolder
我们在配置篇已经结合源码解读了SecurityContextHolder其实本质上就是thread local,放入安全上下文后后续补就可以直接获取。
而AuthenticationContextHolder是我们若依自己写的,底层也是thread local。
/**
* 身份验证信息
*
* @author ruoyi
*/
public class AuthenticationContextHolder
{
private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
public static Authentication getContext()
{
return contextHolder.get();
}
public static void setContext(Authentication context)
{
contextHolder.set(context);
}
public static void clearContext()
{
contextHolder.remove();
}
}
SecurityContextHolder:是 Spring Security 原生核心类,是整个安全框架的基础,负责管理全局的SecurityContext安全上下文。职责更全面,它管理的是整个安全上下文,除了Authentication,还可以存放其他安全相关状态,是 Spring Security 实现线程绑定安全信息的核心。AuthenticationContextHolder:是 若依框架自定义工具类,仅在若依生态中使用,是一个简化版的身份信息持有器。职责非常单一,仅聚焦于Authentication对象的存取,是若依为了简化业务代码而做的轻量封装。- Spring Security 会自动在请求开始时初始化上下文,请求结束时通过过滤器自动清理
ThreadLocal,避免内存泄漏和用户信息串用。 AuthenticationContextHolder:需要若依框架自己处理ThreadLocal的设置与清理,通常会在登录成功时调用setContext(),在请求结束时手动调用remove()来清理。
6.2 AuthenticationContextHolder的存放
思考:
- 为什么不能把用户名和参数作为参数来传递?而是要存入AuthenticationContextHolder,用完再销毁?
-
核心思想是将认证信息(Authentication)与线程绑定,让后续都能统一地从 AuthenticationContextHolder中获取当前用户认证,而不是依赖方法参数传递。
-
后续的每一段代码都需要你手动传递
username或Authentication对象,这会让业务逻辑和认证逻辑高度耦合。 -
AuthenticationContextHolder使用
ThreadLocal存储上下文,这意味着每个请求线程的安全上下文是隔离的,不会出现多用户信息混乱的问题。如果直接用方法参数传递,在异步调用、多线程场景下很容易出现线程安全问题,而框架已经帮你处理了这些细节。
-
- 为什么一定要用完再销毁,不销毁会怎么样?
-
AuthenticationContextHolder 默认是用
ThreadLocal来存储上下文的,而服务器(如 Tomcat)的线程是线程池复用的。请求结束后,线程会被放回线程池,等待处理下一个请求。如果不清除,这个线程的ThreadLocal里还保留着上一个用户的认证信息。当下一个请求被分配到这个线程时,新请求会错误地继承上一个用户的身份,导致用户信息串用。
-
6.3 调用UserDetailsServiceImpl.loadUserByUsername的原理
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
authenticationManager我们已经在配置类里面讲过,是在那个时候注入的。
/**
* 身份验证实现
*/
@Bean
public AuthenticationManager authenticationManager()
{
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
return new ProviderManager(daoAuthenticationProvider);
}
打开ProviderManager的authenticate
//仅展示部分
for(AuthenticationProvider provider : this.getProviders()) {
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException ex) {
this.prepareException(ex, authentication);
throw ex;
} catch (AuthenticationException ex) {
lastException = ex;
}
}
这里在遍历所有provider,并配用provider的authenticate方法。
打开我们配置类中选择的DaoAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
此类继承了AbstractUserDetailsAuthenticationProvider,没有重写authenticate方法。
进入AbstractUserDetailsAuthenticationProvider,进行查看,重点看方法里面这句代码。
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
在DaoAuthenticationProvider重写了retrieveUser方法,我们来查看。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException ex) {
this.mitigateAgainstTimingAttack(authentication);
throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
这里确实在底层调用了UserDetailsService.loadUserByUsername。
七、loadUserByUsername实现
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException(MessageUtils.message("user.not.exists"));
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException(MessageUtils.message("user.password.delete"));
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException(MessageUtils.message("user.blocked"));
}
passwordService.validate(user);
return createLoginUser(user);
}
-
加载用户信息,通过
userService.selectUserByUserName(username)从数据库中查询该用户名对应的用户 -
校验用户状态,如果用户的删除标记为已删除,记录日志并抛出 “用户已删除” 的异常。如果用户状态为已停用,记录日志并抛出 “用户已被停用” 的异常。
-
密码有效性校
-
返回登录用户对象
7.1 密码校验
进入SysPasswordService
public void validate(SysUser user)
{
Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String username = usernamePasswordAuthenticationToken.getName();
String password = usernamePasswordAuthenticationToken.getCredentials().toString();
Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
if (retryCount == null)
{
retryCount = 0;
}
if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
{
throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
}
if (!matches(user, password))
{
retryCount = retryCount + 1;
redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
throw new UserPasswordNotMatchException();
}
else
{
clearLoginRecordCache(username);
}
}
public boolean matches(SysUser user, String rawPassword)
{
return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
}
public void clearLoginRecordCache(String loginName)
{
if (redisCache.hasKey(getCacheKey(loginName)))
{
redisCache.deleteObject(getCacheKey(loginName));
}
}
-
从
AuthenticationContextHolder中获取当前用户的认证信息 -
从 Redis 缓存中读取该用户的密码错误重试次数
-
如果当前重试次数已经达到或超过
maxRetryCount,就直接抛出 “密码重试次数超限” 的异常,阻止用户继续尝试。 -
校验密码是否正确
- 调用
matches(user, password)方法,将用户输入的密码和数据库中存储的加密密码进行比对。 - 如果密码错误:重试次数加 1,更新到 Redis 缓存并设置过期时间,然后抛出 “密码不匹配” 的异常。
- 如果密码正确:清除该用户在 Redis 中的重试次数记录,让计数归零。
- 调用
进入SecurityUtils查看校验密码方法
/**
* 判断密码是否相同
*
* @param rawPassword 真实密码
* @param encodedPassword 加密后字符
* @return 结果
*/
public static boolean matchesPassword(String rawPassword, String encodedPassword)
{
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(rawPassword, encodedPassword);
}
这个用的就是我们配置类里面用的BCryptPasswordEncoder
7.2 创建用户
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
打开SysPermissionService
/**
* 获取角色数据权限
*
* @param user 用户信息
* @return 角色权限信息
*/
public Set<String> getRolePermission(SysUser user)
{
Set<String> roles = new HashSet<String>();
// 管理员拥有所有权限
if (user.isAdmin())
{
roles.add(Constants.SUPER_ADMIN);
}
else
{
roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
}
return roles;
}
这里基于的就是RBAC权限模型,一个用户关联多个角色,一个角色对应多个权限,基于用户查询对应角色,再根据角色查询对应权限。
/**
* 根据用户ID查询权限
*
* @param userId 用户ID
* @return 权限列表
*/
@Override
public Set<String> selectRolePermissionByUserId(Long userId)
{
List<SysRole> perms = roleMapper.selectRolePermissionByUserId(userId);
Set<String> permsSet = new HashSet<>();
for (SysRole perm : perms)
{
if (StringUtils.isNotNull(perm))
{
permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));
}
}
return permsSet;
}
这里用了Set是因为用户对应的角色之间可能会有重复,关于用户发送请求的权限校验我们放到用户登录篇来说。
八、生成令牌
// 生成token
return tokenService.createToken(loginUser);
进入TokenService。
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
claims.put(Constants.JWT_USERNAME, loginUser.getUsername());
return createToken(claims);
}
8.1 uuid
目前我们有两种uuid
- 一个是我们用户唯一标识uuid,会存放再token中,也是我们redis中存放loginuser的键,令牌失效了,这个键也就被删除了,下次生成新令牌再有新的uuid。
- 一个是我们验证码的uuid,当在登录页面时会发送请求生成验证码和验证码的uuid,当验证码使用后就直接删除。
8.2 生成代理对象
/**
* 设置用户代理信息
*
* @param loginUser 登录信息
*/
public void setUserAgent(LoginUser loginUser)
{
String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
String ip = IpUtils.getIpAddr();
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(UserAgentUtils.getBrowser(userAgent));
loginUser.setOs(UserAgentUtils.getOperatingSystem(userAgent));
}
这里的就是一些用户登录ip的信息,用来记录用户登录日志用的。
8.3 刷新令牌
refreshToken(loginUser);
这里的操作逻辑和配置篇的Jwt过滤器的操作流程一样,就不再赘述了。
九、图解

十、后续内容
动态菜单篇
更多推荐
所有评论(0)