博客系统小笔记
BeanUtils(BeanUtils工具)
1.拷贝属性
BeanUtils.copyProperties(类1, 类2)
;
是 Apache Commons BeanUtils 库中的一个常用方法,用于实现两个 JavaBean 对象之间的属性拷贝。
作用说明:
- 该方法会将第一个参数(源对象
blogInfo
)的属性值,复制到第二个参数(目标对象blogInfoResponse
)的同名属性中。 - 要求源对象和目标对象拥有相同名称的属性(且属性的 getter/setter 方法符合 JavaBean 规范),数据类型需兼容。
QueryWrapper构造查询条件
QueryWrapper
是 MyBatis-Plus 提供的强大的查询条件构造器,它能以编程的方式灵活构建 SQL 查询条件,避免手动拼接 SQL 字符串带来的安全问题(如 SQL 注入)和代码可读性问题。
// 构建查询条件
QueryWrapper<BlogInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(BlogInfo::getDeleteFlag, 0);// 执行查询
List<BlogInfo> blogList = blogInfoMapper.selectList(queryWrapper);
这行代码是 MyBatis-Plus 框架中用于构建查询条件的典型用法,具体含义如下:
代码解析:
queryWrapper
:是 MyBatis-Plus 提供的查询条件构造器对象,用于动态 SQL 查询条件.lambda()
:切换到 Lambda 表达式写法,支持通过类的方法引用来指定属性,避免硬编码字段名.eq(BlogInfo::getDeleteFlag, 0)
:添加一个 "等于" 条件,等价于 SQL 中的WHERE delete_flag = 0
BlogInfo::getDeleteFlag
:通过 Lambda 方法引用指定实体类BlogInfo
的deleteFlag
属性0
:表示要匹配的值(通常 0 表示未删除,1 表示已删除,用于逻辑删除场景)
作用:
这行代码的最终效果是在查询中添加条件:筛选出 deleteFlag
字段值为 0 的 BlogInfo
记录,通常用于查询未被逻辑删除的数据。
List<BlogInfoResponse> list=blogInfos.stream().map()
代码解析:
blogInfos.stream()
:将集合转换为 Stream 流,开启流式处理.map(...)
:对流中的每个BlogInfo
对象进行转换操作- 接收一个 lambda 表达式作为参数,定义如何将
BlogInfo
转换为BlogInfoResponse
- 接收一个 lambda 表达式作为参数,定义如何将
.collect(Collectors.toList())
:将转换后的流收集为List<BlogInfoResponse>
Claims
MD5(加密/解密)
加密
/*** 加密* MD5(salt+明文)* return:盐值+MD5(盐值+明文)*/ //明文(密码)public static String encrypt(String password){String salt= UUID.randomUUID().toString().replace("-","");//盐值String securityPassword= DigestUtils.md5DigestAsHex((salt+password).getBytes(StandardCharsets.UTF_8));//盐值+明文return salt+securityPassword;//盐值+(盐值+明文)}
生成盐值(Salt):
String salt = UUID.randomUUID().toString().replace("-", "");
UUID.randomUUID()
生成一个全局唯一的 UUID(如e5c8a7b0-1d3f-4b9c-87a2-567890abcdef
)。replace("-", "")
去掉 UUID 中的短横线,得到纯字母数字的盐值(如e5c8a7b01d3f4b9c87a2567890abcdef
)。- 盐值的作用:每个密码的盐值都不同,即使两个用户密码相同,最终的加密结果也会不同,能抵御 “彩虹表” 攻击。
MD5 哈希加密:
String securityPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));
- 将 “盐值 + 原始密码” 拼接后,转换为 UTF-8 字节数组。
- 用
DigestUtils.md5DigestAsHex
(Spring 提供的工具类)对字节数组做 MD5 哈希,得到 32 位十六进制的哈希值。 - MD5 是不可逆哈希算法,加密后无法从结果反推原始密码。
返回加密结果:
return salt + securityPassword;
- 将 “盐值” 和 “MD5 哈希结果” 拼接后返回。这样在验证密码时,可从结果中提取盐值,再用同样的逻辑验证。
解密
/*** 验证* 数据库中存储的是,盐值+MD5(盐值+明文)*/ //明文(密码) 盐值+(盐值+明文) public static boolean verify(String inputPassword,String sqlPassword){if(!StringUtils.hasLength(inputPassword)){//是否为空或空白字符串。return false;}if(sqlPassword==null&&sqlPassword.length()!=64){return false;}String salt=sqlPassword.substring(0,32);//取盐值String securityPassword=DigestUtils.md5DigestAsHex((salt+inputPassword).getBytes(StandardCharsets.UTF_8));//盐值+明文return sqlPassword.equals(salt+securityPassword);}
1. 参数校验
if(!StringUtils.hasLength(inputPassword)){return false;
}
- 作用:检查用户输入的密码(
inputPassword
)是否为空或空白字符串。 - 逻辑:如果输入密码为空,直接返回
false
(验证失败),避免后续无效计算。
2. 验证数据库存储的加密密码格式
if(sqlPassword==null&&sqlPassword.length()!=64){return false;
}
- 作用:检查数据库中存储的加密密码(
sqlPassword
)是否符合预期格式。 - 预期格式:根据加密方法,
sqlPassword
应为 32 位盐值 + 32 位 MD5 哈希值,总长度必须是 64 位。
3. 提取盐值
String salt=sqlPassword.substring(0,32);
- 作用:从数据库存储的加密密码中提取盐值(与加密时的逻辑对应)。
- 逻辑:加密时盐值是 32 位(UUID 去掉短横线后长度),因此截取前 32 位作为盐值。
4. 重新计算加密结果
String securityPassword=DigestUtils.md5DigestAsHex((salt+inputPassword).getBytes(StandardCharsets.UTF_8));
- 作用:用提取的盐值 + 用户输入的明文密码,重新计算 MD5 哈希值。
- 逻辑:与加密时的算法完全一致(盐值拼接明文后做 MD5 哈希),确保验证的一致性。
5. 对比验证
return sqlPassword.equals(salt+securityPassword);
- 作用:将重新计算的 “盐值 + 哈希值” 与数据库中存储的
sqlPassword
对比。 - 逻辑:如果两者相同,说明输入密码正确(返回
true
);否则验证失败(返回false
)。
JwtUtils
1.引入依赖
2. 核心属性与初始化
// JWT 签名密钥(Base64 编码后的字符串)
private static String SECRET_STRING = "dVnsmy+SIX6pNptQdecLDSJ26EMSPEInvZYKBTtug4k=";
// 签名密钥(用于生成和验证 JWT 的签名)
private static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_STRING));
SECRET_STRING
:是一个 Base64 编码的字符串,作为 JWT 的签名密钥。签名的作用是保证 JWT 在传输过程中不被篡改。key
:通过Keys.hmacShaKeyFor
方法,将 Base64 解码后的密钥转换为Key
对象,用于 JWT 的签名和验签。这里使用的是 HMAC-SHA 算法(一种对称加密签名算法,生成和验签用同一个密钥)。
3. 生成 JWT(genToken
方法)
public static String genToken(Map<String, Object> claims) {String compact = Jwts.builder().setClaims(claims) // 设置 JWT 的 payload(自定义声明,如用户 ID、角色等).signWith(key) // 使用密钥进行签名.compact(); // 压缩生成最终的 JWT 字符串return compact;
}
- 作用:根据传入的
claims
(自定义声明,是一个Map
,可以包含用户 ID、用户名、角色等信息)生成 JWT。 - 流程:
Jwts.builder()
:创建 JWT 构建器。setClaims(claims)
:将自定义声明放入 JWT 的payload
部分。signWith(key)
:使用之前初始化的key
对 JWT 进行签名,确保完整性和真实性。compact()
:将 JWT 各部分(头、payload、签名)压缩成一个字符串返回。
4. 解析 JWT(parseToken
方法)
public static Claims parseToken(String token) {JwtParser builder = Jwts.parserBuilder().setSigningKey(key) // 设置验签密钥(必须与生成时的密钥一致).build();Claims claims = null;try {// 解析 JWT 并获取 payload 中的 claimsclaims = builder.parseClaimsJws(token).getBody();} catch (Exception e) {log.error("token 解析失败,token:" + token);}return claims;
}
- 作用:解析传入的 JWT 字符串,验证其签名并提取
payload
中的声明(Claims
)。 - 流程:
Jwts.parserBuilder()
:创建 JWT 解析器构建器。setSigningKey(key)
:设置验签密钥(必须与生成 JWT 时的密钥一致,否则验签失败)。build()
:构建解析器。parseClaimsJws(token)
:解析 JWT,验证签名。如果签名不匹配或 JWT 格式错误,会抛出异常。getBody()
:获取 JWTpayload
中的Claims
(即生成时传入的自定义声明)。- 异常处理:如果解析失败(如签名无效、JWT 过期等),捕获异常并记录日志,返回
null
。
前端
1.储存、跳转
$.ajax({type:"post",url:"/user/login",contentType:"application/json",//指定请求体数据的格式为 JSONdata:JSON.stringify({// JSON.stringify:JavaScript 对象转换为 JSON 字符串userName:$("#username").val(),//选中页面中 id="username" 的元素password:$("#password").val()//,val()接收用户名}),success: function(result){if(result!=null&&result.code=="SUCCESS"){localStorage.setItem("loginUserId",result.data.userId);localStorage.setItem("userToken",result.data.token);location.href="blog_list.html";}else{alert(result.errMsg);}}});
1. 存储用户信息到本地存储:localStorage.setItem(...)
localStorage.setItem("loginUserId", result.data.userId)
:- 将用户 ID(
result.data.userId
,后端返回的用户唯一标识)存储到浏览器的localStorage
中。 localStorage
是浏览器提供的本地存储机制,数据会长期保存在浏览器中(除非手动清除或代码删除)。
- 将用户 ID(
localStorage.setItem("userToken", result.data.token)
:- 将后端返回的身份令牌(
token
,用于后续请求的身份验证,如 JWT)存储到localStorage
中。 - 后续请求(如查询博客列表、编辑博客)时,前端会从
localStorage
中取出token
,放在请求头(如Authorization
头)中发送给后端,后端验证token
以确认用户身份。
- 将后端返回的身份令牌(
2.:将当前页面跳转到
blog_list.html
(博客列表页面)。- location.href="blog_list.html";
- 通常登录成功后,会跳转到用户的 “个人中心” 或 “资源列表页”,这里是跳转到博客列表页,符合博客系统的业务逻辑。
2.拼接、插入
$.ajax({type:"get",url:"/blog/getList",success:function(result){if(result.code=='SUCCESS'&&result.data!=null&&result.data.length>0){var finalHtml="";for(var blogInfo of result.data){finalHtml+='<div class="blog">';finalHtml+='<div class="title">'+blogInfo.title+'</div>';finalHtml+='<div class="date">'+blogInfo.createTime+'</div>';finalHtml+='<div class="desc">'+blogInfo.content+'</div>';finalHtml+='<a class="detail" href="blog_detail.html?blogId='+blogInfo.id+'">查看全文>></a>';finalHtml+='</div>';}$(".container .right").html(finalHtml);}}});
1.拼接博客列表 HTML
var finalHtml="";
for(var blogInfo of result.data){finalHtml+='<div class="blog">';finalHtml+='<div class="title">'+blogInfo.title+'</div>';finalHtml+='<div class="date">'+blogInfo.createTime+'</div>';finalHtml+='<div class="desc">'+blogInfo.content+'</div>';finalHtml+='<a class="detail" href="blog_detail.html?blogId='+blogInfo.id+'">查看全文>></a>';finalHtml+='</div>';
}
- 作用:遍历博客列表数据,拼接成 HTML 字符串。
for(var blogInfo of result.data)
:循环遍历后端返回的博客数组(result.data
),blogInfo
是单个博客对象。- 拼接的 HTML 结构:
- 每个博客用
<div class="blog">
包裹(用于样式控制)。 - 包含博客标题(
blogInfo.title
)、创建时间(blogInfo.createTime
)、内容(blogInfo.content
)。 - 查看详情链接(
a
标签):跳转至blog_detail.html
并携带blogId
参数(用于查询该博客的详细信息)。
- 每个博客用
2. 渲染到页面
$(".container .right").html(finalHtml);
- 作用:将拼接好的 HTML 字符串插入到页面中。
$(".container .right")
:用 jQuery 选择器定位到页面中class="container"
下的class="right"
元素(通常是博客列表的容器)。.html(finalHtml)
:将容器的内容替换为拼接好的博客列表 HTML,完成页面渲染。
3.获取网站上?后内容(包括?)
方法一:
url:"/user/getUserInfo?userId="+localStorage.getItem("loginUserId"),
loginUserId
这个键名对应的值,来源于用户登录成功后,前端主动存储到 localStorage
中的用户 ID。
来源于1中的localStorage.setItem(...)
方法二:
url:"/blog/delete"+location.search,
4.
$.ajax({type:"get",url:"/user/getUserInfo?userId="+localStorage.getItem("loginUserId"),success:function(result){if(result!=null&&result.code=="SUCCESS"&&result.data!=null){var userInfo=result.data;$(".left .card h3").text(userInfo.userName);$(".left .card a").attr("href".userInfo.githubUrl);}}});
提取并展示用户信息
var userInfo = result.data; // 提取用户信息对象
$(".left .card h3").text(userInfo.userName); // 显示用户名
$(".left .card a").attr("href", userInfo.githubUrl); // 设置 GitHub 链接
var userInfo = result.data
:从响应中提取用户信息对象(result.data
包含后端返回的用户详情,如用户名、GitHub 地址等)。- 显示用户名:
$(".left .card h3")
:用 jQuery 选择器定位到页面中class="left"
→class="card"
下的<h3>
标签(通常是展示用户名的位置)。.text(userInfo.userName)
:将<h3>
标签的文本内容设置为后端返回的用户名(userInfo.userName
)。
- 设置 GitHub 链接:
$(".left .card a")
:定位到卡片中的<a>
标签(通常是 GitHub 链接)。.attr("href", userInfo.githubUrl)
:将链接的href
属性设置为后端返回的 GitHub 地址(userInfo.githubUrl
),点击后可跳转到用户的 GitHub 主页。
5.
if(result.code=="SUCCESS"&&result.data!=null){$(".content .title").text(result.data.title);$(".content .date").text(result.data.createTime);$(".content .detail").text(result.data.content);editormd.markdownToHTML("detail", {markdown: result.data.content});
editormd.markdownToHTML("detail", {markdown: result.data.content
});
第一个参数
"detail"
:表示页面中用于展示渲染结果的 DOM 元素的 ID(即<div id="detail"></div>
这样的容器)。转换后的 HTML 会被插入到这个元素中。第二个参数
{ markdown: result.data.content }
:配置对象,markdown
属性指定需要转换的 Markdown 源文本。result.data.content
通常是后端返回的博客内容(假设这是一篇用 Markdown 编写的博客,内容存储为 Markdown 格式的字符串)