标题

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


文章目录

  • 登陆的实现
  • 一、登陆——前端
  • 二、后端——登陆



登陆的实现

一、登陆——前端

我们在login.vue通过表单提交找到handleLogin方法

handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true;
          if (this.loginForm.rememberMe) {//是否记住密码
            Cookies.set("username", this.loginForm.username, { expires: 30 });
            Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
            Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
          } else {
            Cookies.remove("username");
            Cookies.remove("password");
            Cookies.remove('rememberMe');
          }
          this.$store.dispatch("Login", this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
          }).catch(() => {
            this.loading = false;
            if (this.captchaEnabled) {
              this.getCode();
            }
          });
        }
      });
    }

它先检测了是否勾选了记住密码,是的话就存到cookies里边,没有勾选就移除,看到它通过Login方法进行跳转,我们在src\store\modules\user.js中找到对应的Login方法

// 登录
    Login({ commit }, userInfo) {
      const username = userInfo.username.trim()
      const password = userInfo.password
      const code = userInfo.code
      const uuid = userInfo.uuid
      return new Promise((resolve, reject) => {
        login(username, password, code, uuid).then(res => {
          setToken(res.token)
          commit('SET_TOKEN', res.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

Login方法将用户名,密码,验证码,uuid接收到,将它们传入Promise方法中,实现异步处理,其中的login方法,我们在src\api\login.js可以找到

// 登录方法
export function login(username, password, code, uuid) {
  const data = {
    username,
    password,
    code,
    uuid
  }
  return request({
    url: '/login',
    headers: {
      isToken: false
    },
    method: 'post',
    data: data
  })
}

使用data进行封装,然后向后端发送请求

二、后端——登陆

我们直接在后台搜索login找到若依的登陆方法

/**
     * 登录方法
     *
     * @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());//其中进行了校验,日志的写入,使用jwt生成token
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

首先创建了一个AjaxResult 返回类,我们点进login方法,进入SysLoginService中

boolean captchaEnabled = configService.selectCaptchaEnabled();
        // 验证码开关
        if (captchaEnabled)
        {
            validateCaptcha(username, code, uuid);
        }

先判断验证码是否开启,点进进入validateCaptcha方法

public void validateCaptcha(String username, String code, String uuid)
    {
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");//拼接固定前綴
        String captcha = redisCache.getCacheObject(verifyKey);
        redisCache.deleteObject(verifyKey);//验证码拿出来后直接删除了
        if (captcha == null)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            throw new CaptchaExpireException();//过期异常
        }
        if (!code.equalsIgnoreCase(captcha))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();//错误异常
        }
    }

定义了一个verifyKey来将验证码前缀和传递过来的uuid进行一个拼接,其中使用了StringUtils中的nvl判断uuid不为空,定义一个captcha 将redis中与verifyKey对应的value取出来进行判断,如果为空抛出过期异常,AsyncManager是进行异步写日志,其中的message中的内容是在messages.properties中进行配置的用于显示错误信息的,使用equalsIgnoreCase判断验证码是否正确

// 用户验证
        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();
        }

使用springSecurity安全框架进行用户验证,创建了一个authenticationToken来将用户账号密码进行保存,使用authenticate进行用户的认证,跳转到ruoyi-framework下的UserDetailsServiceImpl.java

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }
        //验证密码
        passwordService.validate(user);

        return createLoginUser(user);
    }

通过查询操作将用户名传入,依据用户名和账号是否被删除进行查询,使用user将查询到的数据进行保存,将user进行数据判断,不通过便抛出异常,通过validate进行密码的验证

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())
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                    MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
            throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
        }

        if (!matches(user, password))
        {
            retryCount = retryCount + 1;
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                    MessageUtils.message("user.password.retry.limit.count", retryCount)));
            redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
            throw new UserPasswordNotMatchException();
        }
        else
        {
            clearLoginRecordCache(username);
        }
    }

validate中首先获取到了在redis中存入的账户名和密码,定义了一个retryCount 来获取缓存中用户密码错误次数,判断了用户错误的最大次数,超过最大次数需要对用户进行锁定默认一分钟,使用matches进行用户输入密码和数据库密码的判断

public boolean matches(SysUser user, String rawPassword)
    {
        //判断输入密码和数据库密码是否一致
        return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
    }

点击matchesPassword,进入若依配置的安全服务工具类找到

public static boolean matchesPassword(String rawPassword, String encodedPassword)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }

其中调用了BCryptPasswordEncoder 中的matches方法,用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较,如果两者相同,说明用户输入的密码正确。密码验证正确后我们回到UserDetailsServiceImpl调用createLoginUser方法

public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }

createLoginUser将用户信息进行一个封装并通过getMenuPermission获取了菜单的权限数据

public Set<String> getMenuPermission(SysUser user)
    {
        Set<String> perms = new HashSet<String>();
        // 管理员拥有所有权限
        if (user.isAdmin())
        {
            perms.add("*:*:*");
        }
        else
        {
            List<SysRole> roles = user.getRoles();
            if (!roles.isEmpty() && roles.size() > 1)
            {
                // 多角色设置permissions属性,以便数据权限匹配权限
                for (SysRole role : roles)
                {
                    Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
                    role.setPermissions(rolePerms);
                    perms.addAll(rolePerms);
                }
            }
            else
            {
                perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
            }
        }
        return perms;
    }

判断用户是否拥有多个角色,来获取用户对菜单所拥有的权限,我们返回到前边用户验证下边

AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));//记录操作日志写入Logininfor数据库
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());//记录用户信息
        // 生成token
        return tokenService.createToken(loginUser);

若依又进行了异步处理将用户登陆成功写入的日志,使用recordLoginInfo来记录用户登陆信息

public void recordLoginInfo(Long userId)
    {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
        sysUser.setLoginDate(DateUtils.getNowDate());
        userService.updateUserProfile(sysUser);
    }

recordLoginInfo将登陆的ip和地址写入数据库,若依使用了IpUtils来获取用户登陆的ip,通过Servlet请求里边获取ip,可以在user表中看到改变的字段
回到前边可以看到若依使用createToken生成token

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);
        return createToken(claims);
    }

createToken中首先获取到了一个uuid,将uuid存到用户里边,调用setUserAgent和refreshToken

public void setUserAgent(LoginUser loginUser)
    {
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
        loginUser.setIpaddr(ip);
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginUser.setBrowser(userAgent.getBrowser().getName());
        loginUser.setOs(userAgent.getOperatingSystem().getName());
    }

setUserAgent获取到用户的ip地址,浏览器,操作系统等

public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

refreshToken是用来刷新token,登陆时间和有效期,有效期默认半个小时
return createToken(claims);方法中的createToken

private String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                //payload载荷
                .setClaims(claims)
                //signature签名
                .signWith(SignatureAlgorithm.HS512, secret)
                //compact()方法将header、payload、signature通过.进行一个拼接
                .compact();
        return token;
    }

使用Jwt算法将信息生成一个字符串返回,最后返回到AjaxResult 里边传到前端

// 登录
    Login({ commit }, userInfo) {
      const username = userInfo.username.trim()
      const password = userInfo.password
      const code = userInfo.code
      const uuid = userInfo.uuid
      return new Promise((resolve, reject) => {
        login(username, password, code, uuid).then(res => {
          setToken(res.token)
          commit('SET_TOKEN', res.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

在前端呢,通过setToken方法进行保存,setToken方法将传递过来的数据保存到Cookies中