【JavaEE】进阶 · 个人博客系统(3)
文章目录
- 【JavaEE】进阶 · 个人博客系统(3)
- 1. 加盐加密验密算法原理
- 1.1 md5加密
- 1.2 md5验密
- 1.3 md5缺漏
- 1.4 加盐加密
- 1.5 后端的盐值拼接约定
- 1.6 代码实现
- 1.6.1 加密
- 1.6.2 验密
- 1.6.3 测试
- 2. 博客注册页
- 2.1 上传头像
- 2.1.1 期待效果
- 2.1.2 约定前后端交互接口
- 2.1.3 后端代码
- 2.1.4 前端代码
- 2.1.5 测试
- 2.2 注册
- 2.2.1 期待效果
- 2.2.2 约定前后端交互接口
- 2.2.3 后端代码
- 2.2.4 前端代码
- 2.2.5 测试
- 3. 博客登录页
- 3.1 期待效果
- 3.2 失焦更新头像
- 3.2.1 约定前后端交互接口
- 3.2.2 后端代码
- 3.2.3 前端代码
- 3.2.4 测试
- 3.3 处理url 以及 注册页面跳转
- 3.3.1 通过key,获取url中的value
- 3.3.2 将username赋值给用户名输入框
- 3.3.3 注册页面跳转
- 3.3.4 测试
- 3.4 登录功能
- 3.4.1 约定前后端交互接口
- 3.4.2 后端代码
- 3.4.3 前端代码
- 3.4.4 测试
【JavaEE】进阶 · 个人博客系统(3)
本文章正式进行前后端交互了!
还是一样的老套路:
- 根据期待效果约定前后端交互接口
- 后端代码
- 三板斧:校验,处理请求,返回响应
- 前端代码
- 三板斧:校验,发送请求,处理响应
先写后端还是先写前端,个人习惯问题~
大方向就是那三板斧,具体按具体改动~
1. 加盐加密验密算法原理
1.1 md5加密
我们原本通过md5进行加密,这是一个不可逆的加密:
在这里插入图片描述](https://img-blog.csdnimg.cn/dbcab6e1574148c0ab41849ba13fc77b.gif)
原理就是通过password生成一个 一一对应 的固定长度的加密密码
1.2 md5验密
为什么不说是解密的,因为这个是一个不可逆的过程,也就是说,如果后端用md5加密后,是无法获取到原密码的,除非你使用“逆天的暴力枚举”
而一个固定的password,生成的是一个固定的加密密码!
这个也是常识,因为我们几乎在任何场景下,都没有遇到过,找回密码是返回原密码的,一般都是通过一些手段验证你的信息,进行修改密码的操作~
所以,后端能做的就是“验证密码”
因为一个固定的password,生成的是一个固定的加密密码!
所以如果密码是正确的话,生成的加密密码也是正确对应的上的!
1.3 md5缺漏
没错,不良用户/黑客,可以通过“逆天的暴力枚举”,也就是他们总结出来的“彩虹表”:
- 这个表,记录了很多很多字符串的加密密码,这样如果攻破了数据库的话,这些用户的密码就会被破解出来!
1.4 加盐加密
加盐,这里是比较形象的说法,也就是加点料,让加密密码无规律:
- 生成一个全球不重复的随机的盐值
- 盐值 + 原密码,进行md5加密,获取加密密码
- 将盐值 + 加密密码保存到数据库
而这个盐值,可见就是UUID!
- 因此,同样的原密码,由于UUID不会重复和md5加密的一一对应,生成的加密密码是不一样的~
- UUID和md5都是用不完的,底层不需要理解,坐享其成即可,不要杞人忧天~
这个算法逻辑上是破解的了的:
- 攻破数据库后,获取一个杂合密码
- 破解出盐值和加密密码(不良用户不知道这个盐值 + 加密码是咋组合的)
- 用彩虹表破解加密密码,获取原生组合,破解出原密码
- 不良用户不知道这个盐值 + 原密码是咋组合的
- 很难映射出这么“主观性这么强,随机性这么强”的原生组合
从逻辑分析上可以看出,破解难度和成本高出的倍数是不能计量的,“逆宇宙级枚举”
但是世界上没有完全的安全,只有你想不到的破解方法,和他们考虑成本是否要进行破解!
补充:加密过程后端是不会记录下来的,这里黑客破解的是持久化的数据
1.5 后端的盐值拼接约定
- 盐值跟原密码直接拼接,生成的加密密码
- 盐值跟加密密码以:
[salt]$[plus password]
格式拼接- 这样方便获取盐值
1.6 代码实现
1.6.1 加密
创建一个用户相关的工具类:UserUtils
public class UserUtils {
public static String encrypt(String password) {
// 1. 获取盐值
String salt = UUID.randomUUID().toString();
// 2. md5加密
String plusPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));
// 3. 将盐值和加密密码组合返回
return salt + "$" + plusPassword;
}
}
1.6.2 验密
根据加密原理推算:
验密就是根据数据库里的组合密码:
- 获取到[盐值]和[正确的加密密码]
- [盐值]拼接[待验证的密码],生成[待验证的加密密码]
- 对比[正确的加密密码]和[待验证的加密密码]
根据md5的一一对应,如果对应的上,那就是正确的密码
public static boolean confirm(String password, String dbPassword) {
// 1. 获取到[盐值]和[正确的加密密码]
String[] group = dbPassword.split("\\$");
// 在split函数的参数字符串里,这个$有特殊含义,需要转义一下
// 2. md5加密
String plusPassword = DigestUtils.md5DigestAsHex((group[0] + password).getBytes(StandardCharsets.UTF_8));
// 3. 对比
return group[1].equals(plusPassword);
}
参数校验调用这些方法之前就确认过了,不必重复~
- dbPassword必然是正确样式的数据,否则之前就不会添加成功,获取不到也更不会调用这个方法~
1.6.3 测试
public static void main(String[] args) {
String password = "abcd";
String dbPassword1 = encrypt(password);
boolean conf1 = confirm(password, dbPassword1);
String dbPassword2 = encrypt(password);
boolean conf2 = confirm(password, dbPassword2);
System.out.println(password);
System.out.println(dbPassword1);
System.out.println(conf1);
System.out.println(dbPassword2);
System.out.println(conf2);
}
结果:
补充:UUID的-
建议去除,我的数据库是65位的组合密码,所以得去掉:
- 这样UUID就是十六进制的三十二位数了
public static String encrypt(String password) {
// 1. 获取盐值
String salt = UUID.randomUUID().toString().replace("-", "");
// 2. md5加密
String plusPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));
// 3. 将盐值和加密密码组合返回
return salt + "$" + plusPassword;
}
2. 博客注册页
2.1 上传头像
2.1.1 期待效果
之前用的是form表单上传文件,现在我们用Ajax上传文件,这样我们就不会被强制跳转且可以获取传递回来的文件名,更新其显示!
2.1.2 约定前后端交互接口
后端:
- /user/picture
- 接受请求中的文件,项目外的D:/blog_userImage目录下
- 不用导致项目空间占用太大
- 应该是项目,映射指向机器的某一个位置的资源
- 返回文件名(包装成的CommonResult对象)
前端:
- /user/picture
- multipart/form-data
- post
- body:文件按钮上传的文件
2.1.3 后端代码
- 工具类ImageUtils,通过getImageUniquePath方法,可获取文件保存路径
public class ImageUtils {
public static String getImageUniquePath(String originName) {
String path = "blog_userImage/";
// 获取唯一id
String id = UUID.randomUUID().toString();
//获取文件后缀
String suffix = originName.substring(originName.lastIndexOf("."));
//拼接
path += id + suffix;
return path;
}
}
- controller层,处理请求,调用service层进行数据持久化
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/picture")
public CommonResult picture(@RequestPart("myfile")MultipartFile file) throws IOException {
if(file == null) {
return CommonResult.fail(-1, "上传文件失败");
}
//获取文件保存路径
String path = ImageUtils.getImageUniquePath(file.getOriginalFilename());
//通过文件保存路径,将文件进行保存
userService.loadImage(file, path);
//返回文件名(包装成统一对象)
return CommonResult.success(path);
}
}
- service层,进行数据持久化
@Service
@Slf4j
public class UserService {
public void loadImage(MultipartFile file, String path) {
log.info("保存成功:" + path);//保存成功日志
//保存文件
try {
file.transferTo(new File("D:/" + path));//spring mvc是可以直接throws异常的,框架内部/异常处理器有处理,但是多级调用,耦合度有点高
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 配置静态资源映射
对于新增的文件:
- 我们的网站能够访问到我们自己的静态资源是因为我们在运行的时候,将这些打包到target里面了,而新增的文件只是在我们开发的时候的路径下,并没有立即加载到target里
- 绝对路径也是一样,无论你保存到项目里,还是保存到项目外,都没有加载到target里面,我们也无法手动写入target
而我们的网站,浏览器考虑到安全性,是不能直接访问不在项目target里的静态资源的
而我们spring boot项目与普通maven项目不同,spring boot项目修改静态资源,例如html/css/js等等,必须保存并重启服务器才能更新~
所以我们需要进行,静态资源的映射!
@Configuration
public class MyWebMvcConfigurerAdapter implements WebMvcConfigurer {
/**
* 配置静态访问资源
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/blog_userImage/**").addResourceLocations("file:D:/blog_userImage/");
}
}
含义就是,将“file:D:/blog_userImage/”路径下的静态资源,映射成“/blog_userImage/**”
- 通过
localhost:8080/blog_userImage/**
,就可以访问~
并且,访问服务器的路由以这个为准,拦截器配置:
目录结构:
2.1.4 前端代码
<div id="fileImage">
<input
id="i"
type="button"
value="请上传头像"
onclick="putImage();"
/>
<input id="f_file" type="file" name="file" style="display: none" />
</div>
putImage(点击普通按钮触发file按钮):
function putImage() {
javascript: jQuery("input[name='file']").click();
}
file按钮上传成功触发的事件(发送请求):
- 固定搭配,无需多问~
jQuery("#f_file").change(function (e) {
// 获取选中的文件
var file = e.target.files[0];
// 创建一个 FormData 对象
var formData = new FormData();
formData.append("myfile", file);
// 用 Ajax 向服务器发送文件
jQuery.ajax({
url: "/user/picture",
type: "POST",
data: formData,
processData: false, // 告诉 jQuery 不要处理发送的数据
contentType: false, // 告诉 jQuery 不要设置 Content-Type 请求头
success: function (res) {
if (res.code === 200) {
//修改图像
var url = "url(" + res.data + ")";
jQuery("#i").css("background-image", url);
jQuery("#i").val("");
} else {
console.log("上传失败: " + res.msg);
}
},
error: function () {
console.log("上传失败,请重试!");
},
});
});
2.1.5 测试
2.2 注册
2.2.1 期待效果
输入必选项:昵称,密码,确认密码
- 确认密码在发送之前进行验证,因为没有确认密码这个字段,也没有必要,这个不需要多说
代码仓库以及头像为非必选
- 注册成功后,弹框提示自动生成的用户名,跳转到登录页面,并帮助用户填写用户名
而在后端:
- 生成一个加密密码
- 生成一个用户名
2.2.2 约定前后端交互接口
后端:
- /user/register
- 返回受影响行数,以及用户名
前端:
- body:image,name,password,git
- post,json,/user/register
2.2.3 后端代码
- UserUtils的获取用户名的方法
public static String getUsername() {
// 获取当前时间戳
long timestamp = System.currentTimeMillis();
// 生成随机数
Random random = new Random();
int randomNumber = random.nextInt(100);
// 结合时间戳和随机数生成唯一标识符
String identifier = String.valueOf(timestamp) + String.valueOf(randomNumber);
return identifier;
}
不适用UUID是因为UUID太长了,没啥规律,带字母,而这里我用的是当前时间戳 + 100以内的随机数组成的15位十进制数
如果恶意注册,用户名才可能重复,由于有unique约束,所以会添加失败,受影响行数返回0~
- controller层
@RequestMapping("register")
public CommonResult register(@RequestBody UserInfo userInfo) {
// 1. 校验参数
if(userInfo == null || !StringUtils.hasLength(userInfo.getName())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return CommonResult.fail(-1, "非法参数");
}
// 2. 生成一个用户名
String username = UserUtils.getUsername();
userInfo.setUsername(username);
// 3. 加密
userInfo.setPassword(UserUtils.encrypt(userInfo.getPassword()));
// 4. 请求service的添加数据库操作
int rows = userService.register(userInfo);
// 5. 执行结果返回
Map<String, Object> map = new HashMap<>();
map.put("rows", rows);
map.put("username", username);
return CommonResult.success(map);
}
补充:
判断字符串为空字符串/null,是的话,返回false
- mapper层
实现:
- 由于我需要用到标签,所以注解的方式不太方便,我用xml去实现
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
</mapper>
register实现(非必选项的判断):
<insert id="register">
insert into userinfo (
username,
name,
<if test="photo != null">
photo,
</if>
<if test="git != null">
git,
</if>
password
) values (
#{username},
#{name},
<if test="photo != null">
#{photo},
</if>
<if test="git != null">
#{git},
</if>
#{password}
)
</insert>
- service层
@Autowired
private UserMapper userMapper;
public int register(UserInfo userInfo) {
return userMapper.register(userInfo);
}
- 拦截器配置
2.2.4 前端代码
- 用户名不能全为空,并且上传时空白符会被去除掉
- 密码不能全为空,并且上传时空白符会被去除掉
实现:
function register() {
var name = jQuery("#username");
var password = jQuery("#password");
var judge_password = jQuery("#judge_password");
var photo = jQuery("#i")
.css("background-image")
.replace("url(", "")
.replace(")", "")
.replace("\"", "")
.replace("\"", "");//去掉两个引号
var git = jQuery("#git");
// 1. 非空校验
if (name.val().trim() == "") {
alert("请输入昵称!");
name.focus();
return false;
}
if (password.val().trim() == "") {
alert("请输入密码!");
password.focus();
return false;
}
if (judge_password.val().trim() == "") {
alert("请输入密码!");
judge_password.focus();
return false;
}
if (password.val() != judge_password.val()) {
alert("两次输入密码不一致!");
return false;
}
// 2. 发送请求
jQuery.ajax({
url: "/user/register",
method: "POST",
contentType: "application/json; charset=utf8",
data: JSON.stringify({
name: name.val().trim(),
password: password.val().trim(),
photo: photo,
git: git.val(),
}),
// 3. 处理响应
success: function (body) {
if (body.code == 200 && body.data.rows == 1) {
alert("注册成功,请记住你的用户名:" + body.data.username + " !");
location.href = "blog_login.html?username=" + body.data.username;
} else {
alert("注册失败:" + data.msg);
}
},
});
}
2.2.5 测试
在这里插入图片描述
3. 博客登录页
3.1 期待效果
- 用户名输入框聚焦到失焦的时候发送请求给后端,尝试获取用户头像
- 根据querystring,如果有用户名将用户名进行赋值,两个输入框都输入值后才能发送登录请求,后端对数据进行校验
- 点击注册按钮,跳转到注册页
3.2 失焦更新头像
3.2.1 约定前后端交互接口
后端:
- /user/get_photo
- 返回头像名
前端:
- /user/get_photo
- 用户名
- json
3.2.2 后端代码
- controller层
@RequestMapping("/get_photo")
public CommonResult getPhoto(@RequestBody UserInfo user) {
String username = user.getUsername();
UserInfo userInfo = userService.getUserByUsername(username);
return userInfo != null ? CommonResult.success(userInfo.getPhoto()) : CommonResult.fail(-1, "没有此用户");
}
- service层
public UserInfo getUserByUsername(String username) {
return userMapper.selectByUsername(username);
}
- mapper层
@Select("select * from userinfo where username = #{username}")
UserInfo selectByUsername(@Param("username") String username);
- 拦截器配置
.excludePathPatterns("/user/get_photo")
3.2.3 前端代码
给用户名输入框一个失焦事件:
jQuery("#username").blur(function () {
var username = jQuery("#username");
if (username.val().trim() != "") {
jQuery.ajax({
url: "/user/get_photo",
method: "post",
contentType: "application/json; charset=utf8",
data: JSON.stringify({
username: username.val().trim(),
}),
success: function (body) {
if (body.code == 200 && body.data != "") {
var img = "url(" + body.data + ")";
jQuery("#i").css("background-image", img);
} else {
jQuery("#i").css(
"background-image",
"url(blog_userImage/avatar.png)"
);
}
},
});
}
});
3.2.4 测试
3.3 处理url 以及 注册页面跳转
3.3.1 通过key,获取url中的value
<script src="js/url_handler.js"></script>
// 根据 key 获取 url 中对应的 value
function getParamValue(key){
// 1.得到当前url的参数部分
var params = location.search;
// 2.去除“?”
if(params.indexOf("?")>=0){
params = params.substring(1);
// 3.根据“&”将参数分割成多个数组
var paramArray = params.split("&");
// 4.循环对比 key,并返回查询的 value
if(paramArray.length>=1){
for(var i=0;i<paramArray.length;i++){
// key=value
var item = paramArray[i].split("=");
if(item[0]==key){
return item[1];
}
}
}
}
return null;
}
3.3.2 将username赋值给用户名输入框
jQuery("#username").val(getParamValue("username"));
jQuery("#username").focus();
3.3.3 注册页面跳转
3.3.4 测试
3.4 登录功能
3.4.1 约定前后端交互接口
后端:
- /user/login
- 返回 true / false
前端:
- /user/login
- 用户名,密码,json,post
- true:跳转到所有人的博客列表页;false:弹窗提示
3.4.2 后端代码
- controller层
@RequestMapping("/login")
public CommonResult login(@RequestBody UserInfo userInfo, HttpServletRequest request) {
//1. 参数校验
if(userInfo.getUsername() == null || !StringUtils.hasLength(userInfo.getUsername())
|| userInfo.getPassword() == null || !StringUtils.hasLength(userInfo.getPassword())) {
return CommonResult.fail(-1, "非法参数!");
}
//2. 根据用户名查询对象
UserInfo user = userService.getUserByUsername(userInfo.getUsername());
if(user == null || user.getId() == 0) {
return CommonResult.fail(-2, "用户名或者密码错误!");
}
//3. 验证密码(左边待测,右边数据库查出来的)
if(!UserUtils.confirm(userInfo.getPassword(), user.getPassword())) {
return CommonResult.fail(-2, "用户名或者密码错误!");
}
//4. 比较成功,将对象存储到session中
SessionUtils.setUser(request, user);
//5. 返回结果
return CommonResult.success("登录成功");
}
- 拦截器配置
.excludePathPatterns("/user/login")
3.4.3 前端代码
function login() {
var username = jQuery("#username");
var password = jQuery("#password");
// 1. 非空校验
if (username.val().trim() == "") {
alert("请输入昵称!");
username.focus();
return false;
}
if (password.val().trim() == "") {
alert("请输入密码!");
password.focus();
return false;
}
// 2. 发送请求
jQuery.ajax({
url: "/user/login",
method: "POST",
contentType: "application/json; charset=utf8",
data: JSON.stringify({
username: username.val().trim(),
password: password.val().trim(),
}),
// 3. 处理响应
success: function (body) {
if (body.code == 200) {
alert("登录成功!");
location.href = "blog_lists.html";
} else {
alert("登录失败:" + body.msg);
}
},
});
}
3.4.4 测试
可以访问需要登录校验的页面:
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!代码:myblog_system · 游离态/马拉圈2023年9月 - 码云 - 开源中国 (gitee.com)