【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 测试
本文章正式进行前后端交互了!
还是一样的老套路:
先写后端还是先写前端,个人习惯问题~
大方向就是那三板斧,具体按具体改动~
我们原本通过md5进行加密,这是一个不可逆的加密:
在这里插入图片描述](https://img-blog.csdnimg.cn/dbcab6e1574148c0ab41849ba13fc77b.gif)
原理就是通过password生成一个 一一对应 的固定长度的加密密码
为什么不说是解密的,因为这个是一个不可逆的过程,也就是说,如果后端用md5加密后,是无法获取到原密码的,除非你使用“逆天的暴力枚举”
而一个固定的password,生成的是一个固定的加密密码!
这个也是常识,因为我们几乎在任何场景下,都没有遇到过,找回密码是返回原密码的,一般都是通过一些手段验证你的信息,进行修改密码的操作~
所以,后端能做的就是“验证密码”
因为一个固定的password,生成的是一个固定的加密密码!
所以如果密码是正确的话,生成的加密密码也是正确对应的上的!
没错,不良用户/黑客,可以通过“逆天的暴力枚举”,也就是他们总结出来的“彩虹表”:
加盐,这里是比较形象的说法,也就是加点料,让加密密码无规律:
而这个盐值,可见就是UUID!
这个算法逻辑上是破解的了的:
- 攻破数据库后,获取一个杂合密码
- 破解出盐值和加密密码(不良用户不知道这个盐值 + 加密码是咋组合的)
- 用彩虹表破解加密密码,获取原生组合,破解出原密码
- 不良用户不知道这个盐值 + 原密码是咋组合的
- 很难映射出这么“主观性这么强,随机性这么强”的原生组合
从逻辑分析上可以看出,破解难度和成本高出的倍数是不能计量的,“逆宇宙级枚举”
但是世界上没有完全的安全,只有你想不到的破解方法,和他们考虑成本是否要进行破解!
补充:加密过程后端是不会记录下来的,这里黑客破解的是持久化的数据
[salt]$[plus password]
格式拼接
创建一个用户相关的工具类: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;
}
}
根据加密原理推算:
验密就是根据数据库里的组合密码:
根据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);
}
参数校验调用这些方法之前就确认过了,不必重复~
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位的组合密码,所以得去掉:
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;
}
之前用的是form表单上传文件,现在我们用Ajax上传文件,这样我们就不会被强制跳转且可以获取传递回来的文件名,更新其显示!
后端:
前端:
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;
}
}
@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
@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/**
,就可以访问~并且,访问服务器的路由以这个为准,拦截器配置:
目录结构:
<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("上传失败,请重试!");
},
});
});
输入必选项:昵称,密码,确认密码
代码仓库以及头像为非必选
而在后端:
后端:
前端:
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~
@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
实现:
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>
@Autowired
private UserMapper userMapper;
public int register(UserInfo userInfo) {
return userMapper.register(userInfo);
}
实现:
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);
}
},
});
}
在这里插入图片描述
后端:
前端:
@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, "没有此用户");
}
public UserInfo getUserByUsername(String username) {
return userMapper.selectByUsername(username);
}
@Select("select * from userinfo where username = #{username}")
UserInfo selectByUsername(@Param("username") String username);
.excludePathPatterns("/user/get_photo")
给用户名输入框一个失焦事件:
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)"
);
}
},
});
}
});
<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;
}
jQuery("#username").val(getParamValue("username"));
jQuery("#username").focus();
后端:
前端:
@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")
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);
}
},
});
}
可以访问需要登录校验的页面:
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭!代码:myblog_system · 游离态/马拉圈2023年9月 - 码云 - 开源中国 (gitee.com)