书接上回,上次完成了注册的后端部分,这次从登录开始。
登录的需求分析
登录接口
- 接收参数:用户账户、密码(请求参数很长时,不建议用get请求,因为get请求会将参数拼接在url上,拼接的长度是有限制的)
- 请求类型:POST
- 请求体:JSON格式的数据
- 返回值:用户信息(脱敏处理)
登录逻辑
- 校验用户账户和密码是否合法(这都不对的话,就省去到数据库查询,降低数据库的压力,节省资源)
a. 非空
b.账户长度不小于4位
c.密码不小于八位
d.账户不包含特殊字符 - 校验密码是否输入正确。要和数据库中的密文密码去对比
- 用户信息脱敏(隐藏敏感信息,防止数据库中的字段泄露)
- 用session记录用户的登录态,将其存到服务器上(用后端的Springboot框架自带的服务器tomcat去记录。session是servlet的api,而tomcat内核里用到的就是sevlet,所以是存在tomcat里)然后前端再次访问服务器的时候,就会带着一个由服务器分配的cookie,然后服务器就根据这个cookie找到对应的session,这就实现了记住前端是哪个用户。
- 返回给前端脱敏后的用户信息
网上找的解释(更加完善)
写代码流程
- 先做需求分析
- 然后设计接口
- 写方法实现
- 持续优化!!!!(在写的过程中,对相同代码、常量提取,进而达到复用,这种优化技巧就要平时多看多写)
前后端交互
- 前端通过AJAX(js代码)来给后端发送请求
- JQury是JS的封装库,可以直接调用里面的Ajax方法,只要在页面上引入库就行,这样就不要写原生的ajax
- 此外,axios对ajax进行了封装,使得发送请求更加简单(完成相同的功能写的代码更少了)在页面上引入:
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
- 说回我们这个项目,前端采用的是开箱即用的ant design pro,里面是利用requset发送请求,可以理解request是更高一级的封装。
题外话:可以发现技术的不断迭代就是一个让人越来越懒的过程,代码的封装程度越来越高,框架那更是对基本API的疯狂封装(springmvc对servlet等的封装)。所以学技术可以试着探寻这个技术是如何来的,从什么原生技术封装而来,不要重复造轮子和不会造轮子是两码事。
- 在写前端的时候,追溯request源码:用到了umi的插件、requestConfig是相关的配置(这个在写前端的时候再深入分析)
写登录的后端代码
从UserService接口开始写(因为Controller只负责接受参数,接受完参数给Service层处理业务逻辑)
/**
request 是为了获取session的
*/
User doLogin(String userAccount,String userPassword,HttpServletRequest request)
写登录方法的实现
将光标放在方法名上,ALT+ENTER,选择实现,直接跳到实现类
@Override
public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
//1.校验
if(StringUtils.isAnyBlank(userAccount,userPassword)){
return null;
}
if(userAccount.length()<4){
return null;
}
if(userPassword.length() < 8){
return null;
}
//2.账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if(matcher.find()){
return null;
}
//5.加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//查询用户是否存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount",userAccount);
queryWrapper.eq("userPassword",encryptPassword);
User user = userMapper.selectOne(queryWrapper);
if (user == null){
log.info("user login failed,userAccount cannot match userPassword");
return null;
}
//用户脱敏
User safetyUser = getSafetyUser(user);
//记录用户的登录态
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE,safetyUser);
return safetyUser;
}
@Override
public User getSafetyUser(User originUser){
User safetyuser = new User();
safetyuser.setId(originUser.getId());
safetyuser.setUsername(originUser.getUsername());
safetyuser.setUserAccount(originUser.getUserAccount());
safetyuser.setAvatarUrl(originUser.getAvatarUrl());
safetyuser.setGender(originUser.getGender());
safetyuser.setUserRole(originUser.getUserRole());
safetyuser.setPhone(originUser.getPhone());
safetyuser.setEmail(originUser.getEmail());
safetyuser.setUserStatus(originUser.getUserStatus());
safetyuser.setCreateTime(originUser.getCreateTime());
return safetyuser;
}
以上的代码实现就是相当于翻译之前写的登入逻辑,再一次体现了写代码的流程:先设计,再写代码。
优化代码
提取公共部分
比如注册和登录里面都用到加密的“盐【final String SALT = Kplusone】”,我们就直接提取到成员变量
/**
*盐值,加密密码
**/
private static final String SALT = "Kplusone";
加上日志
虽然我们没有自定义异常(后续再优化),但是可以记录一些日志【日志框架到时候再看楠老师的文档复习一下,现在就简单用一下,用项目需求带动知识点的学习。】
这里用一个Slf4j的注解,这里不需要引入sl4j的依赖,因为之前引入的lombok依赖包含了对sl4j的依赖。
将@Slf4j打在UserServiceImpl上,就可以在这个类中使用log,用log来记录日志,这样后面系统出问题,可以在日志中去查找,类似于监控。
用户脱敏
我们从数据库查到的用户信息包含了很多不能向用户展示的信息,比如:密码、用户更新时间、用户是否被删除(逻辑删除)
,所以专门写了一个用户脱敏的方法。
逻辑删除
在真实项目中,是不能将数据真正的从数据库中删除的,因为万一之后要查呢,所以我们都是使用逻辑删除,专门设计一个“isDelete”的字段,0表示没有删,1表示被删了。所以mybatis-plus底层封装删除方法其实是改这个字段的值,并不会真正执行delete语句,然后mybatis-plus查询的时候也不会查询被逻辑删除的数据。当然这个功能需要我们在application中配置。具体怎么配置可以查mybatis-plus的官方文档,直接搜逻辑删除就行。
登录接口
开始写Controller,要把业务接口封装成处理请求的Controller
Controller层倾向对请求参数本身的校验,不涉及业务逻辑本身(越少越好)
Service层是对业务逻辑的校验,(除了被Controller调用以外,还有可能被其他类调用,比如Service层互相调用)
在Controller包中新建UserController
专门用来处理和用户业务相关的请求
加上@RestController
这个注解有两个作用:
- 表明这个是一个处理前端发送过来的请求的类
- 类返回给前端的数据格式是application.json
加上@RequestMapping
定义这个类是接受那个路径的请求,这个写在类上,就是公共部分
@RequestMapping(“/user”)
介绍一个插件
Auto filling Java Call arguments,自动填充Java的方法参数。安装好重启就能用。
注册请求
将光标放在括号里,按下ALT+ENETR,选择“自动填充参数”,就可以补齐参数
不过这里我们最好将前端发送过来的json格式的参数在后端封装成一个对象
写上注释:用户注册请求参数的封装,然后这里可以实现Serilizable,就是序列化(应该是考虑了网络传输,关于序列化的知识单独出一篇博客)
实现序列化接口,右键选择"生成"->“serialVersionUID”,生成序列化ID,如果没有serialVersionUID,进行下面的设置:
设置完成后,光标放在UserRegisterRequest,ALT+enter,选择“添加序列号字段”。
加上@Data注解,自动生成get\set方法
Controller就使用封装好的请求参数
给参数加上@RequestBody,目的就是告诉SpringMvc将前端传来的JSON参数和UserRegisterRequest关联上。
最后,完善一下检验参数的代码
用同样的方式写登录接口:
测试
不用Postman,因为在idea自带一个测试工具
以Debug的模式启动项目
添加POST请求,把自动生成的范例删掉
可以在想测试的地方打断点。
点击发送请求的按钮
返回结果:
再测试一下逻辑删除,把user表中一条数据的字段改为1,再次运行测试工具,这个时候是没有返回值的,因为现在这条数据已经不存在了,所以登录失败,测试完毕
写用户管理接口(其实就是查询和删除用户)
需求
!!!必须鉴权(这两个接口只能管理员使用)
- 查询用户(允许根据用户名查询)
- 删除用户
如何鉴权
用到User表的一个字段,userRole(0-普通用户,1-管理员)
因为要执行查训和删除,那肯定是已经登录成功了,此时服务器的session里已经保存了用户信息,那么我们在执行查询和删除前先拿到用户的登录状态,从里面获得用户的UserRole的值,进行判断达到鉴权的目的。
这个时候,getAttribute应该取什么呢?之前我们在UserServiceImpl类里面定义了一个用户状态登录键,提取到UserService接口,因为接口里的成员变量默认是public static final.
参数username可以为空,如果是空,那就是查询所有,不为空,那就是查询一部分。
注意:这里前后端数据交互是采用json,所以并不用加从路径上获取参数的注解,并且json格式的参数也不会出现在路经上。
因为是GET请求,所以不用加@RequestBody注解,框架应该会自动转为json.POST请求就应该加上注解。
一边写代码,要一边优化,因为在经验不足的情况下,都是一边做一边优化,等有经验了,就可以有一个提前的预判,写的代码就可以一步到位。
对常量进行优化
新建一个常量包
新建一个UserConstant接口
优化完以后,把之前UserService里的用户登录状态键删掉,然后把引起的相关问题改一改,很简单的。
然后把用到1的地方改为ADMIN_ROLE
删除接口
继续优化,这两个接口鉴权的代码是一样的,可以提取出来,直接贴出最后的代码
//参数username可以为空,如果是空,那就是查询所有,不为空,那就是查询一部分。
//注意:这里前后端数据交互是采用json,所以并不用加从路径上获取参数的注解,并且json格式的参数也不会出现在路经上。
//因为是GET请求,所以不用加@RequestBody注解,框架应该会自动转为json.POST请求就应该加上注解。
@GetMapping("/search")
public List<User> searchUsers(String username,HttpServletRequest request){
if (!isAdmin(request)){
return new ArrayList<User>();
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if(StringUtils.isNotBlank(username)){
queryWrapper.like("username",username);
}
List<User> userList = userService.list(queryWrapper);
return userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
}
@PostMapping("/delete")
public boolean deleteUser(@RequestBody long id,HttpServletRequest request){
if(!isAdmin(request)){
return false;
}
//id一定不会为空,所以不用校验,拿到直接判断
if(id <= 0){
return false;
}
return userService.removeById(id);//这是逻辑删除
}
/**
* 判断是否是管理员
* @param request
* @return
*/
private boolean isAdmin(HttpServletRequest request){
Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
User user = (User) userObj;
if(user == null || user.getUserRole() != UserConstant.ADMIN_ROLE){
return false;
}
return true;
}
添加session的有效期(1天)
Spring:
session:86400
测试鉴权
先去除之前的登录态(刚才测试的)
再以debug的模式启动,找到之前的测试历史
把一条数据的isDelete改为0,userRole改为1,点击上传按钮
再次运行登录(目的是让服务器记住这个用户信息,下面好测试删除和查询)
测试查询,点击左侧的绿色小图标
运行,得到返回值
这里其实还是有问题,没有做用户脱敏
那就来脱敏,反正之前UserService里写登录的时候就已经写过getSafeUser()方法,这里直接调用就行
这里用了函数式编程(这块编程技巧之后专门出一个博客复习一下)
再优化一下,光标放到return,出现黄色灯泡。点击,选择第一个,直接简写,搞定!
在写这个博客时候,埋了很多坑(以后要复习的知识),之后都要给它填上。
到这,后端代码可以停一下了,下一篇写前端了。
参考资料:别人的笔记