接着Spring Boot&Vue3前后端分离实战wiki知识库系统<十二>--用户管理&单点登录开发一继续往下。
登录功能开发:
接下来则来开发用户的登录功能,先准备后端的接口。
后端增加登录接口:
1、UserLoginReq:
先来准备用户登录的请求实体:
package com.cexo.wiki.req;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
public class UserLoginReq {
@NotEmpty(message = "【用户名】不能为空")
private String loginName;
@NotEmpty(message = "【密码】不能为空")
// @Length(min = 6, max = 20, message = "【密码】6~20位")
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】规则不正确")
private String password;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", loginName=").append(loginName);
sb.append(", password=").append(password);
sb.append("]");
return sb.toString();
}
}
对于用户登录,只需要用户名和密码既可,其中有一个小细节需要说明一下,就是对于之前后台实现的用户管理保存的实体其密码是做了非常详细的规则提示的:
而这里对于用户登录的密码规则校验,则不能提示这么详情了,因为提示这么详情容易被人根据规则来进行密码破解,所以这里就提示了一个比较模糊的校验提示:
2、UserLoginResp:
再来准备登录的response实体:
package com.cexo.wiki.resp;
public class UserLoginResp {
private Long id;
private String loginName;
private String name;
private String token;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public String toString() {
return "UserLoginResp{" +
"id=" + id +
", loginName='" + loginName + '\'' +
", name='" + name + '\'' +
", token='" + token + '\'' +
'}';
}
}
3、Controller增加登录接口:
4、Service实现登录逻辑:
那接下来咱们是直接来校验用户名和密码的正确性么?其实应该先校验用户名是否存在,如果不存在,则密码都没密要校验了,所以先来查一下:
而处理异常可以采用在之前用户管理保存时的方式,抛出异常:
这里咱们先定义一个用户名不存在的异常码:
这里提示语貌似写错了吧。。其实这里是故意写一个模糊的提示的,因为给用户提示得越详细,越容易被攻击,所以这种风险意识在做项目时是很重要的。好,接下来就可以抛出这个异常了:
接下来再来判断密码的正确性:
最后,还有一个细节,就是目前这两块给用户提示了一个一模一样的提示:
很明显这对于后端排错不太好,所以有必要针对这俩加点日志:
前端增加登录模态框:
效果:
先来看一下登录的效果:
实现:
1、增加登录入口:
接下来则先在前端页面中增加一个用户登录的入口,这里很明显是应该在头部区域加,也就是:
此时的效果:
另外这里既然改了头部区域,左上角这块的logo也顺带改一下,目前这里是一个色块,没有什么实际意义,这里改一下:
控制它的css在这:
这里将这个logo的css先去掉,在头部组件里来定义就成:
然后修改为:
此时效果:
2、 点击弹出模态登录窗口:
先来给登录这个a标签增加点击事件:
而模态框的处理在之前的页面中已经多次用到了,所以这里就不过多解释了,直接准备一个登录的模态框:
其中提交的时候得请求接口,需要耗时,交互上肯定需要有loading等待效果,所以先提前把响应式的变量定义好:
此时运行看一下:
3、登录点击事件添加:
接下来给模拟框增加一个提交事件:
这里为了使用方便,给表单输一个默认值,如下:
此时运行:
4、发起登录请求:
最后发起登录请求,这块的网络请求代码也没啥好说的,直接copy原来的逻辑改下接口就可以了,如下:
其中需要导两个组件:
好,此时运行看一下效果:
成功登录。
登录成功处理并集成vuex:
目前我们已经登录成功,但是登录成功之后,其实就给了一个提示,没有做登录成功之后的逻辑处理,接下来咱们则来完善它。
后端保存用户信息:
1、集成Redis:
由于需要将用户登录成功的信息以token形式存到Redis当中,所以先在工程中集成它:
而关于Redis的环境的配置这里就不说明了,当时在https://www.cnblogs.com/webor2006/p/14999543.html这篇已经详细学习过了,这块网上资料也比较多,我本机装好之后,先使用此命令启动一下它:
redis-server
然后再用可视化的图形工具RDM连接看一下:
目前里面没有存任何信息,所以数量都是0,然后再配置一下redis的数据源,类似于mysql一样:
2、登录时生成token并存入Redis中:
此时我们就需要修改一下登录的逻辑时,在登录成功之后,需要生成一个token,这里就用之前使用的雪花算法来生成,如下:
此时则需要将token存入到Redis当中,此时需要这么来做:
其中有个细节需要注意,我们目前存放到Redis中是将实体转换成了一个json串了:
如果想直接保存实体,注意需要让实体实现序列化的接口,不然保存的时候会报如下异常:
另外此token信息也需要返回给前端,因为之后前端也需要将此token信息在请求时携带上进行身份的识别,所以:
其中为了调试方便,将这块加个日志输出:
3、测试:
接下来咱们来登录测试一下是否写入成功了:
显示是成功了,到RDM可视化工具中刷新查看一下:
貌似是有了,不过看了是一串乱码,为了验证这个token写入到Redis是没有问题的,下面在TestController中增加测试方法,如下:
然后咱们就可以这样来测试了:
咱们运行一下:
那对于这里的登录时,生成这么一个token:
此时借助这个测试能查出存在redis中的用户信息么?
木问题。
前端显示登录用户:
接下来在用户登录成功之后,则需要显示出用户的昵称,修改页面:
其中user是一个响应式的变量,定义一下:
另外由于数据库中用户test的密码是test123,所以这里改一下:
然后在登录成功之后则给它进行赋值,如下:
接下来在登录入口处增加显示与隐藏的控制:
接下来运行看一下:
集成vuex:
说明:
接下来说明一个问题,就是目前我们登录后信息是在头部组件中来保存的:
那如果我想在底部组件里也能拿到用户的登录信息该怎么办呢?
很明显是拿不到的,因为定义的头部的响应式变量不能在其它页面使用,此时就得声明一个全局的变量来进行存储,这里就需要使用到vuex了,度娘先了了解一下它:
哦,就是来解决多组件数据通信的问题的,刚好符合我们目前的使用场景,而使用它的位置就在这块:
接下来咱们则来使用一下它。
实现:
1、定义一个全局的变量:
2、定义操作变量的函数:
光定义变量还不行,得定义操作它的行为,这里则会涉及到如下两处:
其中mutations是同步函数,而actions是支持异步的,这里我们在mutations中来进行定义:
3、返回store:
最后需要将这个store返回出去,以便可以在其它界面进行使用:
4、使用store保存用户信息:
接下来我们回到头部组件中,在登录成功之后使用store来保存一下用户信息,写法如下:
5、底部组件中获取用户信息:
接下来咱们就可以使用这个全局的数据了:
其中computed的含义是:如果一个响应式变量是要根据某个变量的变化而计算得来,就可以使用computed。
其中这里有个代码可以简化一下,就是它:
6、运行:
所以vuex其实就是全局响应式的变量,单纯的组件里面的响应式变量是做不到这种效果的。
最后更改一个文案:
而它应该受登录状态的控制,所以加一个显隐逻辑:
再来运行一下:
使用sessionStorage解决刷新数据丢失的问题:
说明:
接下来再来看一个问题:
刷新之后,登录态就没有了,这也反应出vuex有问题,浏览器一刷新就没有了,而要解决这个问题就需要使用sessionStorage了,度娘又来了解一下它:
其中标红的是一些关于它值得关注的,它会随着浏览器页面的存在而存在,如果关闭页面了则数据就不存在了,在搜它的概念时,还搜到了一个在前端经常会提及的话题https://blog.csdn.net/weixin_45541388/article/details/125367823:
关于这块自己网上了解一下既可。
实现:
1、拷一个工具类:
为了使用sessionStorage,拷一个工具类进来:
SessionStorage = {
get: function (key) {
var v = sessionStorage.getItem(key);
if (v && typeof (v) !== "undefined" && v !== "undefined") {
return JSON.parse(v);
}
},
set: function (key, data) {
sessionStorage.setItem(key, JSON.stringify(data));
},
remove: function (key) {
sessionStorage.removeItem(key);
},
clearAll: function () {
sessionStorage.clear();
}
};
为啥要封装一下呢,因为默认的sessionStorage只支持存string,这里扩展保存一个object类型,其实也就是最终会将object转换成Json字符串再保存:
2、引入工具类:
3、使用sessionStorage:
接下来我们在保存user信息时,除了使用vuex保存在全局响应式的变量当中之外,还需要保存到sessionStorage,所以好的做法就是在vuex这个文件中进行处理,先来引入工具类,引入的方法还记得不,其实在之前md5工具方法时已经使用过了,回忆一下:
所以定义一下:
其中let后面的名称取的是js中定义的它:
然后在获取用户信息时加上从sessionStorage来取的逻辑:
另外在设置user信息时,则需要将其存到SessionStorage当中:
其中有个代码可以优化一下:
都是常量,可以将其提取一下,提高维护性:
接下来运行看一下:
其中可以看到footer下的用户登录信息在浏览器刷新之后是能够被保留的,而关闭浏览器当前窗口就清空了,符合SessionStorage的定义场景,只是右上角的header用户信息在浏览器刷新时没有保留住,是因为header页面的user变量没有改成computed,如footer页面报写的这样:
所以咱们将header的代码改成这样:
将其改为:
这样就会去监听store中user的变化,不过这样改之后有个报错:
这里就没必要了,直接将user保存到store中就可以了:
接下来运行看一下效果:
成功修复浏览器刷新用户信息被清空的问题。
增加退出登录功能:
1、UserController.logout():
对于退出登录的接口其实只需要传一个token参数既可,所以定义接口如下:
其实也就是根据token将redis对应的信息给删除既可。
2、前端增加退出按钮:
而通常退出操作是需要给用户一个确认提示的,所以在它外层再包装一层:
3、调用退出登录接口:
接下来咱们就可以处理退出登录的点击事件,如下:
4、运行:
接下来运行看一下效果,发现后台报错了:
这是因为登出接口是一个get请求,而我们在后台声明的是:
修改一下:
再运行:
这里貌似界面上看着有点别扭,就是退出登录一般是在用户登录信息的右侧的,现在在左侧:
更改一下顺序:
因为login-menu的样式是:
最上面的元素就在最右侧边了,最后的样子:
增加登录校验:
有了用户登录之后, 对于页面中的功能则需要进行登录的校验了,对于管理类的入口都是需要登录之后才能预览,而对于文章查看是不需要的,所以,接下来进行登录校验的处理。
后端增加拦截器,校验token有效性:
1、 新建一个拦截器LoginInterceptor:
对于拦截器在之前已经有使用过了:
同样的,对于这个登录拦截器写法也差不多,关于这块的代码就不过多解释了,比较好理解:
package com.jiawa.wiki.interceptor;
import com.alibaba.fastjson.JSON;
import com.jiawa.wiki.resp.UserLoginResp;
import com.jiawa.wiki.util.LoginUserContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);
@Resource
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LoginInterceptor 开始 -------------");
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
// OPTIONS请求不做校验,
// 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
if (request.getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
String path = request.getRequestURL().toString();
LOG.info("接口登录拦截:,path:{}", path);
//获取header的token参数
String token = request.getHeader("token");
LOG.info("登录校验开始,token:{}", token);
if (token == null || token.isEmpty()) {
LOG.info("token为空,请求被拦截");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
Object object = redisTemplate.opsForValue().get(token);
if (object == null) {
LOG.warn("token无效,请求被拦截");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
LOG.info("已登录:{}", object);return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// LOG.info("LogInterceptor 结束");
}
}
逻辑简单来说的话就是先来判断token是否为空,如果不为空,则再到redis中来获取一下。其中校验不通过时,返回一个401:
2、配置拦截器:
接下来要想拦截器生效,则需要到这来配置一下:
然后接下来得进行一下配置,因为有些接口是需要进行登录拦截的,如下:
package com.cexo.wiki.config;
import com.cexo.wiki.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
// @Resource
// LogInterceptor logInterceptor;//配置拦截器
@Resource
LoginInterceptor loginInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(logInterceptor)
// .addPathPatterns("/**");
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/test/**",
"/redis/**",
"/user/login",
"/category/all",
"/ebook/list",
"/doc/all/**",
"/doc/vote/**",
"/doc/find-content/**",
"/ebook-snapshot/**"
);
}
}
其中这里用到了“**”:
则表示是任意值,好,加了拦截器之后咱们再来访问一下页面:
首页查看电子书都正常,但是点击用户管理时,页面的数据加载不出来了,查看一下后台日志,其实就是这个登录拦截器给拦了:
前端请求增加token参数:
目前后端加了token校验之后,在前端则需要在请求头中增加token信息了,很显然也需要在统一接口拦截器中来进行处理,那vue中是如何处理的呢?其实使用的网络组件axios就有相应的处理办法,这块其实在之前https://www.cnblogs.com/webor2006/p/17182186.html已经使用过了:
所以在这里加上token信息的逻辑:
此时再运行你会发现用户管理模块在登录之后内容还是出不来,这里就有一个坑需要说明一下,就是这块:
但是在用户登录时往Redis写入时:
这样类型就不匹配了,所以需要这样改一下:
此时再运行看一下效果:
可以看到在登录之后再查询用户管理的列表就出来了,可见现在的token的校验已经是好使了。
前端页面增加登录校验:
未登录时,管理菜单要隐藏:
现在对于管理界面在用户没有登录时也能被看到,很显然是不合理的,所以接下来处理一下它:
根据之前的经验,对于元素的显隐可以用它:
挺简单,但是你运行发现不好使,反而是把这个v-show加到<router-link>中可以:
但是还是有瑕疵,就是隐藏时这个会占用宽度:
此时就需要自己来写css样式了,如下处理:
解释一下:
而如果没登录,则需要将元素给隐藏,注意这里的style里面是需要定义一个json的,好,再运行看一下:
对路由做判断,防止用户通过手敲url访问管理页面:
现在还有个问题,就是虽说已经在登录菜单上已经根据登录状态进行显隐控制了,但是用户还是可以直接通过管理的路由地址来访问到,比如:
所以接下来处理它。
1、路由中增加meta信息:
为了要处理需要登录的路由地址,这里需要在路由配置中增加一个meta信息,如下:
2、路由登录拦截:
接下来咱们则需要进行路由跳转的一个拦截处理,这块的写法也比较固定,写一次就明白了:
然后接下来就是要判断路由地址中有木有这个mate信息,如下:
而条件体里面的写法如下:
3、运行:
下面来运行看一下效果:
成功进行路由的登录拦截了。
总结:
好了,花了很长的篇幅就将用户登录相关的给学习完了,还是收获很多的,比如密码的双层加层,登录的拦截等等,下次继续。
关注个人公众号,获得实时推送