赞
踩
<el-input> 标签内属性介绍: <el-input v-model="listQuery.orderId" 数据绑定 placeholder="orderId" 当输入框内容为空时的占位符 style="width: 200px;" 输入框宽度 class="filter-item" class名称 @keyup.enter.native="handleFilter" 当按下回车时触发事件调用方法 @keyup.native="handleFilter" 当按钮回弹时触发的方法 > </el-input> <!-- @click与@keyup的区别 ※※※ 1、其中@click用于绑定监听功能: <button @click="test1">test1</button> <button @click="test2('abc')">test2</button> <button @click="test3('abcd', $event)">test3</button> methods: { test1(eve) {//test1函数没有参数,默认传递 $event alert(eve.target.innerHTML) //test1 }, test2 (msg) { //test1函数有参数,传递该参数 alert(msg) // abc }, test3 (msg, event) { //有参数,如果想获取到enevt,则函数中需要写 $event alert(msg+'---'+event.target.textContent) // abcd---test3 } } 2、而@keyup用于按键修饰符: //按下enter时,执行方法test7 <input type="text" @keyup.enter="test7"> methods: { test7 (event) { console.log(event.keyCode) alert(event.target.value) } } --> <!-- 这里关于<el-input></el-input>的不理解,是因为还未学习ElementUI,关于该框架的学习见后文 -->
<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>
首先要明白< svg-icon >标签是一个全局组件,要在对应的icons/index.js中定义并注册成立全局组件;需要svg-sprite-loader的配合,并在config.js中填写相关配置;其图片文件都放在icons/svg文件夹下面。
如何使用< svg-icon >组件:
2.1. 安装svg-sprite-loader依赖,而这个在若依的准备工作,运行npm install时就给安装好了。
2.2. 配置vue.config.js:
//开头 const path = require('path') function resolve(dir) { return path.join(__dirname, dir) } //这里只列一部分,具体配置参考文档 module.exports = { chainWebpack(config) { config.plugins.delete('preload') // TODO: need test config.plugins.delete('prefetch') // TODO: need test // set svg-sprite-loader config.module .rule('svg') .exclude.add(resolve('src/assets/icons')) .end() config.module .rule('icons') .test(/\.svg$/) .include.add(resolve('src/assets/icons')) .end() .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) .end() //这里后面仍可以按需添加config.module操作 } }
2.3. 创建相关文件及文件夹(以若依框架中为例):
2.4.在main/js中引入icons:
2.5.使用< svg-icon >组件(以若依框架中为例):
这里< svg-icon >用在< el-input >< /el-input >内部,其中slot表示所引用图片显示的位置:prefix为input框之前,suffix为框尾部;icon-class表示它引用的是src/assets/icons/svg下的哪一张图片;class为对应css中所定义的样式(同时注意base、head、html、meta、param、script、title这几个不支持使用class)。
<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" /> //suffix表示尾部
</el-input>
</el-form-item>
框前:
class所用的input-icon样式:
//登录按钮对应的keyup回车响应事件 handleLogin() { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true; //如果勾选了“记住密码”选项,就把相关信息存到Cookie中 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 { //如果没有勾选“记住密码”选项,就把相关信息在Cookie中移除 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(); } }); } }); }
关于$store.dispatch方法介绍如下:点我,其中“Login”追溯过去,可以发现其位置与内容如下图所示:
其功能大致为获取表单信息,然后又封装了一个对象Promise,用于实现异步处理,因为Promise中又调用了另一个js中的Login函数,如下图所示:
上图将传过来的数据封装到data中,然后return表示的是带着data数据对url进行post请求,如下图所示,便是在前端抓包抓到的login.js文件中data数据内容:
下图为return requset内容:
且通过上图可以看出,这里又使用了反向代理操作,映射到8080(一看到return request操作,就要想到(一)中讲解的理论请求后端,结果请求前端的反向代理操作)。并且可以在后端找到login注解所在函数位置。
此时若在AjaxResult ajax = AjaxResult.success();
处打一断点,你会发现此时login函数所传入的 LoginBody 变量的内容,正是前端所填写与生成的内容:
(其中AjaxResult ajax = AjaxResult.success();
为SpringBoot [后端]的通用返回类,以此来返回状态码信息)
loginService.login()
来进行验证操作码方式public void validateCaptcha(String username, String code, String uuid) { //用于获取验证码开关 boolean captchaEnabled = configService.selectCaptchaEnabled();//用于获取验证码开关 if (captchaEnabled) { //这一步是通过使用固定前缀与uuid的组合,使其能够在Redis中找到对应的缓存键值 String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //获取缓存键值对应的对象 String captcha = redisCache.getCacheObject(verifyKey); //删除Redis缓存中对应缓存键值的对象 redisCache.deleteObject(verifyKey); //当Rdeis中对应缓存对象为空时(即存入Redis后超过2分钟) if (captcha == null) { //execute是与线程相关,这句用于异步记录日志,使用工厂方法,可实现解耦和,此句与下面的抛出过期异常可同时执行 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); //抛出过期异常 throw new CaptchaExpireException(); } //当code值与captcha值不相等时 if (!code.equalsIgnoreCase(captcha)) { //同上面所讲(业务层面需要) AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); throw new CaptchaException(); } } }
这里之所以没有对正常情况进行处理,是因为若正常他就会直接往下面走,执行下一个验证用户名和密码操作;而不是被抛出的异常而打断该方法进程
/** * 登录前置校验 * @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(); } }
2.2:而后使用Spring Security进行用户验证(本质就是设置Filter过滤器),代码如下所示:
// 用户验证 Authentication authentication = null; try { //这里是创建一个UsernamePasswordAuthenticationToken对象,并将username与password进行赋值操作,就是进行一个包装操作 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(); } //如果成功,异步写操作日志,是写道sys_logininfo这张表中 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); //拿到登录用户 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); //记录账户信息,写到日志中;这一步修改的是数据库中sys_user表中的login_date这一项 //且这里你对recordLoginUnfo一步步追踪,可发现他获取到了userid、ip以及登陆时间; recordLoginInfo(loginUser.getUserId());
2.3:校验用户名和密码中部分知识点讲解:
2.3.1:在recordLoginInfo(loginUser.getUserId());
如何获取的Ip?(Spring Boot项目在不同文件夹下,两种调用java类的方式)
在SysLoginService.java文件中可遭到recordLoginInfo的代码,如下图所示:
再追溯过去,可发现他是对原方法进行了一层封装,在IpUtils.getIpAddr()中通过ServletUtils工具类中的getRequest()方法获取到了IP。
但是!!你会发现IpUtils.java文件是在ruoyi-common文件夹中的,那么在ruoyi-admin中的SysLoginController.java是如何调用到IpUtils.Java这个工具类的呢?
答案在ruoyi-admin中的pom文件中,已经提前对ruoyi-framework、ruoyi-quartz以及ruoyi-generator另外三个包配置了依赖,因而在SysLoginController.java中可以调用ruoyi-framwork包中的SysLoginService.java,又因为在SysLoginService.java有对ruoyi-common包的导入,因而可以调用IpUtils.Java工具类。(Spring Boot项目在不同文件夹下,两种调用java类的方式)
这里ruoyi-admin引入了上文提到的三个包,因而在Controller类使用时就要引入其他未添加依赖且需要的包:
这里ruoyi-framework仅引入了ruoyi-system包,因而在Service类使用时就要引入其他未添加依赖且需要的包:
2.3.2:更新用户信息
跟进去,代码如下所示:
这里是调用Mapper,当这个出现的时候,证明它开始与mybatis一起发力了,再跟进去会看到mybatis的相关代码(需在idea下载MyBatisX插件),如下图所示:
return tokenService.createToken(loginUser);
用于生成token/** * 创建令牌 * * @param loginUser 用户信息 * @return 令牌 */ public String createToken(LoginUser loginUser) { //获取uuid String token = IdUtils.fastUUID(); //把获取到的uuid存到用户里面 loginUser.setToken(token); //用于获取ip、浏览器和os等,并存到用户里面(跟进去发现的) setUserAgent(loginUser); //刷新token令牌有效期,并将用户登陆信息loginUser存到Redis中 refreshToken(loginUser); //这里的claims是jwt中加载所用到的用户信息 Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); //用于从数据声明生成令牌,生成点进createToken中会发现调用的是jwt,如下面代码所示 return createToken(claims); } /** * 从数据声明生成令牌 * * @param claims 数据声明 * @return 令牌 */ private String createToken(Map<String, Object> claims) { String token = Jwts.builder() .setClaims(claims) //用这个算法将信息生成字符串(加密操作) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; }
最终生成的token通过ajax返回前端。
首先看到前端对应返回的代码处,跟着Login这个action点进去
会有下图所示代码:
其中的login()方法你依次追进,会发现如下代码:
对应着后台调用的方法,因而其方法内的 setToken(res.token)
(经点进去可发现)是用于将token存到Cookies中(因为将token令牌存至了Cookies中,因而此用户在30min内再次登录【不点击退出的那种登录】,浏览器会自动登录此用户,无需输入密码验证码)。
红框1中所标内容便是带着前台数据到后台进行操作,而红框2中便是后端在对表单信息内容全部检验合格后进行的操作(原因:从前面操作可看出若是验证码校验和用户校验不合格时,会抛出异常,因而也就走不到这里),即跳转到下一个页面,流程如下图所示:
watch()监听代码:
watch: {
$route: {
handler: function(route) {
console.log("route的值: " + route);
console.log("route.query 和 route.query.redirect 的值: " + route.query +" " + route.query.redirect);
this.redirect = route.query && route.query.redirect;
},
immediate: true
}
},
首先,我们知道$route
是获取当前路由信息对象,包含路由的状态信息、URL解析得到的信息,还有URL匹配到的路由信息。其次route.query表示URL查询参数(若没有查询参数,为空),如下图所示,可看到这里的route.query的值为\index
。
(在查看URLEncode编码之后的数据可以知晓 ‘ %2F ’ 就表示 ’ / ')
(若依框架的前端路由控制的核心是在src/permission.js中)
首先找到了ruoyi-ui/src/permission.js中,增加这么一句:
会发现这里的to.fulPath便是URL中/login?redirect=%2Findex
(注意:当未登陆过,首次打开login.vue页面时,是执行if ( getToken() ) { } else{ }
中else内的内容,因为token令牌是在登录按钮点击后才生成的 )
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', '/register'] //挂载路由导航守卫:这里是全局前置路由守卫————初始化及每次路由切换之前调用 router.beforeEach((to, from, next) => { //to 将要访问的路径 //from 从哪个路径跳转而来 //next是一个函数,表示放行;next() 放行,next('/login') 强制跳转 console.log(" default test: " + to.fullPath); console.log(" to.path : " + to.path); //请求路由进度条开始 NProgress.start() //如果有token令牌(即登陆成功) 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) { //如果为0,表示未获取用户信息,需要重新获取 //重新登陆弹窗 isRelogin.show = true // 判断当前用户是否已拉取完user_info信息 store.dispatch('GetInfo').then(() => { isRelogin.show = false //从vue中触发GenerateRoutes方法,获取后台返回的路由信息(也就是在这里获取到了路由的对应字符串) store.dispatch('GenerateRoutes').then(accessRoutes => { // 根据roles权限生成可访问的路由表(即上面两个方法完成后,动态添加到路由中,并渲染到页面上) router.addRoutes(accessRoutes) // 动态添加可访问路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 }) }).catch(err => { //获取用户信息失败,清除token,跳转到登录页面进行重新登陆操作 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}`) // 否则全部重定向到登录页 //console.log(" default test: " + to.fullPath); NProgress.done() } } }) router.afterEach(() => { //每次请求结束后,进度条结束 NProgress.done() })
这里经在控制台输出语句检测到,首次进入此页面时,是进入到下图所示中放行的:
流程图所示:
以GenerateRouters方法中的const sidebarRoutes = filterAsyncRouter(sdata)
为例,其filterAsyncRouter
方法代码如下所示:
// 遍历后台传来的路由字符串,转换为组件对象 function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { return asyncRouterMap.filter(route => { if (type && route.children) { route.children = filterChildren(route.children) } // 字符串变组件操作 if (route.component) { // Layout ParentView 组件特殊处理 if (route.component === 'Layout') { route.component = Layout } else if (route.component === 'ParentView') { route.component = ParentView } else if (route.component === 'InnerLink') { route.component = InnerLink } else { //若route.component中的字符串与上面三个不匹配则进行下面的操作 route.component = loadView(route.component) } } if (route.children != null && route.children && route.children.length) { route.children = filterAsyncRouter(route.children, route, type) } else { delete route['children'] delete route['redirect'] } return true }) }
上面代码中提到的loadView
方法如下面所示:
此函数可根据传过来的view内容,利用require对${view}进行替换后,形成一个组件
// 箭头函数的使用
export const loadView = (view) => {
if (process.env.NODE_ENV === 'development') {
// 其中require是用于找组件的
return (resolve) => require([`@/views/${view}`], resolve)
} else {
// 使用 import 实现生产环境的路由懒加载
return () => import(`@/views/${view}`)
}
}
// 将箭头函数转化为function函数
function loadView(view){
return function(resolve) {require(['@/views/${view}'],resolve)}
}
已知后台给前台返回数据时,使用的是Jwt给前台生成一个token值。
其中SysLoginService.java中使用的return tokenService.createToken(loginUser);
,调用的下列函数方法:
/** * 创建令牌 * * @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); }
而此处createToken的return中调用的createToken方法如下:
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
因此可知最后使用Jwt来生成token。
在登陆成功后,每一个请求发送到后台,都需要对token进行权限校验,若依框架对于token的校验是用过滤器实现的。(过滤器位于:ruoyi-framework/com/ruoyi/framework/security/filter包中的JwtAuthenticationTokenFilter,代码展示如下)
/** * token过滤器 验证token有效性 * * @author ruoyi */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private TokenService tokenService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { LoginUser loginUser = tokenService.getLoginUser(request); if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) { tokenService.verifyToken(loginUser); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } }
{
从getLoginUser中追踪可发现使用了getToken方法,且对header使用了getHeader方法,(header使在TokenService.java中自定义的令牌标识),也就是对从前端传过来的request的头部信息中取到token值(这个值就是前置拦截器放入的)
然后将token返回getLoginUser方法。
}
{
随后在此方法中进行对token的解析操作
这里获取的uuid正是前面的createToken方法使用String token = IdUtils.fastUUID();
生成的uuid,并将uuid放到Redis中。
然后 String userKey = getTokenKey(uuid);
这里的getTokenKey是把uuid作为参数,拼接成想要的数据模式,然后使用userkey去Redis中拿值及用户的详细信息userLoginUser user = redisCache.getCacheObject(userKey);
。(user值展示:)
}
通过上述操作,会得到LoginUser loginUser对象,随后便是对loginUser用户信息进行其他判断:
首先利用tokenService.verifyToken(loginUser);
来验证token的有效期,然后 UsernamePasswordAuthenticationToken authenticationToken
是对loginUser的token令牌时间进行一个刷新,最后执行chain.doFilter(request, response);
,过滤器放行。
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。