目录
前端
前端对应文件在
ruoyi-ui/src/views/login.vue
表单校验
由于登录表单使用了:rules,所以用户在输入时,前端会进行校验:
具体校验规则如下:
loginRules
: 包含表单验证规则,用于确保用户输入的账号、密码和验证码是有效的。上图的loginRules的内容的具体含义是:
username
字段的验证规则:
required: true
: 表示密码字段是必填项。trigger: "blur"
: 同样是当输入框失去焦点时触发验证。message: "请输入您的密码"
: 验证失败时,显示的提示信息是"请输入您的密码"
。
password
字段的验证规则:
required: true
: 表示密码字段是必填项。trigger: "blur"
: 同样是当输入框失去焦点时触发验证。message: "请输入您的密码"
: 验证失败时,显示的提示信息是"请输入您的密码"
。
code
字段的验证规则:
required: true
: 表示验证码是必填项,用户必须输入验证码。trigger: "change"
: 这里验证的触发事件是change
,即当输入框的内容发生变化时(即用户输入或修改验证码时)触发验证。message: "请输入验证码"
: 验证失败时显示的提示信息是"请输入验证码"
。
使用 trigger: "change"
可以在用户每次输入或修改验证码时进行检查,这样可以确保:
用户每次修改输入的验证码时,系统能及时检查验证码是否符合要求(例如长度、格式等),即使用户只是稍微修改了验证码,也能立刻得到反馈。
登录处理
当用户通过了前端的表单校验后,就可以点击登录按钮:
程序将执行下面的方法:
表单验证
this.$refs.loginForm.validate(valid => {
if (valid) {
// 进入登录逻辑
}
});
this.$refs.loginForm.validate(valid => { ... })
调用Element UI
提供的表单验证方法,进行表单字段的验证。valid
是一个布尔值,表示表单验证是否通过。如果通过(即valid
为true
),则继续执行后续的登录逻辑,否则不执行。
处理“记住密码”功能 (Cookies
)
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');
}
rememberMe
: 这是一个用户勾选的复选框,表示是否“记住密码”。- 如果用户勾选了“记住密码”,会使用
Cookies.set()
将用户名、加密后的密码和rememberMe
状态保存到浏览器的 cookies 中,并设置过期时间为 30 天。 - 如果用户未勾选“记住密码”,则删除 cookies 中保存的用户名、密码和
rememberMe
信息(使用Cookies.remove()
)。
发送登录请求
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(() => {});
}).catch(() => {
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
this.$store.dispatch("Login", this.loginForm)
: 调用 Vuex 的dispatch
方法,触发Login
动作来执行实际的登录操作。登录数据(如用户名、密码)通过this.loginForm
传递到后端。then()
中的回调函数:this.$router.push({ path: this.redirect || "/" })
: 登录成功后,路由会跳转到用户之前尝试访问的页面(通过this.redirect
获取),如果没有指定,则跳转到根路径/
。
catch()
中的回调函数:this.loading = false
: 登录失败时,停止加载状态(loading
)。if (this.captchaEnabled) { this.getCode(); }
: 如果启用了验证码(this.captchaEnabled
),则重新获取验证码。
如果不了解什么是vuex的dispatch方法,可以看这篇博客:vuex核心概念-actions_vuex mapactions-CSDN博客
整体流程概述
- 用户点击登录按钮,触发
handleLogin()
方法。 - 方法首先验证表单是否填写完整且合法。如果验证失败,则不会执行登录逻辑。
- 如果表单验证通过,根据是否勾选“记住密码”来保存或删除 cookies。
- 调用 Vuex 中的
Login
方法,发送登录请求。 - 登录成功后,页面跳转到指定的路径。
- 登录失败时,恢复加载状态,并(如果启用)重新获取验证码。
登录逻辑的核心功能
- 验证和反馈:表单验证确保用户输入的用户名、密码等字段是有效的。
- 记住我功能:利用 cookies 存储用户的登录信息,并在下次访问时自动填充表单。
- 动态验证码处理:当登录失败时,如果验证码开启,会重新加载验证码,增强登录的安全性。
请求转发到后端
调用dispatch
前端在通过表单验证后,会调用Vuex的dispatch方法:
"Login"
是在 Vuex store 中定义的 action 名称。在组件中调用 dispatch("Login", ...)
会执行这个 Login
action。
Login action
这个Login action的具体位置如下图:
Login
是在 Vuex store 中定义的一个 action。- 它的第一个参数
{ commit }
是 Vuex 自动传递给action
的对象。commit
用于提交 mutation,修改 state。 userInfo
是传递给 action 的第二个参数,它包含了用户登录所需要的信息,如用户名、密码、验证码、uuid 等。这里的userInfo对应的就是this.$store.dispatch("Login", this.loginForm)中的this.loginForm。- 从
userInfo
中提取出用户名、密码、验证码和uuid
。 - 调用
login
函数发送异步请求,这个请求可能是一个 API 调用,用于验证用户名、密码和验证码。login(username, password, code, uuid)
返回一个 Promise(假设它是一个异步的 API 调用)。- 如果请求成功,得到的
res
(响应)包含一个token
,该 token 被用于身份验证。
- 使用
setToken(res.token)
将token
保存到本地(存储在浏览器的cookies
中)。 - 使用
commit('SET_TOKEN', res.token)
提交一个 mutation,更新 Vuex 的状态,保存token
。 - 最后,调用
resolve()
来表示 Promise 已成功完成。
请求转发
在login action中,调用了login:
这实际上调用的是名为login的api,用于将请求转发给后端,具体内容在:
函数定义
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
- 该函数接受四个参数:
username
(用户名),password
(密码),code
(验证码),uuid
(用户会话的唯一标识符)。 - 这些参数随后被封装成一个对象
data
,用来组织发送给服务器的登录数据。username
:用户输入的用户名。password
:用户输入的密码。code
:用户输入的验证码(如果启用了验证码)。uuid
:验证码的唯一标识符(通常是用来识别当前会话,防止攻击)。
发送请求
return request({
url: '/login',
headers: {
isToken: false,
repeatSubmit: false
},
method: 'post',
data: data
})
}
-
request
:request
是一个封装过的 HTTP 请求方法,是基于axios
来发送请求。它接受一个配置对象,该对象定义了请求的 URL、请求头、请求方法和请求数据等内容。 -
url: '/login'
:请求的目标地址是/login
,这意味着该请求会发送到服务器的登录接口,通常是一个处理用户登录的 API。 -
headers
:isToken: false
:这个标头isToken: false
表示该请求不需要携带身份验证的 Token,通常在登录请求中第一次提交时,不需要包含 Token。repeatSubmit: false
:这个标头repeatSubmit: false
是为了防止重复提交登录请求。可以防止用户快速连续点击登录按钮,导致多次提交相同的请求。
-
method: 'post'
:请求的方法是 POST,表示这是一个数据提交请求,通常用于提交用户的登录信息。 -
data: data
:这里传递的数据是之前封装的data
对象,其中包含了username
、password
、code
和uuid
,即用户的登录信息。
请求返回
return request({ ... })
login
函数返回的是request
方法的调用结果。request
返回一个 Promise,代表异步操作的结果。通常情况下,request
函数会发送 HTTP 请求并在成功后解析返回的响应数据,或者在请求失败时抛出错误。- 由于
request
返回的是一个 Promise,你可以在调用login
函数时使用.then()
来处理响应数据或错误。
总结
这段代码实现了一个发送用户登录请求的功能。函数通过将用户输入的数据(用户名、密码、验证码、UUID)封装到请求体中,使用 POST 方法发送给服务器的 /login
接口进行验证。
主要步骤:
- 封装数据:将
username
、password
、code
和uuid
组成一个data
对象。 - 发送请求:使用
request
方法发起一个 POST 请求,提交封装的数据到/login
接口。 - 处理请求头:通过自定义请求头来控制请求的行为,如禁用 Token 和防止重复提交。
- 返回 Promise:返回一个
Promise
,允许外部调用该函数时处理请求的结果或错误。
后端
业务逻辑
后端对应代码如图所示:
AjaxResult ajax = AjaxResult.success();
这里创建了一个 AjaxResult
对象 ajax
,并调用 AjaxResult.success()
方法。AjaxResult.success()
返回一个表示请求成功的标准化响应对象,通常会包含一个 code
字段和 msg
字段,用来表示响应状态(如成功与否),以及返回的数据(如果有的话)。
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid());
该行调用了 loginService
的 login
方法进行业务处理,进行用户登录验证。
loginService
是一个服务类,负责处理实际的登录逻辑。loginBody.getUsername()
、loginBody.getPassword()
、loginBody.getCode()
、loginBody.getUuid()
从loginBody
对象中获取用户输入的用户名、密码、验证码和uuid
。loginService.login()
方法返回一个 令牌(token),这个令牌是一个 JWT的身份认证 token,用于标识已认证的用户。
ajax.put(Constants.TOKEN, token);
ajax.put(Constants.TOKEN, token)
:将生成的 token
存储到 AjaxResult
的返回数据中。Constants.TOKEN
是一个常量,通常表示 "token" 这个字段的键,用于在响应中标识登录令牌。
- 这样,客户端在收到响应时,可以从
AjaxResult
中获取token
,并将其用于后续的认证请求。
总结
这段代码实现了一个典型的用户登录接口,客户端向服务器发送登录请求,服务器验证用户信息并生成一个登录令牌(token),然后将其返回给客户端。客户端接收到令牌后可以在后续的请求中使用该令牌来验证用户身份。
loginService的login方法
我们来分析一下loginService中的login方法(下面是我从若依框架中截取的代码):
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);
}
下面是具体的分析:
方法定义
public String login(String username, String password, String code, String uuid)
login
方法接收四个参数:username
:用户的用户名password
:用户的密码code
:验证码uuid
:验证码的唯一标识符
- 返回值类型是
String
,返回登录成功后生成的 token,这是一个 JWT 的身份验证令牌。
验证码校验
validateCaptcha(username, code, uuid);
- 调用
validateCaptcha
方法进行验证码的校验,通常是用来防止暴力破解等攻击。 username
、code
和uuid
参数用于验证用户提交的验证码是否有效。username
:用户名,用于关联验证码。code
:用户输入的验证码。uuid
:验证码的唯一标识符,通常是生成验证码时提供的标识,防止不同用户之间混淆验证码。
登录前置校验
loginPreCheck(username, password);
调用 loginPreCheck
方法,执行一些登录前的校验逻辑。这个方法可能会校验以下内容:
- 用户名是否存在。
- 用户账户是否被锁定、禁用等。
- 密码的格式是否符合要求等。
身份验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
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();
}
UsernamePasswordAuthenticationToken
:- 创建一个
UsernamePasswordAuthenticationToken
对象,表示用户名和密码的认证请求。此对象封装了用户的用户名和密码,用于后续的认证处理。
- 创建一个
AuthenticationContextHolder.setContext(authenticationToken)
:- 将认证请求存入
AuthenticationContextHolder
,以便后续可以使用上下文中的认证信息。AuthenticationContextHolder
是一个用于管理当前认证信息的上下文容器。
- 将认证请求存入
authenticationManager.authenticate(authenticationToken)
:- 调用
authenticationManager
对象的authenticate
方法进行身份验证。这个方法会委托给具体的UserDetailsService
(通常是自定义的UserDetailsServiceImpl
)来加载用户详情,验证用户名和密码是否匹配。
- 调用
- 异常处理:
BadCredentialsException
:如果密码不正确,捕获此异常并记录登录失败信息。然后抛出自定义异常UserPasswordNotMatchException
,表示密码错误。- 其他异常:如果出现其他异常,记录登录失败信息并抛出
ServiceException
,表示服务器端的错误。
AuthenticationContextHolder.clearContext()
:- 最终清除认证上下文,确保每次认证操作完成后都能清理上下文信息,避免造成信息泄露。
记录登录成功信息
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
- 调用
AsyncManager
异步执行记录登录信息的操作。这里会记录用户的登录成功信息,如用户名、登录时间等。 AsyncFactory.recordLogininfor
是一个工厂方法,用来创建记录日志的操作。
获取登录用户信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
authentication.getPrincipal()
获取认证的用户信息,通常返回一个实现了UserDetails
接口的对象,包含了用户的详细信息。- 这里将其强制转换为
LoginUser
类型(LoginUser
是自定义的用户类,包含用户ID、角色等信息)。
记录登录信息
recordLoginInfo(loginUser.getUserId());
记录用户的登录信息,例如登录时间、IP 地址等,存储到数据库或日志系统中。
生成并返回 Token
return tokenService.createToken(loginUser);
- 调用
tokenService.createToken(loginUser)
方法生成一个令牌(JWT)。loginUser
是通过身份验证获得的用户信息。 - 该方法会根据
loginUser
的信息生成一个 token,通常用于后续请求的身份验证。 - 返回的 token 是一个 JWT,它会包含用户的标识、过期时间、角色等信息,用于后续的认证和授权。
总结
- 验证码校验:首先检查用户提供的验证码是否正确。
- 登录前置校验:进行一些前置的验证,如检查用户名和密码格式、账户状态等。
- 身份验证:通过
UsernamePasswordAuthenticationToken
创建认证请求,并使用authenticationManager
进行用户身份验证。 - 异常处理:处理身份验证过程中可能出现的异常(如密码错误)。
- 记录登录信息:通过
AsyncManager
异步记录登录信息(成功或失败)。 - 生成 Token:成功认证后,生成一个令牌,并返回给客户端。