【springBoot】博客系统
SSM版本的博客系统
1. 项目亮点
- 使用MD5+加盐算法进行密码的加密
- 使用Redis持久化存储Session
- 使用拦截器验证用户登录
2. 项目创建
1.项目框架的选择
2. 项目依赖的引入
3. 静态页面的代码文件:
program/博客系统(静态页面).rar · 叁伍/java语言练习 - 码云 - 开源中国 (gitee.com)
4. 配置文件的书写:
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
username: root
password: abc123
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
configuration: # 配置打印 MyBatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
level:
com:
example:
demo: debug
5. 初始化数据库
-- 创建数据库
drop database if exists mycnblog_ssm;
create database mycnblog_ssm DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog_ssm;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null unique,
password varchar(65) not null,
photo varchar(500) default '',
createtime timestamp default current_timestamp,
updatetime datetime not null,
`state` int default 1
) default charset 'utf8mb4';
-- 创建文章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime timestamp default current_timestamp,
updatetime datetime not null,
uid int not null,
rcount int not null default 1,
`state` int default 1
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog_ssm`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid, `createtime`, `updatetime`)
values('Java','Java正文',1, '2021-12-06 17:10:48', '2021-12-06 17:10:48');
6. 项目分层
7. 统一数据的返回
统一数据返回,在common包中:
@Data
public class AjaxResult implements Serializable {
// 状态码
private Integer code;
// 状态码描述信息
private String msg;
// 返回的数据
private Object data;
/**
* 操作成功返回的结果
*/
public static AjaxResult success(Object data) {
AjaxResult result = new AjaxResult();
result.setCode(200);
result.setMsg("");
result.setData(data);
return result;
}
public static AjaxResult success(int code, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg("");
result.setData(data);
return result;
}
public static AjaxResult success(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
/**
* 返回失败结果
*/
public static AjaxResult fail(int code, String msg) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
public static AjaxResult fail(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
}
保底策略:在返回数据之前,检测数据的类型是否为统一的对象,如果不是,封装成统一的对象。在config包中:
@ControllerAdvice//表示控制器通知类
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 开关,如果是true才会调用beforeBodyWrite
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 对数据格式进行效验和封装
*/
@SneakyThrows//异常抛出,相当于方法上throw一个异常
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof AjaxResult){//进行了统一对象的封装,直接返回body
return body;
}
if (body instanceof String){//字符串类型特殊处理。手动对其进行ajax类型的转化
return objectMapper.writeValueAsString(AjaxResult.success(body));
}
return AjaxResult.success(body);//未进行封装的对其进行统一封装
}
}
3. 实现注册功能
1. 前端实现
前端是在reg.html页面进行实现注册页面的。
<div class="login-container">
<!-- 中间的注册框 -->
<div class="login-dialog">
<!-- …… -->
<div class="row">
<button id="submit" onclick="mysub()">提交</button>
</div>
</div>
</div>
<script>
//提交注册事件
function mysub(){
// 1.非空效验
var username = jQuery("#username");
var password = jQuery("#password");
var password2 = jQuery("#password2");
if(username.val()==""){
alert("请先输入用户名!");
username.focus(); // 将鼠标光标设置到用户名控件
return;
}
if(password.val()==""){
alert("请先输入密码!");
password.focus();
return;
}
if(password2.val()==""){
alert("请先输入确认密码!");
password2.focus();
return;
}
// 2.判断两次密码是否一致
if(password.val()!=password2.val()){
alert("两次密码输入的不一致,请先检查!");
password.focus();
return;
}
// 3.ajax 提交请求
jQuery.ajax({
url:"/user/reg",//请求地址
type:"POST",//请求类型
data:{"username":username.val(),"password":password.val()},//请求数据
success:function(result){
// 响应的结果
if(result!=null && result.code==200 && result.data==1){
// 执行成功
if(confirm("恭喜:注册成功!是否要跳转到登录页面?")){
location.href = "/login.html";
}
}else{
alert("抱歉执行失败,请稍后再试!");
}
}
});
}
</script>
2. 后端实现
Userinfo实体类的实现(在entity包中实现):
@Data
public class Userinfo {
private Integer id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private Integer state;
}
在mapper包中定义UserMapper接口:
@Mapper
public interface UserMapper {
// 注册
int reg(Userinfo userinfo);
}
在resources中的mapper文件夹中创建 UserMapper.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">
<insert id="reg">
insert into userinfo(username,password,updatetime) values(#{username},#{password},#{updatetime})
</insert>
</mapper>
在 service包中创建UserService类
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public int reg(Userinfo userinfo) {
return userMapper.reg(userinfo);
}
}
在 controller包中创建UserController类
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/reg")
public AjaxResult reg(Userinfo userinfo) {
// 非空效验和参数有效性效验
if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||
!StringUtils.hasLength(userinfo.getPassword())) {
return AjaxResult.fail(-1, "非法参数");
}
userinfo.setUpdatetime(LocalDateTime.now());//设置更新时间
return AjaxResult.success(userService.reg(userinfo));
}
}
前端注册成功,并且数据库userinfo表中添加了一条对应的数据,证明注册模块已经实现成功
4. 登录功能
前端页面
<div class="login-container">
<!-- 中间的注册框 -->
<div class="login-dialog">
<!-- …… -->
<div class="row">
<button id="submit" onclick="mysub()">提交</button>
</div>
</div>
</div>
<script>
function mysub(){
// 1.非空效验
var username = jQuery("#username");
var password = jQuery("#password");
if(username.val()==""){
alert("请先输入用户名!");
username.focus();
return;
}
if(password.val()==""){
alert("请先输入密码!");
password.focus();
return;
}
// 2.ajax 请求登录接口
jQuery.ajax({
url:"/user/login",
type:"POST",
data:{"username":username.val(),"password":password.val()},
success:function(result){
if(result!=null && result.code==200 && result.data!=null){
// 登录成功
location.href = "myblog_list.html";
}else{
alert("抱歉登录失败,用户名或密码输入错误,请重试!");
}
}
});
}
</script>
后端实现
在userMapper接口中定义抽象方法:
Userinfo getUserByName(@Param("username") String username);
在 UserMapper.xml文件中添加sql语句:
<select id="getUserByName" resultType="com.example.demo.entity.Userinfo">
select * from userinfo where username=#{username}
</select>
在userService类中实现getUserByName方法:
public Userinfo getUserByName(String username){
return userMapper.getUserByName(username);
}
在UserController类中实现login方法:
实现这方法的时候,我们存储用户登录信息在session中。我们session的key值后续在很多地方都会用到,我们在common包中定义一个全局变量:
public class AppVariable {
// 用户 session key
public static final String USER_SESSION_KEY = "USER_SESSION_KEY";
}
实现login方法:
@RequestMapping("/login")
public AjaxResult login(HttpServletRequest request,String username,String password){
// 非空效验和参数有效性效验
if (username == null || !StringUtils.hasLength(username) ||
!StringUtils.hasLength(password) ){
return AjaxResult.fail(-1, "非法参数");
}
// 2.查询数据库
Userinfo userinfo = userService.getUserByName(username);
if (userinfo != null && userinfo.getId() > 0) { // 有效的用户
// 两个密码是否相同
if (password.equals(userinfo.getPassword())) {
// 登录成功
// 将用户存储到 session
HttpSession session = request.getSession();
session.setAttribute(AppVariable.USER_SESSION_KEY, userinfo);
userinfo.setPassword(""); // 返回前端之前,隐藏敏感(密码)信息
return AjaxResult.success(userinfo);
}
}
return AjaxResult.success(0,null);
}
登录拦截器的实现
在config包中,实现切面类和定义拦截规则
实现切面类:
public class LoginInterceptor implements HandlerInterceptor {
/**
* true -> 用户已登录
* false -> 用户未登录
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute(AppVariable.USER_SESSION_KEY) != null) {
// 用户已登陆
System.out.println("当前登录用户为:" +
((Userinfo) session.getAttribute(AppVariable.USER_SESSION_KEY)).getUsername());
return true;
}
// 调整到登录页面
response.sendRedirect("/login.html");
return false;
}
}
实现拦截规则:
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/css/**")
.excludePathPatterns("/editor.md/**")
.excludePathPatterns("/img/**")
.excludePathPatterns("/js/**")
.excludePathPatterns("/login.html")
.excludePathPatterns("/reg.html")
.excludePathPatterns("/blog_list.html")
.excludePathPatterns("/blog_content.html")
.excludePathPatterns("/art/detail")
.excludePathPatterns("/art/incr-rcount")
.excludePathPatterns("/user/getuserbyid")
.excludePathPatterns("/art/listbypage")
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/reg");
}
}
5.博客列表页的实现
1. 显示当前用户信息
前端实现
<!-- 版心 -->
<div class="container">
<!-- 左侧个人信息 -->
<div class="container-left">
<div class="card">
<img src="img/doge.jpg" class="avtar" alt="">
<h3 id="username"></h3>
<a href="http:www.github.com">github 地址</a>
<div class="counter">
<span>文章</span>
<span>分类</span>
</div>
<div class="counter">
<span id="artCount"></span>
<span>1</span>
</div>
</div>
</div>
<!-- 右侧内容详情 -->
</div>
<script>
// 获取左侧个人信息
function showInfo(){
jQuery.ajax({
url:"/user/showinfo",
type:"POST",
data:{},
success:function(result){
if(result!=null && result.code==200){
jQuery("#username").text(result.data.username);
jQuery("#artCount").text(result.data.artCount);
}else{
alert("个人信息加载失败,请重新刷新再试!");
}
}
});
}
showInfo();
</script>
后端实现
在mapper包中创建ArticleMapper接口,并且创建getArtCountByUid抽象方法。
@Mapper
public interface ArticleMapper {
//查询文章总数
int getArtCountByUid(@Param("uid") Integer uid);
}
在static的mapper包中创建ArticleMapper.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.ArticleMapper">
<select id="getArtCountByUid" resultType="Integer">
select count(*) from articleinfo where uid=#{uid}
</select>
</mapper>
在service包中创建ArticleService类并且在getArtCountByUid方法中调用articleMapper中的getArtCountByUid方法
@Service
public class ArticleService{
@Resource
private ArticleMapper articleMapper;
public int getArtCountByUid(Integer uid) {
return articleMapper.getArtCountByUid(uid);
}
}
在entity包中定义 UserinfoVO继承Userinfo类,新增artCount属性用来存储查询出的文章数量
@Data
public class UserinfoVO extends Userinfo {
private Integer artCount; // 此人发表的文章总数
}
把对当前登录用户相关的操作进行用一个类进行封装
public class UserSessionUtils {
/**
* 得到当前的登录用户
*/
public static Userinfo getUser(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null &&
session.getAttribute(AppVariable.USER_SESSION_KEY) != null) {
// 说明用户已经正常登录
return (Userinfo) session.getAttribute(AppVariable.USER_SESSION_KEY);
}
return null;
}
}
在controller包中的UserController类中定义showInfo方法
@RequestMapping("/showinfo")
public AjaxResult showInfo(HttpServletRequest request) {
UserinfoVO userinfoVO = new UserinfoVO();
// 1.得到当前登录用户(从 session 中获取)
Userinfo userinfo = UserSessionUtils.getUser(request);
if (userinfo == null) {
return AjaxResult.fail(-1, "非法请求");
}
// Spring 提供的深克隆方法
BeanUtils.copyProperties(userinfo, userinfoVO);
// 2.得到用户发表文章的总数
userinfoVO.setArtCount(articleService.getArtCountByUid(userinfo.getId()));
return AjaxResult.success(userinfoVO);
}
也可以对1号账户新增一片博客进行验证
insert into articleinfo (title,content,updatetime,uid) value ('第二篇文章','第二篇文章,大家好','2021-12-06 17:10:48',1);
2. 获取文章列表的数据
获取自己的文章列表不用传递参数,后端通过session获取用户id。如果传参可能被有心人通过传参获取别人的文章。
前端实现
// 获取我的文章列表数据
function getMyArtList(){
jQuery.ajax({
url:"/art/mylist",
type:"POST",
data:{},
success:function(result){
if(result!=null && result.code==200){
// 有两种情况,一种是发表了文章,一种是没有发表任何文章
if(result.data!=null && result.data.length>0){
// 此用户发表文章了
var artListDiv ="";
for(var i=0;i<result.data.length;i++){
var artItem = result.data[i];
artListDiv += '<div class="blog">';
artListDiv += '<div class="title">'+artItem.title+'</div>';
artListDiv += '<div class="date">'+artItem.updatetime+'</div>';
artListDiv += '<div class="desc">';
artListDiv += artItem.content;
artListDiv += '</div>';
artListDiv += '<a href="blog_content.html?id='+
artItem.id + '" class="detail">查看全文 >></a> ';
artListDiv += '<a href="blog_edit.html?id='+
artItem.id + '" class="detail">修改 >></a> ';
artListDiv += '<a href="javascript:myDel('+
artItem.id+');" class="detail">删除 >></a>';
artListDiv += '</div>';
}
jQuery("#artDiv").html(artListDiv);
}else{
// 当前用户从未发过任何文章
jQuery("#artDiv").html("<h3>暂无文章~</h3>");
}
}else{
alert("查询文章列表出错,请重试!");
}
}
});
}
getMyArtList();
// 删除文章
function myDel(id){
}
上述完成后右侧内容详情删除详情页的信息,留下以下内容:
<!-- 右侧内容详情 -->
<div id="artDiv" class="container-right">
<!-- 每一篇博客包含标题, 摘要, 时间 -->
</div>
后端实现
在entity包中定义一个Articleinfo实体类
@Data
public class Articleinfo {
private Integer id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private Integer uid;
private Integer rcount;
private Integer state;
}
在mapper包中的ArticleMapper接口中定义getMyList方法
List<Articleinfo> getMyList(@Param("uid") Integer uid);
在static中的mapper包中的ArticleMapper.xml文件中定义书写内容
<select id="getMyList" resultType="com.example.demo.entity.Articleinfo">
select * from articleinfo where uid=#{uid}
</select>
在service包中的ArticleService接口中定义getMyList方法
public List<Articleinfo> getMyList(Integer uid) {
return articleMapper.getMyList(uid);
}
在controller包中定义ArticleController类并且在内部定义getMyList方法
@RestController
@RequestMapping("/art")
public class ArticleController {
@Autowired
private ArticleService articleService;
@RequestMapping("/mylist")
public AjaxResult getMyList(HttpServletRequest request) {
Userinfo userinfo = UserSessionUtils.getUser(request);
if (userinfo == null) {
return AjaxResult.fail(-1, "非法请求");
}
List<Articleinfo> list = articleService.getMyList(userinfo.getId());
return AjaxResult.success(list);
}
}
设置返回时间格式:
- 在yml中设置全局配置
spring:
jackson:
date-format: 'yyyy-MM-dd HH:mm:ss'
time-zone: 'GMT+8'
这种方式的问题是针对LocalDateTime不起作用,对Date变量才起作用
- 使用@JsonFormat注解来返回时间格式
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDateTime createtime;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDateTime updatetime;
6. 注销和删除博客功能
1. 注销登录的前端实现
<a href="javascript:logout()">注销</a>
<script>
function logout(){
if(confirm("确认注销登录?")){
jQuery.ajax({
url:"/user/logout",
type:"POST",
data:{},
success:function(res){
if(res != null && res.code == 200){
location.href = "/login.html";
}else{
alert("抱歉:操作失败,请重试!"+res.msg);
}
}
});
}
}
</script>
2. 注销登录的后端实现
在UserController类中实现logout方法:
/**
* 注销(退出登录)
*/
@RequestMapping("/logout")
public AjaxResult logout(HttpSession session) {
session.removeAttribute(AppVariable.USER_SESSION_KEY);
return AjaxResult.success(1);
}
3. 删除博客功能前端实现
在博客详情页(myblog_list.html)中实现下面功能:
需要注意的时不能传递用户id参数,登录用户信息靠后端从session中获取:
<a href="javascript:logout()">注销</a>
<script>
// 删除文章
function myDel(id){
if(confirm("确实删除?")){
// 删除文章
jQuery.ajax({
url:"art/del",
type:"POST",
data:{"id":id},
success:function(result){
if(result!=null && result.code==200 && result.data==1){
alert("恭喜:删除成功!");
// 刷新当前页面
location.href = location.href;
}else{
alert("抱歉:删除失败,请重试!");
}
}
});
}
}
</script>
4. 删除博客功能后端实现
在ArticleMapper接口定义del抽象方法
int del(@Param("id") Integer id, @Param("uid") Integer uid);
在ArticleMapper.xml中书写sql语句
<delete id="del">
delete from articleinfo where id=#{id} and uid=#{uid}
</delete>
service包中的ArticleService类实现del方法
public int del(Integer id, Integer uid) {
return articleMapper.del(id, uid);
}
在controller包中的ArticleController类下实现 del方法
@RequestMapping("/del")
public AjaxResult del(HttpServletRequest request, Integer id) {
if (id == null || id <= 0) {
// 参数有误
return AjaxResult.fail(-1, "参数异常");
}
Userinfo user = UserSessionUtils.getUser(request);
if (user == null) {
return AjaxResult.fail(-2, "用户未登录");
}
return AjaxResult.success(articleService.del(id, user.getId()));
}
7. 博客详情页
1.展示博客内容前端实现
<!-- 右侧内容详情 -->
<div class="container-right">
<div class="blog-content">
<!-- 博客标题 -->
<h3 id="title"></h3>
<!-- 博客时间 -->
<div class="date">发布时间:<span id="updatetime"></span>
阅读量:<span id="rcount"></span>
</div>
<!-- 博客正文 -->
<div id="editorDiv">
</div>
</div>
</div>
<script>
// 获取当前url参数的方法
function getUrlValue(key){
// ex:?id=1&v=2
var params = location.search;
if(params.length>1){
// ex:id=1&v=2
params = location.search.substring(1);
var paramArr = params.split("&");
for(var i=0;i<paramArr.length;i++){
var kv = paramArr[i].split("=");
if(kv[0]==key){
// 是我要查询的参数
return kv[1];
}
}
}
return "";
}
// 查询文章详情
function getArtDetail(id){
if(id==""){
alert("非法参数!");
return;
}
jQuery.ajax({
url:"art/detail",
type:"POST",
data:{"id":id},
success:function(result){
if(result!=null && result.code==200){
jQuery("#title").html(result.data.title);
jQuery("#updatetime").html(result.data.updatetime);
jQuery("#rcount").html(result.data.rcount);
initEdit(result.data.content);
}else{
alert("查询失败,请重试!");
}
}
});
}
getArtDetail(getUrlValue("id"));
</script>
2.展示博客内容后端实现
在ArticleMapper接口定义getDetail抽象方法
Articleinfo getDetail(@Param("id") Integer id);
在ArticleMapper.xml中书写sql语句
<select id="getDetail" resultType="com.example.demo.entity.Articleinfo">
select * from articleinfo where id=#{id}
</select>
service包中的ArticleService类实现getDetail方法
public Articleinfo getDetail(Integer id) {
return articleMapper.getDetail(id);
}
在controller包中的ArticleController类下实现 getDetail方法
@RequestMapping("/detail")
public AjaxResult getDetail(Integer id) {
if (id == null || id <= 0) {
return AjaxResult.fail(-1, "非法参数");
}
return AjaxResult.success(articleService.getDetail(id));
}
3.详情页作者信息展示前端
<!-- 左侧个人信息 -->
<div class="container-left">
<div class="card">
<img src="img/avatar.png" class="avtar" alt="">
<h3 id="username"></h3>
<a href="http:www.github.com">github 地址</a>
<div class="counter">
<span>文章</span>
<span>分类</span>
</div>
<div class="counter">
<span id="artCount"></span>
<span>1</span>
</div>
</div>
</div>
<script>
//此方法在getArtDetail方法中查询成功后调用showUser(result.data.uid);
// 查询用户的详情信息
function showUser(id){
//注意后端往前端传递数据的时候,不要传递敏感信息,比如:个人密码
jQuery.ajax({
url:"/user/getuserbyid",
type:"POST",
data:{"id":id},
success:function(result){
if(result!=null && result.code==200 && result.data.id>0){
jQuery("#username").text(result.data.username);
jQuery("#artCount").text(result.data.artCount);
}else{
alert("抱歉:查询用户信息失败,请重试!");
}
}
});
}
</script>
4.详情页作者信息展示后端
在ArticleMapper接口定义getUserById抽象方法
Userinfo getUserById(@Param("id") Integer id);
在ArticleMapper.xml中书写sql语句
<select id="getUserById" resultType="com.example.demo.entity.Userinfo">
select * from userinfo where id=#{id}
</select>
service包中的ArticleService类实现getUserById方法
public Userinfo getUserById(Integer id) {
return userMapper.getUserById(id);
}
在controller包中的ArticleController类下实现 getUserById方法
@RequestMapping("/getuserbyid")
public AjaxResult getUserById(Integer id) {
if (id == null || id <= 0) {
// 无效参数
return AjaxResult.fail(-1, "非法参数");
}
Userinfo userinfo = userService.getUserById(id);
if (userinfo == null || userinfo.getId() <= 0) {
// 无效参数
return AjaxResult.fail(-1, "非法参数");
}
// 去除 userinfo 中的敏感数据,ex:密码
userinfo.setPassword("");
UserinfoVO userinfoVO = new UserinfoVO();
BeanUtils.copyProperties(userinfo, userinfoVO);
// 查询当前用户发表的文章数
userinfoVO.setArtCount(articleService.getArtCountByUid(id));
return AjaxResult.success(userinfoVO);
}
5. 实现增加阅读量前端
<script>
// 阅读量 +1
function updataRCount(){
// 先得到文章 id
var id = getUrlValue("id");
if(id!=""){
jQuery.ajax({
url:"/art/incr-rcount",
type:"POST",
data:{"id":id},
success:function(result){}
});
}
}
updataRCount();
</script>
6.实现增加阅读量后端
在ArticleMapper接口定义incrRCount抽象方法
public int incrRCount(@Param("id") Integer id);
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<update id="incrRCount">
update articleinfo set rcount=rcount+1 where id=#{id}
</update>
service包中的ArticleService类实现incrRCount方法
public int incrRCount(Integer id) {
return articleMapper.incrRCount(id);
}
在controller包中的ArticleController类下实现 incrRCount方法
@RequestMapping("/incr-rcount")
public AjaxResult incrRCount(Integer id) {
if (id != null && id > 0) {
return AjaxResult.success(articleService.incrRCount(id));
}
return AjaxResult.fail(-1, "未知错误");
}
8. 新增文章页
前端实现
<!-- 编辑框容器 -->
<div class="blog-edit-container">
<!-- 标题编辑区 -->
<div class="title">
<input type="text" id="title" placeholder="在这里写下文章标题">
<button onclick="mysub()">发布文章</button>
</div>
<!-- 创建编辑器标签 -->
<div id="editorDiv">
<textarea id="editor-markdown" style="display:none;"></textarea>
</div>
</div>
<script>
// 提交
function mysub(){
if(confirm("确认提交?")){
// 1.非空效验
var title = jQuery("#title");
if(title.val()==""){
alert("请先输入标题!");
title.focus();
return;
}
if(editor.getValue()==""){
alert("请先输入文章内容!");
return;
}
// 2.请求后端进行博客添加操作
jQuery.ajax({
url:"/art/add",
type:"POST",
data:{"title":title.val(),"content":editor.getValue()},
success:function(result){
if(result!=null && result.code==200 && result.data==1){
if(confirm("恭喜:文章添加成功!是否继续添加文章?")){
// 刷新当前页面
location.href = location.href;
}else{
location.href = "/myblog_list.html";
}
}else{
alert("抱歉,文章添加失败,请重试!");
}
}
});
}
}
</script>
后端实现
在ArticleMapper接口定义add抽象方法
int add(Articleinfo articleinfo);
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<insert id="add">
insert into articleinfo(title,content,uid,updatetime) values(#{title},#{content},#{uid},#{updatetime})
</insert>
service包中的ArticleService类实现add方法
public int add(Articleinfo articleinfo) {
return articleMapper.add(articleinfo);
}
在controller包中的ArticleController类下实现 add方法
@RequestMapping("/add")
public AjaxResult add(HttpServletRequest request, Articleinfo articleinfo) {
// 1.非空效验
if (articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||
!StringUtils.hasLength(articleinfo.getContent())) {
// 非法参数
return AjaxResult.fail(-1, "非法参数");
}
// 2.数据库添加操作
// a.得到当前登录用户的 uid
Userinfo userinfo = UserSessionUtils.getUser(request);
if (userinfo == null || userinfo.getId() <= 0) {
// 无效的登录用户
return AjaxResult.fail(-2, "无效的登录用户");
}
articleinfo.setUid(userinfo.getId());
articleinfo.setUpdatetime(LocalDateTime.now());//设置更新时间
// b.添加数据库并返回结果
return AjaxResult.success(articleService.add(articleinfo));
}
9. 博客修改页
前端实现
博客编辑页的初始化功能复用之前的博客详情页写的接口即可实现。需要新增的后端接口就是修改操作。
<!-- 编辑框容器 -->
<div class="blog-edit-container">
<!-- 标题编辑区 -->
<div class="title">
<input id="title" type="text">
<button onclick="mysub()">修改文章</button>
</div>
<!-- 创建编辑器标签 -->
<div id="editorDiv">
<textarea id="editor-markdown" style="display:none;"></textarea>
</div>
</div>
<script>
// 提交
function mysub(){
// 1.非空效验
var title = jQuery("#title");
if(title.val()==""){
alert("请先输入标题!");
title.focus();
return;
}
if(editor.getValue()==""){
alert("请先输入正文!");
return;
}
// 2.进行修改操作
jQuery.ajax({
url:"/art/update",
type:"POST",
data:{"id":id,"title":title.val(),"content":editor.getValue()},
success:function(result){
if(result!=null && result.code==200 && result.data==1){
alert("恭喜:修改成功!");
location.href = "myblog_list.html";
}else{
alert("抱歉:操作失败,请重试!");
}
}
});
}
// 文章初始化
function initArt(){
// 得到当前页面 url 中的参数 id(文章id)
id = getUrlValue("id");
if(id==""){
alert("无效参数");
location.href = "myblog_list.html";
return;
}
// 请求后端,查询文章的详情信息
jQuery.ajax({
url:"art/detail",
type:"POST",
data:{"id":id},
success:function(result){
if(result!=null && result.code==200){
jQuery("#title").val(result.data.title);
initEdit(result.data.content);
}else{
alert("查询失败,请重试!");
}
}
});
}
initArt();
</script>
后端实现
在ArticleMapper接口定义update抽象方法
int update(Articleinfo articleinfo);
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<update id="update">
update articleinfo set title=#{title},content=#{content},updatetime=#{updatetime}
where id=#{id} and uid=#{uid}
</update>
service包中的ArticleService类实现update方法
public int update(Articleinfo articleinfo) {
return articleMapper.update(articleinfo);
}
在controller包中的ArticleController类下实现 update方法
@RequestMapping("/update")
public AjaxResult update(HttpServletRequest request, Articleinfo articleinfo) {
// 非空效验
if (articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||
!StringUtils.hasLength(articleinfo.getContent()) ||
articleinfo.getId() == null) {
// 非法参数
return AjaxResult.fail(-1, "非法参数");
}
// 得到当前登录用户的 id
Userinfo userinfo = UserSessionUtils.getUser(request);
if (userinfo == null && userinfo.getId() == null) {
// 无效用户
return AjaxResult.fail(-2, "无效用户");
}
// 很核心的代码(解决了修改文章归属人判定的问题)
articleinfo.setUid(userinfo.getId());
articleinfo.setUpdatetime(LocalDateTime.now());
return AjaxResult.success(articleService.update(articleinfo));
}
10. 密码MD5加盐加密
1.自实现加密的工具类
加盐加密的实现思路:
- 加密的思路:每次调用方法时产生唯一的盐值 + 密码 = 最终密码(MD5加密)
- 解密思路:需要两个密码:需要验证的密码 + 最终加密的密码
- 核心思想:得到盐值。将盐值存放到最终密码的某个为止,然后在最终密码中得到盐值
- 通过: 需要验证的密码+盐值,走一遍加密同样的过程得到的密码和最终密码比较
自己定义一个工具类,其中定义如何进行加密和解密的方法
public class PasswordUtils {
/**
* 1.加盐并生成密码
* @param password 明文密码
* @return 保存到数据库中的密码
*/
public static String encrypt(String password) {
// 1.产生盐值(32位)
String salt = UUID.randomUUID().toString().replace("-", "");
// 2.生成加盐之后的密码
String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
// 3.生成最终密码(保存到数据库中的密码)【约定格式:32位盐值+$+32位加盐之后的密码】
String finalPassword = salt + "$" + saltPassword;
return finalPassword;
}
/**
* 2.生成加盐的密码(方法1的重载)
* @param password 明文
* @param salt 固定的盐值
* @return 最终密码
*/
public static String encrypt(String password, String salt) {
// 1.生成一个加盐之后的密码
String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
// 2.生成最终的密码【约定格式:32位盐值+$+32位加盐之后的密码】
String finalPassword = salt + "$" + saltPassword;
return finalPassword;
}
/**
* 3.验证密码
* @param inputPassword 用户输入的明文密码
* @param finalPassword 数据库保存的最终密码
* @return
*/
public static boolean check(String inputPassword, String finalPassword) {
if (StringUtils.hasLength(inputPassword) && StringUtils.hasLength(finalPassword) &&
finalPassword.length() == 65) {
// 1.得到盐值
String salt = finalPassword.split("\\$")[0];
// 2.使用之前加密的步骤,将明文密码和已经得到的盐值进行加密,生成最终的密码
String confirmPassword = PasswordUtils.encrypt(inputPassword, salt);
// 3.对比两个最终密码是否相同
return confirmPassword.equals(finalPassword);
}
return false;
}
}
2. 在注册和登录页面修改加密
主要是对UserController类中的reg方法和login方法进行修改
@RequestMapping("/reg")
public AjaxResult reg(Userinfo userinfo) {
// 非空效验和参数有效性效验
if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||
!StringUtils.hasLength(userinfo.getPassword())) {
return AjaxResult.fail(-1, "非法参数");
}
// 密码加盐处理
userinfo.setPassword(PasswordUtils.encrypt(userinfo.getPassword()));
userinfo.setUpdatetime(LocalDateTime.now());//设置更新时间
return AjaxResult.success(userService.reg(userinfo));
}
@RequestMapping("/login")
public AjaxResult login(HttpServletRequest request,String username,String password){
// 非空效验和参数有效性效验
if (username == null || !StringUtils.hasLength(username) ||
!StringUtils.hasLength(password) ){
return AjaxResult.fail(-1, "非法参数");
}
// 2.查询数据库
Userinfo userinfo = userService.getUserByName(username);
if (userinfo != null && userinfo.getId() > 0) { // 有效的用户
// 两个密码是否相同
if (PasswordUtils.check(password, userinfo.getPassword())) {
// 登录成功
// 将用户存储到 session
HttpSession session = request.getSession();
session.setAttribute(AppVariable.USER_SESSION_KEY, userinfo);
userinfo.setPassword(""); // 返回前端之前,隐藏敏感(密码)信息
return AjaxResult.success(userinfo);
}
}
return AjaxResult.success(0,null);
}
3. 使用spring security进行加盐加密
- 引入spring security框架
spring security有自己的认证体系,我们对其进行访问的时候,必须先登录spring security的登录界面
产生这个的原因是:这个加入框架之后,是会产生spring boot的自动注入的。
- 排除spring security自动加载
在启动类上加:@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class MycnblogApplication {
public static void main(String[] args) {
SpringApplication.run(MycnblogApplication.class, args);
}
}
- 调用 实现加盐和验证
@Test
void testSecurity(){
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = "123456";
String finalPassword = passwordEncoder.encode(password);
System.out.println(finalPassword);
System.out.println(passwordEncoder.encode(password));
System.out.println(passwordEncoder.encode(password));
//解密:参数1:明文密码。参数2:最终密码
String inputPassword = "12345";
System.out.println("错误密码比对结果:" + passwordEncoder.matches(inputPassword,finalPassword));
String inputPassword2 = "123456";
System.out.println("错误密码比对结果:" + passwordEncoder.matches(inputPassword2,finalPassword));
}
在测试类中实现上面的测试方法。发现每次加盐的结果都是不同的。
11. 分页展示实现
1.分页功能的后端
分页公式的值 = (当前页码 - 1) * 每页显示的条数
在ArticleMapper接口定义getListByPage和getCount抽象方法
List<Articleinfo> getListByPage(@Param("psize") Integer psize,
@Param("offsize") Integer offsize);
int getCount();
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<select id="getListByPage" resultType="com.example.demo.entity.Articleinfo">
select * from articleinfo limit #{psize} offset #{offsize}
</select>
<select id="getCount" resultType="Integer">
select count(*) from articleinfo
</select>
service包中的ArticleService类实现getListByPage和getCount方法
public List<Articleinfo> getListByPage(Integer psize, Integer offsize) {
return articleMapper.getListByPage(psize, offsize);
}
public int getCount() {
return articleMapper.getCount();
}
在controller包中的ArticleController类下实现 getListByPage方法
@RequestMapping("/listbypage")
public AjaxResult getListByPage(Integer pindex, Integer psize) {
// 1.参数校正
if (pindex == null || pindex <= 1) {
pindex = 1;
}
if (psize == null || psize <= 1) {
psize = 2;
}
// 分页公式的值 = (当前页码-1)*每页显示条数
int offset = (pindex - 1) * psize;
// 文章列表数据
List<Articleinfo> list = articleService.getListByPage(psize, offset);
// 当前列表总共有多少页
// a.总共有多少条数据
int totalCount = articleService.getCount();
// b.总条数/psize(每页显示条数)
double pcountdb = totalCount / (psize * 1.0);
// c.使用进一法得到总页数
int pcount = (int) Math.ceil(pcountdb);
HashMap<String, Object> result = new HashMap<>();
result.put("list", list);
result.put("pcount", pcount);
return AjaxResult.success(result);
}
2. 分页功能的前端
版心部分的html代码:
<!-- 版心 -->
<div class="container">
<!-- 右侧内容详情 -->
<div class="container-right" style="width: 100%;">
<div id="artListDiv">
</div>
<hr>
<div class="blog-pagnation-wrapper">
<button onclick="goFirstPage()" class="blog-pagnation-item">首页</button>
<button onclick="goBeforePage()" class="blog-pagnation-item">上一页</button>
<button onclick="goNextPage()" class="blog-pagnation-item">下一页</button>
<button onclick="goLastPage()" class="blog-pagnation-item">末页</button>
</div>
</div>
</div>
js代码
// 当前页码
var pindex = 1;
// 每页显示条数
var psize = 2;
// 最大页数
var pcount =1;
// 1.先尝试得到当前 url 中的页码
pindex = (getUrlValue("pindex")==""?1:getUrlValue("pindex"));
// 2.查询后端接口得到当前页面的数据,进行展示
function initPage(){
jQuery.ajax({
url:"/art/listbypage",
type:"POST",
data:{"pindex":pindex,"psize":psize},
success:function(result){
if(result!=null && result.code==200 && result.data.list.length>0){
var artListHtml = "";
for(var i=0;i<result.data.list.length;i++){
var articleinfo = result.data.list[i];
artListHtml +='<div class="blog">';
artListHtml +='<div class="title">'+articleinfo.title+'</div>';
artListHtml +='<div class="date">'+articleinfo.updatetime+'</div>';
artListHtml +='<div class="desc">'+articleinfo.content+'</div>';
artListHtml +='<a href="blog_content.html?id='+ articleinfo.id
+'" class="detail">查看全文 >></a>';
artListHtml +='</div>';
}
jQuery("#artListDiv").html(artListHtml);
pcount = result.data.pcount;
}
}
});
}
initPage();
// 跳转到首页
function goFirstPage(){
if(pindex<=1){
alert("当前已经在首页了");
return;
}
location.href = "blog_list.html";
}
// 点击上一页按钮
function goBeforePage(){
if(pindex<=1){
alert("当前已经在首页了");
return;
}
pindex = parseInt(pindex) -1;
location.href ="blog_list.html?pindex="+pindex;
}
function goNextPage(){
if(pindex>=pcount){
alert("已经在末页了");
return;
}
pindex = parseInt(pindex)+1;
location.href ="blog_list.html?pindex="+pindex;
}
function goLastPage(){
if(pindex>=pcount){
alert("已经在末页了");
return;
}
location.href ="blog_list.html?pindex="+pcount;
}
上述就实现了博客系统最基本的功能了