RuoYi-Vue源码阅读(二):登陆模块

1 登录模块实现思路

  1. 首先,进行验证码的校验。如果验证码校验失败,将抛出异常;
  2. 然后,进行登录前置校验。如果前置校验失败,将抛出异常;
  3. 接下来,会进行用户认证。 如果认证过程中出现异常,可能是由于密码不匹配导致的。此时,它会记录登录失败信息,并抛出用户密码不匹配异常;
  4. 如果认证成功,它会记录登录成功信息,获取认证后的用户信息,记录登录信息,并生成一个token;
  5. 最后,它返回生成的token。

登录模块逻辑流程图

2 后端代码实现步骤

登录模块Controller层的代码位于com.ruoyi.web.controller.system包下的SysLoginController.java
登录验证Controller方法

登录方法如下:

/**
 * 用户登录方法
 * 
 * @param loginBody 登录请求体,包含用户名、密码、验证码和UUID
 * @return AjaxResult 登录结果,包含状态码、消息和令牌(如果登录成功)
 */
public AjaxResult login(@RequestBody LoginBody loginBody) {
    // 创建一个AjaxResult对象,初始状态为成功
    AjaxResult ajax = AjaxResult.success();

    // 调用loginService的login方法来处理登录逻辑
    // 这个方法可能返回一个令牌(token),用于后续的身份验证
    String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
            loginBody.getUuid());

    // 如果登录成功,将令牌放入AjaxResult对象中
    // 这里的Constants.TOKEN是一个常量,表示令牌的键
    ajax.put(Constants.TOKEN, token);

    // 返回AjaxResult对象
    return ajax;
}

代码逻辑如下:

  1. 首先创建一个的AjaxResult对象,它被初始化为一个表示成功的响应;
  2. 通过loginService调用service层的login()方法,来处理登录逻辑。这个方法接收用户名、密码、验证码和UUID作为参数,并返回一个令牌(token)。这个令牌通常用于后续的身份验证;
  3. 如果登录成功,即login方法返回了一个令牌,那么这个令牌会被放入AjaxResult对象中。这里的Constants.TOKEN是一个常量,表示令牌的键。
  4. 最后,这个AjaxResult对象会被返回给调用者。这个对象可能包含了状态码、消息和令牌等信息。

2.1 Service层login方法逻辑

在上面的Controller层代码里,点击进入SysLoginService.java文件,查看其中的login方法,该方法的目的是处理用户登录请求,包括验证码校验、登录前置校验、用户认证、记录登录信息以及生成token。

/**
 * 用户登录方法
 * 
 * @param username 用户名
 * @param password 密码
 * @param code     验证码
 * @param uuid     UUID
 * @return 生成的token
 */
public String login(String username, String password, String code, String uuid) {
    // 验证码校验
    // 如果验证码校验失败,将抛出异常
    validateCaptcha(username, code, uuid);

    // 登录前置校验
    // 如果前置校验失败,将抛出异常
    loginPreCheck(username, password);

    // 用户验证
    Authentication authentication = null;
    try {
        // 创建一个UsernamePasswordAuthenticationToken对象,用于存储用户名和密码
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        // 设置当前的Authentication对象到SecurityContextHolder中
        AuthenticationContextHolder.setContext(authenticationToken);
        // 尝试进行用户认证
        // 这个过程可能会调用UserDetailsServiceImpl.loadUserByUsername方法来加载用户信息
        authentication = authenticationManager.authenticate(authenticationToken);
    } catch (Exception e) {
        // 如果认证过程中出现异常,可能是由于密码不匹配导致的BadCredentialsException
        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 {
        // 清除SecurityContextHolder中的Authentication对象
        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);
}

其代码逻辑如下:

  1. 首先,它调用validateCaptcha方法来验证验证码。如果验证码校验失败,将抛出异常。

  2. 然后,它调用loginPreCheck方法进行登录前置校验。如果前置校验失败,将抛出异常。

  3. 接下来,它创建一个UsernamePasswordAuthenticationToken对象,用于存储用户名和密码。然后,它设置当前的Authentication对象到SecurityContextHolder中。

  4. 接着,它尝试调用authenticationManagerauthenticate方法来进行用户认证。这个过程可能会调用UserDetailsServiceImpl.loadUserByUsername方法来加载用户信息。

  5. 如果认证过程中出现异常,可能是由于密码不匹配导致的BadCredentialsException。此时,它会记录登录失败信息,并抛出用户密码不匹配异常。

  6. 如果认证成功,它会记录登录成功信息,获取认证后的用户信息,记录登录信息,并生成一个token。

  7. 最后,它返回生成的token。

2.2 校验验证码

在login方法里,首先调用了validateCaptcha方法来验证验证码,这个方法的主要作用是验证用户输入的验证码是否正确,如果验证码过期或者错误,就会抛出相应的异常。

/**
 * 校验验证码
 * 
 * @param username 用户名
 * @param code 用户输入的验证码
 * @param uuid 验证码的唯一标识
 * @throws CaptchaExpireException 验证码过期异常
 * @throws CaptchaException 验证码错误异常
 */
public void validateCaptcha(String username, String code, String uuid) throws CaptchaExpireException, CaptchaException {
    // 检查系统是否启用了验证码功能
    boolean captchaEnabled = configService.selectCaptchaEnabled();
    if (captchaEnabled) {
        // 构建验证码在Redis中的键名,键名由验证码常量前缀和uuid组成
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
        // 从Redis中获取验证码
        String captcha = redisCache.getCacheObject(verifyKey);
        // 获取验证码后,从Redis中删除这个键值对,因为验证码是一次性的
        redisCache.deleteObject(verifyKey);
        // 如果从Redis中获取的验证码为空,说明验证码已经过期
        if (captcha == null) {
            // 异步记录登录失败信息
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            // 抛出验证码过期异常
            throw new CaptchaExpireException();
        }
        // 如果用户输入的验证码与从Redis中获取的验证码不匹配
        if (!code.equalsIgnoreCase(captcha)) {
            // 异步记录登录失败信息
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            // 抛出验证码错误异常
            throw new CaptchaException();
        }
    }
}

其代码逻辑如下:

  1. 首先,通过configService调用selectCaptchaEnabled方法来检查系统是否启用了验证码功能。

  2. 如果启用了验证码功能,则构建验证码在Redis中的键名。

  3. 从Redis中获取验证码,并删除这个键值对,因为验证码是一次性的。

  4. 如果从Redis中获取的验证码为空,说明验证码已经过期,抛出CaptchaExpireException异常。

  5. 如果用户输入的验证码与从Redis中获取的验证码不匹配,抛出CaptchaException异常。

  6. 如果验证码校验通过,方法正常返回,不抛出异常。

注意:

  • CaptchaExpireException和CaptchaException是自定义的异常类,用于表示验证码过期或验证码错误。
  • AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message(“user.jcaptcha.expire”)))是异步记录登录失败信息的操作,
  • MessageUtils.message(“user.jcaptcha.expire”)是获取验证码过期或错误的消息。

2.3 登录前置校验

login方法里,接下来调用loginPreCheck方法进行登录前置校验。该方法主要目的是在用户尝试登录之前进行一些预先检查,以确保用户名和密码的有效性,以及IP地址是否在黑名单中。如果任何检查失败,将记录登录失败信息,并抛出相应的异常。

/**
 * 登录前置校验
 * @param username 用户名
 * @param password 用户密码
 * @throws UserNotExistsException 用户不存在异常
 * @throws UserPasswordNotMatchException 用户密码不匹配异常
 * @throws BlackListException 黑名单异常
 */
public void loginPreCheck(String username, String password) throws UserNotExistsException, UserPasswordNotMatchException, BlackListException {
    // 如果用户名或密码为空,抛出用户不存在异常
    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");
    // 如果当前IP在黑名单中,抛出黑名单异常
    if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) {
        // 异步记录登录失败信息
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
        throw new BlackListException();
    }
}

其代码逻辑如下:

  1. 检查用户名和密码是否为空,如果为空,抛出UserNotExistsException异常。

  2. 检查密码长度是否在允许的范围内,如果不在范围内,抛出UserPasswordNotMatchException异常。

  3. 检查用户名长度是否在允许的范围内,如果不在范围内,同样抛出UserPasswordNotMatchException异常。

  4. 从系统配置中获取黑名单IP列表,并检查当前请求的IP是否在黑名单中。

  5. 如果当前IP在黑名单中,抛出BlackListException异常。

注意:

  • UserNotExistsException、UserPasswordNotMatchException和BlackListException是自定义异常,用于表示用户不存在、用户不匹配、密码不匹配或IP在黑名单中的情况。
  • AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message(“not.null”)))是异步记录登录失败信息的操作
  • MessageUtils.message(“not.null”)是获取相应消息的方法。

2.4 异步任务管理器记录登陆日志

在上述的验证码校验和登录前置校验过程中,只要登陆失败,都会调用异步任务管理器,通过异步的方式记录日志到数据库中的sys_logininfor中,例如:

AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));

这行代码的目的是异步记录用户登录失败的信息。让我们逐步解析这行代码:

  1. AsyncManager.me()

    • AsyncManager 是一个管理异步任务执行的类,它可能是一个单例类,提供了一个全局的异步任务执行环境。me() 方法通常是这个类的静态方法,用于获取该类的单例实例。
  2. execute(...)

    • executeAsyncManager 类的一个方法,用于执行一个 RunnableCallable 任务。这些任务将在后台线程中异步执行。
  3. AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))

    • AsyncFactory 是一个工厂类,用于创建异步任务。recordLogininfor 方法是这个工厂类的一个方法,用于创建一个记录登录信息的 Runnable 任务。
    • username 是尝试登录的用户名。
    • Constants.LOGIN_FAIL 是一个常量,表示登录失败的状态。
    • MessageUtils.message("not.null") 是一个工具方法,用于获取一个消息字符串。在这个上下文中,它可能返回一个错误消息,表示用户名或密码为空。

这行代码的整体逻辑是:

  • 通过 AsyncManager.me() 获取异步管理器的单例实例。
  • 使用异步管理器的 execute 方法来执行一个异步任务。
  • 这个异步任务是通过 AsyncFactory.recordLogininfor 方法创建的,该方法创建了一个 TimerTask 对象,该对象在 run 方法中记录登录失败的信息。
  • 这个任务将在后台线程中执行,不会阻塞当前的执行流程。

这种异步记录日志的方式通常用于提高系统的响应速度,因为它不会阻塞当前的请求处理线程。然而,它也可能增加了系统的复杂性,因为它需要确保异步任务能够正确地记录日志,并且不会因为异步执行而丢失日志信息。

点击进入AsyncFactory.recordLogininfor方法,该方法用于异步记录用户登录信息,因为它返回一个TimerTask对象,可以在后台线程中执行。这样做的好处是不会阻塞当前的请求处理线程,提高了系统的响应速度。同时,它也确保了即使在高并发的情况下,日志记录也不会成为系统性能的瓶颈。

/**
 * 记录登录信息
 * 
 * @param username 用户名
 * @param status 状态
 * @param message 消息
 * @param args 可变参数列表
 * @return 任务task
 */
public static TimerTask recordLogininfor(final String username, final String status, final String message,
        final Object... args)
{
    // 解析用户代理字符串以获取用户的操作系统和浏览器信息
    final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
    // 获取客户端IP地址
    final String ip = IpUtils.getIpAddr();
    // 创建一个新的TimerTask对象
    return new TimerTask()
    {
        @Override
        public void run()
        {
            // 通过IP地址获取地理位置信息
            String address = AddressUtils.getRealAddressByIP(ip);
            StringBuilder s = new StringBuilder();
            // 构建日志信息
            s.append(LogUtils.getBlock(ip));
            s.append(address);
            s.append(LogUtils.getBlock(username));
            s.append(LogUtils.getBlock(status));
            s.append(LogUtils.getBlock(message));
            // 将日志信息打印到日志系统
            sys_user_logger.info(s.toString(), args);
            // 获取客户端操作系统名称
            String os = userAgent.getOperatingSystem().getName();
            // 获取客户端浏览器名称
            String browser = userAgent.getBrowser().getName();
            // 创建SysLogininfor对象,用于存储登录信息
            SysLogininfor logininfor = new SysLogininfor();
            logininfor.setUserName(username);
            logininfor.setIpaddr(ip);
            logininfor.setLoginLocation(address);
            logininfor.setBrowser(browser);
            logininfor.setOs(os);
            logininfor.setMsg(message);
            // 根据状态设置登录信息的状态
            if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
            {
                logininfor.setStatus(Constants.SUCCESS);
            }
            else if (Constants.LOGIN_FAIL.equals(status))
            {
                logininfor.setStatus(Constants.FAIL);
            }
            // 使用Spring的工具类获取ISysLogininforService实例,并插入登录信息
            SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
        }
    };
}

其代码整体逻辑如下:

  1. 解析用户代理字符串以获取用户的操作系统和浏览器信息。

  2. 获取客户端的IP地址。

  3. 创建一个TimerTask对象,该对象在run方法中执行以下操作:

    • 通过IP地址获取地理位置信息。

    • 构建日志信息,包括IP地址、地理位置、用户名、状态和消息。

    • 将日志信息打印到日志系统。

    • 获取客户端的操作系统和浏览器信息。

    • 创建一个SysLogininfor对象,用于存储登录信息,包括用户名、IP地址、地理位置、浏览器、操作系统和消息。

    • 根据状态设置登录信息的状态(成功、失败)。

    • 使用Spring的工具类获取ISysLogininforService实例,并将登录信息插入到数据库中。

sys_logininfor表如下:

sys_logininfor表

2.5 用户验证

使用Spring Security框架进行用户验证,它处理了用户认证的整个过程,包括异常处理、记录登录信息、生成token等。

// 创建一个Authentication对象,用于存储用户认证信息
Authentication authentication = null;
try {
    // 创建一个UsernamePasswordAuthenticationToken对象,它是Authentication接口的一个实现
    // 这个对象包含了用户名和密码,Spring Security会使用这些信息来进行认证
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
    // 将authenticationToken设置到SecurityContext中,这样Spring Security就可以获取到用户的认证信息
    AuthenticationContextHolder.setContext(authenticationToken);
    // 使用authenticationManager进行用户认证,这个manager会调用UserDetailsServiceImpl.loadUserByUsername方法
    // 如果认证失败,会抛出异常
    authentication = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
    // 如果认证过程中发生异常,可能是由于密码不匹配导致的BadCredentialsException
    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 {
    // 无论认证是否成功,都清除SecurityContext中的认证信息
    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);

其代码整体逻辑如下:

  1. 创建一个UsernamePasswordAuthenticationToken对象,它包含了用户名和密码,然后将其设置到SecurityContext中。

  2. 调用authenticationManager.authenticate(authenticationToken)进行用户认证。如果认证成功,authenticationManager.authenticate(authenticationToken)将返回一个包含了认证用户信息的Authentication对象。

  3. 如果在认证过程中发生异常,可能是由于密码不匹配导致的BadCredentialsException,此时记录登录失败信息,并抛出UserPasswordNotMatchException异常。如果不是密码不匹配异常,记录错误信息,并抛出ServiceException异常。

  4. 无论认证是否成功,都清除SecurityContext中的认证信息。

  5. 如果认证成功,记录登录成功信息,并获取认证成功的用户信息。

  6. 调用recordLoginInfo(loginUser.getUserId())方法记录登录信息。

  7. 调用tokenService.createToken(loginUser)生成一个token,并返回这个token。

2.6 记录登录信息

调用recordLoginInfo(loginUser.getUserId())方法记录用户最近的登录信息,包括用户ID,IP地址和登陆时间,并将该用户信息更新到数据库sys_user中

    /**
     * 记录登录信息
     *
     * @param userId 用户ID
     */
    public void recordLoginInfo(Long userId)
    {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(IpUtils.getIpAddr());
        sysUser.setLoginDate(DateUtils.getNowDate());
        userService.updateUserProfile(sysUser);
    }

updateUserProfile就是修改数据库表中对应的用户信息。
sys_user表如下:
sys_user表

2.7 生成Token并返回

SysLoginService.java中的login方法里调用tokenService.createToken(loginUser)生成一个token,并返回这个token。

// 生成token并返回
return tokenService.createToken(loginUser);

点击进入createToken方法,这个方法的主要目的是创建一个新的JWT令牌,并将其与用户信息关联起来。JWT令牌通常用于无状态的身份验证,因为它包含了用户的所有必要信息,并且可以被签名以确保其完整性和真实性。在这个方法中,我们生成一个唯一的令牌,将其与用户信息关联,并创建一个包含这个令牌的JWT声明。

/**
 * 创建令牌
 *
 * @param loginUser 用户信息
 * @return 令牌
 */
public String createToken(LoginUser loginUser) {
    // 生成一个唯一的字符串作为令牌
    String token = IdUtils.fastUUID();
    // 将生成的令牌设置到用户信息中
    loginUser.setToken(token);
    // 设置用户代理信息
    setUserAgent(loginUser);
    // 刷新令牌
    refreshToken(loginUser);

    // 创建一个新的Map对象,用于存储JWT的声明
    Map<String, Object> claims = new HashMap<>();
    // 将令牌作为声明的一部分添加到Map中
    claims.put(Constants.LOGIN_USER_KEY, token);
    // 使用声明创建一个新的令牌
    return createToken(claims);
}

其代码逻辑如下:

  1. 首先,使用IdUtils.fastUUID()生成一个唯一的字符串作为令牌。

  2. 将生成的令牌设置到loginUser对象中。

  3. 调用setUserAgent(loginUser)方法设置用户代理信息,包括IP地址、地理位置、浏览器类型和操作系统类型。

  4. 调用refreshToken(loginUser)方法刷新令牌,更新登陆时间,设置过期时间,将登录用户信息存入Redis缓存。

  5. 创建一个新的HashMap对象claims,用于存储JWT(Json Web Token)的声明。

  6. 将令牌作为声明的一部分添加到claims中。

  7. 调用createToken(claims)方法使用声明创建一个新的令牌,并返回这个令牌。

2.7.1 设置用户代理信息

进入查看setUserAgent方法,该方法的主要目的是从用户的请求中获取loginUser用户的完整信息,包括IP地址、地理位置、浏览器类型和操作系统类型,并将其设置到loginUser对象中。

/**
 * 设置用户代理信息
 *
 * @param loginUser 登录信息
 */
public void setUserAgent(LoginUser loginUser) {
    // 解析请求头中的User-Agent字符串,获取用户代理信息
    UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
    // 获取客户端的IP地址
    String ip = IpUtils.getIpAddr();
    // 设置登录用户的IP地址
    loginUser.setIpaddr(ip);
    // 通过IP地址获取地理位置信息
    loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
    // 设置登录用户的浏览器类型
    loginUser.setBrowser(userAgent.getBrowser().getName());
    // 设置登录用户的操作系统类型
    loginUser.setOs(userAgent.getOperatingSystem().getName());
}

代码的整体逻辑如下:

  1. 从请求头中获取User-Agent字符串,并使用UserAgent.parseUserAgentString方法解析出用户代理信息。
  2. 使用IpUtils.getIpAddr()方法获取客户端的IP地址。
  3. 使用AddressUtils.getRealAddressByIP(ip)方法通过IP地址获取地理位置信息,并将其设置到loginUser对象中。
  4. 从用户代理信息中获取浏览器类型,并将其设置到loginUser对象中。
  5. 从用户代理信息中获取操作系统类型,并将其设置到loginUser对象中。

2.7.2 刷新令牌有效期

进入查看refreshToken方法,该方法的主要目的是刷新loginUser的Token的有效期。它会更新用户的登录时间和过期时间(默认是30分钟),然后将这些信息存入Redis缓存中,这样在后续的验证过程中,可以通过检查令牌是否在缓存中以及当前时间是否在过期时间之前来判断令牌是否仍然有效。如果令牌已经过期,那么用户需要重新登录以获取新的令牌。

/**
 * 刷新令牌有效期
 *
 * @param loginUser 登录信息
 */
public void refreshToken(LoginUser loginUser) {
    // 设置登录时间为当前时间
    loginUser.setLoginTime(System.currentTimeMillis());
    // 计算过期时间,当前时间加上配置的过期时间(分钟),得到过期时间
    loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);

    // 根据令牌生成缓存的键
    String userKey = getTokenKey(loginUser.getToken());
    // 将登录用户信息存入Redis缓存,设置过期时间为配置的过期时间,单位为分钟
    redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

代码的整体逻辑如下:

  1. 首先,设置登录用户的登录时间为当前时间。
  2. 然后,计算登录用户的过期时间。过期时间是当前时间加上配置的过期时间(以分钟为单位)。
  3. 接下来,根据登录用户的令牌生成一个缓存键。
  4. 最后,将登录用户的信息存入Redis缓存中,并设置过期时间为配置的过期时间。

2.7.3 从数据声明生成令牌

进入createToken方法,该方法的主要目的是创建一个JWT(Json Web Token),它是一个紧凑且URL安全的表示,用于在双方之间安全地传递信息。JWT通常包含三部分:头部(header)、声明(claims)和签名(signature)。头部通常包含了令牌的类型和签名算法的信息,声明包含了用户的信息,而签名则用于验证令牌的完整性和真实性。在这个方法中,我们使用HS512算法和一个密钥来签名JWT,以确保其安全性。

/**
 * 从数据声明生成令牌
 *
 * @param claims 数据声明
 * @return 令牌
 */
private String createToken(Map<String, Object> claims) {
    // 使用JWT的builder模式创建一个新的JWT
    // 设置声明(claims),这些声明包含了用户的信息
    // 使用HS512算法和预定义的密钥对JWT进行签名
    // 最后调用compact()方法生成一个紧凑的URL安全的JWT字符串
    String token = Jwts.builder()
            .setClaims(claims)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    // 返回生成的令牌
    return token;
}

代码的整体逻辑如下:

  1. 使用Jwts.builder()创建一个新的JWT构建器。
  2. 调用setClaims(claims)方法设置JWT的声明(claims),这些声明包含了用户的信息。
  3. 调用signWith(SignatureAlgorithm.HS512, secret)方法使用HS512算法和预定义的密钥对JWT进行签名。
  4. 调用compact()方法生成一个紧凑的URL安全的JWT字符串。
  5. 返回生成的令牌。

3 前端代码实现步骤

在user.js文件里,有一个Login的action,它负责处理用户登录逻辑。它接收用户信息,调用login函数进行登录,并在登录成功后更新Vuex状态中的token。如果登录过程中发生错误,它会拒绝Promise并将错误信息传递出去。

actions: {
  // 登录
  Login({ commit }, userInfo) {
    // 去除用户名前后的空格
    const username = userInfo.username.trim();
    // 获取密码
    const password = userInfo.password;
    // 获取验证码
    const code = userInfo.code;
    // 获取UUID
    const uuid = userInfo.uuid;

    // 返回一个新的Promise对象
    return new Promise((resolve, reject) => {
      // 调用login函数进行登录
      login(username, password, code, uuid)
        .then(res => {
          // 如果登录成功,设置token,把后台产生的token存到前端的cookie里面
          setToken(res.token);
          // 提交SET_TOKEN mutation,将token存入Vuex状态管理
          commit('SET_TOKEN', res.token); 
          // 标记Promise为已解决
          resolve();
        })
        .catch(error => {
          // 如果登录过程中发生错误,标记Promise为已拒绝,并传递错误信息
          reject(error);
        });
    });
  },
  // ... 其他操作
}

代码的整体逻辑如下:

  1. userInfo对象中获取用户名、密码、验证码和UUID,并去除用户名前后的空格。
  2. 创建一个新的Promise对象,用于处理异步登录操作。
  3. 调用login函数进行登录,传入用户名、密码、验证码和UUID。
  4. 如果登录成功,调用setToken函数设置token,会将获得JWT令牌设置为前端的token,也就是存到cookie中,并提交SET_TOKEN mutation到Vuex状态管理中,将token存入Vuex状态,最终完成登录。
  5. 如果登录过程中发生错误,拒绝Promise,并将错误信息传递出去。

4 参考链接

  1. 若依使用及源码解析(前后端分离版):https://blog.csdn.net/Ostkakah/article/details/132984838
  2. 若依框架学习(前后端分离)——(登录代码学习篇):https://blog.csdn.net/yeahyeah81/article/details/135066893
  3. Token的作用及原理:https://blog.csdn.net/Huozhiwu_11/article/details/107230718
  4. JWT详解:https://blog.csdn.net/weixin_45070175/article/details/118559272
  5. SpringBoot整合kaptcha(谷歌验证码工具)实现验证码功能:https://blog.csdn.net/qq_50954744/article/details/126810009
  6. SpringSecurity-从入门到精通:https://blog.csdn.net/weixin_43847283/article/details/124075302
  7. Vuex中文网站:https://vuex.vuejs.org/zh/
  • 13
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值