从零开始的SpringBoot前后端分离入门级项目(五)

陌路散爱 提交于 2020-10-01 08:21:35

前情回顾

在前一篇博客我们成功的编写了登录与注册的接口,但是还存在着一些问题,这篇博客我们将会更深入的对登录与注册的进行讲解。

数据传输对象(DTO)

我们在登录或者注册时,其实只用到了User对象的某些属性,在访问别的用户时也不该把别的用户的密码等字段给查询出来,这时候如果我们全都使用User对象则会产生部分冗余的字段,这些冗余的部分就如我们前面说过的一样会降低数据传输的效率,所以我们首先从这里入手,创建出不同情形下所需要用到的DTO。

创建登录和注册功能的DTO

UserLoginDTO

用户登录时我们只需要用户输入用户名和密码,所以UserLoginDTO中仅有两个字段:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginDTO {
   
   
    @NotNull(message = "账号不允许为空")
    private String username;
    @NotNull(message = "密码不允许为空")
    private String password;
}

而注册时我们只希望用户去填写用户名、密码、昵称即可完成注册:

UserRegisterDTO

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterDTO {
   
   
    @NotNull(message = "用户名不允许为空")
    private String username;
    @NotNull(message = "密码不允许为空")
    private String password;
    @NotNull(message = "昵称不允许为空")
    private String nickname;
}

完成了两个字段的编写之后我们就要去Controller层以及Service层对我们的代码进行修改,将原本接受的User对象转为相应的DTO对象

修改UserController

	@PostMapping("/register")
    public CommonResult<String> register(@RequestBody UserRegisterDTO urDTO) {
   
   
        userService.userRegister(urDTO);
        return new CommonResult<>(20000, "OK", "注册成功");
    }

    @PostMapping("/login")
    public CommonResult<String> login(@RequestBody UserLoginDTO ulDTO) {
   
   
        String res = userService.userLogin(ulDTO);
        return new CommonResult<>(20000, "OK", res);
    }

修改UserService

    public void userRegister(UserRegisterDTO urDTO) {
   
   
        User user = new User();
        BeanUtils.copyProperties(urDTO,user);
        try {
   
   
            user.setId(idWorker.nextId() + "");
            user.setEnable(1);
            user.setAddtime(DateTimeTransferUtil.getNowTimeStamp());
            userDao.userRegister(user);
        } catch (Exception e) {
   
   
            throw new DuplicateKeyException("用户名重复");
        }
    }

    public String userLogin(UserLoginDTO ulDTO) {
   
   
        User trueUser = userDao.userLogin(ulDTO.getUsername());
        if (trueUser.getPassword().equals(ulDTO.getPassword())){
   
   
            return "登陆成功";
        }
        throw new InternalAuthenticationServiceException("用户名或密码有误");
    }

在userRegister方法中我们使用了BeanUtils.copyProperties方法,这是Spring为我们为我们提供的一个方法,可以帮助我们快速的从一个DTO中读取值注入到一个pojo中,反之亦可,关于该方法可以上网查询相关资料。
那么我们就已经完成了从接收pojo到接收DTO的转换,但是问题仅此而已吗?让我们走进本篇博客的下一个部分–设置用户权限

用户初始权限的设置

在我们之前的博客中我们讲述了本次项目的权限验证主要是通过token实现的,但是我们上次编写的代码中没有在注册时给用户提供初始的权限,也没有在用户完成登录时生成一个token返回给前端,这些内容我们将在本节慢慢讲述。

初始权限的设置

我们需要在用户成功注册之后完成用户初始权限的赋予,所以让我们在UserDao中添加相应的方法完成这个功能。

添加初始权限和查询用户角色

    /**
     * 用户注册时分配用户角色
     *
     * @param uid 用户id
     */
    @Insert("INSERT INTO tb_role(uid,rolename) VALUES(#{uid},default)")
    void allocateUserRole(@Param("uid") String uid);
    
	/**
     * 通过用户id查询用户角色
     *
     * @param id 用户id
     * @return 用户角色
     */
    @Select("SELECT rolename FROM tb_role WHERE uid = #{id}")
    String getUserRole(@Param("id") String id);

在这里我们使用default是因为我们在数据库的tb_role表中设置了默认的角色名是MEMBER

授予初始权限

我们在为UserDao添加了相关方法后把目光投回UserService中:

	public void userRegister(UserRegisterDTO urDTO) {
   
   
        User user = new User();
        BeanUtils.copyProperties(urDTO,user);
        try {
   
   
            user.setId(idWorker.nextId() + "");
            user.setEnable(1);
            user.setAddtime(DateTimeTransferUtil.getNowTimeStamp());
            userDao.userRegister(user);
            userDao.allocateUserRole(user.getId());
        } catch (Exception e) {
   
   
            throw new DuplicateKeyException("用户名重复");
        }
    }

在用户注册之后执行添加用户角色的allocateUserRole()方法,这样就完成了注册时的初始权限赋予。让我们先执行程序使用PostMan进行测试 [先将数据库中同名的数据删除!]:
在这里插入图片描述
提示我们注册成功,前往数据库查看tb_user表和tb_role表,可以看到用户的已经成功注册了并且也分配好了默认的MEMBER角色,但这样真的完成了吗?我们回到UserService里的userRegister方法添加如下代码

//请先将try catch的有关代码注释
userDao.userRegister(user);
//在两行代码中插入下面这一行代码
int i = 1 / 0;
userDao.allocateUserRole(user.getId());

很明显现在会出现错误,让我们运行程序并重新访问注册接口
在这里插入图片描述

这时候我们打开数据库,发现数据库中用户名为test1的用户已经注册成功了,但是因为出现了错误我们没能为它分配一个角色!
在这里插入图片描述
在这里插入图片描述

启用事物管理

这种情况很显然是我们不想出现的,我们希望它要么一起成功,要么一起失败,而不像现在这样成功一半失败一半,这时候我们需要在UserService这个类上添加@Transactional(rollbackForClassName = "Exception.class") 这个来自Spring框架注解可以让我们启用事物功能并在出现指定类型的错误时发生回滚(在这里我们指定任意的异常时都进行回滚)。重启项目,删除刚刚注册的test1,并用同样的数据进行注册,这时候我们就会发现虽然出错了,但是我们两个步骤都没有生效,就完成了要么一起成功,要么一起失败的实现。【测试完成后记得把被注释的TryCatch代码取消注释】
到这里我们就完成了用户初始权限的设置,接下来我们就要改进用户登录的模块了

改进用户登录模块

我们前面说到用户成功登录之后应该使用用户的id和权限名生成一个Token返回给前端,那么我们就来完成这个功能,首先我们要先前往UserDao将userLogin方法进行如下修改:

    /**
     * 通过登录的账号查询是否存在该用户
     *
     * @param loginName 登录名,可能是username也可能是email
     * @return 返回用户密码
     */
    @Select("SELECT id,password FROM tb_user WHERE username = #{loginName}")
    User userLogin(@Param("loginName") String loginName);

然后将UserService修改如下:

    public String userLogin(UserLoginDTO ulDTO) {
   
   
        User trueUser = userDao.userLogin(ulDTO.getUsername());
        if (trueUser.getPassword().equals(ulDTO.getPassword())){
   
   
            return jwtUtil.createJWT(trueUser.getId(),userDao.getUserRole(trueUser.getId()));
        }
        throw new InternalAuthenticationServiceException("用户名或密码有误");
    }

我们按照要求返回了使用用户id和用户角色创建的token,那么我们就在Controller的统一请求返回体里携带上这个Token。

    @PostMapping("/login")
    public CommonResult<String> login(@RequestBody UserLoginDTO ulDTO) {
   
   
        String token = userService.userLogin(ulDTO);
        return new CommonResult<>(20000, "OK", toekn);
    }

那么我们向前端返回token的任务也已经完成,我们改进用户登录模块的工作也告一段落,让我们回看整个注册和登录的功能,目前已经是比较完善的了,但是仍然存在着一些问题,接下来就让我们进一步的改进注册流程。

保护用户的隐私信息!

随着互联网的大发展,每隔一段时间就会出现一些用户密码泄露的新闻,不管是外部的攻击或者是内部工作人员的泄露,我们都应该对此提高警惕,无论是业务逻辑的编写的过程亦或者是用户信息的存储过程我们都要提高警惕,目前项目中用户的密码都是明文存储的,我们应该使用某种加密方法对用户的密码进行加密,这样即使储存于数据库中的用户密码不幸泄露也无法被破解。
那么我们应该怎么进行加密呢?我们可以采用SpringSecurity框架中自带的BCryptPasswordEncoder类对隐私信息进行加密!

关于加密所涉及的算法笔者又不在此赘述,如果比较感兴趣可以访问SpringSecurity的官方文档进行学习与研究

具体的使用方法如下,首先在主启动类中注册这个类:

	@Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
   
   
        return new BCryptPasswordEncoder();
    }

然后在UserService中进行如下修改:
首先通过构造方法注入这个加密类

	private final UserDao userDao;
    private final IdWorker idWorker;
    private final JwtUtil jwtUtil;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    
    public UserService(UserDao userDao, IdWorker idWorker, JwtUtil jwtUtil, BCryptPasswordEncoder bCryptPasswordEncoder) {
   
   
        this.userDao = userDao;
        this.idWorker = idWorker;
        this.jwtUtil = jwtUtil;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

然后在用户注册时对用户的密码进行加密

public void userRegister(UserRegisterDTO urDTO) {
   
   
        User user = new User();
        BeanUtils.copyProperties(urDTO,user);
        try {
   
   
            user.setId(idWorker.nextId() + "");
            user.setEnable(1);
            user.setAddtime(DateTimeTransferUtil.getNowTimeStamp());
            //进行加密
            user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
            userDao.userRegister(user);
            userDao.allocateUserRole(user.getId());
        } catch (Exception e) {
   
   
            throw new DuplicateKeyException("用户名重复");
        }
    }

在用户登录时我们要判断用户的密码是否与数据库中存储的密码相同,既然注册时存储的密码已经被加密了,那我们怎么去判断呢?其实框架的作者也已经想到了这一点,他为我们提供了matches()方法进行判断,如果相同则为true,不同则为false,那我们就对用户登录模块进行修改:

    public String userLogin(UserLoginDTO ulDTO) {
   
   
        User trueUser = userDao.userLogin(ulDTO.getUsername());
        //前面放的是用户输入的密码,后面是加密过的密码
        if (bCryptPasswordEncoder.matches(ulDTO.getPassword(), trueUser.getPassword())) {
   
   
            return jwtUtil.createJWT(trueUser.getId(), userDao.getUserRole(trueUser.getId()));
        }
        throw new InternalAuthenticationServiceException("用户名或密码有误");
    }

那么到这里我们对用户隐私信息的加密也已经完成了,启动项目,让我们来通过postman对两个功能进行测试吧~
在这里插入图片描述
首先对注册接口进行测试(测试前记得删除之前的测试账号!),发现注册成功,来到数据库,我们可以看到用户的密码已经被加密过了!在这里插入图片描述
接下来再让我们测试一下登录接口:
在这里插入图片描述
同样发现登陆成功,并且且成功的拿到了生成的token令牌!有了token令牌前端每次请求时我们就能直接拿到用户的id和权限,对我们后续的业务代码编写提供了强有力的支撑。




到这里本篇博客的内容也基本告一段落,经过这篇博客,我们的登录注册接口已经比较完善了,但是真的就一点问题都没有了吗?请读者自行思考,笔者将在下一篇博客揭晓答案。
本次博客的内容也到此为止了,如果对博客内容有疑问可以私信联系笔者,如果这篇文章对你有用希望你能点一个赞,谢谢~

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!