用户登录

在之前的文章中我们实现了用户注册和验证功能,接下来我们继续实现它的登录,以及登录成功之后要在页面上显示的信息。

接下来,我们来编写代码。


实现service

com.liferunner.service.IUserService接口中添加用户登录方法:

public interface IUserService {
...
/**
* 用户登录
* @param userRequestDTO 请求dto
* @return 登录用户信息
* @throws Exception
*/
Users userLogin(UserRequestDTO userRequestDTO) throws Exception;
}

然后,在com.liferunner.service.impl.UserServiceImpl实现类中实现:

@Service
@Slf4j
public class UserServiceImpl implements IUserService {
...
@Override
public Users userLogin(UserRequestDTO userRequestDTO) throws Exception {
log.info("======用户登录请求:{}", userRequestDTO);
Example example = new Example(Users.class);
val condition = example.createCriteria();
condition.andEqualTo("username", userRequestDTO.getUsername());
condition.andEqualTo("password", MD5GeneratorTools.getMD5Str(userRequestDTO.getPassword()));
val user = this.usersMapper.selectOneByExample(example);
log.info("======用户登录处理结果:{}", user);
return user;
}
}

Error Tips:

这里有一个小小的坑点,大家一定要注意,在使用selectOneByExample()查询的时候,该方法传入的参数一定注意是tk.mybatis.mapper.entity.Example实例,而不是tk.mybatis.mapper.entity.Example.Criteria,否则会报动态SQL生成查询错误,信息如下:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'distinct' in 'class tk.mybatis.mapper.entity.Example$Criteria'
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:92)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
at com.sun.proxy.$Proxy106.selectOne(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:159)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:93)
at com.sun.proxy.$Proxy109.selectOneByExample(Unknown Source)
at com.liferunner.service.impl.UserServiceImpl.userLogin(UserServiceImpl.java:80)
...

新人在写代码的时候,特别容易在上一行写了查询变量,下一行就直接开用了,越是简单的错误越是让人无从下手。

实现Controller

@RestController
@RequestMapping(value = "/users")
@Slf4j
@Api(tags = "用户管理")
public class UserController {
...
@ApiOperation(value = "用户登录", notes = "用户登录接口")
@PostMapping("/login")
public JsonResponse userLogin(@RequestBody UserRequestDTO userRequestDTO,
HttpServletRequest request,
HttpServletResponse response) {
try {
if (StringUtils.isBlank(userRequestDTO.getUsername()))
return JsonResponse.errorMsg("用户名不能为空");
if (StringUtils.isBlank(userRequestDTO.getPassword()) ||
userRequestDTO.getPassword().length() < 8) {
return JsonResponse.errorMsg("密码为空或长度小于8位");
}
val user = this.userService.userLogin(userRequestDTO);
UserResponseDTO userResponseDTO = new UserResponseDTO();
BeanUtils.copyProperties(user, userResponseDTO);
log.info("BeanUtils copy object {}", userResponseDTO);
if (null != userResponseDTO) {
// 设置前端存储的cookie信息
CookieTools.setCookie(request, response, "user",
JSON.toJSONString(userResponseDTO), true);
return JsonResponse.ok(userResponseDTO);
}
} catch (Exception e) {
e.printStackTrace();
log.error("用户登录失败,{},exception = {}", userRequestDTO, e.getMessage());
}
return JsonResponse.errorMsg("用户登录失败");
}
}

在上面的代码中,基本校验问题就不再赘述,我们主要关注几点新的特性信息:

  • com.liferunner.dto.UserResponseDTO 将我们需要展示给前端的数据封装为一个新的返回对象,我们从数据库中查询出来的Userspojo包含用户的所有数据,比如其中的passwordmobile等等一些用户私密的数据是不应该展示给前端的,即便要展示,那也是需要经过脱敏以及加密。因此,常见的做法就是封装一个新的返回对象,其中只需要包含前端需要的数据字段就可以了。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel(value = "用户信息返回DTO", description = "用户登录成功后需要的返回对象")
public class UserResponseDTO {
/**
* 主键id
*/
private String id; /**
* 用户名
*/
private String username; /**
* 昵称 昵称
*/
private String nickname; /**
* 头像
*/
private String face; /**
* 性别 1:男 0:女 2:保密
*/
private Integer sex;
}

在这里建议大家使用Ctrl+C我们的com.liferunner.pojo.Users对象,然后删除掉我们不需要的字段就可以了,为什么这么建议呢,是因为下一个好处啦。

  • org.springframework.beans.BeanUtils.copyProperties(user, userResponseDTO);

    大家可以看到,这里直接使用的是Spring BeanUtils工具类进行的值拷贝,就减少了我们循环遍历每一个字段去挨个赋值(SetValue)的工作。(也是一种偷懒小技巧哦,这样是不对的~)

  • CookieTools.setCookie();

    之前我们有提过,一般情况下,我们用户登录之后,数据都会被存储在本地浏览器Cookie中,比如我登录的baidu.com:



    此时,鼠标在图片中左侧的Cookies => www.baidu.com右键clear,然后再次刷新我们当前界面,效果如下:



    我们可以看到,从登录状态已经变为退出状态了,并且Cookies中的内容也少了很多,这就说明,百度是把我们的用户登录信息加密后存储在了浏览器cookie中。

    大家可以查看京东,淘宝等等,也是基于这种方式实现的,开篇之初就说过,我们的系统是基于生产来实现的demo,那么我们就是用主流的实现方法来做。当然,有的同学会说,这个应该我们把数据传递给前端,让前端来实现的!!!当然,你说的对,可是我们掌握一种实现方式,对于我们个人而言应该是没有坏处的吧?

    这里就需要一个工具类了,大家可以在github传送门来下载相关代码。目录com.liferunner.utils.CookieTools.

  • com.alibaba.fastjson.JSON.toJSONString(userResponseDTO)

    因为我们要返回的是一个对象,但是cookie中我们需要放入的是String,这里我们引入了alibaba的JSON工具,在mscx-shop-common/pom.xml,加入依赖:

        <dependencies>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.56</version>
    </dependency>
    </dependencies>

用户登出

在用户操作结束之后,我们需要将用户从系统中退出登录,因为我们的用户登录信息会存储在浏览器cookie中,因此,我们需要根据用户的登出操作来删除相关用户缓存:

    @ApiOperation(value = "用户登出",notes = "用户登出",httpMethod = "POST")
@PostMapping("/logout")
public JsonResponse userLogout(@RequestParam String uid,
HttpServletRequest request,HttpServletResponse response){
// clear front's user cookies
CookieTools.deleteCookie(request,response,"user");
// return operational result
return JsonResponse.ok();
}

开发调试小福利

java日志追踪

一般在电商场景中,对于请求的响应时间有着极其严格的要求,比如你在一个网站买商品的时候,如果每点击一次按钮都要等待,或者系统感觉卡顿一下,你会毫不犹豫的选择右上角的小红叉,把它干掉。因此,在我们系统的开发过程中,很多时候需要对我们的请求响应时间进行监控,甚至会通过压力测试来进行测试。但是,让我们在每一个方法中都做这种请求的实现,显然是不合理甚至说是让开发人员难受的,所以,我们来实现一种通用的做法,那就是通过AOP,面向切面来实现。关于切面的基本使用,大家可以参考AOP传送门,接下来,开始我们的编码。

根据springboot实现功能三部曲:

setp 1. 添加依赖

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

step 2. 启动配置(没有就忽略掉这一步)

setp 3. 加注解

在我们的mscx-shop-api项目中,创建com.liferunner.api.aspectpackage,然后创建com.liferunner.api.aspect.CommonLogAspect,代码如下:

package com.liferunner.api.aspect;

import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component; import java.util.Date; /**
* CommonLogAspect for : AOP切面实现日志确认
*
* @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
* @since 2019/11/11
*/
@Component
@Aspect
@Slf4j
public class CommonLogAspect { @Around("execution(* com.liferunner.api.controller..*.*(..))")
public void recordLogTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("----------- {}.{} process log time started.---------------",
proceedingJoinPoint.getTarget().getClass(),
proceedingJoinPoint.getSignature().getName()); val startTime = System.currentTimeMillis();
proceedingJoinPoint.proceed();
val afterTime = System.currentTimeMillis();
if (afterTime - startTime > 1000) {
log.warn("cost : {}", afterTime - startTime);
} else {
log.info("cost : {}", afterTime - startTime);
} log.info("----------- {}.{} process log time ended.---------------",
proceedingJoinPoint.getSourceLocation().getClass(),
proceedingJoinPoint.getSignature().getName());
}
}
  • 第一行日志代表我们想要监控的是哪个类的哪个方法
  • proceedingJoinPoint.proceed();表示方法执行
  • 当请求查过1000ms的时候,我们使用log.warn(...)进行日志告警

step 4. 效果演示

  • 查询用户耗时

  • 注册用户耗时

从上图,我们明显能看出来我们每一次的请求耗时,之后就可以针对性的对每一个方法进行优化!!!

sql日志追踪

在我们开发的过程中,往往会遇到针对数据库的CRUD的操作,但是,因为我们使用了mybatis 动态生成了简单的SQL查询,而不是手动编写的,比如我们在UserServiceImpl.java中实现的用户查询以及用户注册代码中的tk.mybatis.mapper.entity.Example 以及 this.usersMapper.insertSelective(user);


public Users findUserByUserName(String username) {
// 构建查询条件
Example example = new Example(Users.class);
val condition = example.createCriteria()
.andEqualTo("username", username);
return this.usersMapper.selectOneByExample(example);
} @Transactional(propagation = Propagation.REQUIRED)
@Override
public Users createUser(UserRequestDTO userRequestDTO) throws Exception {
log.info("======begin create user : {}=======", userRequestDTO);
val user = Users.builder()
.id(sid.next()) //生成分布式id
.username(userRequestDTO.getUsername())
.password(MD5GeneratorTools.getMD5Str(userRequestDTO.getPassword()))
.birthday(DateUtils.parseDate("1970-01-01", "yyyy-MM-dd"))
.nickname(userRequestDTO.getUsername())
.face(this.FACE_IMG)
.sex(SexEnum.secret.type)
.createdTime(new Date())
.updatedTime(new Date())
.build();
this.usersMapper.insertSelective(user);
log.info("======end create user : {}=======", userRequestDTO);
return user;
}

一旦遇到了问题之后,我们往往不知道到底是哪里出现了错误,这个时候我们的SQL是否有问题我们也不知道,因此,接下来我们来配置一种可以让我们到SQL的小实现:

1.设置日志配置(如图)



2.修改mybatis配置(log-impl: org.apache.ibatis.logging.stdout.StdOutImpl



3.SELECT效果演示



4.INSERT效果演示



从上图可以看出控制台JDBC操作进行了2次,其实第一次是对我们的用户名进行校验。第二次INSERT是真实的插入。

通过上面的演示结果,大家可以想到,这个日志针在我们日常的开发中解决问题是非常有必要的。但是一定记得,在上生产的时候,日志一定要关闭,否则数据量一旦大了之后,会对系统的性能造成严重伤害!!!

源码下载

Github 传送门

Gitee 传送门

下节预告


下一节我们将继续开发我们电商的核心部分-商品以及广告的展示,在过程中使用到的任何开发组件,我都会通过专门的一节来进行介绍的,兄弟们末慌!

gogogo!

[springboot 开发单体web shop] 5. 用户登录及首页展示的更多相关文章

  1. [springboot 开发单体web shop] 4. Swagger生成Javadoc

    Swagger生成JavaDoc 在日常的工作中,特别是现在前后端分离模式之下,接口的提供造成了我们前后端开发人员的沟通 成本大量提升,因为沟通不到位,不及时而造成的[撕币]事件都成了日常工作.特别是 ...

  2. [springboot 开发单体web shop] 1. 前言介绍和环境搭建

    前言介绍和环境搭建 简述 springboot 本身是为了做服务化用的,我们为什么要反其道使用它来开发一份单体web应用呢? 在我们现实的开发工作中,还有大量的业务系统使用的是单体应用,特别是对于中小 ...

  3. [springboot 开发单体web shop] 6. 商品分类和轮播广告展示

    商品分类&轮播广告 因最近又被困在了OSGI技术POC,更新进度有点慢,希望大家不要怪罪哦. 上节 我们实现了登录之后前端的展示,如: 接着,我们来实现左侧分类栏目的功能. ## 商品分类|P ...

  4. [springboot 开发单体web shop] 7. 多种形式提供商品列表

    上文回顾 上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示. 需求分析 首先,在我们开始本节编码之前,我们先来分析一下都 ...

  5. [springboot 开发单体web shop] 2. Mybatis Generator 生成common mapper

    Mybatis Generator tool 在我们开启一个新项目的研发后,通常要编写很多的entity/pojo/dto/mapper/dao..., 大多研发兄弟们都会抱怨,为什么我要重复写CRU ...

  6. [springboot 开发单体web shop] 3. 用户注册实现

    目录 用户注册 ## 创建数据库 ## 生成UserMapper ## 编写业务逻辑 ## 编写user service UserServiceImpl#findUserByUserName 说明 U ...

  7. [springboot 开发单体web shop] 8. 商品详情&评价展示

    上文回顾 上节 我们实现了根据搜索关键词查询商品列表和根据商品分类查询,并且使用到了mybatis-pagehelper插件,讲解了如何使用插件来帮助我们快速实现分页数据查询.本文我们将继续开发商品详 ...

  8. 开发单体web shop] 6. 商品分类和轮播广告展示

    目录 商品分类&轮播广告 商品分类|ProductCategory 需求分析 开发梳理 编码实现 轮播广告|SlideAD 需求分析 开发梳理 编码实现 福利讲解 源码下载 下节预告 商品分类 ...

  9. 实现Web上的用户登录功能

    关于如何实现web上的自动登录功能 文章来源http://coolshell.cn/articles/5353.html Web上的用户登录功能应该是最基本的功能了,可是在我看过一些站点的用户登录功能 ...

随机推荐

  1. 开发一个带UI的库(asp.net core 3.0)

    在GitHub上有个项目,本来是作为自己研究学习.net core的Demo,没想到很多同学在看,还给了很多星,所以觉得应该升成3.0,整理一下,写成博分享给学习.net core的同学们. 项目名称 ...

  2. Vue中使用key的作用

    key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度 key具有唯一性 vue中循环需加 :key=“唯一标识” ,唯一标识可以使item里面id index 等,因为vue组 ...

  3. numpy.rollaxis函数

    numpy.rollaxis numpy.rollaxis 函数向后滚动特定的轴到一个特定位置,格式如下: numpy.rollaxis(arr, axis, start) 参数说明: arr:数组 ...

  4. Android Studio:多包名打包

    来自:http://m.blog.csdn.net/u011315960/article/details/73251196 前言 最近有点小忙,博客都落下了,今天赶紧写点东西补上. 前几天商务找我,想 ...

  5. netty源码解解析(4.0)-25 ByteBuf内存池:PoolArena-PoolChunk

    PoolArena实现了用于高效分配和释放内存,并尽可能减少内存碎片的内存池,这个内存管理实现使用PageRun/PoolSubpage算法.分析代码之前,先熟悉一些重要的概念: page: 页,一个 ...

  6. 高通电源管理函数的power_supply的调用关系

    以msm8909为例,高通的主要文件有几个: qpnp-linear-charger.c(线性充电器) qpnp-vm-bms.c(BMS管理) power_supply_core.c(power_s ...

  7. 提权案例(一)渗透某asp.net网站通过sql server数据库public 提权 思路分享

    先罗列出sql server 角色用户的权限 按照从最低级别角色(bulkadmin)到最高级别角色(sysadmin)的顺序进行描述:1.bulkadmin:这个角色可以运行BULK INSERT语 ...

  8. Python编程系列---使用装饰器传参+字典实现动态路由

    # 实现一个空路由表,利用装饰器将url和功能函数的对应关系自动存到这个字典中 router_dict = {} # 定义一个装饰器 # 再给一层函数定义,用来传入一个参数,这个参数就是访问的页面地址 ...

  9. python学习-类的继承

    1.继承的语法 2.多继承 3.override(子类重写父类的方法) 4.子类调用父类中被重写的实例方法 5.使用super函数调用父类的构造方法

  10. Linux Centos7 基于Docker 搭建 Nexus私服搭建

    创建Blob Stores[本地文件存储目录,统一管理] 1.设置名称和工作路径: ps[注意事项]: 1.storage name:自定义名称 2.storage path:存储路径,默认[/nex ...