一、登录引入
前端目录为ruoyi-ui,为单页面模式,只有一个html文件:public/index.html
。vue-cli3之后为public/index.html, vue-cli2版本的是根目录下的index.html。
index.html:
<body>
<div id="app">
<div id="loader-wrapper">
<div id="loader"></div>
<div class="loader-section section-left"></div>
<div class="loader-section section-right"></div>
<div class="load_title">正在加载系统资源,请耐心等待</div>
</div>
</div>
</body>
这里的id=app
关联src/main.js
中的#app
main.js:
import App from './App.vue'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
main.js
中又引入了src/App.vue
App.vue:
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App',
metaInfo() {
return {
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
titleTemplate: title => {
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
}
}
}
}
</script>
在App.vue
文件中,router-view
标签是vue的路由,读取src/router/index.js
文件,根据用户请求的路径,加载不同的页面。
router/index.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
/* Layout */
import Layout from '@/layout'
// 公共路由
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect')
}
]
},
{
path: '/login',
component: () => import('@/views/login'),
hidden: true
},
{
path: '/register',
component: () => import('@/views/register'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error/401'),
hidden: true
},
{
path: '',
component: Layout,
redirect: 'index',
children: [
{
path: 'index',
component: () => import('@/views/index_v1'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
]
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
]
// 防止连续点击多次路由报错
let routerPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push (location) {
return routerPush.call(this, location).catch(err => err)
}
export default new VueRouter({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
登录页面如下:
登录页面的url为 http://localhost:82/login?redirect=%2Findex
vue-cli-service
内部依赖了一个portfinder
的库,如果配置的端口被占用了,则自动加1。
{
path: '',
component: Layout,
redirect: 'index',
children: [
{
path: 'index',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
上面这段路由,页面本应该加载的是view/index.vue
页面,但现在路由为/login
,加载的是view/login.vue
页面。这是由于路由守卫,具体见src/permission.js
文件。
路由守卫流程图:
src/permission.js:
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/auth-redirect', '/bind', '/register']
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
该文件src/permission.js
被main.js引入。
router.beforeEach
是vue-route
r提供的导航守卫,即在路由跳转前先进行判断。
因为此时还没有登录,getToken
肯定无法返回,且“/”
请求也不在白名单中,所以最后会走到next(/login?redirect=${to.fullPath})
, 也就和我们的地址http://localhost:81/login?redirect=%2Findex
对应起来了。这里的%2F
是/的URL编码。
二、登录过程
1、登录过程介绍
后端包含ruoyi-admin,ruoyi-common,ruoyi-framework等多个模块,ruoyi-admin为启动模块。后端目录结构如下:
com.ruoyi
├── common // 工具类
│ └── annotation // 自定义注解
│ └── config // 全局配置
│ └── constant // 通用常量
│ └── core // 核心控制
│ └── enums // 通用枚举
│ └── exception // 通用异常
│ └── filter // 过滤器处理
│ └── utils // 通用类处理
├── framework // 框架核心
│ └── aspectj // 注解实现
│ └── config // 系统配置
│ └── datasource // 数据权限
│ └── interceptor // 拦截器
│ └── manager // 异步处理
│ └── security // 权限控制
│ └── web // 前端控制
├── ruoyi-generator // 代码生成(可移除)
├── ruoyi-quartz // 定时任务(可移除)
├── ruoyi-system // 系统代码
├── ruoyi-admin // 后台服务
├── ruoyi-xxxxxx // 其他模块
先看一下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
class="login-form"
:model="loginForm"
:loginRules="loginRules"
ref="loginForm"
>
<h3 class="title">若依后台管理系统</h3>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" placeholder="账号">
<svg-icon
class="el-input__icon input-icon"
slot="prefix"
icon-class="user"
></svg-icon>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon
class="el-input__icon input-icon"
slot="prefix"
icon-class="password"
></svg-icon>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaOnOff">
<el-input
v-model="loginForm.code"
placeholder="验证码"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<svg-icon
class="el-input__icon input-icon"
slot="prefix"
icon-class="validCode"
></svg-icon>
</el-input>
<div class="login-code">
<img class="login-code-img" :src="codeUrl" @click="getCode" />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0"
>记住密码</el-checkbox
>
<el-form-item>
<el-button
:loading="loading"
type="primary"
style="width: 100%"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登录</span>
<span v-else>登录中...</span>
</el-button>
<div style="float: right" v-if="register">
<router-link class="link-type" :to="'/register'"
>立即注册</router-link
>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2022 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",
components: {},
data() {
return {
codeUrl: "",
loginForm: {
username: "",
password: "",
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,
// 验证码开关
captchaOnOff: true,
// 注册开关
register: false,
redirect: undefined,
};
},
watch: {
$route: {
handler: function (route) {
console.log(route);
this.redirect = route.query && route.query.redirect;
},
immediate: true,
},
},
created() {
this.getCode();
this.getCookie();
},
methods: {
getCode() {
getCodeImg().then((res) => {
this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff;
console.log(res);
if (this.captchaOnOff) {
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:
this.username === undefined ? this.loginForm.username : username,
password:
this.password === undefined
? this.loginForm.password
: decrypt(password),
rememberMe: this.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(() => {});
})
.catch(() => {
this.loading = false;
if (this.captchaOnOff) {
this.getCode();
}
});
}
});
},
},
};
</script>
页面加载前,created()调用this.getCode()和this.getCookie()获取验证码和cookie。
2、获取验证码
后端生成一个表达式,1+1=?@2
其中,1+1=?转成图片流,传到前端进行展示;2存入redis中,当前端登录时,校验前端输入的答案与redis中的答案是否相同,若相同则校验通过。
3、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"));
}
}
}
就是比较根据用户名获取到的用户的密码和认证信息参数中的密码进行比较。关于认证的详细过程请参照如下网址:https://blog.csdn.net/yuanlaijike/article/details/84703690
认证完成后,根据用户的信息,构建token,进行返回。前端收到返回后,存储token,并跳转网页。this.$router.push({ path: this.redirect || “/” });
以上为登录的大致过程。