用户登录相关

1.1. 环境准备

准备一张表tb_user

列名 类型
id 用户表主键,非空且唯一
username 用户名,非空且唯一
password 密码,非空
status 状态。 0禁用,1启用

建表语句:

CREATE TABLE user (
`id`  int NOT NULL AUTO_INCREMENT ,
`username`  varchar(50) unique NOT NULL ,
`password`  varchar(100) NOT NULL ,
`status`  varchar(100)  ,
PRIMARY KEY (`id`)
);

1.2. 创建springboot

略,创建工程,引入依赖,设置配置文件。

1.3. MD5增加密文的安全性

MD5是一种数字摘要算法

同一个原文,通过md5计算出来的内容,结果是一致的,

我们是无法通过密文还原出原来的内容的

原文不同,密文肯定不同

1.3.1. MD5简单实现

String password = "PASSWORD";
String md5Pass = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
System.out.println(md5Pass);

1.3.2. 什么是加密解密

加密就是把原文变成密文
解密就是把加密过的密文变成明文

1.3.3. 什么是数字摘要算法

将任意长度的消息变成固定长度的短消息

1.3.4. 加密和非对称加密的特点是什么

对称加密是指,加密和解密使用同一个密钥。特点是算法公开,加解密速度快,适合对大数据进行加密

非对称加密中,分为公钥和私钥。公钥用来加密,私钥用来解密。公钥是公开的,任何人都可以使用。特点是速度慢,只适合加密少量数据

1.3.5. 常见的对称和非对称加密算法

对称加密算法:DES、3DES、AES
非对称加密算法:RES,ECC

1.3.6. md5数字摘要算法的执行过程(不用去考虑数学运算)

MD5算法用于生成一个128位(16字节)的摘要值,通常用于验证数据完整性和唯一性。

1. 填充(Padding)

首先,将需要进行摘要的数据按照512位(64字节)为一块进行分组。如果最后一块不足512位,则需要填充剩余的位数,使其正好为512位。

填充的方法是在数据末尾添加一个1,后面全部补0,接着在后面写入原始信息长度与2^64的模,最终达到512位。

2. 初始值(Initial Values)

MD5算法使用四个32位的初始值,作为压缩函数中循环运算的变量,这四个值分别为A,B,C,D,其值的由连续的十六进制数字转换成的。

A=0x67452301,B=0xefcdab89,C=0x98badcfe,D=0x10325476

3. 循环运算(Loop Operation)

在每个512位的数据块上进行循环运算,每个512位数据块被划分成16个32位的小块,然后使用压缩函数进行四轮循环运算,每轮运算都会改变A,B,C,D四个中间变量的值,最后生成该块的128位摘要值。

4. 合并(Merge)

最终,所有512位数据块的结果被合并,得到128位的摘要值,该摘要值可以用于验证数据是否篡改。
总结来说,MD5数字摘要算法的执行过程涉及到填充、初始值、循环运算和合并四个步骤,通过将数据分块、进行四轮循环运算,并合并所有数据块的结果,最终生成128位的摘要值。

1.3.7. md5加盐是什么?解决了什么问题?

在原文的固定位置加入一个或多个字符串,再对原文进行加密操作,从而避免哈希碰撞
盐一般是随机生成的,存放在用户表中。把盐和明文密码按照自己定义的规则组合,之后再计算哈希值,就可以得到最终的密文。

1.3.8. SHA1和SHA256算法是什么

也是两种数字摘要算法,不过SHA1的摘要长度比MD5长,SHA256比SHA1长。安全性越来预高,消耗的时间也越来越多。

1.3.9. 代码实现SHA256

我们以一个单元测试中的代码为例

@Test
// 使用sha256算法
public void sha256Test() {
    // 原文,需要被加密的部分
    String password = "PASSWORD";
    // 密文
    String encodestr = "";

    MessageDigest messageDigest;
    try {
        // 设置使用什么算法
        messageDigest = MessageDigest.getInstance("SHA-256");
        // 明文转字符数组,设置明文
        messageDigest.update(password.getBytes("UTF-8"));
        // 获取加密后的字节数组
        byte[] digest = messageDigest.digest();
        // 把字节数组转换为16进制字符串
        encodestr = byte2Hex(digest);
    } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    System.out.println(encodestr);
}

// byte转16进制字符串
private static String byte2Hex(byte[] bytes) {
    StringBuffer stringBuffer = new StringBuffer();
    String temp = null;
    for (int i = 0; i < bytes.length; i++) {
        temp = Integer.toHexString(bytes[i] & 0xFF);
        if (temp.length() == 1) {
            //1得到一位的进行补0操作
            stringBuffer.append("0");
        }
        stringBuffer.append(temp);
    }
    return stringBuffer.toString();
}

1.3.10. 在SQL语句中计算MD5值

我们可以使用mysql中自带的函数帮我们计算md5的值,即

md5(值)

以创建新用户为例子,如果我们只需要对用户的密码进行md5加密,则可以使用如下的方式

insert into user(username,password) values ("tom",md5("123"));

最终插入数据库中的数据,password字段的内容就是 123 经过md5加密后得到的值

1.4. 登录功能

1.4.1. 接口说明

请求路径:/login

请求方式:POST

接口描述:该接口用于员工登录Tlias智能学习辅助系统

参数格式:application/json

参数说明:

名称 类型 是否必须 备注
username string 必须 用户名
password string 必须 密码

请求数据样例:

{   
    "username": "jinyong",    
    "password": "123456"
}

响应格式

参数格式:application/json

参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 1 成功 ; 0 失败
msg string 非必须 提示信息
data string 非必须

响应数据样例:

{
  "code": 1,
  "msg": "success",
  "data": ""
}

1.4.2. 业务分析

  • 登录失败分为三种

      - 用户名不存在
    
      - 密码错误
    
      - 用户被禁用
  • 实现思路

      - 密码需要使用md5加密
    
      - 对比用户名和密码
    
      - 如果用户名不存在,提示用户名不存在
    
      - 如果用户名和密码比对不成功,提示密码错误
    
      - 如果状态是0,提示用户禁用

1.4.3. 代码实现

Controller

@PostMapping("/login")
public Result login(@RequestBody User user){
    User result = userService.login(user);
    return Result.OK();
}

Service

@Override
public User login(User user) {
    // 根据用户名获取对应的用户数据
    User result = userMapper.findUserByUserName(user.getUsername());
    // 判断用户是否存在
    if(result == null){
        throw new RuntimeException("用户名不存在");
    }
    // 计算md5加密后的密码
    String md5Pass = DigestUtils.md5DigestAsHex(user.getPassword().getBytes(StandardCharsets.UTF_8));

    // 判断密码是否正确
    if(!result.getPassword().equals(md5Pass)){
        throw new RuntimeException("密码错误");
    }

    // 判断用户是否被禁用
    if(result.getStatus() == 0){
        throw new RuntimeException("用户已禁用");
    }
    return result;
}

1.5.全局异常处理器

在开发中,我们通常对于业务是有一条明确的主线的。我们后期处理的原则很简单,只要不符合主线预期,我们可以手动的控制异常的形式,来控制程序执行。

分析上面的代码,不难发现,登陆功能虽然实现了,但是登陆失败都是通过抛出异常来解决的

,这对我们前端显示不友好。所以我们使用全局异常处理来解决。

1.jpg

1.5.1. 代码实现

// 这个注释用于标记,这个类是处理异常的
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 这个注释用于标记,这个方法是用来处理哪个异常的
    @ExceptionHandler(RuntimeException.class)
    public Result handleException(RuntimeException runtimeException){
        return Result.ERR(runtimeException.getMessage());
    }
}

在实际开发中,我们不能对RuntimeException进行处理,因为这个异常可能是系统抛出的,所以我们在开发的时候可以针对业务进行定制,这类异常叫做自定义异常(业务异常)

所以我们定义一个业务异常的父类BusinessException,然后创建多个具体的业务异常,都继承自BusinessException。这样,我们只需要在全局异常处理器中处理BusinessException这个业务异常的父类,就能捕捉并处理所有具体的业务异常。

修改后的全局异常处理类

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result handleException(BusinessException exception){
        return Result.ERR(exception.getMessage());
    }
}

业务异常父类

public class BusinessException extends RuntimeException{

    public BusinessException() {
        super();
    }

    public BusinessException(String message) {
        super(message);
    }
}

具体的业务异常(以用户未找到异常为例)

/**
 * 未找到用户异常
 * @Author Mosfield
 * @Date 2023-6-6
 */
public class UserNotFoundException extends BusinessException{
    public UserNotFoundException() {
        super();
    }

    public UserNotFoundException(String message) {
        super(message);
    }
}

1.6. 添加用户

1.6.1. 接口说明

请求方式: POST

请求路径: /user

请求参数:application/json

参数 说明
username 用户名
password 密码
status 状态 默认是0
{
 "username":"admin",
 "password":"admin",
 "status":0
}

响应结果

{
 "code":200,
 "errMsg":null,
 "result":""
}

1.6.2. 代码实现

web层

@PostMapping
public Result add(@RequestBody User user){
    userService.add(user);
    return Result.OK("添加成功");
}

service层

public void add(User user) {
    String userPass = user.getPassword();
    //任务:sql语句中能不能对密码直接进行md5操作???
    String md5Pass = DigestUtils.md5DigestAsHex(userPass.getBytes(StandardCharsets.UTF_8));
    user.setPassword(md5Pass);
    userMapper.add(user);
}

mapper层

<insert id="add">
    insert into tb_user values(null,#{username},#{password},#{status})
</insert>

注意事项

  1. 密码在入库的时候需要使用MD5数字摘要算法
  2. 当我们插入的用户名出现重复数据的时候会抛出异常(用户名唯一)
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MySQLIntegrityConstraintViolationException.class)
    public Result handleException(MySQLIntegrityConstraintViolationException exception){
        //获取消息
        //Duplicate entry 'lucy' for key 'username'
        String errMsg = exception.getMessage().split(" ")[2] + "已存在";
        return Result.ERR(errMsg);
    }
}

1.7. 令牌

登录成功后,服务器向用户返回令牌。

令牌中记录了用户的基本信息,用于识别用户

1.7.1. 会话

在从浏览器访问服务器开始,到访问服务器结束,浏览器关闭为止的这段时间里,用户和服务器之间的交互,称为一次会话。

1.7.2. 令牌 Token

令牌就是服务端生成的一串加密字符串,它能够通过令牌确认到使用令牌的是哪个用户。

1.7.3. 用户身份识别

当用户登录成功的时候,服务器就会将一个有过期时间的令牌发送给用户。
之后用户的每次访问其他业务的时候,他的请求中都会带有令牌。服务器在接收请求以后,只有确认令牌有效,才会进行相关的数据查找和结果返回,否则会不允许该用户访问业务。

1.8. 拦截器

1.8.1. 概念

1.8.2. 入门开发

第一步:创建类实现拦截器接口

项目中创建拦截器包,包下创建拦截器类继承拦截器接口(org.springframework.web.servlet.HandlerInterceptor)

@Component  //=> 创建拦截器对象到spring容器中
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 这方方法是我们实现登陆拦截的核心中的核心
     * 在这里面我们需要编写放行的业务逻辑
     * return true => 放行逻辑 => 可以继续走三层
     * return false => 拦截 =>到此为止了
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //当用户登陆的时候,放行
        // ======================== 验证的逻辑 ========================
        // 动态改变的
        String header = request.getHeader("auth"); //username=tom
        if(header!=null && header.endsWith("tom")){//登陆的是tom
            return true;
        }
        // ==================================+========================
        //被拦截了,我们需要给提示(请登陆后继续操作)
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonStr = objectMapper.writeValueAsString(Result.ERR("请登陆后重试"));
        //把json格式的数据写给前端
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(jsonStr);
        return false;
    }
}

注意事项

  1. 拦截器对象需要创建后放入Spring容器中
  2. 必须要实现接口org.springframework.web.servlet.HandlerInterceptor
  3. 我们需要通过preHandle方法来控制我们走的是放行,还是拦截
  4. 我们通常会对拦截的内容进行统一处理

第二步:创建SpringMVC的配置对象

/**
 * 对springMVC框架进行控制
 * //todo @Configuration 和 @Component 注解区别和各自的作用
 */
@Configuration  //标记当前的类是一个配置类,它单独加载
public class WebMVCConfig implements WebMvcConfigurer {
    @Autowired
    LoginInterceptor loginInterceptor;
    /**
     * 对拦截器进行配置
     * 拦截器对象 => spring容器中
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                //设置拦截的路径 => 管你啥玩意,全给你拦截了
                .addPathPatterns("/**")
                //设置放行的路径
                .excludePathPatterns("/user/login");
    }
}
如人饮水,冷暖自知。
最后更新于 2023-08-05