上一篇写了下若依前后端分离框架中前端至弹出登陆界面的过程,本片来详细了解下登录的整个过程。
后端包含ruoyi-admin,ruoyi-common,ruoyi-framework等多个模块,ruoyi-admin为启动模块。先看一下ruoyi-admin/src/main/application.yml配置文件。
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
指定了服务端启动的端口8080。我们运行ruoyi-admin/src/main/java/com/ruoyi/
RuoYiApplication.java即可启动后端,监听8080端口。
我们回到前端的登录界面。 views/login.vue
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">若依后台管理系统</h3>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input
v-model="loginForm.code"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2020 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</template>
<script>
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
export default {
name: "Login",
data() {
return {
codeUrl: "",
cookiePassword: "",
loginForm: {
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
},
loginRules: {
username: [
{ required: true, trigger: "blur", message: "用户名不能为空" }
],
password: [
{ required: true, trigger: "blur", message: "密码不能为空" }
],
code: [{ required: true, trigger: "change", message: "验证码不能为空" }]
},
loading: false,
redirect: undefined
};
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect;
},
immediate: true
}
},
created() {
this.getCode();
this.getCookie();
},
methods: {
getCode() {
getCodeImg().then(res => {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
});
},
getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
},
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(() => {
this.loading = false;
this.getCode();
});
}
});
}
}
};
</script>
页面加载前,created()调用this.getCode()和this.getCookie()获取验证码和cookie。这里不详细说明。我们重点关注下handleLogin登录函数。this.$refs.loginForm.validate判断用户名、密码和验证码的合法性。this.loginForm.rememberMe判断是否选择了记住密码。如果记住密码,我们把用户名密码存进cookie,下次跳到登录界面就不需要输用户名密码。如果不选择记住密码,将清空cookie,下次跳到登录界面需要手动输入用户名和密码。
this.$store.dispatch(“Login”, this.loginForm),这里是我们最需要关注的地方。调用store里的Login对应的函数,把loginForm作为参数。我们跳到src/store/modules/user.js。
import { login, logout, getInfo } from '@/api/login'
......
actions: {
// 登录
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)
})
})
},
......
}
这里新建了一个Promise,调用了login函数,login函数是从@/api/login引入的。我们再跳到src/api/login.js看一下这个函数:
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
url: '/login',
method: 'post',
data: data
})
}
这里构建了一个ajax的POST请求,url为/login,data为用户名、密码、验证码的结构数据。我们再简单看一下request对应的src/utils/request.js。
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API,
// 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
我们看到请求的地址是process.env.VUE_APP_BASE_API, 这是怎么访问到后端的的呢?我们回头看一下vue.config.js文件
devServer: {
host: '0.0.0.0',
port: port,
open: true,
proxy: {
// detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: {
target: `http://localhost:8080`,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
},
disableHostCheck: true
},
这里做了一个代理,把process.env.VUE_APP_BASE_API地址映射成了http://localhost:8080。所以我们login最终访问后端的地址就变为http://localhost:8080/login。
现在看一下后端接收这个请求的代码。ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java。
/**
* 登录方法
*
* @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;
}
接收到/login的POST请求后,调用loginService.login方法进行验证,并生成token返回给客户端。接下来,看一下loginService.login方法。
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String code, String uuid)
{
......
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
......
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
这里牵扯到Spring Security 的知识,我们看一下Spring Security的配置ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{ /**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
这里设置了userDetailService。UserDetailsServiceImpl实现了UserDetailService接口,实现了loadUserByUsername方法。我们再看一下UserDetailsServiceImpl.loadUserByUsername的实现:
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new BaseException("对不起,您的账号:" + username + " 已停用");
}
return createLoginUser(user);
}
}
就是根据用户名去数据库查询用户信息并返回。然后 再调用Spring Security内部的认证代码进行认证。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
就是比较根据用户名获取到的用户的密码和认证信息参数中的密码进行比较。更为详细的过程,这里引用了一位大神的文章,介绍的很详细。
认证完成后,根据用户的信息,构建token,进行返回。前端收到返回后,存储token,并跳转网页。this.$router.push({ path: this.redirect || “/” });
大体的登陆过程就是这样子。