用户登录相关
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.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>
注意事项
- 密码在入库的时候需要使用MD5数字摘要算法
- 当我们插入的用户名出现重复数据的时候会抛出异常(用户名唯一)
@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;
}
}
注意事项
- 拦截器对象需要创建后放入Spring容器中
- 必须要实现接口
org.springframework.web.servlet.HandlerInterceptor
- 我们需要通过
preHandle
方法来控制我们走的是放行,还是拦截 - 我们通常会对拦截的内容进行统一处理
第二步:创建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");
}
}
Comments NOTHING