相比于之前使用Servlet来完成的博客系统,SpringBoot版本的博客系统功能更完善,使用到的技术更接近企业级,快来看看吧~
目录
1.项目介绍
2.数据库准备
3.实体化类
4.返回格式
5.登录和注册功能
6.登出(注销)功能
7.判定是否登录
8.添加、修改、查询、删除文章
9.查询文章列表、分页、展示阅读次数
查询文章列表
展示阅读次数
分页功能
10.拦截器
11.统一异常处理和统一数据返回格式
统一异常处理
统一数据返回格式
12.加盐处理
本项目集成了用户注册、用户登录、用户登出(注销)、验证登录状态、添加文章、查询文章、修改文章、展示阅读次数、文章列表查询、删除文章、分页功能。
其中,使用了统一功能处理返回的数据,对密码进行了加盐加密操作,使用拦截器完成验证用户登录状态。
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(65) not null,
photo varchar(500) default '',
createtime timestamp default current_timestamp,
updatetime timestamp default current_timestamp,
`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 timestamp default current_timestamp,
uid int not null,
rcount int not null default 1,
`state` int default 1
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2024-2-12 17:10:48', '2024-2-12 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正文',1);
在createtime和updatetime之前加上时间格式化注释JsonFormat,可以统一时间格式。
在和前端进行交互的时候,我们新建一个类来完成对所有返回的结果的规定。
包括了返回的状态码code,返回的信息msg,返回的数据data。并且重载success和fail方法。
每个controller返回给前端的数据就是一个AjaxResult success()或者AjaxResult fail();
@Data
public class AjaxResult implements Serializable {
private Integer code;
private String msg;
private Object data;
public static AjaxResult success(Object data) {
AjaxResult ajaxResult = new AjaxResult();
ajaxResult.setCode(200);
ajaxResult.setMsg("");
ajaxResult.setData(data);
return ajaxResult;
}
public static AjaxResult success(Object data, String msg) {
AjaxResult ajaxResult = new AjaxResult();
ajaxResult.setCode(200);
ajaxResult.setMsg(msg);
ajaxResult.setData(data);
return ajaxResult;
}
public static AjaxResult fail(Integer code, String msg) {
AjaxResult ajaxResult = new AjaxResult();
ajaxResult.setCode(code);
ajaxResult.setMsg(msg);
ajaxResult.setData("");
return ajaxResult;
}
public static AjaxResult fail(Integer code, String msg, Object data) {
AjaxResult ajaxResult = new AjaxResult();
ajaxResult.setCode(code);
ajaxResult.setMsg(msg);
ajaxResult.setData(data);
return ajaxResult;
}
}
insert into userinfo(username, password) value (#{username},#{password})
@Mapper
public interface UserMapper {
int reg(UserInfo userInfo);
UserInfo login(@Param("username") String username);
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public int reg(UserInfo userInfo) {
return userMapper.reg(userInfo);
}
public UserInfo login(String username) {
return userMapper.login(username);
}
}
@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.getUsername())) {
return AjaxResult.fail(-1, "参数非法");
}
userInfo.setPassword(PasswordTooles.encrypt(userInfo.getPassword()));
int result = userService.reg(userInfo);
return AjaxResult.success(result);
}
@RequestMapping("/login")
public AjaxResult login(String username, String password, HttpServletRequest request) {
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return AjaxResult.fail(-1, "参数非法");
}
UserInfo userInfo = userService.login(username);
if (userInfo == null || userInfo.getId() <= 0) {
return AjaxResult.fail(-2, "用户名或密码错误");
}
if (PasswordTooles.decrypt(password, userInfo.getPassword())) {
return AjaxResult.fail(-2, "用户名或密码错误");
}
HttpSession session = request.getSession();
session.setAttribute(ApplicationVariale.SESSION_USERINFO_KEY,userInfo);
return AjaxResult.success(1);
}
在登录过程中,用到了session来判定登录状态,所以在传形参的时候,不光是要传用户名和密码,还需要把request也一起传输过去,用来设置当前session。因为session有多个地方需要使用,所以把session单独拿出来定义:
public class ApplicationVariale {
public static final String SESSION_USERINFO_KEY = "SESSION_KEY_USERINFO";
}
只需要使用removeAttribute来移除session就可以实现注销功能。
@RequestMapping("/logout")
public AjaxResult logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
session.removeAttribute(ApplicationVariale.SESSION_USERINFO_KEY);
return AjaxResult.success(1);
}
在某些页面,需要判定用户是否登录了,比如在博客正文的页面,就需要根据用户登录情况来显示不同的按钮。未登录则显示登录按钮,已经登录了则显示博客主页按钮。
@RequestMapping("/islogin")
public AjaxResult isLogin(HttpServletRequest request) {
if (UserSessionTools.getLoginUser(request) == null) {
return AjaxResult.success(0);
}
return AjaxResult.success(1);
}
并且因为判定登录功能可能会多次使用到,所以把相关的功能写到common中,方便调用。传递的参数是request,用来判定当前session的情况。
public class UserSessionTools {
public static UserInfo getLoginUser(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute(ApplicationVariale.SESSION_USERINFO_KEY) != null) {
return (UserInfo) session.getAttribute(ApplicationVariale.SESSION_USERINFO_KEY);
}
return null;
}
}
insert into articleinfo(title,content,uid)
values(#{title},#{content},#{uid})
update articleinfo set title=#{title},content=#{content},updatetime=#{updatetime}
where id=#{id} and uid=#{uid}
delete from articleinfo where id = #{id} and uid = #{uid}
@Mapper
public interface ArticleMapper {
int add(ArticleInfo articleInfo);
ArticleInfo getDetailByIdAndUid(@Param("id")Integer id,@Param("uid")Integer uid);
int update(ArticleInfo articleInfo);
int del(@Param("id") Integer id, @Param("uid") Integer uid);
@Service
public class ArticleService {
@Autowired
private ArticleMapper articleMapper;
public int add(ArticleInfo articleInfo){
return articleMapper.add(articleInfo);
}
public ArticleInfo getDetailByIdAndUid(Integer id,Integer uid){
return articleMapper.getDetailByIdAndUid(id,uid);
}
public int update(ArticleInfo articleInfo){
return articleMapper.update(articleInfo);
}
public int del(Integer id, Integer uid) {
return articleMapper.del(id, uid);
}
@RestController
@RequestMapping("/art")
public class ArticleController {
@Autowired
private ArticleService articleService;
@RequestMapping("/add")
public AjaxResult add(ArticleInfo articleInfo, HttpServletRequest request){
if (articleInfo == null ||
!StringUtils.hasLength(articleInfo.getTitle()) ||
!StringUtils.hasLength(articleInfo.getContent())){
return AjaxResult.fail(-1,"参数异常");
}
UserInfo userInfo = UserSessionTools.getLoginUser(request);
articleInfo.setUid(userInfo.getId());
int result = articleService.add(articleInfo);
return AjaxResult.success(result);
}
@RequestMapping("/getdetailbyid")
public AjaxResult getDetailByIdAndUid(Integer id,HttpServletRequest request){
if(id == null || id <=0){
return AjaxResult.fail(-1,"参数非法");
}
UserInfo userInfo = UserSessionTools.getLoginUser(request);
return AjaxResult.success(articleService.getDetailByIdAndUid(id, userInfo.getId()));
}
@RequestMapping("/update")
public AjaxResult update(ArticleInfo articleInfo,HttpServletRequest request){
if(articleInfo == null || articleInfo.getId() <= 0 || !StringUtils.hasLength(articleInfo.getContent()) || !StringUtils.hasLength(articleInfo.getTitle())){
return AjaxResult.fail(-1,"参数有误");
}
UserInfo userInfo = UserSessionTools.getLoginUser(request);
articleInfo.setUid(userInfo.getId());
articleInfo.setUpdatetime(userInfo.getUpdatetime());
return AjaxResult.success(articleService.update(articleInfo));
}
@RequestMapping("/del")
public AjaxResult del(Integer id, HttpServletRequest request) {
if (id == null || id <= 0) {
return AjaxResult.fail(-1, "参数错误");
}
UserInfo userInfo = UserSessionTools.getLoginUser(request);
int result = articleService.del(id, userInfo.getId());
return AjaxResult.success(result);
}
可以看到,几乎所有的方法都是先对传入的参数进行非空校验,再对session的情况进行判定。实现都很简单。
在getDetailByIdAndUid和del中,一次性传入了文章id和uid。这是为了校验当前查看文章的用户是文章的作者,只有这种情况才能够对文章进行后续的修改和删除。
相比于前面的增删查改操作来说,这个部分要复杂很多。
List getListByUid(@Param("uid") Integer id);
public List getListByUid(Integer id) {
return articleMapper.getListByUid(id);
}
@RequestMapping("/mylist")
public AjaxResult mylist(HttpServletRequest request) {
UserInfo userInfo = UserSessionTools.getLoginUser(request);
List list = articleService.getListByUid(userInfo.getId());
//todo:将文章正文截取成文章摘要
for (ArticleInfo item : list) {
String content = StringTools.subLength(item.getContent(), 200);
item.setContent(content);
}
return AjaxResult.success(list);
}
定义一个先rcount变量,记录文章的阅读次数。先做查询操作,等前面页面刷新时,rcount自增1并且存储到数据库中。
update articleinfo
set rcount=rcount + 1
where id = #{id}
public Integer getCount() {
return articleMapper.getCount();
}
@RequestMapping("/getcount")
public AjaxResult getCount() {
return AjaxResult.success(articleService.getCount());
}
分页功能也就是实现这四个功能。
我们先定义几个变量:
- pageIndex:记录页面当前的页码(从1开始)
- pageSize:记录每页最大条数
- pageCount:记录总页数
- offset:数据库中从第几条开始查询
首页功能很好实现,规定当前页码是1,offset只需要是0开始就行了。
当设置pageSize为2时,总页数和offset都可以被算出来。同时需要再数据库中查询两次,一次是根据相关的参数查询文章,另一次是查询一共有多少条文章。
例如当前是第一页,查询的文章为从第0条开始查询,当前页码为1,offset为0。
如果到了第二页,查询的文章从第2条开始查询,当前页码为1,offset为2。
也就是说,offset有如下公式:offset = (pageIndex - 1)× pageSize
@RequestMapping("/getlistbypage")
public AjaxResult getListByPage(Integer pageSize, Integer pageIndex) {
if (pageSize == null || pageSize == 0) {
pageSize = 2;
}
if (pageIndex == null || pageIndex <= 1) {
pageIndex = 1;
}
int offset = (pageIndex - 1) * pageSize;
List list = articleService.getListByPage(pageSize, offset);
list.stream().parallel().forEach((item -> {
item.setContent(StringTools.subLength(item.getContent(), 150));
}));
return AjaxResult.success(list);
}
同时用一个list来存储相关的文章,通过list.stream.forEach来遍历,并且可以使用parallel()多线程处理,效率更快。
处理完了首页、上一页和下一页,末页需要用到getCount,获取到一共有多少条文章,然后再根据这个数字来判定总页数,通过getCount/pageSize就可以得到。得到总页数后,再传回数据库查询,得到末页的文章。
虽然前面已经对session做了判定,判断用户是否登录,但是我们用一个统一的拦截器来完成对代码的过滤等等。
比如在登入一个页面时,如果要求用户密码、权限等的验证,就可以用自定义的拦截器进行密码验证和权限限制。对符合的登入者才跳转到正确页面。这样如果有新增权限的话,不用在controller里修改任何代码,直接在interceptor里修改就行了。
Spring中的拦截器是通过动态代理的方式实现和环绕通知的方式实现的,并且是作用域controller之前,先把相关的请求预处理完,再进行程序的正常流程。
@Configuration
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(ApplicationVariale.SESSION_USERINFO_KEY) != null) {
return true;
}
response.sendRedirect("/login.html");
return false;
}
}
需要实现一个LoginInterceptor并且implements HandlerInterceptor
重写preHandle方法,来判定用户是否登录的功能,并且把需要登录才能访问的内容添加到MyConfig中。
首先添加所有的路径,然后把不需要的路径排除掉,就是拦截器拦截的内容。简单的理解为,用户需要登录才能够访问的内容都被拦截器拦截了,不需要登录就能够访问的则不添加到拦截器中。
对于异常处理来说,最简单直接的方式就是使用 try catch 代码块来捕获系统异常。但是这种处理方式需要我们编写大量的代码,而且异常信息不易于统一维护,增加了开发工作量,甚至可能还会出现异常没有被捕获的情况。为了能够高效的处理好各种系统异常,我们需要在项目中统一集中处理我们的异常。
@RestControllerAdvice
public class MyExceptionAdvice {
@ExceptionHandler(Exception.class)
public AjaxResult doException(Exception e) {
return AjaxResult.fail(-1, e.getMessage());
}
}
@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知, 也就是执行某个方法事件。
在所有的controller中,我们返回给前端的都是一个AjaxResult对象,也就是包含了code、msg、data三个属性的变量。但是如果某个controller中返回的是错误的,假设返回的是1,那么就会出现错误。我们可以规定统一返回的一种格式,有助于帮助前后端程序员沟通。
//统一返回数据格式的封装
//当返回的数据不是AjaxResult的时候转换成AjaxResult
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@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 AjaxResult) {
return body;
}
if (body instanceof String) {
return objectMapper.writeValueAsString(AjaxResult.success(body));
}
return null;
}
}
统⼀的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现。
用户输入的密码到数据库中时,非常不安全,很容易被破解。所以当我们储存到数据库中时,最好存储的是加密的密码。所以我们在服务器中需要完成对密码的加密。通过加盐的方式来处理。
密码如何加盐加密?http://t.csdnimg.cn/9q7lU
public class PasswordTooles {
public static String encrypt(String password) {
//产生盐值
String salt = UUID.randomUUID().toString().replace("-", "");
//使用盐值+明文密码得到加密的密码
String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
//将盐值和加密的密码共同返回
String dbPassword = salt + "$" + finalPassword;
return dbPassword;
}
//验证加盐加密密码
public static boolean decrypt(String password, String dbPassword) {
boolean result = false;
if (StringUtils.hasLength(password) && StringUtils.hasLength(dbPassword) && dbPassword.length() == 65 && dbPassword.contains("$")) {
String[] passwordArr = dbPassword.split("\\$");
String salt = passwordArr[0];
String checkPassword = encrypt(password);
if (dbPassword.equals(checkPassword)) {
result = true;
}
}
return result;
}
}