若依知识点分享(用户登录)

目录

1、功能实现的意义

2、功能定位

1)验证码校验

2)登录前置校验

3)用户认证

结语


若依框架作为一款功能全面,上手简单的项目,值得学习的地方还是不少的,上期我们分享了若依验证码生成功能的相关知识点,本期我们来进行下一步的分享。

本次分享也是和验证码有些许关联的,那就是用户登录功能。

用户登录想必大家并不陌生,而且这个功能再实际开发中是必不可少的一个功能,在现实生活中也实时可见。目前主流的登录方式主要有手机验证码登录,扫码登录,人脸识别登录等等相对方便的登陆方式。相比而言若依的用户名密码登录功能就显得有些原始。但是原始并不妨碍我们学习它,有了原始技能的基础我们才能更好的去学习更多主流、实用、复杂的技能。

废话不多说,让我们学起来。

1、功能实现的意义

登录功能看似是一个简单常见的功能,但是这个功能所肩负的责任对于开发者来说却是意义非凡。

为什么这么说嘞?

试想,若是有非法用户想要窃取项目数据,登录功就是我们项目的第一道屏障,若是没有这道屏障或者是这道屏障不够坚固,攻击者便可长驱直入,这对数据无疑是一大损失。

登录功能通常用于验证用户身份,确保用户访问特定的内容或执行特定的操作时具有相应的权限。通过登录,系统可以识别用户是谁,并根据其身份和权限控制对资源的访问。登录功能可以帮助确保用户账号的安全性,并能够提供个性化的用户体验,例如保存用户偏好设置、查看个人信息等。

2、功能定位

首先我们需要找到我们的登录功能的页面以及相关的功能代码,页面并不难找,当我们启动若依的前端项目就会在浏览器中自动打开登陆页面。当然了,为了更加方便我们的学习,后端项目也是一定要启动的。启动项目这块土豆在这就不多加赘述了。如果小伙伴需要,但是还没有下载若依项目或者在启动项目方面有些问题,可以参考:若依(ruoyi)框架初识

项目启动后我们就可以看到若依的登陆页面:

为了方便我们的学习,建议大家打开浏览器F12的开发者工具,重新刷新下我们的页面,输入用户名,密码以及验证码,点击登录按钮,在控制台找到我们的登录接口

在这里我们可以看到我们的登录接口以及调用登录接口时发送给后端的请求中携带的相关参数,然后我们去后端项目中找到登录接口相关的代码。

/**
     * 登录方法
     *
     * @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;
    }
    /**
     * 登录验证
     *
     * @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);
    }

从登录功能的主要实现方法,login方法中我们大致总结除了三个流程

1)验证码校验

    /**
     * 校验验证码
     *
     * @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);
            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();
            }
        }
    }

验证码存在的作用就是用于保证访问系统的“用户”是真实的人类,而是不是攻击者控制的机器人,这是其一。还有就是在用户登录时,若用户恶意登录,输入不存在的用户名反复调用后端接口,没访问一次就要做一次登录校验,这无疑是不合理的。久而久之也是对服务器资源的浪费,严重更会影响正常用户的使用。

验证码校验这块的详细知识点如果有需要的小伙伴可以参考:若依知识点分享(登录验证码)

2)登录前置校验

登陆前置校验也和验证码有着相似的作用,防止恶意用户的攻击,较少系统资源的浪费,再通过验证码校验之后,系统会对用户的登录信息进行合法性校验,这种校验会根据用户生成时的规则提供相对应的校验方式,若依这里的校验是这样的:

    /**
     * 登录前置校验
     *
     * @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();
        }
    }

这个校验主要是对用户的用户名、密码的合法性进行校验以及IP黑名单进行校验。

除了上面代码中的校验方式我们也可以使用其他方式达到相同的功能,常用的一些方法有正则表达式,或者在参数实体类中添加相关的校验注解等方式来实现。上面的校验相对简单更好理解,有兴趣的小伙伴可以自行研究一番。

3)用户认证

用户认证这块做的操作通俗来说就是用来校验用户的账号、密码是否一致,其中这块的代码实现逻辑大概是这样子的:

1.将用户名和密码封装成一个UsernamePasswordAuthenticationToken对象,然后将其设置到AuthenticationContextHolder中。
2.通过AuthenticationManager进行认证,如果认证成功,则将认证结果保存到Authentication对象中。
3.如果认证失败,则根据异常类型进行不同的处理,分别记录登录失败日志并抛出对应的异常。
4.无论认证成功还是失败,最后都会记录登录成功日志,并返回一个LoginUser对象表示登录成功后的用户信息。

这块的操作可以说是当前登录功能的核心部分,主要涉及到了身份认证和异常处理,使用了Spring Security框架来实现用户认证功能。在代码中,通过UsernamePasswordAuthenticationToken构建了认证凭证,并使用AuthenticationManager来进行身份认证。在异常处理部分,通过捕获不同的异常类型来记录用户登录失败信息,并抛出相应的自定义异常。最后,在finally块中清除了认证凭证。同时,代码中还使用了AsyncManager来异步记录用户登录信息。这段代码结合了Spring Security、异步处理等技术,实现了用户登录功能的完整流程。

Spring Security:

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,提供了全面的安全性解决方案来保护应用程序。它可以用于在Spring应用程序中实现身份认证、授权、密码管理、会话管理等安全特性。Spring Security基于过滤器链的体系结构,通过配置安全规则、认证提供者和权限配置等来保护应用的资源。

 代码解读:

  • Authentication:Spring Security中的接口,表示经过认证的安全主体。通常是一个UsernamePasswordAuthenticationToken对象。
  • AuthenticationContextHolder:一个自定义的上下文管理器,用于保存当前线程的Authentication对象。
  • UsernamePasswordAuthenticationToken:Spring Security提供的实现了Authentication接口的具体类,用于封装用户名和密码。
  • UserDetailsServiceImpl.loadUserByUsername:一个自定义的UserDetailsService接口的实现类,用于根据用户名从数据库中查询用户信息。
  • authenticationManager.authenticate(authenticationToken):通过AuthenticationManager来进行认证,该方法会委托给UserDetailsService来验证用户名和密码。
  • BadCredentialsException:Spring Security提供的异常类,当用户名或密码不匹配时会抛出该异常。
  • AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message(“user.password.not.match”))):异步记录登录失败日志的操作。
  • UserPasswordNotMatchException:自定义的异常类,表示用户密码不匹配。
  • AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message(“user.login.success”)):异步记录登录成功日志的操作。

通过debug调试我们可以发现,首先他先到包org.springframework.security.config.annotation.web.configuration下执行以下代码

这个方法的主要功能是用于身份认证,对传入的Authentication对象进行验证。在方法中,如果已设置了委托者(delegate),则委托委托者进行身份验证;如果未设置委托者,则通过委托者构建器(delegateBuilder)初始化委托者,然后进行身份验证。通过同步块确保了在委托者初始化期间的线程安全。

执行完毕之后再去到org.springframework.security.authentication包下的ProviderManager类中调用authenticate方法,这个方法实现了AuthenticationManager的authenticate方法

    /**
     * 尝试对传入的{@link Authentication}对象进行身份认证。
     * <p>
     * 将逐个尝试认证提供者列表,直到某个AuthenticationProvider表示能够认证传入的Authentication对象类型。
     * 然后将使用该AuthenticationProvider进行认证。
     * <p>
     * 如果多个AuthenticationProvider支持传入的Authentication对象,
     * 则能够成功认证Authentication对象的第一个AuthenticationProvider将确定result,
     * 覆盖之前支持的AuthenticationProvider可能抛出的任何AuthenticationException。
     * 在成功认证时,将不再尝试后续的AuthenticationProvider。
     * 如果没有任何支持的AuthenticationProvider成功通过身份认证,则将重新抛出最后一个抛出的AuthenticationException。
     *
     * @param authentication 认证请求对象。
     * @return 包括凭据在内的完全认证对象。
     * @throws AuthenticationException 如果认证失败。
     */
    @Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then it will
		// publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
		// parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

主要是对传入的Authentication对象进行身份认证。在方法中,首先获取传入Authentication对象的类型,并初始化一些变量用于保存认证结果。然后遍历所有的认证提供者(providers),对于支持当前Authentication类型的认证提供者,依次调用其authenticate方法进行认证。如果某个认证提供者成功认证,则会将认证结果拷贝到最终结果中,并结束认证过程。如果认证失败,会捕获相关异常并记录。如果所有认证提供者都无法认证成功,并且存在父级认证管理器(parent),则尝试由父级认证管理器进行认证。最终会根据认证结果是否为空和异常情况进行相应处理,包括发布认证成功事件和抛出认证异常。

在这段代码执行完毕之后,会在去执行UserDetailsServiceImpl的loadUserByUsername方法:

这个方法会从数据库中查询当前用户的信息状态,最后返回authentication对象,最后会在login中处理返回的对象,记录本次登录的状态,在最后获取到用户的相关信息之后调用createToken方法生成token,将token存储在Redis中,并且将生成的token返回给前端,表示当前用户允许登录。

    /**
     * 创建令牌
     *
     * @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);
        return createToken(claims);
    }


    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    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);
    }

    /**
     * 设置用户代理信息
     *
     * @param loginUser 登录信息
     */
    public void setUserAgent(LoginUser loginUser) {
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String ip = IpUtils.getIpAddr();
        loginUser.setIpaddr(ip);
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginUser.setBrowser(userAgent.getBrowser().getName());
        loginUser.setOs(userAgent.getOperatingSystem().getName());
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

结语

登录功能对于任何项目都是至关重要的,它不仅可以提升用户体验,让用户能够方便快捷地使用系统,同时也是保障系统安全的重要一环。在实际项目中,设计和实现登录功能需要考虑到用户友好性和安全性的平衡。并确保用户信息的安全性和系统的可靠性。

同时,为了加强系统安全性,可以采用加密传输、限制登录失败次数、实现安全的会话管理等措施来防止恶意攻击和数据泄露。

希望各位小伙伴在自己的项目中认真思考和实现登录功能,不仅能够提升用户体验和保障系统安全,也是项目成功的关键一步。愿大家在项目中都能设计出优秀的登录功能,为用户带来更好的体验和保障系统的安全运行。

本次若依项目的登录功能只是万千设计中的一种,本次分享一方面是土豆学习的记录,另一方面也是希望为各位小伙伴提供更多学习上的帮助。希望本次分享对你有所帮助,这是土豆最大的心愿。

如有不足之处还请各位小伙伴多多指点,不吝赐教。

感激不尽,感激不尽。。。

最后的最后,祝愿大家劳动节快乐,Happy

  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue.js是一种用于构建用户界面的JavaScript框架,由Evan You于2014年创建。它采用了响应式的数据绑定和组件化的开发方式,使得开发者能够更轻松地构建高效、可维护的前端应用程序。 在Vue的知识点中,首先需要了解Vue的基本概念和核心特性。Vue的核心是响应式数据绑定,它允许开发者将数据与DOM元素进行绑定,使得数据的变化能够自动反映到界面上。此外,Vue还提供了指令、计算属性、监听器等功能,方便开发者处理复杂的数据逻辑和交互行为。 另外,Vue的组件化开发模式也是非常重要的知识点。通过将应用程序拆分成一系列独立的组件,开发者可以更好地管理和复用代码。Vue提供了组件的定义、组件通信、组件生命周期等功能,方便开发者构建可扩展的应用程序。 除了基本的概念和核心特性,Vue的知识点还包括路由管理、状态管理、动画效果等方面。路由管理允许开发者通过URL的变化来实现页面之间的切换和导航,使得应用程序可以呈现更好的用户体验;状态管理用于管理应用程序的全局状态,方便各个组件之间的数据通信和共享;动画效果可以为应用程序增加交互性和视觉效果。 综上所述,一个完整的Vue知识点的PDF分享应当包括Vue的基本概念、响应式数据绑定、组件化开发模式、路由管理、状态管理和动画效果等方面的内容。通过学习这些知识点,开发者可以更好地掌握Vue的使用方法,提高前端开发的效率和质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值