目录
- 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 {
@ExceptionHandler
public Result error(Exception e){
return Result.fail(-1,e.getMessage());
}
}
数据统一返回格式:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
//在数据返回之前进行处理
@SneakyThrows //异常处理注解
@Override
public 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 {
@Autowired
private BlogService blogService;
@RequestMapping("/getlist")
public List<Blog> getBlogList(){
return blogService.selectAllBlog();
}
}
在BlogService 中添加⽅法:
public class BlogService {
@Autowired
private 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 {
@Autowired
private 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,"用户名密码错误");
}
//设置session
HttpSession session= request.getSession(true);
session.setAttribute(Constants.USER_INFO_SESSION,user);
return Result.success("登录成功");
}
}
在UserService 中添加⽅法:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private 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 {
@Override
public 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"
);
@Autowired
private LoginInterceptor loginInterceptor;
//添加拦截器
@Override
public 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;