RuoYi-Cloud-Plus 2.X登录逻辑分析

2024-12-12 264点热度 0人点赞 0条评论

前端页面

如何能够保证在未登录的情况下跳转到登录页面,这里使用了vue的路由守卫。

路由守卫(如果未登录,强制跳转到登录页面)

在src/permission.ts文件中通过对路由添加前置守卫,首先通过@/utils/auth中的getToken()方法判断tokenStorage中是否存在token,
如果已经存在token说明已经登录,如果没有token,则判断是否在白名单中,如果在白名单中,则直接跳转到页面,如果不在白名单中,则强制转到登录页面,即执行如下语句:

next(`/login?redirect=${redirect}`); // 否则全部重定向到登录页

根据src/router/index.ts中的路由配置,login路由对应@/views/login,即/views/login.vue文件。# 路由守卫(如果未登录,强制跳转到登录页面)
在src/permission.ts文件中通过对路由添加前置守卫,首先通过@/utils/auth中的getToken()方法判断cookie中是否存在token,
如果已经存在token说明已经登录,如果没有token,则判断是否在白名单中,如果在白名单中,则直接跳转到页面,如果不在白名单中,则强制转到登录页面,即执行如下语句:

next(`/login?redirect=${redirect}`); // 否则全部重定向到登录页

根据src/router/index.ts中的路由配置,login路由对应@/views/login,即/views/login.vue文件。

  {
    path: '/login',
    component: () => import('@/views/login.vue'),
    hidden: true
  }

登录页面显示逻辑

登录页面有一个form表单,绑定的变量是loginForm,ref绑定的是loginRef。登录页面有3个输入框:用户名和密码必填,验证码根据后台captchaEnabled配置决定是否显示。

当页面加载的时候会调用3个函数:

onMounted(() => {
  getCode();
  initTenantList();
  getLoginData();
});
  1. getCode()的作用是判断是否显示验证码:如果captchaEnabled为true,则显示验证码,并且提交的时候后台也会验证验证码,如果captchaEnabled为false,则不显示验证码,后台也不会验证验证码,验证码验证逻辑后面详细分析。

  2. initTenantList()的作用是加载租户列表,如果tenant.enable配置为true,则加载租户列表,显示在下拉框中;

  3. getLoginData()的作用是从localStorage中获取tenantId、username、password、rememberMe的值设置到loginForm中。

    以下详述这几个方法。

getCode()获取验证码

验证码由auth生成,get请求auth的/code路径,即/auth/code,会返回一个captchaEnabled的配置信息,如果captchaEnabled为false,即nacos中auth配置如下:

# 安全配置
security:
  # 验证码
  captcha:
    # 是否开启验证码
    enabled: false

请求/auth/code返回信息如下:

{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "captchaEnabled": false,
        "uuid": null,
        "img": null
    }
}

如果captchaEnabled配置为true,get请求/auth/code返回信息如下:

{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "captchaEnabled": true,
        "uuid": "e0839a56af2245c4b818bd4613fab2dc",
        "img": "iVBORw0KGgoAAAANSUhEUgAAAKAA……=="
    }
}

会返回一个图片,和uuid,此uuid会作为redis key的一部分保存到redis中,以备后续验证,redis的value保存图片验证的计算结果以与用户输入的结果相比较。图片会显示在验证码输入框后面。

initTenantList()获取租户列表

请求/auth/tenant/list可以获取租户列表信息,在application-common.yml中配置了是否启用多租户以及相关信息,配置信息如下:

# 多租户配置
tenant:
  # 是否开启
  enable: true
  # 排除表
  excludes:
    - sys_menu
    - sys_tenant
    - sys_tenant_package
    - sys_role_dept
    - sys_role_menu
    - sys_user_post
    - sys_user_role
    - sys_client
    - sys_oss_config

如果不启用多租户,接口返回内容如下:

{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "tenantEnabled": false,
        "voList": null
    }
}

如果tenantEnabled默认为true,如果通过接口获取到的tenantEnabled为false,则会隐藏多租户的下拉框。

如果启用了多租户,则接口返回内容如下:

{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "tenantEnabled": true,
        "voList": [
            {
                "tenantId": "000000",
                "companyName": "XXX有限公司",
                "domain": null
            }
        ]
    }
}

租户列表可以有多个,用户可以手动选择。

getLoginData()获取用户的登录数据

该方法从localStorage中获取tenantId、username、password、rememberMe四个参数,如果不为空,则直接填入到对应到表单中。

登录页面验证逻辑

通过分析登录页面逻辑,状态处理以及后台处理几部分进行分析

页面验证处理

登录验证逻辑在login.vue中由handleLogin函数处理。首先调用loginRef.value.validate()方法进行表单验证,如果验证通过,进行如下逻辑:
判断是否选中了“记住密码”,如果选中,则在localStorage中记录租户id、用户名和密码和“记住密码”的值;
如果未选中“记住密码”,则清除localStorage中租户id、用户名、密码和“记住密码”的值。
通过调用userStore中的action方法,调用登录验证逻辑,即调用userStore.login方法,参数是LoginData,即登录表单内容。

  const login = async (userInfo: LoginData): Promise<void> => {
    const [err, res] = await to(loginApi(userInfo));
    if (res) {
      const data = res.data;
      setToken(data.access_token);
      token.value = data.access_token;
      return Promise.resolve();
    }
    return Promise.reject(err);
  };

userStore中的login方法调用loginApi中的登录方法获取登录结果。

export function login(data: LoginData): AxiosPromise<LoginResult> {
  const params = {
    ...data,
    clientId: data.clientId || clientId,
    grantType: data.grantType || 'password'
  };
  return request({
    url: '/auth/login',
    headers: {
      isToken: false,
      isEncrypt: true,
      repeatSubmit: false
    },
    method: 'post',
    data: params
  });
}

isEncrypt为true表示对参数进行加密,即在数据传输的过程中参数是加密的,由后台解密后再进行处理。前端参数加密逻辑放在src\utils\request.ts中的请求拦截器中处理:

    // 是否需要加密
    const isEncrypt = config.headers?.isEncrypt === 'true';
    ……
    if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
      // 当开启参数加密
      if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
        // 生成一个 AES 密钥
        const aesKey = generateAesKey();
        config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
        config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
      }
    }

前端和后端传递参数的时候使用AES对称加密,秘钥保存在header的encrypt-key中。后端收到请求之后,首先进行解密操作,解密由ruoyi-common-encrypt的CryptoFilter实现,有加密头则解密请求参数:

        // 是否为 put 或者 post 请求
        if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
            // 是否存在加密标头
            String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
            if (StringUtils.isNotBlank(headerValue)) {
                // 请求解密
                requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
            } else {
                // 是否有注解,有就报错,没有放行
                if (ObjectUtil.isNotNull(apiEncrypt)) {
                    HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
                    exceptionResolver.resolveException(
                        servletRequest, servletResponse, null,
                        new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
                    return;
                }
            }
        }

接口是否加密由@ApiEncrypt注解实现,由于login方法有@ApiEncrypt注解,如果没有加密头,则直接报错。

    @ApiEncrypt
    @PostMapping("/login")
    public R<LoginVo> login(@RequestBody String body) {

报错响应如下:

{
    "code": 403,
    "msg": "没有访问权限,请联系管理员授权",
    "data": null
}

因此请求有@ApiEncrypt注解的请求必须有加密头,即在header中添加encrypt-key的值,且请求内容必须是加密的。响应是否加密,在@ApiEncrypt注解注解中配置,默认不加密。登录成功之后的返回信息如下:

{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "scope": null,
        "openid": null,
        "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJI……s",
        "refresh_token": null,
        "expire_in": 604799,
        "refresh_expire_in": null,
        "client_id": "e5cd7e4891bf95d1d19206ce24a7b32e"
    }
}

成功之后通过setToken(data.access_token);方法把token保存到tokenStorage中。如果登录成功则跳转到根路径"/",如果失败,则重新获取验证码继续登录。

后端验证逻辑

前端登录请求加密前的参数如下:

{
  "tenantId": "000000",
  "username": "admin",
  "password": "admin123",
  "rememberMe": false,
  "clientId": "e5cd7e4891bf95d1d19206ce24a7b32e",
  "grantType": "password"
}

验证参数

后端获取到请求参数之后,首先对参数进行验证,由LoginBody中的属性注解实现。

    /**
     * 客户端id
     */
    @NotBlank(message = "{auth.clientid.not.blank}")
    private String clientId;

    /**
     * 授权类型
     */
    @NotBlank(message = "{auth.grant.type.not.blank}")
    private String grantType;

如果参数为空,则直接报错:

{
    "code": 500,
    "msg": "认证客户端id不能为空",
    "data": null
}

验证客户端

首先根据clientId远程查询客户端,如果客户端id找不到获取客户端的认证类型不包含grantType中的参数,返回报错响应:

{
  "code": 500,
  "msg": "认证权限类型错误",
  "data": null
}

如果客户端已停用,则返回报错响应如下:

{
  "code": 500,
  "msg": "认证权限类型已禁用",
  "data": null
}

验证租户信息

如果一切正常,则继续验证租户信息:

// 验证租户
sysLoginService.checkTenant(loginBody.getTenantId());

验证多租户是否启用、租户是否存在、状态是否正常、是否超过期限。

如果租户验证通过,则进入登录验证逻辑:

// 登录
LoginVo loginVo = IAuthStrategy.login(body, clientVo, grantType);

验证策略

IAuthStrategy.login是个接口中的静态方法:

    /**
     * 登录
     *
     * @param body      登录对象
     * @param client    授权管理视图对象
     * @param grantType 授权类型
     * @return 登录验证信息
     */
    static LoginVo login(String body, RemoteClientVo client, String grantType) {
        // 授权类型和客户端id
        String beanName = grantType + BASE_NAME;
        if (!SpringUtils.containsBean(beanName)) {
            throw new ServiceException("授权类型不正确!");
        }
        IAuthStrategy instance = SpringUtils.getBean(beanName);
        return instance.login(body, client);
    }

根据授权类型获取处理bean,比如授权类型是password,则获取到的登录类是PasswordAuthStrategy,调用PasswordAuthStrategy的login方法。

PasswordAuthStrategy验证逻辑

参数验证

首先从请求参数中构造出PasswordLoginBody,PasswordLoginBody继承了LoginBody,添加了用户名和密码不能为空和长度的限制。

@Data
@EqualsAndHashCode(callSuper = true)
public class PasswordLoginBody extends LoginBody {

    /**
     * 用户名
     */
    @NotBlank(message = "{user.username.not.blank}")
    @Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
    private String username;

    /**
     * 用户密码
     */
    @NotBlank(message = "{user.password.not.blank}")
    @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
    private String password;

}

用户名:最少2个字符,最多20个字符;

密码:最少5个字符,最对20个字符;

由系统中的常量定义:

/**
 * 用户常量信息
 *
 * @author ruoyi
 */
public interface UserConstants {
    /**
     * 用户名长度限制
     */
    int USERNAME_MIN_LENGTH = 2;
    int USERNAME_MAX_LENGTH = 20;

    /**
     * 密码长度限制
     */
    int PASSWORD_MIN_LENGTH = 5;
    int PASSWORD_MAX_LENGTH = 20;
}

验证码验证

参数验证完成之后,完成验证码的验证,如果启用的验证码,则验证验证码:

        // 验证码开关
        if (captchaProperties.getEnabled()) {
            validateCaptcha(tenantId, username, code, uuid);
        }

验证码的验证逻辑是从redis中根据生成图片时的uuid获取缓存的值:

    /**
     * 验证验证码
     *
     * @param username 用户名
     * @param code     验证码
     * @param uuid     唯一标识
     */
    private void validateCaptcha(String tenantId, String username, String code, String uuid) {
        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
        String captcha = RedisUtils.getCacheObject(verifyKey);
        RedisUtils.deleteObject(verifyKey);
        if (captcha == null) {
            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
            throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha)) {
            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
            throw new CaptchaException();
        }
    }

如果未获取到缓存说明验证码已失效,记录登录日志,抛出验证码已失效的异常(CaptchaExpireException)。如果用户输入的值与缓存中的结果不一致,则记录登录日志,抛出验证码错误的异常。

登录密码验证

通过调用租户方法TenantHelper获取登录用户

LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
    LoginUser user = remoteUserService.getUserInfo(username, tenantId);
    loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
    return user;
});

在dynamic方法中调用函数中的代码:

    LoginUser user = remoteUserService.getUserInfo(username, tenantId);
    loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));

首先通过远程调用获取登录用户,然后调用loginService.checkLogin方法验证用户的密码,以下详细分析loginService.checkLogin方法:

    /**
     * 登录验证
     */
    public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
        String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
        String loginFail = Constants.LOGIN_FAIL;
        Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
        Integer lockTime = userPasswordProperties.getLockTime();

        // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
        int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
        // 锁定时间内登录 则踢出
        if (errorNumber >= maxRetryCount) {
            recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        }

        if (supplier.get()) {
            // 错误次数递增
            errorNumber++;
            RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
            // 达到规定错误次数 则锁定登录
            if (errorNumber >= maxRetryCount) {
                recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
                throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
            } else {
                // 未达到规定错误次数
                recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
                throw new UserException(loginType.getRetryLimitCount(), errorNumber);
            }
        }

        // 登录成功 清空错误次数
        RedisUtils.deleteObject(errorKey);
    }

登录检查过程中会检查输入密码错误次数是否已经超过了限制,redis中的key为"pwd_err_cnt:+账号名",如:"pwd_err_cnt:admin"。密码的最大错误次数和锁定时间,在nacos中通过以下属性配置:

# 用户配置
user:
  password:
    # 密码最大错误次数
    maxRetryCount: 5
    # 密码锁定时间(默认10分钟)
    lockTime: 10

配置的意思是如果在10分钟内错误次数超过了5,那么提示“密码输入错误{0}次,帐户锁定{1}分钟”。如果密码未达到5次,则提示“密码输入错误{0}次”。、

登录成功,则清除登录错误次数。

记录登录日志

检查过程中,如果失败,则记录失败登录日志。例如失败次数超过最大次数,则记录日志:

if (errorNumber >= maxRetryCount) {
            recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        }recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));

记录日志方法如下:

    /**
     * 记录登录信息
     *
     * @param username 用户名
     * @param status   状态
     * @param message  消息内容
     * @return
     */
    public void recordLogininfor(String tenantId, String username, String status, String message) {
        // 封装对象
        LogininforEvent logininforEvent = new LogininforEvent();
        logininforEvent.setTenantId(tenantId);
        logininforEvent.setUsername(username);
        logininforEvent.setStatus(status);
        logininforEvent.setMessage(message);
        SpringUtils.context().publishEvent(logininforEvent);
    }

日志记录了以下信息:租户、用户名、登录状态、登录消息。通过SpringUtils.context().publishEvent(logininforEvent);发布事件。发布的事件由ruoyi-common-log中的LogEventListener处理,LogininforEvent类型的日志由以下方法处理:

    /**
     * 保存系统访问记录
     */
    @EventListener
    public void saveLogininfor(LogininforEvent logininforEvent) {
        HttpServletRequest request = ServletUtils.getRequest();
        final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
        final String ip = ServletUtils.getClientIP(request);
        // 客户端信息
        String clientId = request.getHeader(LoginHelper.CLIENT_KEY);
        RemoteClientVo clientVo = null;
        if (StringUtils.isNotBlank(clientId)) {
            clientVo = remoteClientService.queryByClientId(clientId);
        }

        String address = AddressUtils.getRealAddressByIP(ip);
        StringBuilder s = new StringBuilder();
        s.append(getBlock(ip));
        s.append(address);
        s.append(getBlock(logininforEvent.getUsername()));
        s.append(getBlock(logininforEvent.getStatus()));
        s.append(getBlock(logininforEvent.getMessage()));
        // 打印信息到日志
        log.info(s.toString(), logininforEvent.getArgs());
        // 获取客户端操作系统
        String os = userAgent.getOs().getName();
        // 获取客户端浏览器
        String browser = userAgent.getBrowser().getName();
        // 封装对象
        RemoteLogininforBo logininfor = new RemoteLogininforBo();
        logininfor.setTenantId(logininforEvent.getTenantId());
        logininfor.setUserName(logininforEvent.getUsername());
        if (ObjectUtil.isNotNull(clientVo)) {
            logininfor.setClientKey(clientVo.getClientKey());
            logininfor.setDeviceType(clientVo.getDeviceType());
        }
        logininfor.setIpaddr(ip);
        logininfor.setLoginLocation(address);
        logininfor.setBrowser(browser);
        logininfor.setOs(os);
        logininfor.setMsg(logininforEvent.getMessage());
        // 日志状态
        if (StringUtils.equalsAny(logininforEvent.getStatus(), Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
            logininfor.setStatus(Constants.SUCCESS);
        } else if (Constants.LOGIN_FAIL.equals(logininforEvent.getStatus())) {
            logininfor.setStatus(Constants.FAIL);
        }
        remoteLogService.saveLogininfor(logininfor);
    }

登录日志处理逻辑如下:

  1. 获取HttpServletRequest请求,从HttpServletRequest中获取User-Agent,使用hutool的UserAgentUtil进行解析获取到UserAgent详细信息;

  2. 通过hutool获取终端IP地址的方法获取到客户端的IP;

  3. 从http请求header中获取clientId,通过远程调用获取远程客户端的详情;

  4. 通过AddressUtils.getRealAddressByIP(ip)方法获取登录IP所在的省市信息,记录到loginLocation字段;根据IP获取省市信息的方法使用IPv4离线库ip2region,不支持IPv6获取地址,目前IPv6只能原样展示;

  5. 通过User-Agent获取操作系统和浏览器信息设置到os和browser字段;

  6. 保存IP、登录账号、登录消息、状态等信息之后,远程调用保存日志的方法remoteLogService.saveLogininfor(logininfor)保存日志。远程保存auth模块为dubbo客户端

       @DubboReference
       private RemoteLogService remoteLogService;
    

    system模块为dubbo服务端:

    @Service
    @DubboService
    public class RemoteLogServiceImpl implements RemoteLogService {
    
       private final ISysOperLogService operLogService;
       private final ISysLogininforService logininforService;
    
       ……
    
       /**
        * 保存访问记录
        *
        * @param remoteLogininforBo 访问实体
        */
       @Async
       @Override
       public void saveLogininfor(RemoteLogininforBo remoteLogininforBo) {
           SysLogininforBo sysLogininforBo = MapstructUtils.convert(remoteLogininforBo, SysLogininforBo.class);
           logininforService.insertLogininfor(sysLogininforBo);
       }
    

设置登录信息

根据登录用户信息构建SaLoginModel:
1. 登录设备:model.setDevice(client.getDeviceType());
2. 超时时间:不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置,例如: 后台用户30分钟过期 app用户1天过期,model.setTimeout(client.getTimeout());
3. 登录 token 最低活跃频率:model.setActiveTimeout(client.getActiveTimeout());
4. clientId设置到附加参数中:model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
根据loginUser和SaLoginModel生成token,生成token逻辑后续分析。

LoginHelper.login(loginUser, model);

根据生成的token构造LoginVo返回前端:

LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;

生成token逻辑

LoginHelper使用sa-token的StpUtil生成token:

    /**
     * 登录系统 基于 设备类型
     * 针对相同用户体系不同设备
     *
     * @param loginUser 登录用户信息
     * @param model     配置参数
     */
    public static void login(LoginUser loginUser, SaLoginModel model) {
        model = ObjectUtil.defaultIfNull(model, new SaLoginModel());
        StpUtil.login(loginUser.getLoginId(),
            model.setExtra(TENANT_KEY, loginUser.getTenantId())
                .setExtra(USER_KEY, loginUser.getUserId())
                .setExtra(USER_NAME_KEY, loginUser.getUsername())
                .setExtra(DEPT_KEY, loginUser.getDeptId())
                .setExtra(DEPT_NAME_KEY, loginUser.getDeptName())
                .setExtra(DEPT_CATEGORY_KEY, loginUser.getDeptCategory())
        );
        StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
    }

同样,获取token相关信息也是使用StpUtil,如上一步使用StpUtil.getTokenValue()获取token的值,StpUtil.getTokenTimeout()获取token剩余有效时间。

首页

上一步返回的访问令牌通过以下方法设置到userStore中:

  const login = async (userInfo: LoginData): Promise<void> => {
    const [err, res] = await to(loginApi(userInfo));
    if (res) {
      const data = res.data;
      setToken(data.access_token);
      token.value = data.access_token;
      return Promise.resolve();
    }
    return Promise.reject(err);
  };

setToken方法定义在@/utils/auth中,把获取到的令牌保存到tokenStorage中:

export const setToken = (access_token: string) => (tokenStorage.value = access_token);

根据src/permission.ts文件中对路由添加的前置守卫,首先@/utils/auth中的getToken()方法判断tokenStorage中是否存在token,
如果已经存在token说明已经登录,就可以调用后端的接口获取用户信息,如果用户角色列表为空,则调用用户信息接口获取用户信息:

if (useUserStore().roles.length === 0) {
  isRelogin.show = true;
  // 判断当前用户是否已拉取完user_info信息
  const [err] = await tos(useUserStore().getInfo());
  if (err) {
    await useUserStore().logout();
    ElMessage.error(err);
    next({ path: '/' });
  } else {
    isRelogin.show = false;
    const accessRoutes = await usePermissionStore().generateRoutes();
    // 根据roles权限生成可访问的路由表
    accessRoutes.forEach((route) => {
      if (!isHttp(route.path)) {
        router.addRoute(route); // 动态添加可访问路由表
      }
    });
    // @ts-ignore
    next({ path: to.path, replace: true, params: to.params, query: to.query, hash: to.hash, name: to.name as string }); // hack方法 确保addRoutes已完成
  }
}

用户信息获取之后调用usePermissionStore().generateRoutes()更新用户路由列表,最后跳转到首页。

王显锋

激情工作,快乐生活!

文章评论