在线体验项目:登陆页面
项目连接:huhublog_ssm: 个人博客系统 技术栈:SpringBoot、SpringMVC、Mybatis、Redis、JQuery、Ajax、Json (gitee.com)
1.项目技术点分析
SpringBoot、SpringWeb(SpringMVC)、MyBatis、MySQL(8.x)、Redis(存储验证码及用户登录信息)
2.数据库设计分析
注册页面、登录页面 --> 用户表(显式字段:用户编号、登录用户名、密码) 不需要(确认密码只是前端使用一下,验证码存到redis)(隐式字段:用户状态(正常,异常,被永久冻结,被临时冻结))
博客列表页面 --> 用户表(显示字段:头像、昵称、个人gitee地址)
博客列表、添加、修改页面、文章详情页面 --> 文章表(显式字段:文章编号、标题、创建时间、修改时间、文章详情、文章正文)(隐式字段:文章状态(发布状态、草稿)、访问量、用户主键(文章作者id))
文章详情页面 --> 评论表(显式字段:评论表主键、文章编号、用户主键(评论发表人是谁)、评论的正文、评论的发表时间)
3.准备工作
3.1准备数据库
-- 创建数据库
drop database if exists huhublog;
create database huhublog DEFAULT CHARACTER SET utf8mb4;-- 使用数据库
use huhublog;-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(uid bigint auto_increment primary key comment '用户编号',username varchar(100) not null unique comment '用户名', nickname varchar(100) default '' comment '昵称',password varchar(65) not null comment '密码',gitee varchar(255) comment 'gitee地址',photo varchar(255) comment '头像',state tinyint not null default 1 comment '用户状态,1=正常|2=异常|3=永久冻结|4=临时冻结'
) default charset 'utf8mb4';-- 创建表[文章表]
drop table if exists articleinfo;
create table articleinfo(aid bigint auto_increment primary key comment '文章编号',title varchar(100) not null comment '文章标题',createtime timestamp not null default CURRENT_TIMESTAMP comment '创建时间',updatetime timestamp not null default CURRENT_TIMESTAMP comment '修改时间',description varchar(255) not null comment '文章简介',content longtext not null comment '文章正文',state tinyint default 1 comment '状态:1=草稿|2=发布',uid bigint not null comment '作者id',rcount bigint default 1 comment '阅读量'
)default charset 'utf8mb4';-- 创建表[评论表]
drop table if exists commentinfo;
create table commentinfo(cid bigint auto_increment primary key comment '评论编号',aid bigint not null comment '文章编号',uid bigint not null comment '用户编号',content varchar(255) not null comment '评论正文',createtime timestamp default CURRENT_TIMESTAMP() comment '评论发表时间'
)default charset 'utf8mb4';
总共三张表用户表、文章表、评论表
3.2搭建开发环境创建项目
3.3准备项目的配置文件
在resources 文件下创建一个 application.properties 的文件, 并根据自己的数据库信息和mapper 文件夹的命名将以下 xml 文件配置到 application.properties 文件中.
# 配置连接的 mysql 数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/huhublog?characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=2337144
# 设置 Mybatis 的 xml 保存路径
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
# 配置 Mybatis 执行的SQL
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging.level.com.huhu=debug
准备好properties配置文件后, 根据里面的mapper-locations中的约定创建一个叫做 mapper 的文件夹, 并在文件夹下创建三个以 Mapper.xml 为后缀名的文件.
在.xml文件添加格式代码拿UserMapper举例
<?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.huhu.mapper.UserMapper"></mapper>
3.4准备项目的公共模块
1控制层(controller) - 控制器
2业务层(service) - 业务类
3持久层(mapper) - mapper
4实体层(entity) - 实体类
5工具层(util) - 统一返回结果类
3.4.1控制层(拿User举例)
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;
}
3.4.2业务层(这里只显示接口实现类,具体代码看网址)
@Service
public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;
}
3.4.3持久层
@Mapper
public interface UserMapper {
}
3.4.4实体层
@Data
public class UserInfo {private long uid;private String username;private String nickname;private String password;private String gitee;private String photo;private int state;
}
@Data
public class ArticleInfo {private long aid;private String title;private String description;private LocalDateTime createtime;private LocalDateTime updatetime;private String content;private int state; //文章状态private long uid; //作者idprivate long rcount; //l阅读人数
}
@Data
public class CommentInfo {private long cid;private long aid;private long uid;private String content;private LocalDateTime createtime;
}
3.4.5工具层(统一返回类)
1.使用自定义拦截器LoginInterceptor
@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(Constant.SESSION_USERINFO_KEY) != null) {return true;}// 401 : 用户没有登录所有没有权限 403 : 用户登录了但没有权限response.setStatus(401);return false;}
}
2.配置拦截规则AppConfig
@Configuration
public class AppConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;List<String> excludes = new ArrayList<String>() {{add("/**/*.html");add("/js/**");add("/editor.md/**");add("/css/**");// add("/**/*.jsp"); // 这样写穷举不完add("/img/**"); // 放行 img 下的所有文件add("/user/login"); // 放行登录add("/user/reg"); // 放行注册add("/art/detail"); // 放行详情页add("/art/rcount");// 放行访问人数add("/getcaptcha");// 放行验证码add("/image/**");// 放行本地图片验证码add("/art/list"); // 放行文章分页列表的接口add("/art/totalpage"); // 放行获取文章分页的总页数add("/comment/list"); // 放行评论列表add("/user/myinfo");add("/user/info");add("/user/isartbyme");}};@Overridepublic void addInterceptors(InterceptorRegistry registry) {InterceptorRegistration registration =registry.addInterceptor(loginInterceptor);registration.addPathPatterns("/**");registration.excludePathPatterns(excludes);}
}
3.统一异常的处理
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {@ExceptionHandler(Exception.class) // 异常类型public Object exceptionAdvice(Exception e) {return AjaxResult.fail(-1, e.getMessage());}
}
4.统一数据的返回
对返回的数据进行分类:成功返回/失败
定义一个数据返回类 - AjaxResult.java
/*** 接口统一返回对象*/
public class AjaxResult {/*** 业务执行成功时进行返回的方法* @param data* @return*/public static HashMap<String, Object> success(Object data) {HashMap<String, Object> result = new HashMap<>();result.put("code", 200);result.put("msg", "");result.put("data", data);return result;}/*** 业务执行成功时进行返回的方法* @param data* @return*/public static HashMap<String, Object> success(String msg, Object data) {HashMap<String, Object> result = new HashMap<>();result.put("code", 200);result.put("msg", msg);result.put("data", data);return result;}/*** 业务执行失败时进行返回的方法* @param code* @param msg* @return*/public static HashMap<String, Object> fail(int code, String msg) {HashMap<String, Object> result = new HashMap<>();result.put("code", code);result.put("msg", msg);result.put("data", "");return result;}/*** 业务执行失败时进行返回的方法* @param code* @param msg* @param data* @return*/public static HashMap<String, Object> fail(int code, String msg, Object data) {HashMap<String, Object> result = new HashMap<>();result.put("code", code);result.put("msg", msg);result.put("data", data);return result;}
}
定义一个将返回数据类进行统一处理的类 - ResponseBodyAdvice.java
@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) {// 1.本身已经是封装好的对象if(body instanceof HashMap) {return body;}// 2.返回类型是 String (特殊)if(body instanceof String) {ObjectMapper objectMapper = new ObjectMapper();return objectMapper.writeValueAsString(AjaxResult.success(body));}return AjaxResult.success(body);}
}
5.统一session验证
public class SessionUtil {public static UserInfo getLoginUser(HttpServletRequest request) {HttpSession session = request.getSession(false);if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {return (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);}return null;}
}
6.统一常量类
此处的常量类主要是用户登录成功后, 将其身份信息存储 session 时, 需要多次写字符串常量, 为了避免写错, 所以专门定义一个常量类, 就只需要写一次即可.
public class Constant {public static final String SESSION_USERINFO_KEY = "SESSION_USERINFO_KEY";
}
4.实现相关功能
4.1实现注册功能
前端操作reg.html页面
第一步: 引入用于发送 ajax 请求的依赖
<script src="js/jquery.min.js"></script>
第二步 :约定前后端交互接口
url :"/user/reg"
type : "POST"
data : "username,password"
后端返回 data=1, code=200 表示代码执行成功, 返回 data != 1, code != 200 表示执行失败.
第三步:写前端代码
1.分别给用户名、密码、确认密码、验证码加上id属性,并给提交按钮绑定点击事件
<!-- 中间的注册框 --><div class="login-dialog" style="height: 460px;"><h3>注册</h3><div class="row"><span>登录名</span><input type="text" id="username"></div><!--<div class="row"><span>昵称</span><input type="text" id="nickname"></div>--><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row"><span>确认密码</span><input type="password" id="password2"></div><div class="row" style="margin-bottom: 20px;"><span>验证码</span><input id="code_input" style="width: 66px;"> <img onclick="refreshCode()" id="codeimg" src="img/check_code.png"style="height: 50px;width: 128px;"></div><div class="row"><button id="submit" onclick="doReg()">提交</button></div></div>
2.点击提交按钮触发ajax请求
function doReg() {// 进行非空校验var username=jQuery('#username')var password=jQuery('#password')var password2=jQuery('#password2')var checkCode=jQuery('#code_input')if(username.val().trim()==''){alert('请输入用户名!')username.focus()return false}if(password.val().trim()==''){alert('请输入密码!')password.focus()return false}if(password2.val().trim()==''){alert('请输入确认密码!')password2.focus()return false}if(checkCode.val().trim()==''){alert('请输入验证码!')checkCode.focus()return false}// 密码与确认密码是否一致if(password.val()!=password2.val()){alert('两次密码输入不一致,请重新输入密码!')password.focus()return false}// 发送Ajax请求jQuery.ajax({url:'/user/reg',type:'post',data:{"username":username.val(),"password":password.val(),"checkCode":checkCode.val()},// 接受返回结果success:function(body) {if(body.code==200 && body.data==1) {alert("恭喜,注册成功!");if(confirm("是否要去登录页面 ?")) {location.href = "login.html";}} else if(body.data == -1) {alert("抱歉, 注册失败, 请重新注册!")} else {alert("该用户名已被使用, 请重新输入!");}}}) }
第四步写后端代码
注册功能即数据库新增用户功能
步骤一:先从mapper层写起(UserMapper)
// 新增用户(注册功能)public int addUser(UserInfo userInfo);
步骤二:写配置文件.xml(UserMapper.xml)
<insert id="addUser">insert into userinfo(username,password) values (#{username},#{password})
</insert>
步骤三:写service层代码(UserServiceImpl)
@Overridepublic int addUser(UserInfo userInfo) {return userMapper.addUser(userInfo);}
步骤四:写controller层代码(UserController)
/*** 注册功能实现* @param userInfoVO* @return*/@RequestMapping("/reg")public Object reg(UserInfoVO userInfoVO) {// 1.非空校验if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())|| !StringUtils.hasLength(userInfoVO.getPassword())|| !StringUtils.hasLength(userInfoVO.getCheckCode())) {return AjaxResult.fail(-1, "非法参数请求!");}// 2.进行数据库添加操作int result = userService.addUser(userInfoVO);if (result == 1) {return AjaxResult.success("注册成功!", 1);} else {return AjaxResult.fail(-1, "数据库添加出错!");}}
此处先进行明文存储, 方便后续操作; 等全部功能基本实现时, 再将密码升级为加盐存储.
另外注册页面导航栏的右上角只有登录和主页两个按钮:
登录 : 有些用户已经有账号了, 就可以点击跳转到登录页面;
主页 : 没有登录也是可以访问主页别人写的博客的(例如CSDN、博客园等)
页面效果:
4.2实现登录功能
前端操作login.html页面
第一步: 引入用于发送 ajax 请求的依赖
<script src="js/jquery.min.js"></script>
第二步 :约定前后端交互接口
url :"/user/login"
type : "POST"
data : "username,password"后端返回 data=1, code=200 表示代码执行成功, 返回 data != 1, code != 200 表示执行失败.
第三步:写前端代码
1.分别给用户名、密码、确认密码、验证码加上id属性,并给提交按钮绑定点击事件
<!-- 中间的登陆框 --><div class="login-dialog"><h3>登陆</h3><div class="row"><span>用户名</span><input type="text" id="username"></div><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row" style="margin-bottom: 20px;"><span>验证码</span><input id="code_input" style="width: 66px;"> <img onclick="refreshCode()" id="codeimg" src="img/check_code.png"style="height: 50px;width: 128px;"></div><div class="row"><button id="submit" onclick="doLogin()">提交</button></div></div>
2.点击提交按钮触发ajax请求
function doLogin() {// 非空参数的校验var username=jQuery('#username')var password=jQuery('#password')var checkCode=jQuery('#code_input')if(username.val().trim()==''){alert('请输入用户名!')username.focus()return false}if(password.val().trim()==''){alert('请输入密码!')password.focus()return false}if(checkCode.val().trim()==''){alert('请输入验证码!')checkCode.focus()return false}// 2. 发送请求给客户端jQuery.ajax({url:"/user/login",type:"post",data:{// 前面的 username 可以加引号, 也可以不加username:username.val(),password:password.val(),checkCode:checkCode.val()},success:function(body) {if(body.code == 200 && body.data == 1) {// alert("登录成功!");location.href = "myblog_list.html";} else {alert("用户名或密码错误, 请重新输入!");username.focus();}}});
}
第四步:写后端代码
登录功能即查找用户是否存在
步骤一:先mapper层写起
// 查询用户public UserInfo getUserByUsername(@Param("username") String username);
步骤二:写映射配置文件.xml
<select id="getUserByUsername" resultType="com.huhu.entity.UserInfo">select * from userinfo where username=#{username}</select>
步骤三:写service层代码
@Overridepublic UserInfo getUserByUsername(String username) {return userMapper.getUserByUsername(username);}
步骤四:写controller层代码
/*** 登录功能实现* @param userInfoVO* @param request* @return*/@RequestMapping("/login")public int login(UserInfoVO userInfoVO, HttpServletRequest request) {// 1.非空校验if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())|| !StringUtils.hasLength(userInfoVO.getPassword())|| !StringUtils.hasLength(userInfoVO.getCheckCode())) {return 0;}// 2.查询操作// 2.1.通过用户名查找对象UserInfo userInfo = userService.getUserByUsername(userInfoVO.getUsername());// 2.2 如果对象为空或者为无效对象,则返回-1if (userInfo == null || userInfo.getUid() <= 0) {return -1;}// 2.3 如果用户输入的密码与数据库输入密码不一致,则返回-1if (!userInfoVO.getPassword().equals(userInfo.getPassword())) {return -1;} else {HttpSession session = request.getSession();session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);return 1;}}
此处的登录功能也是没有通过明文查询, 后面升级为加盐处理时, 会有对应的解密方法来处理, 后面再进行代码的大改.
另外登录页面导航栏的右上角只有注册和主页两个按钮:
注册 : 有些用户需要注册, 但是不小心点到登录界面, 所以对于用户来说, 两个页面需要来回跳转.
主页 : 像CSDN网站没有登录也是可以访问主页别人写的博客的.
页面效果:
此处就可以在工具包下添加:常量类Constant存储用户信息、拦截器LoginInterceptor类和AppConfig配置类
4.3实现退出登录功能
前端操作myblog_list.html页面
第一步: 引入用于发送 ajax 请求的依赖
<script src="js/jquery.min.js"></script>
第二步 :约定前后端交互接口
url :"/user/logout"
type : "POST"
data : {} //不传,即空对象
后端如果返回状态码200,就触发success分支,如果返回状态码401,触发error分支,两分支一样,都是跳转到登录页面
第三步:写前端代码
function onExit() {if(confirm("确认退出? ")) {jQuery.ajax({url:"/user/logout",type:"POST",data:{},// 200success:function(body) {// alert(JSON.stringify(body)); // 将 json 转换成 Stringlocation.href = "/login.html";},// 非 200error:function(err) {// alert(JSON.stringify(err))if(err != null && err.status==401) {alert("用户未登录, 即将跳转到登录页!")// 已经被拦截器拦截了 - 未登录location.href = "/login.html";}}});}}
第四步:写后端代码
// 退出登录 【拦截】@RequestMapping("/logout")public boolean logout(HttpServletRequest request) {// 设置false原因是:如果request里面没有session,那么不会去新建sessionHttpSession session = request.getSession(false);if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {// 清除 sessionsession.removeAttribute(Constant.SESSION_USERINFO_KEY);}// 默认返回 true, 如果不清除 session, session 也有一个过期时间 30分钟return true;}
注意:登录情况下点击退出登录结果如图
未登录情况下点击退出登录结果如图
4.4实现登录人的博客列表页
前端操作myblog_list.html
该页面要实现的功能:
1. 展现登录人写的所有博客, 如果此人没写博客, 就显示暂无数据.
2. 在左侧展示登录人的个人信息, 此处要动态展示的个人信息有用户名(username)和以及文章数量
功能一:展示所有博客
第一步: 引入用于发送 ajax 请求的依赖
<script src="js/jquery.min.js"></script>
第二步:约定前后端接口
url : "art/mylist"
type : "GET"
data : 不传, 让后端从 session 中拿, 避免出现张三传了李四的 id, 把李四的文章查出来了.
后端返回 code == 200, 如果 data 不为空, 且 data.length > 0 表示该作者写了文章, 否则表示该作者没有文章. 后端返回非 200 就走 error 分支.
第三步:写前端代码
// 获取右侧文章列表function initMyList() {jQuery.ajax({url: '/art/mylist',type: 'GET',data: {},success: function (body) {// 获取右侧的容器var rightContainer = document.querySelector('.container-right');if (body.code == 200 && body.data != null && body.data.length > 0) {// 遍历body的博客列表for (let blog of body.data) {let blogDiv = document.createElement('div');blogDiv.className = 'blog';// 创建博客标题let titleDiv = document.createElement('div');titleDiv.className = 'title';titleDiv.innerHTML = blog.title;blogDiv.appendChild(titleDiv);// 创建发布日期let dateDiv = document.createElement('div');dateDiv.className = 'date';dateDiv.innerHTML = blog.createtime;blogDiv.appendChild(dateDiv);// 创建博客简介let descDiv = document.createElement('div');descDiv.className = 'desc';// descDiv.id='descMark';// 博客简介即截取文章内容前100个字符var content = subContent(blog.content);descDiv.innerHTML = content;blogDiv.appendChild(descDiv);// 创建一个divlet div = document.createElement('div');div.className = 'ownDetail';// 创建查看详情按钮let a1 = document.createElement('a');a1.innerHTML = '<b>查看详情>></b>';a1.style = 'border:2px solid black;background-color:#eaea9a';a1.href = 'blog_content.html?aid=' + blog.aid;div.appendChild(a1);// 创建修改按钮let a2 = document.createElement('a');a2.innerHTML = '<b>修改>></b>';a2.style = 'border:2px solid black;background-color:#d9ecf2';a2.href = 'blog_edit.html?aid=' + blog.aid;div.appendChild(a2);// 创建删除按钮let a3 = document.createElement('a');a3.innerHTML = '<b>删除>></b>';a3.style = 'border:2px solid black;background-color:#ed9898';a3.href = 'javascript:del(' + blog.id + ')';div.appendChild(a3);// 把这三个按钮添加到外层元素blogDiv.appendChild(div);// 将博客盒子加入右侧容器中rightContainer.appendChild(blogDiv);}} else {// 未发表任何数据let blogDiv = document.createElement('div');blogDiv.innerHTML = "<h1>暂无数据</h1>";blogDiv.style = "text-align: center";rightContainer.appendChild(blogDiv);}},error: function (err) {if (err != null && err.status == 401) {// alert("用户未登录, 即将跳转到登录页!")// 已经被拦截器拦截了 - 未登录location.href = "/login.html";}}})};initMyList();
此处的文章列表页展示的内容只是博客的一部分, 这里规定展示前 100 个字符为博客简介操作, 并且该接口在主页中所有人的列表页也需要调用, 所以将前面的内容截取函数封装在一个公共的 js 方法中.
js/util.js
var descLength=100;
function subContent(content) {if (content.length>descLength){return content.substring(0,descLength);}return content;
}
第四步:写后端代码
步骤一:先从mapper层写起
// 获取我的所有文章public List<ArticleInfo> getMyList(@Param("uid") Long uid);
步骤二:映射配置文件.xml
<select id="getMyList" resultType="com.huhu.entity.ArticleInfo">select * from articleinfo where uid=#{uid}</select>
步骤三:业务层
@Overridepublic List<ArticleInfo> getMyList(Long uid) {return articleMapper.getMyList(uid);}
步骤四:控制层
/*** 获取我的所有文章* @param request* @return*/@RequestMapping("/mylist")public List<ArticleInfo> getMyList(HttpServletRequest request){// 在session中验证登录状态UserInfo userInfo = SessionUtil.getLoginUser(request);// 如果获取用户对象为空或者无效对象if (userInfo==null||userInfo.getUid()<=0){return null;}return articleService.getMyList(userInfo.getUid());}
我的文章列表页要实现的功能还有很多, 先看看整体页面:
我的博客列表页实现的功能有 : 查看详情, 修改 , 删除, 以及左侧个人信息的 username, 以及文章数量. 后面会一一实现.
功能二:获取博客列表页的个人信息
第一步:约定前后端接口
url : "/user/myinfo"
type : "GET"
data : {} //不传
当后端返回 200 , 并且 data 不为空, 就构造数据, 否则就是未登录, 直接跳转到登陆页面.
第二步:写前端代码
// 获取个人信息function getMyInfo() {jQuery.ajax({url:'/user/myinfo',type:'GET',data:{},success:function (body) {let h3=document.querySelector('.card h3');h3.innerHTML=body.data.username;},error:function (err) {location.href='/login.html'}})}getMyInfo();
第三步:写后端代码
/*** 获取个人信息* @param request* @return*/@RequestMapping("/myinfo")public UserInfo myinfo(HttpServletRequest request){return SessionUtil.getLoginUser(request);}
功能三:获取登录人的文章数量
第一步:约定前后端接口
url : "/art/myartcount"
type : "GET"
data : 不传
后端返回 200 状态码并且 data 不为空就构造数据, 否则就是未登录.
第二步:写前端代码
// 获取文章数量function getMyArtCount() {jQuery.ajax({url:'/art/myartcount',type:'GET',data:{},success:function (body) {if (body.code=200&&body.data!=null){let artSpan=document.querySelector('#cnt');artSpan.innerHTML=body.data;}},error:function (err) {location.href='/login.html';}})}getMyArtCount();
第三步:写后端代码
步骤一:写mapper层
// 获取我的文章数量public int myArtCount(@Param("uid") Long uid);
步骤二:映射配置文件.xml
<select id="myArtCount" resultType="java.lang.Integer">select count(*) from articleinfo where uid=#{uid}</select>
步骤三:service层
@Overridepublic int myArtCount(Long uid) {return articleMapper.myArtCount(uid);}
步骤四:controller层
/*** 获取我的文章数量* @param request* @return*/@RequestMapping("myartcount")public int myArtCount(HttpServletRequest request){UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo!=null&&userInfo.getUid()>0){return articleService.myArtCount(userInfo.getUid());}return 0;}
4.5实现公开文章列表分页功能
实现该功能之前, 先要知道一个公式, 下面推算一下这个公式 >>
1. 页码 (pIndex) : 要查询第几页的数据.
2. 每页显示的最大长度的数据 (pSize) : 每页显示多少条数据.
找规律:假设我们设置每页显示2条数据
第一页显示aid=1和aid=2的数据
第二页显示aid=3和aid=4的数据
由此得出一个公式偏移量offset=(pindex-1)*psize,例如第一页偏移量0=(1-1)*2,第二页偏移量2=(2-1)*2,最终分页语法查询后面接上limit psize offset (pindex-1)*psize
公开文章列表顾名思义就是所有人都可以访问,因此不需要拦截,效果如下
我们先处理页面上分页的数据,后面再实现四个按钮功能,先初始化pindex和psize(这两参数可可以从url传递,也可以不传)
// 分页功能的默认参数值var psize = 2; // 每页显示的文章数量var pindex = 1; // 当前页码// 初始化分页功能的的参数,从 url 中获取 pindex 和 psizefunction initPageParam() {var pageindex = getUrlParam("pindex");if (pageindex != "") {pindex = pageindex;}var pagesize = getUrlParam("psize");if (pagesize != "") {psize = pagesize;}}initPageParam();
页面数据功能实现
第一步:约定前后端接口
url : "/art/list"
type : "GET"
data : psize, pindex
后端返回 code == 200 并且 data != null 就构造数据.
第二步:写前端代码
// 查询分页数据function initList() {jQuery.ajax({url: "/art/list",type: "GET",data: {"psize": psize,"pindex": pindex},success: function (body) {let listDiv = document.querySelector("#listDiv");if (body.code == 200 && body.data != null) {// 遍历 body 中的 datafor (let blog of body.data) {let blogDiv = document.createElement('div');blogDiv.className = 'blog';// 创建博客标题let titleDiv = document.createElement('div');titleDiv.className = 'title';titleDiv.innerHTML = blog.title;blogDiv.appendChild(titleDiv);// 创建发布日期let dateDiv = document.createElement('div');dateDiv.className = 'date';dateDiv.innerHTML = formatDate(blog.createtime);blogDiv.appendChild(dateDiv);// 创建摘要let descDiv = document.createElement('div');descDiv.className = 'desc';descDiv.innerHTML = subContent(blog.content);blogDiv.appendChild(descDiv);// 创建查看全文按钮let a = document.createElement('a');a.className = 'listdetail';a.innerHTML = '查看详情 >>';a.href = 'blog_content.html?id=' + blog.id;blogDiv.appendChild(a);// 把 blogDiv 加入外层元素listDiv.appendChild(blogDiv);}}}});}initList();
第三步:写后端代码
步骤一:写mapper层
// 获取分页列表页public List<ArticleInfo> getList(@Param("psize") Integer psize,@Param("offset") Integer offset);
步骤二:映射配置文件.xml
<select id="getList" resultType="com.huhu.entity.ArticleInfo">select * from articleinfo limit #{psize} offset #{offset}</select>
步骤三:写service层
@Overridepublic List<ArticleInfo> getList(Integer psize, Integer offset) {return articleMapper.getList(psize,offset);}
步骤四:写controller层
/*** 获取分页列表* @param psize* @param pindex* @return*/@RequestMapping("/list")public List<ArticleInfo> getList(Integer psize, Integer pindex) {if (psize == null || pindex == null) {return null;}// 分页公式 : 计算偏移量int offset = psize * (pindex - 1);return articleService.getList(psize, offset);}
接下来实现四个按钮功能,给按钮绑定点击事件
<button class="blog-pagnation-item" onclick="firstPage()">首页</button>
<button class="blog-pagnation-item" onclick="prevPage()">上一页</button>
<button class="blog-pagnation-item" onclick="nextPage()">下一页</button>
<button class="blog-pagnation-item" onclick="lastPage()">末页</button>
首页按钮、上一页按钮功能实现
// 首页function firstPage() {location.href = "blog_list.html";}// 上一页function prevPage() {if (pindex <= 1) {alert("当前已经是第一页了!");return false;}pindex = parseInt(pindex) - 1;location.href = "blog_list.html?pindex=" + pindex + "&psize=" + psize;}
末页按钮、下一页按钮功能实现
实现之前需要先统计文章数量,然后计算出分页列表总共有多少页数据,并且该接口也是不能拦截的(因为这也是所有人都可以访问的页面所需的接口)
总页数(向上取整):totalPage = Math.ceil (total * 1.0 / psize)
第一步:约定前后端接口
url : "/art/totalpage"
type : "GET"
data : psize
后端返回 code == 200 并且 data 不为空, 就构造数据.
第二步:写前端代码
// 查询总共有多少页的数据function getTotalPage() {jQuery.ajax({url: "/art/totalpage",type: "get",data: {"psize": psize},success: function (body) {if (body.code == 200 && body.data != null) {totalpage = body.data;}}});}
第三步:写后端代码
步骤一:写mapper层
// 查询数据库总共有多少条数据(所有文章)public int getTotalCount();
步骤二:映射配置文件.xml
<select id="getTotalCount" resultType="java.lang.Integer">select count(*) from articleinfo</select>
步骤三:写service层
@Overridepublic int getTotalCount() {return articleMapper.getTotalCount();}
步骤四:写controller层
/*** 计算分页功能的总页数 【不拦截】* @param psize* @return*/@RequestMapping("/totalpage")// 此处返回包装类, 是为了和前端中的 body.data != null相对应public Integer totalPage(Integer psize) {if (psize != null) {// 数据库中总条数int totalCount = articleService.getTotalCount();// 总页数int totalPage = (int) Math.ceil(totalCount * 1.0 / psize);return totalPage;}return null;}
实现下一页和末页的代码
// 下一页function nextPage() {if (parseInt(pindex) + 1 > totalpage) {alert("当前已经是最后一页了!");return false;}location.href = "blog_list.html?pindex=" + (parseInt(pindex) + 1) + "&psize=" + psize;}// 末页function lastPage() {pindex = totalpage;location.href = "blog_list.html?pindex=" + pindex + "&psize=" + psize;}
4.6实现添加文章功能
添加文章需要登录了才能进行添加文章的操作, 所以需要过拦截器.(前端操作 blog_add.html)
第一步:约定前后端接口
url : "/art/add"
type : "POST"
data : title, content
后端返回 code == 200 并且返回data ==1 , 就表示发布成功. 如果 401, 则表示未登录.
第二步:写前端代码
// 发布文章function mysub() {// alert(editor.getValue()); // 获取值// editor.setValue("#123") // 设置值// 1.非空校验var title=jQuery('#title');if (title.val().trim()==""){alert('请先输入标题');title.focus();return false;}if(editor.getValue().trim()==""){alert('请先输入正文');return false;}// 2.将前端输入的内容发送给后端jQuery.ajax({url:'/art/add',type:'POST',data:{"title":title.val(),"content":editor.getValue()},success:function (body) {// 3.将结果展示给用户if (body.code=200&&body.data==1){alert('文章添加成功!');location.href='myblog_list.html';}else{alert('文章添加失败,请重试!');}},error:function (err) {if (err!=null&&err.status==401){// 用户未登录,即将跳转到登录页// 已经被拦截器拦截了,未登录location.href="/login.html";}}})}
第三步:写后端代码
步骤一:写mapper层
// 添加文章int addArticle(ArticleInfo articleInfo);
步骤二:映射配置文件.xml
<insert id="addArticle">insert into articleinfo(uid,title,description,content) values (#{uid},#{title},#{description},#{content})</insert>
步骤三:写service层
@Overridepublic int addArticle(ArticleInfo articleInfo) {return articleMapper.addArticle(articleInfo);}
步骤四:写controller层
/*** 添加文章* @param articleInfo* @param request* @return*/@RequestMapping("/add")public Object addArtcile(ArticleInfo articleInfo, HttpServletRequest request) {// 1.非空校验(标题,内容)if (articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle())|| !StringUtils.hasLength(articleInfo.getContent())) {return AjaxResult.fail(-1, "非法参数!");}// 2.组装数据(文章作者,标题,内容,简介)UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {// 无效对象或者对象为空return AjaxResult.fail(-2, "请先登录!");}// 作者articleInfo.setUid(userInfo.getUid());String description = articleInfo.getContent();if (description.length()>MAX_DESCRIPTION_LENGTH){description=description.substring(0,MAX_DESCRIPTION_LENGTH);}// 简介articleInfo.setDescription(description);int result = articleService.addArticle(articleInfo);if (result==1){return AjaxResult.success("添加成功!",1);}else{return AjaxResult.fail(-1,"添加失败!");}}
4.7实现修改文章功能
修改博客按钮在我的博客列表页才有, 主页的列表页没有修改功能. 并且修改功能是需要用户登录才能进行操作的, 所以需要过拦截器,前端操作myblog_list.html
实现修改博客有两个大的步骤 :
1. 从数据库中查询出文章的内容和标题, 然后初始化在 Markdown 编辑器上.
2. 修改文章内容/标题, 然后进行提交.
功能一:查询出文章内容和标题
第一步:约定前后端接口
url : "/art/mydetail"
type : "GET"
data : aid
后端返回状态码 200 并且 data 不为空, 就构造数据, 如果是 401 就跳转至登录页面.
第二步:写前端代码
// 获取文章详情function getArtDetail(){var aid=getUrlParam("aid");// 1.非空校验if (aid==null||aid==""){alert('参数有误!')return false;}// 2.发送ajax请求jQuery.ajax({url:'/art/mydetail',type:'GET',data:{aid:aid},success:function (body) {if(body.code == 200 && body.data != null) {// 填充标题jQuery("#title").val(body.data.title);initEdit(body.data.content); // 初始化编译器的值 (待修改文章的正文)} else {// 强行访问别人的文章alert('查询失败, 请重试!');}},error:function (err) {if (err!=null&&err.status==401){// 用户未登录,即将跳转到登录页// 已经被拦截器拦截了,未登录location.href="/login.html";}}})}getArtDetail();
3.写后端代码
步骤一:写mapper层
// 根据aid获取博客详情
public ArticleInfo getDetailByAid(Long aid);
步骤二:映射配置文件.xml
<select id="getDetailByAid" resultType="com.huhu.entity.ArticleInfo">select * from articleinfo where aid=#{aid}</select>
步骤三:写service层
@Overridepublic ArticleInfo getDetailByAid(Long aid) {return articleMapper.getDetailByAid(aid);}
步骤四:写controller层
/*** 根据aid查询博客详情* @param aid* @return*/@RequestMapping("/mydetail")public Object getMyDetail(Long aid){// 1.非空校验if (aid==null||aid<=0){return AjaxResult.fail(-1,"非法参数!");}// 2.操作数据库ArticleInfo articleInfo = articleService.getDetailByAid(aid);// 3.将结果返回给前端return AjaxResult.success(articleInfo);}
功能二:修改文章并提交
第一步:约定前后端接口
url : "/art/update"
type : "POST"
data : aid, title, content
后端返回 code == 200 并且返回data==1 就成功执行, 如果 401 就跳转至登录页面.
第二步:写前端代码
// 修改文章function mysub() {// alert(editor.getValue()); // 获取值// editor.setValue("#123") // 设置值// 1.参数校验var title=jQuery('#title');if (title.val().trim()==""){alert('请先输入标题');title.focus();return false;}if(editor.getValue().trim()==""){alert('请先输入正文');return false;}// 2.将数据发送给后端(文章标题、文章正文、文章id)jQuery.ajax({url:'/art/update',type:'POST',data:{"title":title.val(),"content":editor.getValue(),"aid":aid // 全局变量},success:function (body) {// 3.将结果展示给用户if (body.code==200&&body.data==1){// 文章修改成功alert('文章修改成功!');location.href='/myblog_list.html';}else{alert('文章修改失败!');}},error:function (err) {if (err!=null&&err.status==401){// 用户未登录,即将跳转到登录页// 已经被拦截器拦截了,未登录location.href="/login.html";}}})}
第三步:写后端代码
步骤一:写mapper层
// 修改文章public int updateArticle(ArticleInfo articleInfo);
步骤二:映射配置文件.xml
<update id="updateArticle">update articleinfo set title=#{title},description=#{description}, content=#{content} where aid=#{aid} and uid=#{uid}</update>
步骤三:写service层
@Overridepublic int updateArticle(ArticleInfo articleInfo) {return articleMapper.updateArticle(articleInfo);}
步骤四:写controller层
/*** 修改文章* @param articleInfo* @param request* @return*/@RequestMapping("/update")public Object updateArticle(ArticleInfo articleInfo,HttpServletRequest request){// 1.非空校验if (articleInfo==null||!StringUtils.hasLength(articleInfo.getTitle())||!StringUtils.hasLength(articleInfo.getContent())||articleInfo.getAid()<=0){return AjaxResult.fail(-1,"非法参数!");}// 2.组装数据UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo==null||userInfo.getUid()<=0){return AjaxResult.fail(-2,"请先登录!");}// 作者articleInfo.setUid(userInfo.getUid());String description = articleInfo.getContent();if (description.length()>MAX_DESCRIPTION_LENGTH){description=description.substring(0,MAX_DESCRIPTION_LENGTH);}// 简介articleInfo.setDescription(description);// 3.操作数据库int result = articleService.updateArticle(articleInfo);// 4.返回结果给前端if (result==1){return AjaxResult.success("修改成功!",1);}else{return AjaxResult.fail(-3,"修改失败!");}}
4.8实现删除文章功能
删除博客功能需要用户登录了才能进行操作, 所以需要拦截. 删除博客时需要弹窗是否要删除, 防止用户误点. 并且只有我的博客列比页才有删除功能. 主页的博客列表页是没有的.
前端操作myblog_list.html
第一步:约定前后端接口
url : "/art/delete"
type : "POST"
data : aid
后端返回 code == 200 并且 data == 1 就弹出删除成功. 否则弹出删除失败.
第二步:写前端代码
// 删除博客function del(aid) {if (confirm('确定删除?')){jQuery.ajax({url:'/art/delete',type:'POST',data:{aid:aid},success:function (body) {if (body.code==200&&body.data==1){// alert('删除成功!');location.href=location.href;}else{alert('删除失败,请重试!');}},error:function (err) {if (err!=null&&err.status==401){// alert("用户未登录, 即将跳转到登录页!")// 已经被拦截器拦截了 - 未登录location.href="/login.html";}}})}}
第三步:写后端代码
步骤一:写mapper层
// 删除博客
public int deleteArticle(@Param("aid")Long aid,@Param("uid") Long uid);
步骤二:映射配置文件.xml
<delete id="deleteArticle">delete from articleinfo where aid=#{aid} and uid=#{uid}</delete>
步骤三:写service层
@Overridepublic int deleteArticle(Long aid, Long uid) {return articleMapper.deleteArticle(aid,uid);}
步骤四:写controller层
/*** 删除博客* @param aid* @param request* @return*/@RequestMapping("/delete")public Object deleteArticle(Long aid,HttpServletRequest request){// 1.非空校验if (aid==null||aid<=0){return AjaxResult.fail(-1,"非法参数!");}// 2.获取登录用户UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo==null||userInfo.getUid()<=0){return AjaxResult.fail(-2,"请先登录!");}ArticleInfo articleInfo = articleService.getDetailByAid(aid);if (articleInfo==null){return AjaxResult.fail(-5,"查询不到该文章!");}if (articleInfo.getUid()!=userInfo.getUid()){return AjaxResult.fail(-4,"不能删除别人的文章!");}// 3.操作数据库int result = articleService.deleteArticle(aid, userInfo.getUid());if (result==1){return AjaxResult.success("删除成功!",1);}else{return AjaxResult.fail(-3,"删除失败");}}
4.9实现密码的加盐加密
加盐处理
加盐就是在每次加密之前, 给密码加上一个不同的盐值(32位UUID生成), 此时再对这个最终密码进行 md5 操作, 那么每次加密得到的 md5 值就不一样了. 于是, 想要解密就必须拿到最终的 md5 值和盐值, 才能解密出原始的密码.
示例
使用Java提供的UUID每次生成不同的盐值,然后与密码拼接在一起生成不同的md5值,那么每次加密的密码肯定就不一样了
加盐加密我们解决了,但是关键在于加密后的密码要拿什么对照去解密出原始密码
校验密码的有效性
1.待验证密码
2.盐值
既然知道了加密后的密码与盐值共同作用下才能解密,那么盐值也需要存储到数据库
盐值该怎么存储呢
1.新增一个字段来存放盐值
2.将盐值和密码存储在一个字段中(正确选择)
为什么不把盐值重新作为一个字段
首先违背了数据库的第三范式,因为盐值与表中业务逻辑无关
其次,如果单独把盐值存储一列,那么很容易让人根据规律猜想并破解出来
因此多角度考虑后选择将盐值和密码存储在一个字段中, 最终通过某种特征来将加密密码和盐值区分.
上面所说的某种特征代表什么呢请看下图
从代码中及运行结果中可以看出如下特征 :
1. Java 提供的 UUID 生成的随机盐值, 如果替换中间的 "-" 之后 (让别人看不出是 UUID 的特征), 始终都是 32 位数据.
2. md5 加密后的数据, 不论密码的长短, 生成的加密密码也始终是 32 位数据.
既然长度都是固定32位,那么就可以在长度上做文章了.
我们约定将盐值和$和最终的加密密码拼接在一起存储在数据库中. 也就是一个 65位的数据. (盐值在前 , 密码在后)
总结:做到这种程度的加密后, 你的密码就已经足够安全了, 就算黑客进行一个密码的破解, 至少也要花上一段时间,当同样的时间所带来的收益很少时候, 黑客自然就没有动力去做这些事情了.
加盐加密代码的真正实现
ublic class PasswordUtil {/*** 密码加盐操作 (格式:盐值32 $ 加密之后的密码)** @param password 原密码* @return*/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;}/*** 密码解密操作* @param inputPassword* @param dbPassword* @return*/public static boolean decrypt(String inputPassword,String dbPassword){// 验证参数if (!StringUtils.hasLength(inputPassword)||!StringUtils.hasLength(dbPassword)||dbPassword.length()!=65||!dbPassword.contains("$")){return false;}// 将用户输入的密码和数据库的盐值进行加密,得到待验证密码的加密密码// 得到盐值 $ 最终正确的密码String[] dbPasswordArray=dbPassword.split("\\$");String salt=dbPasswordArray[0];String finalPassword=dbPasswordArray[1];// 将数据库中的盐值加上用户输入的密码,进行加密 = 待验证的加密密码String userPassword=DigestUtils.md5DigestAsHex((salt+inputPassword).getBytes());// 将待验证的加密密码和数据库的加密密码进行对比if (userPassword.equals(finalPassword)){return true;}// 将结果返回给调用方return false;}
}
加盐加密密码实现之后,前面注册和登录密码相关的功能也要修改了
4.10加盐之后注册功能的变化
加盐处理后, 我们存储数据库的密码, 就需要存储进行加密后的最终密码和盐值拼接在一起的结果了. (约定盐值在前, 密码在后)
controller层变化
/*** 注册功能实现* @param userInfoVO* @return*/@RequestMapping("/reg")public Object reg(UserInfoVO userInfoVO) {// 1.非空校验if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())|| !StringUtils.hasLength(userInfoVO.getPassword())|| !StringUtils.hasLength(userInfoVO.getCheckCode())) {return AjaxResult.fail(-1, "非法参数请求!");}// todo:密码进行加盐userInfoVO.setPassword(PasswordUtil.encrypt(userInfoVO.getPassword()));// 2.进行数据库添加操作int result = userService.addUser(userInfoVO);if (result == 1) {return AjaxResult.success("注册成功!", 1);} else {return AjaxResult.fail(-1, "数据库添加出错!");}}
4.11加盐之后登录功能的变化
注册的时候, 我们存储的是加盐处理后的 65 位密码, 那么登录的时候, 验证密码用户名密码正确性的时候, 就不能像之前直接从数据库取出密码和前端传递过来的密码进行比较了. 做出以下调整
controller层变化
/*** 登录功能实现* @param userInfoVO* @param request* @return*/@RequestMapping("/login")public int login(UserInfoVO userInfoVO, HttpServletRequest request) {// 1.非空校验if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())|| !StringUtils.hasLength(userInfoVO.getPassword())|| !StringUtils.hasLength(userInfoVO.getCheckCode())) {return 0;}// 2.查询操作// 2.1.通过用户名查找对象UserInfo userInfo = userService.getUserByUsername(userInfoVO.getUsername());// 2.2 如果对象为空或者为无效对象,则返回-1if (userInfo == null || userInfo.getUid() <= 0) {return -1;}// 2.3 如果用户输入的密码与数据库输入密码不一致,则返回-1
// if (!userInfoVO.getPassword().equals(userInfo.getPassword())) {if (PasswordUtil.decrypt(userInfoVO.getPassword(),userInfo.getPassword())) {return -1;} else {HttpSession session = request.getSession();session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);return 1;}}
4.12实现博客详情页
此处的博客详情页, 是不能被拦截器拦截的, 因为主页有一个没有登录就可以访问的所有人的分页列表页, 在用户没有登录的情况下也是可以查看别人的文章详情页的. 此处自己的详情页就是多了两个按钮 : 修改,删除. (前端操作 blog_content.html)
博客详情页要实现的功能
1. 展示博客详情内容.
2. 展示博客的访问量.
3. 展示左侧博客对应的作者的个人信息(头像,昵称和发表文章数量)
第一步:引入需要ajax请求的依赖
<script src="js/jquery.min.js"></script>
第二步:约定前后端接口
获取文章详情、展示个人信息
url : "/art/detail"
type : "GET"
data : aid - 文章 id (从URL中获取)
后端返回 200 , 并且 data 不为空就构造数据.
更新文章的访问人数
url : "/art/rcount"
type : "POST"
data : aid - 文章 id (从URL中获取)
第三步:写前端代码
从 URL 中获取 id 的方法封装到独立的 js 中,因为经常要使用到它util.js
// 获取url某个参数的值
function getUrlParam(key) {// 查询字符串var params=location.search;// 判断字符串是否含有?if (params!=null&¶ms.indexOf("?")>=0){// 截取从下标1开始到结束的字符串params=params.substring(1);// 键值对之间使用&分割var paramArr=params.split("&");for(let i=0;i<paramArr.length;i++){// 键和值使用=分割var kv=paramArr[i].split("=");if (kv[0]==key){return kv[1];}}}return "";
}
获取博客详情以及个人信息
var aid = getUrlParam("aid");//得到文章idvar editormd;function initEdit(md) {editormd = editormd.markdownToHTML("editorDiv", {markdown: md, // Also, you can dynamic set Markdown text// htmlDecode : true, // Enable / disable HTML tag encode.// htmlDecode : "style,script,iframe", // Note: If enabled, you should filter some dangerous HTML tags for website security.});}// 获取文章的详细信息 [不拦截]function getArtDetail() {if (aid != null && aid > 0) {// 访问后端查寻文章详情jQuery.ajax({url: '/art/detail',type: 'GET',data: {"aid": aid},success: function (body) {if (body.code == 200 && body.data != null) {// 填充标题let title = document.querySelector('#title');title.innerHTML = body.data.title;// 填充日期let createtime = document.querySelector('#createtime');createtime.innerHTML = formatDate(body.data.createtime);// 填充访问量let rcount = document.querySelector('#rcount');rcount.innerHTML = body.data.rcount+1;//为了和数据库保持相同 // 填充内容initEdit(body.data.content);// 左侧个人信息if (body.data.photo!=null&&body.data.photo!=""){let photo=document.querySelector("#photo");photo.src=body.data.photo;}let nickname=document.querySelector("#nickname");nickname.innerHTML=body.data.nickname;let artCount=document.querySelector("#artCount");artCount.innerHTML=body.data.artCount;}}});}}getArtDetail();
更新访问人数
// 更新访问量function updateCount() {jQuery.ajax({url: '/art/rcount',type: 'POST',data: {"aid": aid},success: function (res) {}})}updateCount();
第四步:写后端代码
查询 : 查询文章详情, 查询作者的信息(头像,昵称,文章数量)
修改 : 每次访问, 都让访问量 + 1.
步骤一:mapper层ArticleMapper
// 获取文章详情(多表联查)ArticleInfoVO getDetail(Long aid);// 更新访问人数int updateRCount(Long aid);
步骤二:对应的xml实现
<select id="getDetail" resultType="com.huhu.entity.vo.ArticleInfoVO">select a.*,u.photo,u.nickname from articleinfo aleft join userinfo u on a.uid=u.uidwhere aid=#{aid}</select><update id="updateRCount">update articleinfo set rcount=rcount+1 where aid=#{aid}</update>
步骤三:service层
@Overridepublic ArticleInfoVO getDetail(Long aid) {return articleMapper.getDetail(aid);}@Overridepublic int updateRCount(Long aid) {return articleMapper.updateRCount(aid);}
步骤四:controller层
/*** 获取文章详细信息(查看详情)* @param aid* @return*/@RequestMapping("/detail")public Object getDetail(Long aid){// 1.参数校验if (aid == null || aid <= 0) {return AjaxResult.fail(-1, "非法参数!");}// 2.查询数据库中的文章信息ArticleInfoVO articleInfoVO = articleService.getDetail(aid);if (articleInfoVO == null || articleInfoVO.getAid() <= 0) {return AjaxResult.fail(-2, "文章查询失败!");}// 3.组装数据,查询当前用户总共发表的文章数int count = articleService.myArtCount(articleInfoVO.getUid());articleInfoVO.setArtCount(count);// 4.将最终组装好的数据返回给前端return AjaxResult.success(articleInfoVO);}/*** 更新阅读人数** @param aid* @return*/@RequestMapping("/rcount")public Object updateRCount(Long aid) {// 参数校验if (aid == null || aid <= 0) {return AjaxResult.fail(-1, "参数有误!");}// 修改数据库int result = articleService.updateRCount(aid);// 将结果返回给前端return AjaxResult.success(result);}
5.扩展功能
5.1验证码实现功能
实现思路
1.生成验证码
2.将本地验证码发布成url
3.后端返回验证码的url给前端
4.前端将用户输入的密码发送给后端
5.后端校验验证码
5.1.1生成验证码
步骤一:添加 hutool 验证码依赖(顺便加上redis)
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--验证码工具包--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version></dependency>
步骤二:创建验证码的控制器
@RestController
public class CaptchaController {@Value("${imagepath}")private String imagepath;// 1.生成验证码到本地// 定义图形验证码的长和宽LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(128, 50);String uuid = UUID.randomUUID().toString().replace("-", "");// 图形验证码写出,可以写出到文件 (也可以写出到流)lineCaptcha.write(imagepath + uuid + ".png");return AjaxResult.success(imagepath+uuid+".png");
}
application.propertities 中添加验证码保存路径
# 图片保存路径
imagepath=E:\\image\\
注意事项
如果项目配置了拦截器,一定要让拦截器给该获取验证码接口放行
使用 UUID 每次生成不同地址的验证码
步骤三:前端验证码代码
<div class="row" style="margin-bottom: 20px;"><span>验证码</span><input id="code_input" style="width: 66px;"> <img onclick="refreshCode()" id="codeimg" src="img/check_code.png"style="height: 50px;width: 128px;"></div>
项目启动访问127.0.0.1:8080/getcaptcha
5.1.2将本地验证码发布成url
步骤一:配置映射图片路径
@Configuration
public class AppConfig implements WebMvcConfigurer {@Value("${imagepath}")private String imagepath;/*** 映射图片路径* @param registry*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/image/**").addResourceLocations("file:" + imagepath);}
}
注意事项:如果项目配置了拦截器,一定要让拦截器给/image/**接口放行
如果没有配置这个映射图片路径,后续就会发生以下情况,图片显示不出来
步骤二:使用映射后的网络路径访问验证码
浏览器输入:http://127.0.0.1:8080/image/1d47179e5d26445d87b0e52fec326877.png
5.1.3后端返回验证码的 URL 给前端
后端不仅要返回验证码给前端, 还需要返回一个 "验证码的 key" 给前端
后端返回验证码的 URL 给前端可以理解, 前端需要展示给用户看;
那么验证码的 key 是为什么呢
后端需要将验证码存储 redis, 因为在某一时间内可以生成很多验证码, 而用户输入的正确与否, 需要在后端进行判断, 后端进行判断时, 就得把生成的验证码存储到redis ,而存储 redis 我们可以借着前面生成的 UUID , 把 UUID 作为 key, 验证码作为 value 去存储. 然后再将 key 去传给前端, 前端就可以带着输入的验证码和 key 一起传给后端, 后端就可以拿着 key作为条件从redis找到相应的一个验证码, 然后与前端传过来的即用户输入的验证码,两者相互比较就行了.
CaptchaController.java 代码完整版
@RestController
public class CaptchaController {@Value("${imagepath}")private String imagepath;@Resourceprivate RedisTemplate redisTemplate;@RequestMapping("/getcaptcha")public Object getCaptcha() {// 1.生成验证码到本地// 定义图形验证码的长和宽LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(128, 50);String uuid = UUID.randomUUID().toString().replace("-", "");// 图形验证码写出,可以写出到文件 (也可以写出到流)lineCaptcha.write(imagepath + uuid + ".png");// url地址String url = "/image/" + uuid + ".png";// 将验证码存储到redisredisTemplate.opsForValue().set(uuid, lineCaptcha.getCode());HashMap<String, String> result = new HashMap<>();result.put("codeUrl", url);result.put("codeKey", uuid);return AjaxResult.success(result);}
}
5.1.4前端将用户输入的验证码传给后端
前端访问页面时刷新验证码
// 验证码keyvar codeKey = "";// 获取并显示验证码function refreshCode() {jQuery.ajax({url:'/getcaptcha',type:'GET',data:{},success:function (body) {if(body.code==200&&body.data!=null&&body.data!=""){// 获取验证码成功codeKey=body.data.codeKey;jQuery('#codeimg').attr('src',body.data.codeUrl);}}})}refreshCode();
前端将验证码和 key 传给后端
function doLogin() {// 非空参数的校验var username=jQuery('#username')var password=jQuery('#password')var checkCode=jQuery('#code_input')if(username.val().trim()==''){alert('请输入用户名!')username.focus()return false}if(password.val().trim()==''){alert('请输入密码!')password.focus()return false}if(checkCode.val().trim()==''){alert('请输入验证码!')checkCode.focus()return false}// 2. 发送请求给客户端jQuery.ajax({url:"/user/login",type:"post",data:{codeKey:codeKey,// 前面的 username 可以加引号, 也可以不加username:username.val(),password:password.val(),checkCode:checkCode.val()},success:function(body) {if(body.code == 200 && body.data == 1) {// alert("登录成功!");location.href = "myblog_list.html";} else {alert("用户名或密码错误, 请重新输入!");username.focus();}}});}
5.1.5后端校验验证码
这里拿登录来举例,注册也是同样的道理,就不一一进行展示了
/*** 登录功能实现* @param userInfoVO* @param request* @return*/@RequestMapping("/login")public int login(UserInfoVO userInfoVO, HttpServletRequest request) {// 1.非空校验if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())|| !StringUtils.hasLength(userInfoVO.getPassword())|| !StringUtils.hasLength(userInfoVO.getCheckCode())||!StringUtils.hasLength(userInfoVO.getCodeKey())) {return 0;}// redis 里面 key 对应的真实的验证码String redisCodeValue = (String) redisTemplate.opsForValue().get(userInfoVO.getCodeKey());// 验证码错误if (!StringUtils.hasLength(redisCodeValue) || !redisCodeValue.equals(userInfoVO.getCheckCode())) {return -1;}// 验证码正确,清除当前验证码redisTemplate.opsForValue().set(userInfoVO.getCodeKey(), "");// 2.查询操作// 2.1.通过用户名查找对象UserInfo userInfo = userService.getUserByUsername(userInfoVO.getUsername());// 2.2 如果对象为空或者为无效对象,则返回-1if (userInfo == null || userInfo.getUid() <= 0) {return -1;}// 2.3 如果用户输入的密码与数据库输入密码不一致,则返回-1
// if (!userInfoVO.getPassword().equals(userInfo.getPassword())) {if (!PasswordUtil.decrypt(userInfoVO.getPassword(),userInfo.getPassword())) {return -1;} else {HttpSession session = request.getSession();session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);return 1;}}
5.2博客详情页(评论功能)实现
实现思路
1.初始化评论列表
2.发表评论
3.删除评论
评论文本框下方如果用户未登录,显示请先登录;如果用户登录了,显示用户昵称
5.2.1初始化评论列表
- 加载博客下面的评论(功能一)
- 加载当前登录人的名字到评论框下面 (登录了才能评论)(功能二)
操作blog_content.html
第一步:约定前后端接口
获取评论列表
url:"/comment/list",
type:"GET",
data:{aid:aid},
后端返回 200 , 并且 data 不为空就构造数据.
获取用户登录信息
url:"/user/info",
type:"GET",
data:{}
后端返回 200 , 并且 data 不为空以及返回用户uid>0就构造数据.
判断当前文章是否是当前用户发表的
url:"/user/isartbyme",
type:"GET",
data:{aid:aid}
后端返回 200 , 并且 data ==1就显示删除按钮
第二步:写前端代码
<!-- 右侧内容详情 --><div class="container-right"><div class="blog-content"><!-- 博客标题 --><h3 id="title"></h3><!-- 博客时间 --><div class="date"><span id="createtime"></span> | 访问量:<span id="rcount"></span></div><!-- 博客正文 --><div id="editorDiv"></div><hr><div><h2 style="text-align: left;margin: 20px 0 20px 0;">评论</h2><div id="addcomment"><div><textarea placeholder="此处输入评论内容" style="text-align: left;width: 50%;height: 100px;"></textarea></div><div><span id="comment_login_name" style="margin-left: 90px">请先登录</span><input type="button" value="发表评论" class="btn" style="margin-left: 40px;margin-top: 10px;"></div></div><h3 id="commentCount" style="text-align: left;margin-top: 10px;"></h3><div id="commentlist"></div></div></div></div>
// 获取评论列表function getCommentList() {jQuery.ajax({url:"/comment/list",type:"GET",data:{"aid":aid},success: function (body) {if (body.code == 200 && body.data != null) {var commentListHtml = "";for (let i = 0; i < body.data.length; i++) {var comment = body.data[i];commentListHtml += '<div style="margin-bottom: 26px;">';commentListHtml += comment.nickname + ':' + comment.content;commentListHtml += ' ';commentListHtml += '<a class="comment_del_class" style="display: none" href="javascript:del(' + comment.cid + ')">删除</a>';commentListHtml += "</div>";}jQuery('#commentlist').html(commentListHtml);var commentCount = body.data.length;jQuery("#commentCount").html('共 ' + commentCount + ' 条评论');}}});}
此处要给删除按钮添加 display: none 样式, 后续在 getLoginUser() 方法中会做判断, 判断当前登录人和文章作者是否为同一个人, 如果是则显示删除按钮, 如果不是, 则不显示删除按钮.
第三步:写后端代码
步骤一:mapper层
// 获取评论列表List<CommentInfoVO> getList(@Param("aid") Long aid);
步骤二:对应的xml实现
<select id="getList" resultType="com.huhu.entity.vo.CommentInfoVO">select c.*,u.nickname from commentinfo cleft join userinfo u on c.uid=u.uidwhere aid=#{aid} order by c.createtime desc</select>
步骤三:service层
@Overridepublic List<CommentInfoVO> getList(Long aid) {return commentMapper.getList(aid);}
步骤四:controller层
/*** 获取评论列表* @param aid* @return*/@RequestMapping("/list")public Object getCommentList(Long aid) {// 1. 参数效验if(aid==null || aid<=0) {return AjaxResult.fail(-1, "参数有误!");}// 2. 查询数据库List<CommentInfoVO> list = commentService.getList(aid);// 3. 就结果返回给前端return AjaxResult.success(list);}
后端接口 /comment/list 记得要在拦截器里边放行, 如果拦截了, 那么用户不登录, 就看不到评论了,而我们想要的效果是: 就算用户不登录, 所有人的的博客列表页查看文章详情的时候, 也能看到评论, 只是不管文章归属人是谁都不会显示删除按钮
功能二的实现
第一步:写前端代码
var isLogin=false;
// 获取当前用户登录信息function getLoginUser() {jQuery.ajax({url: '/user/info',type: 'GET',data: {},success: function (body) {if (body.code == 200 && body.data != null && body.data.uid >= 0) {// 当前用户登录了isLogin = true;jQuery('#comment_login_name').html(body.data.nickname);// 判断当前文章是否是当前用户发表的isArticleByMe(aid);} else {// 当前用户未登录}}})}// 判断当前文章是否是当前用户发表的function isArticleByMe(aid) {jQuery.ajax({url: '/user/isartbyme',type: 'GET',data: {"aid": aid},success: function (body) {if (body.code == 200 && body.data == 1) {// 当前文章归属于当前登录用户jQuery('.comment_del_class').each(function (i) {jQuery(this).show();})}}})}
getLoginUser() 方法的作用是控制登录了才能发表评论,未登录不能发表评论.
isArticleByMe() 方法的作用是控制是否显示删除按钮.
【注意】此处最好将 isArticleByMe() 方法写在 getLoginUser()中 ajax 最后, 如果写在外面, 会出现这样一个问题 :
" 因为同一个页面下面的 ajax 请求的执行顺序是不一定的, 那么就有可能先执行了控制是否显示删除按钮的 ajax, 再执行 getLoginUser(), 如果是这样, 那么这篇文章属不属于当前登录人, 都不会显示删除按钮, 没有登录怎么判断文章归属人是吧."所以 isArticleByMe() 方法要在 getLoginUser() 方法执行后再调用.
第二步:写后端代码
/*** 获取个人信息(博客详情页)* @param request* @return*/@RequestMapping("/info")public Object info(HttpServletRequest request) {// 从 session 工具类中拿用户登录信息UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {return AjaxResult.fail(-2, "当前用户未登录!");}return AjaxResult.success(userInfo);}/*** 判断文章是否是当前登录人所写的* @param aid* @param request* @return*/@RequestMapping("/isartbyme")public Object isArticleByMe(Long aid, HttpServletRequest request) {if (aid == null || aid <= 0) {return AjaxResult.fail(-1, "参数有误! ");}UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {return AjaxResult.fail(-2, "当前用户未登录! ");}ArticleInfo articleInfo = articleService.getDetailByAid(aid);if (articleInfo != null && articleInfo.getAid() >= 0&& articleInfo.getUid() == userInfo.getUid()) {// 文章归属于当前登录人return AjaxResult.success(1);}return AjaxResult.success(0);}
特别注意:此处的 /user/info 和 /user/isartbyme 接口都需要在拦截器里边放行.
5.2.2实现发表评论功能
第一步:约定前后端接口
url:"/comment/add",
type:"POST",
data:{aid,content},
结果返回200且data==1就构造数据
第二步:写前端代码
// 发表评论function addComment() {// 评论正文var comment_content = jQuery('#comment_content');// 非空判断if (comment_content.val().trim() == "") {alert('请先输入评论的内容!');comment_content.focus();return false;}// 判断是否登录if (!isLogin) {alert('请先登录!');return false;}// 将前端数据发送给后端(文章id、评论内容)jQuery.ajax({url: '/comment/add',type: 'POST',data: {"aid": aid,"content": comment_content.val()},success: function (body) {// 将后端返回的数据显示给用户if (body.code == 200 && body.data == 1) {// 评论添加成功alert('评论发表成功!');// 刷新评论location.href = location.href;} else {alert('抱歉,发表失败,请重试!' + body.msg);}}})}
第三步:写后端代码
步骤一:mapper层
// 发表评论boolean addComment(CommentInfo commentInfo);
步骤二:对应的xml实现
<insert id="addComment">insert into commentinfo values (#{cid},#{aid},#{uid},#{content},#{createtime});</insert>
步骤三:service层
@Overridepublic boolean addComment(CommentInfo commentInfo) {return commentMapper.addComment(commentInfo);}
步骤四:写controller层
/*** 发表评论** @param aid* @param content* @param request* @return*/@RequestMapping("/add")public Object addComment(Long aid, String content, HttpServletRequest request) {// 1.参数校验if (aid == null || aid <= 0 || !StringUtils.hasLength(content)) {return AjaxResult.fail(-1, "非法参数!");}// 2.组装数据(将 uid 从session中获取出来)UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {return AjaxResult.fail(-2, "请先登录");}CommentInfo commentInfo = new CommentInfo();commentInfo.setAid(aid);commentInfo.setUid(userInfo.getUid());commentInfo.setContent(content);// 3.将评论的对象存入数据库boolean result = commentService.addComment(commentInfo);// 4.将数据执行的结果返回给前端return AjaxResult.success(result == true ? 1 : 0);}
发表评论的路由 /comment/add 需要在拦截器里边配置拦截, 因为只有登录了才能发表评论,即不需要放行
5.2.3实现删除评论功能
第一步:约定前后端接口
url:"/comment/del",
type:"POST",
data:{aid,content},
结果返回200且data==1就构造数据
第二步:写前端代码
// 评论删除功能function del(cid) {if (!confirm('确定删除?')){return false;}// 1.参数校验if (cid == "" || cid <= 0) {alert('抱歉,操作失败,请刷新页面之后重试!');return false;}if (aid == "" || aid <= 0) {alert('抱歉,评论删除失败,请刷新页面之后重试!');return false;}// 2.发送数据给后端(评论的id,文章的id)jQuery.ajax({url: '/comment/del',type: 'POST',data: {"aid": aid,"cid": cid},success: function (body) {// 3.将后端结果显示给用户if (body.code == 200 && body.data == 1) {// 删除成功alert('恭喜,评论删除成功!')location.href = location.href;} else {alert('抱歉,评论删除失败!' + body.msg);}}})}
想要删除评论, 那么至少得传递两个参数给后端, 一个是 cid (评论 ID), 一个是 aid (文章 ID), cid 在前边加载评论列表的时候, 已经在返回数据 body 中的 comment 对象中拿到了, 而aid呢 ,前面已经调用了工具方法 getUrlParam 获取并保存 aid 全局变量中, 因此也可以直接拿到.
【注意】虽然是否能够删除评论需要拿着 aid 查询出具体的 articleinfo 对象, 再拿着这个对象的 uid 和登录人的 uid 进行比较, 相同才可以删除评论. 但是此处不能将 uid (用户人的 ID) 通过 ajax 发给后端, 一旦 uid 通过参数来接受登录人的 ID 了, 那么就有被篡改的风险, 别人可以写一个接口绕过你的 ajax 直接访问后端接口 (例如: postman), 这样就非常不安全, 所以 uid 最好从后端的 session 中获取.
第三步:写后端代码
步骤一:写mapper层
// 删除评论boolean deleteCommentById(@Param("cid") Long cid);
步骤二:对应的xml实现
<delete id="deleteCommentById">delete from commentinfo where cid=#{cid}</delete>
步骤三:写service层
@Overridepublic boolean deleteCommentById(Long cid) {return commentMapper.deleteCommentById(cid);}
步骤四:写controller层
/*** 删除评论** @param aid* @param cid* @return*/@RequestMapping("/del")public Object delComment(Long aid, Long cid, HttpServletRequest request) {// 1.参数校验if (aid == null || aid <= 0 || cid == null || cid <= 0) {return AjaxResult.fail(-1, "非法参数!");}// 2.校验权限(判断当前文章是否属于当前登录用户写的)// 2.1获取当前登录用户UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {return AjaxResult.fail(-2, "请先登录!");}// 2.2获取当前文章idArticleInfo articleInfo = articleService.getDetailByAid(aid);if (articleInfo == null && articleInfo.getAid() <= 0) {return AjaxResult.fail(-3, "非法的文章id!");}// 2.3比较文章归属的用户和当前登录的用户是否一致if (userInfo.getUid() != articleInfo.getUid()) {return AjaxResult.fail(-4, "非法操作!");}// 3.删除评论(操作数据库)boolean result = commentService.deleteCommentById(cid);// 4.将结果返回给前端return AjaxResult.success(result == true ? 1 : 0);}
删除评论的路由 /comment/del 需要在拦截器里边配置拦截, 因为只有登录了才能删除评论,即不需要放行
5.3个人中心功能实现
5.3.1修改用户头像
需要实现的功能有:
1.获取原来的用户头像和用户名
2.保存头像
功能一这样处理的原因是:想要修改头像, 那么就得先获取数据库中原来的头像, 此处顺便将原用户名一起获取并展示出来. 原密码最好不要获取出来, 因为有可能你在修改密码的中途有事情要处理,如果这时有人刚好看到你密码那么别人就有机可乘修改你的信息了,所以我们最好不用把原密码获取出来
操作myinfo.html
第一步:约定前后端接口
url:"/user/info",
type:"GET",
data:{},
返回的结果code==200且数据不为空以及用户uid>0,则构造数据,否则弹窗显示错误信息
第二步:写前端代码
// 获取用户头像和用户名function initPage() {jQuery.ajax({url:"/user/info",type:"GET",data:{},success:function(body) {if(body.code==200 && body.data!=null && body.data.uid>=0) {// 得到了当前的 userinfovar userinfo = body.dataif(userinfo.photo!=null && userinfo.photo!="") {jQuery("#photo").attr("src",userinfo.photo);}jQuery("#username").val(userinfo.username);} else {alert("抱歉: 查询用户信息出错, 请刷新页面再试! " + body.msg);}}});}initPage();
1. 获取当前登录人的用户名和头像, 只需在后端的 session 中拿到具体的 userinfo 即可,所以不需要传参数uid到后端
2. success 回调函数中的注意事项: 因为我们在构造数据 (userinfo) 的时候, 头像一般都是写死的本地图片, 所以新用户一般都是默认头像, 所以我们在构造 photo 的 src 属性时, 一定要判断 photo 是否为 null 或者是否为 "", 如果是就不要设置 photo 的 src 属性, 否则会导致用户没有头像.
第三步:写后端代码
/*** 获取个人信息(博客详情页、个人信息页)** @param request* @return*/@RequestMapping("/info")public Object info(HttpServletRequest request) {// 从 session 工具类中拿用户登录信息UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {return AjaxResult.fail(-2, "当前用户未登录!");}return AjaxResult.success(userInfo);}
功能二保存头像
我们上传新的头像后, 并点击保存按钮时, 就是修改头像成功了
第一步:约定前后端接口
url:"/user/save_photo",
type:"POST",
data:{},
返回的结果code==200且数据不为空,则构造数据,否则弹窗显示错误信息
第二步:写前端代码
// 保存头像function savePhoto() {// 得到图片var photo = jQuery("#file")[0].files[0];if (photo == null) {alert("请先选择要上传的头像!");return false;}// 构建一个 form 表单var formData = new FormData();formData.append("file", photo);jQuery.ajax({url:"/user/save_photo",type:"POST",data:formData,processData:false, // 告诉 jQuery 不要去加工数据contentType:false, // 告诉 jQuery 不要设置类型success:function(body) {if(body.code==200 && body.data!=null && body.data!="") {jQuery("#photo").attr("src",body.data);} else {// 图片上传失败alert("抱歉: 上传图片失败, 请重试! " + body.msg);}}});}
1. 此处的得到图片代码比较特殊
2. 发送 ajax 时, 参数是发送一个 form 表单给后端, 并携带两个参数 : processData 和 contentType设置属性(疑点)
3. 表单传给后端时, 后端针对图片生成一个网络地址映射到本地保存的地址, 然后将网络地址返回给前端, 前端将其设置给 photo 的 src 属性.
第三步:写后端代码
步骤一:映射图片路径
在配置文件中application.properties设置头像保存的本地路径(这里跟验证码保存路径相同,所以如果前面已经配置了可以跳过)
# 图片保存路径
imagepath=E:\\image\\
在全局配置类App.config类中添加映射图片路径方法
/*** 映射图片路径** @param registry*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/image/**").addResourceLocations("file:" + imagepath);}
步骤二:保存图片到服务器, 保存图片地址到数据库
mapper层
// 更新头像int updatePhoto(@Param("uid") Long uid,@Param("photo") String imgUrl);
UserMapper.xml
<update id="updatePhoto">update userinfo set photo=#{photo} where uid=#{uid}</update>
service层
@Overridepublic int updatePhoto(Long uid, String imgUrl) {return userMapper.updatePhoto(uid,imgUrl);}
controller层
/*** 个人中心:保存头像** @param file* @param request* @return*/@RequestMapping("/save_photo")public Object savePhoto(MultipartFile file, HttpServletRequest request) {// 1.保存图片到服务器// 得到图片的后缀String imageType = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));// 生成图片名称String imgName = UUID.randomUUID().toString().replace("-", "") + imageType;try {file.transferTo(new File(imagePath + imgName));} catch (IOException e) {return AjaxResult.fail(-1, "图片上传失败!");}String imgUrl = "/image/" + imgName;// 2.将图片地址保存到数据库UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {// 请先登录return AjaxResult.fail(-2, "请先登录!");}int result = userService.updatePhoto(userInfo.getUid(), imgUrl);if (result == 1) {// 更新头像到session中userInfo.setPhoto(imgUrl);HttpSession session = request.getSession();session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);return AjaxResult.success(imgUrl);} else {return AjaxResult.fail(-3, "数据库修改失败!");}}
1. 保存图片到服务器
当项目部署云服务器时, 用户访问项目并修改头像时, 是从用户的电脑上选取了一张图片上传到后端, 那么后端需要将这张图片保存到云服务器的一个本地路径, 然后再生成图片对应的网络地址. 最后将新头像对应的网络地址返回给前端, 前端就可以通过设置头像 photo 对应的 src 属性为返回的网络地址.
2. 保存图片到数据库
修改头像不仅要更新当前页面展示的头像, 数据库中的头像对应的网络地址也要更新, 另外如果更新数据成功了, 要同时更新 session 中的 userinfo 信息. 因为博客列表页的当前用户的身份信息, 后端都是从 session 中取出来返回给前端的, 所以需要同时更新数据库和 session 中的 photo 字段(属性).
5.3.2修改用户名或者密码
此处我们点击修改个人信息跳转到用户信息的页面时, 它只是将头像和用户名展示出来了, 而原密码, 新密码和确认密码需要手动输入(要么三个都不为空 - 改, 要么都为空 - 不改).
两种情况:
一:如果不修改密码, 只是修改用户名或者都不修改, 然后点击修改按钮, 就提示修改成功, 并跳转到我的内容管理页面.
二:如果修改了密码, 并点击了修改按钮, 就提示修改成功, 并强制用户重新登录.
第一步:约定前后端接口
url:"/user/update",
type:"POST",
data:{
username,oldPassword,newPassword,isUpdatePassword
},
返回的结果code==200且data==1,则构造数据(如果isUpdatePassword为true,跳转到登录页面,反之则跳转到我的博客列表页面)
否则弹窗显示错误信息
第二步:写前端代码
// 修改用户个人信息function updateUser() {var isUpdatePassword = false; // 是否修改密码// 1.非空效验var username = jQuery("#username");var oldPassword = jQuery("#old_password");var password = jQuery("#password");var password2 = jQuery("#password2");if (username.val().trim() == "") {alert("请先输入新用户名!");username.focus();return false;}if (oldPassword.val() != "" ||password.val() != "" || password2.val() != "") {// 需要修改密码isUpdatePassword = true;if (oldPassword.val().trim() == "") {alert("请先输入原密码!");oldPassword.focus();return false;}if (password.val().trim() == "") {alert("请先输入新密码!");password.focus();return false;}if (password2.val().trim() == "") {alert("请先输入确认密码!");password2.focus();return false;}// 判断新密码和确认密码是否一致if (password.val() != password2.val()) {alert("两次输入的新密码不一致,请先确认!");return false;}}// 2.将前端的数据提交给后端jQuery.ajax({url: "/user/update",type: "POST",data: {"username": username.val(),"oldPassword": oldPassword.val(),"newPassword": password.val(),"isUpdatePassword": isUpdatePassword},success: function (body) {// 3.将返回的结果展现给用户if (body.code == 200 && body.data == 1) {// 修改成功if (isUpdatePassword) {alert("修改成功,请重新登录!");// 修改密码,重新登录location.href = "login.html";} else {alert("修改成功!");location.href = "myblog_list.html";}} else {// 修改失败alert("抱歉:修改失败,请重试!" + body.msg);}}});}
第三步:写后端代码
mapper层
// 根据id查询用户UserInfo getUserById(@Param("uid") Long uid);// 修改用户信息(用户名和密码)boolean updateUser(UserInfo userInfo);
UserMapper.xml
<select id="getUserById" resultType="com.huhu.entity.UserInfo">select * from userinfo where uid=#{uid}</select><update id="updateUser">update userinfo set username=#{username},password=#{password} where uid=#{uid}</update>
service层
@Overridepublic UserInfo getUserById(Long uid) {return userMapper.getUserById(uid);}@Overridepublic boolean updateUser(UserInfo userInfo) {return userMapper.updateUser(userInfo);}
controller层
/*** 个人中心:修改用户名或密码** @param username* @param oldPassword* @param newPassword* @param isUpdatePassword* @param request* @return*/@RequestMapping("/update")public Object update(String username, String oldPassword, String newPassword, Boolean isUpdatePassword, HttpServletRequest request) {// 1.参数校验if (!StringUtils.hasLength(username)) {return AjaxResult.fail(-1, "非法参数!");}// 是否需要修改密码if (isUpdatePassword) {// 修改原密码if (!StringUtils.hasLength(oldPassword) || !StringUtils.hasLength(newPassword)) {return AjaxResult.fail(-1, "非法参数!");}}// 2.组装数据(从session获取用户)UserInfo userInfo = SessionUtil.getLoginUser(request);if (userInfo == null || userInfo.getUid() <= 0) {return AjaxResult.fail(-2, "请先登录!");}// 如果需要修改密码if (isUpdatePassword) {// 判断用户输入的旧密码和数据库的密码是否一致UserInfo dbUser=userService.getUserById(userInfo.getUid());boolean checkPassword = PasswordUtil.decrypt(oldPassword,dbUser.getPassword());if (!checkPassword) {return AjaxResult.fail(-3, "原密码输入错误!");}// 修改密码newPassword=PasswordUtil.encrypt(newPassword);userInfo.setPassword(newPassword);}// 3.修改数据库userInfo.setUsername(username);boolean result = userService.updateUser(userInfo);// 更新 session 中的用户名if(result) {userInfo.setUsername(username);HttpSession session = request.getSession();session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);}// 4.将结果返回给前端return AjaxResult.success(result ? 1 : 0);}
1. 非空效验
前端传递了新用户名, 原密码, 新密码, 以及是否修改了密码的标志, 于是在做判断时, 如果只修改了用户名, 就可以使用 isUpdatePassword 标志位来跳过接下来更新数据密码的操作. 否则都要进行修改.
2. 组装数据
组装好一个新的 userinfo (新的用户名或密码), 为更新数据库操作提供数据源, 此处更新密码成功的前提是原密码和数据库密码要保持一致, 而数据库中存储的是加密后的密码, 所以需要先拿着原密码和数据库中的密码去调用解密方法去比较, 如果匹配成功,就可以进行修改密码操作,反之,则返回错误信息(原密码不正确)
【注意】session 中的对象存储机制 >>
由于我们是可以拿到当前用户的 session, 所以想要拿数据库中存储的密码, 我就会想着去 session 中拿到 userinfo, 再去拿到对应的密码, 这确实挺方便. 但是我在实现登录页面时, 登录成功后并将 session 存储 redis, 但是在返回数据给前端之前, 我执行了将密码置为空字符串这一操作, 因为密码如果通过网络传输返回给前端, 是不安全的.
<问题的出处> 正因为我的这一步置空字符串操作, 就会导致 session 中的密码也变成了空字符串. 这是为什么呢 ??
因为 session 的底层是用 concurrentHashMap 来保存数据的, 而 map 中并没有直接存储新的对象, 而是存储了对象的引用, 也就是 userinfo 的引用, 虽然我是在设置密码为空之前就将 userinfo 存储 session 了, 但是这也同样影响了 session 中的 password 了. 此时 session 中的 password 已经是空字符串了.
再回到调用解密方法这一步, 我们就不能拿着原密码和 session 中的密码去调用解密码方法了, 而是需要拿着从 session 获取到的 userinfo 中的用户 id, 去查数据库得到一个新的 userinfo, 此时这个 userinfo 的密码才不为空, 才可以拿着它的 password 去和原密码去调用解密方法.
3. 修改数据库
经过了第二步的组装数据, 第三步就变得简单了, 我们可以自己写个修改方法将userinfo对象传递过去,根据uid修改用户名和密码就可以了
另外就是修改完数据库之后, 要及时更新 session 中的用户名, 因为如果只修改了用户名, 不修改密码, 修改完成后会跳转博客列表页, 而博客列表页的用户身份信息都是从后端的 session 中来的, 如果不及时更新 session 的话, 那么在你下次重新登录之前, 用户名都不会变.
6.部署环境
mysql
redis
在部署环境时遇到问题:
nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
### The error may exist in class path resource [mapper/ArticleMapper.xml]
### The error may involve com.huhu.mapper.ArticleMapper.getList
### The error occurred while executing a query
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
大概意思就是:不允许公钥检索(个人不理解)后来查找资料:就在配置文件加上属性
allowPublicKeyRetrieval=true就解决了