目录
- 1.项目展示
- 2.项目结构设计
- 3.项目功能设计
- 4 数据库准备
- 4.1 建表
- 4.2 DB相关数据
- 5.项目模块
- 6.添加项目公共模块
- 6.1 common
- 6.2 实现前端界面
- 7.功能实现
- 7.1实现博客列表
- 约定前后端交互接口
- 实现服务器代码
- 实现客户端代码
- 7.2实现博客详情
- 约定前后端交互接口
- 实现服务器代码
- 实现客户端代码
- 7.3实现登录
- 约定前后端交互接口
- 实现服务器代码
- 实现客户端代码
- 7.4实现强制要求登录
- 添加拦截器
- 实现客户端代码
- 7.5实现显示用户信息
- 约定前后端交互接口
- 实现服务器代码
- 7.6实现用户退出
- 约定前后端交互接口
- 实现服务器代码
- 实现客户端代码
- 7.7实现发布博客
- 约定前后端交互接口
- 实现服务器代码
- 实现客户端代码
- 7.8实现删除/编辑博客
- 约定前后端交互接口
- 实现服务器代码
- 实现客户端代码
- 7.9实现加密加盐
- 加密工具类
- 使用
- 修改数据库密码
1.项目展示
项目已经发布到云服务器上,想要使用的小伙伴可以点击下面这个链接:
博客项目
由于目前没有实现注册功能,所以这里直接提供一个账号,用以登录:
账号:lisi
密码:123456
2.项目结构设计
后端框架:SpringBoot
数据库:mybatis
前后端交互:ajax
3.项目功能设计
主要功能如下图所示:
一共四个页面:
登录页面:
博客列表页面:
博客详情页:
写博客页面:
4 数据库准备
4.1 建表
一共两张表:
user(用户表)
blog(博客表)
创建数据库:
create database if not exists `java_blog_spring` charset utf8mb4;
创建user表:
drop table if exists `java_blog_spring`.`user`;
CREATE TABLE `java_blog_spring`.`user` (`id` INT NOT NULL AUTO_INCREMENT,`user_name` VARCHAR(128) NOT NULL,`password` VARCHAR(128) NOT NULL,`github_url` VARCHAR(128) NULL,`delete_flag` TINYINT(4) NULL DEFAULT 0,`create_time` TIMESTAMP NULL DEFAULT current_timestamp(),PRIMARY KEY (`id`),UNIQUE INDEX `user_name_UNIQUE` (`user_name` ASC))
ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '⽤户表';
创建blog表:
drop table if exists `java_blog_spring`.`blog`;
CREATE TABLE `java_blog_spring`.`blog` (`id` INT NOT NULL AUTO_INCREMENT,`title` VARCHAR(200) NULL,`content` TEXT NULL,`user_id` INT(11) NULL,`delete_flag` TINYINT(4) NULL DEFAULT 0,`create_time` TIMESTAMP NULL DEFAULT current_timestamp(),PRIMARY KEY (`id`))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';
增加一些测试数据:
insert into `java_blog_spring`.`user` (`user_name`, `password`,`github_url
`)values("zhangsan","123456","https://gitee.com/bubble-fish666/class-java4
5");
insert into `java_blog_spring`.`user` (`user_name`, `password`,`github_url
`)values("lisi","123456","https://gitee.com/bubble-fish666/class-java45");
insert into `java_blog_spring`.`blog` (`title`,`content`,`user_id`) values
("第⼀篇博客","111我是博客正⽂我是博客正⽂我是博客正⽂",1);
insert into `java_blog_spring`.`blog` (`title`,`content`,`user_id`) values
("第⼆篇博客","222我是博客正⽂我是博客正⽂我是博客正⽂",2);
4.2 DB相关数据
DB相关查询:
- 获取所有博客列表
- 根据博客Id获取博客详情
- 插⼊博客
- 更新博客
- 根据id查询user信息
- 根据name查询user信息
User类:
@Data
public class User {private Integer id;private String userName;private String password;private String githubUrl;private Byte deleteFlag;private Date createTime;
}
Blog类:
@Data
public class Blog {private Integer id;private String title;private String content;private Integer userId;private Integer deleteFlag;private Date createTime;//是否为登录用户,1表示为登录用户private Integer loginUser;public String getCreateTime() {//对时间进行格式化return DateUtils.formatDate(createTime);}
}
UserMapper类:
@Mapper
public interface UserMapper {@Select("select id,user_name,password,github_url,delete_flag,create_time from user where delete_flag=0 and id=#{id}")User selectById(Integer id);@Select("select id,user_name,password,github_url,delete_flag,create_time from user where delete_flag=0 and user_name=#{name}")User selectByName(String name);
}
BlogMapper类:
@Mapper
public interface BlogMapper {@Select("select * from blog where delete_flag=0")List<Blog> selectAllBlog();@Select("select * from blog where delete_flag=0 and id=#{blogId}")Blog selectBlogById(Integer blogId);Integer updateBlog(Blog blog);@Insert("insert into blog(title,content,user_id) values (#{title},#{content},#{userId})")Integer insertBlog(Blog blog);
}
BlogMapper.xml:
<mapper namespace="com.example.springblog.mapper.BlogMapper"><update id="updateBlog">update blog<set><if test="title!=null">title=#{title},</if><if test="content!=null">content=#{content},</if><if test="userId!=null">user_id=#{userId},</if><if test="deleteFlag!=null">delete_flag=#{deleteFlag},</if></set>where id=#{id}</update>
</mapper>
5.项目模块
6.添加项目公共模块
6.1 common
统一异常抽取为一个类:
@Data
public class Result {//业务处理状态码 200成功 <=0失败private Integer code;//业务返回提示信息private String msg;//业务返回数据private Object data;/*** 失败时处理内容* @return*/public static Result fail(Integer code,String msg) {Result result=new Result();result.setCode(code);result.setMsg(msg);result.setData("");return result;}public static Result fail(Integer code,String msg,Object data) {Result result=new Result();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}/*** 业务处理成功* @param data* @return*/public static Result success(Object data) {Result result=new Result();result.setCode(200);result.setMsg("");result.setData(data);return result;}public static Result success(String msg,Object data) {Result result=new Result();result.setCode(200);result.setMsg(msg);result.setData(data);return result;}
}
出错时统一异常处理:
@ControllerAdvice
public class ErrorAdvice {@ExceptionHandlerpublic Result error(Exception e){return Result.fail(-1,e.getMessage());}
}
数据统一返回格式:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}//在数据返回之前进行处理@SneakyThrows //异常处理注解@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if(body instanceof Result){return body;}if(body instanceof String){ObjectMapper objectMapper=new ObjectMapper();return objectMapper.writeValueAsString(Result.success(body));}return Result.success(body);}
}
6.2 实现前端界面
把之前写好的博客系统静态⻚⾯拷⻉到static⽬录下:
7.功能实现
7.1实现博客列表
约定前后端交互接口
[请求]
/blog/getlist
[响应]
[{blogId: 1,title: "第⼀篇博客",content: "博客正⽂",userId: 1,postTime: "2021-07-07 12:00:00"},{blogId: 2,title: "第⼆篇博客",content: "博客正⽂",userId: 1,postTime: "2021-07-07 12:10:00"},...
]
我们约定, 浏览器给服务器发送⼀个 /blog/getlist 这样的 HTTP 请求, 服务器给浏览器返回了⼀个 JSON 格式的数据.
实现服务器代码
在 BlogController 中添加⽅法:
@Slf4j
@RequestMapping("/blog")
@RestController
public class BlogController {@Autowiredprivate BlogService blogService;@RequestMapping("/getlist")public List<Blog> getBlogList(){return blogService.selectAllBlog();}
}
在BlogService 中添加⽅法:
public class BlogService {@Autowiredprivate BlogMapper blogMapper;public List<Blog> selectAllBlog(){return blogMapper.selectAllBlog();}
}
部署程序, 验证服务器是否能正确返回数据 (使⽤ URL http://127.0.0.1:8080/blog/getlist 即可)
实现客户端代码
修改 blog_list.html, 删除之前写死的博客内容, 并新增js 代码处理 ajax 请求.
<script src="./js/jquery.min.js"></script><script src="./js/common.js"></script><script>$.ajax({type:"get",url:"/blog/getlist",success:function(result){if(result.code==200 && result.data!=null && result.data.length>0){var blogs=result.data;var finalHtml="";for(var blog of blogs){finalHtml += '<div class="blog">';finalHtml += '<div class="title">'+blog.title+'</div>'finalHtml += '<div class="date">'+blog.createTime+'</div>'finalHtml += '<div class="desc">'+blog.content+'</div>'finalHtml += '<a class="detail" href="blog_detail.html?blogId='+blog.id+'">查看全文>></a>'finalHtml += '</div>'}$(".right").html(finalHtml);}},error:function(error){console.log(error);if(error!=null && error.status==401){//用户未登录location.assign("blog_login.html");}}});var url="/user/getUserInfo";getUserInfo(url);</script>
7.2实现博客详情
⽬前点击博客列表⻚的 “查看全⽂” , 能进⼊博客详情⻚, 但是这个博客详情⻚是写死的内容. 我们期望能够根据当前的 博客 id 从服务器动态获取博客内容.
约定前后端交互接口
/blog/getBlogDetail?blogId=1
[响应]
{blogId: 1,title: "第⼀篇博客",content: "博客正⽂",userId: 1,postTime: "2021-07-07 12:00:00"
}
实现服务器代码
在 BlogController 中添加getBlogDeatail ⽅法:
/*** 获取博客详情* @param blogId* @return*/@RequestMapping("/getBlogDetail")public Result getBlogDetail(Integer blogId,HttpSession session){log.info("blogId:"+blogId);if(blogId == null){return Result.fail(-1,"非法博客id");}Blog blog=blogService.selectBlogById(blogId);//获取登录用户信息User loginUser=(User) session.getAttribute(Constants.USER_INFO_SESSION);//判断登录用户和博客作者是否是同一个人if(loginUser!=null && loginUser.getId()== blog.getUserId()){blog.setLoginUser(1);}return Result.success(blog);}
在BlogService 中添加getBlogDeatil⽅法:
public Blog selectBlogById(Integer blogId){return blogMapper.selectBlogById(blogId);}
实现客户端代码
<script>$.ajax({type:"get",url:"/blog/getBlogDetail"+location.search,success:function(result){if(result.code==200 && result.data!=null){var blog=result.data;$(".title").text(blog.title);$(".date").text(blog.createTime);editormd.markdownToHTML("content", {markdown: blog.content ,});//$(".detail").text(blog.content);if(blog.loginUser==1){var html="";html+= '<button onclick="window.location.href=\'blog_update.html?blogId='+blog.id+'\'">编辑</button>';html+='<button onclick="deleteBlog()">删除</button>';$(".operating").html(html);}}},error:function(error){consolo.log(error);if(error!=null && error.status==401){//用户未登录location.assign(blog_login.html);}}});var url= "/user/getAuthorInfo" + location.search;getUserInfo(url);
common.js代码:
function getUserInfo(url){$.ajax({type:"get",url:url,success:function(result){if(result!=null && result.code==200 && result.data!=null){var user=result.data;$(".left .card h3").text(user.userName);$(".left .card a").attr("href",user.githubUrl);}}});
}
7.3实现登录
-
登陆⻚⾯提供⼀个 form 表单, 通过 form 的⽅式把⽤户名密码提交给服务器.
-
服务器端验证⽤户名密码是否正确. 如果密码正确,
-
则在服务器端创建 Session , 并把 sessionId 通过 Cookie 返回给浏览器
约定前后端交互接口
[请求]
/user/login
username=test&password=123
[响应]
200 登录成功
<0 登录失败
实现服务器代码
在 UserController 中添加⽅法:
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/login")public Result login(HttpServletRequest request,String username, String password){//参数校验if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){return Result.fail(-1,"用户名密码不能为空");}//验证密码User user=userService.selectByName(username);if(user==null || !SecurityUtils.decrypt(password,user.getPassword())){return Result.fail(-2,"用户名密码错误");}//设置sessionHttpSession session= request.getSession(true);session.setAttribute(Constants.USER_INFO_SESSION,user);return Result.success("登录成功");}
}
在UserService 中添加⽅法:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate BlogMapper blogMapper;public User selectByName(String name){return userMapper.selectByName(name);}
}
实现客户端代码
<script src="./js/jquery.min.js"></script><script>function login(){$.ajax({type:"post",url:"/user/login",data:{username:$("#userName").val(),password:$("#password").val()},success:function(result){if(result.code==200){location.href="blog_list.html";return;}else if(result.code<0 && result.msg!=''){alert(result.msg);return;}},error:{}});}</script>
7.4实现强制要求登录
当⽤户访问 博客列表⻚ 和 博客详情⻚ 时, 如果⽤户当前尚未登陆, 就⾃动跳转到登陆⻚⾯.
添加拦截器
登录拦截器:
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session=request.getSession(false);if(session!=null && session.getAttribute(Constants.USER_INFO_SESSION)!=null){//用户已经登录了return true; //不拦截}response.setStatus(401);return false;}
}
使用拦截器:
@Configuration
public class AppConfig implements WebMvcConfigurer {private final List<String> excludePaths = Arrays.asList("/**/*.html","/blog-editormd/**","/css/**","/js/**","/pic/**","/user/login");@Autowiredprivate LoginInterceptor loginInterceptor;//添加拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**") //拦截所有路径.excludePathPatterns(excludePaths); //不拦截excludePaths包括的类型文件}
}
实现客户端代码
1.修改 blog_datail.html
- 访问⻚⾯时, 添加失败处理代码
- 使⽤ location.assign 进⾏⻚⾯跳转.
error:function(error){console.log(error);if(error!=null && error.status==401){//用户未登录location.assign("blog_login.html");}}
2.修改 blog_list.html
- 访问⻚⾯时, 添加失败处理代码
- 使⽤ location.assign 进⾏⻚⾯跳转.
error:function(error){consolo.log(error);if(error!=null && error.status==401){//用户未登录location.assign(blog_login.html);}}
7.5实现显示用户信息
- 如果当前⻚⾯是博客列表⻚, 则显示当前登陆⽤户的信息.
- 如果当前⻚⾯是博客详情⻚, 则显示该博客的作者⽤户信息.
约定前后端交互接口
在博客列表⻚, 获取当前登陆的⽤户的⽤户信息.
[请求]
/user/getUserInfo
[响应]
{
userId: 1,
username: test...
}
在博客详情⻚, 获取当前⽂章作者的⽤户信息
[请求]
/user/getAuthorInfo?blogId=1
[响应]
{
userId: 1,
username: test
}
实现服务器代码
在 UserController 中添加⽅法:
/*** 获取登录用户信息* @return*/@RequestMapping("/getUserInfo")public Result getUserInfo(HttpSession session){if(session==null || session.getAttribute(Constants.USER_INFO_SESSION)==null){return Result.fail(-1,"用户未登录");}User user=(User)session.getAttribute(Constants.USER_INFO_SESSION);return Result.success(user);}/*** 获取博客作者信息* @return*/@RequestMapping("/getAuthorInfo")public Result getAuthorInfo(Integer blogId){if(blogId==null || blogId<=0){return Result.fail(-1,"博客不存在~");}User user=userService.selectAuthorByBlogId(blogId);return Result.success(user);}
在UserService 中添加⽅法:
public User selectAuthorByBlogId(Integer blogId){User user=null;Blog blog=blogMapper.selectBlogById(blogId);if(blog!=null && blog.getUserId()>0){user=userMapper.selectById(blog.getUserId());}if(user!=null){user.setPassword("");}return user;}
7.6实现用户退出
约定前后端交互接口
[请求]
/user/logout
[响应]
true
实现服务器代码
在 UserController 中添加⽅法:
/*** 注销* @return*/@RequestMapping("/logout")public Result logout(HttpSession session){session.removeAttribute(Constants.USER_INFO_SESSION);return Result.success(true);}
实现客户端代码
客户端代码, 注销改为⼀个a标签, href 设置为logout, 点击的时候就会发送GET/logout请求
<a class="nav-span" href="#" onclick="logout()">注销</a>
在common.js中添加logout⽅法:
function logout(){$.ajax({type:"get",url:"/user/logout",success:function(result){if(result!=null && result.data==true){location.href="blog_login.html";}}});
}
7.7实现发布博客
约定前后端交互接口
[请求]
/blog/add
title=标题&content=正⽂...
[响应]
true 成功
false 失败
实现服务器代码
在 BlogController 中添加⽅法:
/*** 发布博客* @return*/@RequestMapping("/add")public Result addBlog(String title, String content,HttpSession session){if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)){return Result.fail(-1,"标题或内容不能为空");}User user= (User) session.getAttribute(Constants.USER_INFO_SESSION);if(user==null || user.getId()<=0){return Result.fail(-1,"用户不存在");}try{Blog blog=new Blog();blog.setTitle(title);blog.setContent(content);blog.setUserId(user.getId());blogService.insertBlog(blog);}catch (Exception e){return Result.fail(-1,"博客发布失败~");}return Result.success(true);}
在BlogService 中添加⽅法:
public Integer insertBlog(Blog blog){return blogMapper.insertBlog(blog);}
实现客户端代码
给提交按钮添加click事件 <input type=“button” value="发布⽂章"id=“submit” οnclick=“submit()”>
$("#submit").click(function(){$.ajax({type:"post",url:"/blog/add",data:{title:$("#title").val(),content:$("#content").val()},success:function(result){if(result!=null && result.code==200 && result.data==true){location.href="blog_list.html";}else{alert(result.msg);}},error:function(error){if(error!=null && error.status==401){alert("请先登录!!!");}}});});
7.8实现删除/编辑博客
进⼊⽤户详情⻚时, 如果当前登陆⽤户正是⽂章作者, 则在导航栏中显示 “删除” 按钮, ⽤户点击时则删除该⽂章.
需要实现两件事:
- 判定当前博客详情⻚中是否要显示 删除 按钮
- 实现删除逻辑.
约定前后端交互接口
编辑博客
[请求]
/blog?BlogId=1
[响应]
{blogId: 1,title: "第⼀篇博客",content: "博客正⽂",userId: 1,postTime: "2021-07-07 12:00:00",loginUser: 1
}
删除博客
[请求]
GET /blog/delete?blogId=1
[响应]
true 删除成功
实现服务器代码
在 BlogController 中添加⽅法:
/*** 更新博客* @param blog* @return*/@RequestMapping("/updateBlog")public Result updateBlog(Blog blog){if(!StringUtils.hasLength(blog.getTitle()) || !StringUtils.hasLength(blog.getContent()) || blog.getId()==null){return Result.fail(-1,"标题或内容不合法");}blogService.updateBlog(blog);return Result.success(true);}/*** 删除博客* @return*/@RequestMapping("/deleteBlog")public Result deleteBlog(Integer blogId){if(blogId==null){return Result.fail(-1,"博客不存在~");}Blog blog=new Blog();blog.setId(blogId);blog.setDeleteFlag(1);blogService.updateBlog(blog);return Result.success(true);}
}
在BlogService 中添加⽅法:
public Integer updateBlog(Blog blog){return blogMapper.updateBlog(blog);}
实现客户端代码
删除博客:
function deleteBlog(){$.ajax({type:"post",url:"/blog/deleteBlog" + location.search,success:function(result){if(result!=null && result.code==200 && result.data==true){location.href="blog_list.html";}else{alert(result.msg);}},eeror:function(error){if(error!=null && error.status==401){//用户未登录location.assign("blog_login.html");}}});}
编辑博客:
//获取博客的详细内容,并且反应到页面上$.ajax({type:"get",url:"/blog/getBlogDetail" + location.search,success:function(result){if(result!=null && result.code==200 && result.data!=null){var blog=result.data;$("#blogId").val(blog.id);$("#title").val(blog.title);$("#content").val(blog.content);}else if(result!=null){alert(result.msg);}},error:function(error){if(error!=null && error.status==401){//用户未登录location.assign(blog_login.html);}}});$("#submit").click(function(){$.ajax({type:"post",url:"/blog/updateBlog",data:{id:$("#blogId").val(),title:$("#title").val(),content:$("#content").val()},success:function(result){if(result!=null && result.code==200 && result.data==true){location.href="blog_list.html";}else if(result!=null){alert(result.msg);}}});});</script>
7.9实现加密加盐
加密工具类
使用md5进行密码加密:
public class SecurityUtils {/*** 加密* 根据明文,返回密文(salt+加密后的密文)* @return*/public static String encry(String inputPassword){//生成盐值String salt= UUID.randomUUID().toString().replace("-","");//md5加密(明文+盐值)String password= DigestUtils.md5DigestAsHex((inputPassword+salt).getBytes());return salt+password;}/*** 验证密码是否正确* @return*/public static boolean decrypt(String inputPassword,String finalPassword){//判空if(!StringUtils.hasLength(inputPassword) || !StringUtils.hasLength(finalPassword)){return false;}//验证长度if(finalPassword.length()!=64){return false;}//验证密码String salt=finalPassword.substring(0,32);String password= DigestUtils.md5DigestAsHex((inputPassword+salt).getBytes());return (salt+password).equals(finalPassword);}
}
使用
//验证密码User user=userService.selectByName(username);if(user==null || !SecurityUtils.decrypt(password,user.getPassword())){return Result.fail(-2,"用户名密码错误");}
修改数据库密码
使⽤测试类给密码123456⽣成密⽂:
e2377426880545d287b97ee294fc30ea6d6f289424b95a2b2d7f8971216e39b7
执行SQL:
update user set password='e2377426880545d287b97ee294fc30ea6d6f289424b95a2b2
d7f8971216e39b7' where id=1;