SSM项目 - 博客系统

在线体验 : http://43.139.1.94:8080/login.html

项目 Gitee 链接 : 博客系统 - SSM

1.SSM 版本的博客系统相较于 Servlet 版本的升级

1. 框架升级 : SSM (SpringBoot + Spring MVC + MyBatis) + MySQL + Redis + jQuery.
2. 密码升级: 明文存储/md5存储 -> 加盐处理.
3. 用户登录状态持久化升级: session 持久化到内存 - > session 持久化到 Redis. (后期有空实现)
4. 功能升级: 增加了分页功能.
5. 使用拦截器升级用户登录验证.

2. 准备工作

2.1 准备数据库

-- 创建数据库
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) unique, -- 唯一约束
    password varchar(64) not null,
    photo varchar(500) default '',
    createtime 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,
    uid int not null,
    rcount int not null default 1,
    `state` int default 1
)default charset 'utf8mb4';

总共两张表: 文章表, 用户表

文章表

SSM项目 - 博客系统_第1张图片

用户表

SSM项目 - 博客系统_第2张图片

2.2 创建一个SSM 项目

具体流程在这篇博客中有详细介绍: SpringBoot 项目的创建和使用 (开发环境: IDEA, JDK 1.8)

在 SpringBoot 项目的基础上添加 2 个依赖即可:

SSM项目 - 博客系统_第3张图片

2.3 准备项目的配置文件

在resources 文件下创建一个 application.yml 的文件, 并根据自己的数据库信息和mapper 文件夹的命名将以下 xml 文件配置到 application.yml 文件中.

# 配置数据库的连接字符串
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mycnblog?characterEncoding=utf8&useSSL=false
    username: root
    password: 316772
    driver-class-name: com.mysql.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
  level:
    com:
      example:
        demo: debug

准备好 yml 配置文件后, 根据里面的mapper-locations 中的约定创建一个叫做 mapper 的文件夹, 并在文件夹下创建两个以 Mapper.xml 为后缀名的文件.

SSM项目 - 博客系统_第4张图片

并分别在 xml 中添加必须代码:









2.4 准备项目中的公共模块

1. 实体层 (model) - 实体类
2. 控制器层 (controller) -> 控制器
3. 服务层 (service) -> 服务类
4. 持久层 (mapper) -> mapper
5. 工具层 (common) -> 统一返回类

2.4.1 实体层 (model) - 实体类

@Data
public class ArticleInfo {
    private int id;
    private String title;
    private String content;
    private String createtime; // 发布时间
    private int uid; // 文章对应的作者
    private int rcount; // 阅读量
    private int state;
}
@Data
public class UserInfo {
    private int id;
    private String username;
    private String password;
    private String photo;
    private String createtime;
    private int state;
}

2.4.2 控制器层 (controller) -> 控制器

@RestController
@RequestMapping("/art")
public class ArticleController {
    @Autowired
    private ArticleService articleService;
}
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
}

2.4.3 服务层 (service) -> 服务类

@Service
public class ArticleService {
    @Autowired
    private ArticleMapper articleMapper;
}
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
}

2.4.4 持久层 (mapper) -> mapper

@Mapper
public interface ArticleMapper {
}
@Mapper
public interface UserMapper {
}

2.5.5 工具层 (common) -> 统一返回类

此处对统一功能的处理不熟悉的可以先从这篇博客中学习 - Spring AOP 统一功能的处理

使用拦截器统一处理用户登录验证

  1. 自定义拦截器 - LoginInterceptor.java

@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(Constant.SESSION_USERINFO_KEY) != null) {
            return true;
        }
        // 401 : 用户没有登录所有没有权限  403 : 用户登录了但没有权限
        response.setStatus(401);
        return false;
    }
}
  1. 配置拦截规则 - AppConfig.java

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    List excludes = new ArrayList() {{
        add("/**/*.html");
        add("/js/**");
        add("/editor.md/**");
        add("/css/**");
        // add("/**/*.jsp"); // 这样写穷举不完
        add("/img/**"); // 放行 img 下的所有文件
        add("/user/login"); // 放行登录
        add("/user/reg"); // 放行注册
        add("/art/detail"); // 放行详情页
        add("/user/author"); // 放行博客作者的个人信息的 username
        add("/art/list"); // 放行文章分页列表的接口
        add("/art/totalpage"); // 放行获取文章分页的总页数
        add("/art/artcount"); // 放行分页列表页个人信息的 文章数量
    }};

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration registration =
                registry.addInterceptor(loginInterceptor);
        registration.addPathPatterns("/**");
        registration.excludePathPatterns(excludes);
    }
}

此处没有拦截的接口, 在后面都会一一进行说明.

统一异常的处理

@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class) // 异常类型
    public Object exceptionAdvice(Exception e) {
        return AjaxResult.fail(-1, e.getMessage());
    }
}

此处对异常类别没有进行细分, 如果想要细分, 就参考博客 - Spring AOP 统一功能的处理

统一数据的返回

对返回的数据进行分类 :

  • 成功返回

  • 失败

1. 定义一个数据返回类 - AjaxResult.java

public class AjaxResult {
    /**
     * 业务执行成功时进行返回的方法
     * @param data
     * @return
     */
    public static HashMap success(Object data) {
        HashMap result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", data);
        return result;
    }

    /**
     * 业务执行成功时进行返回的方法
     * @param data
     * @return
     */
    public static HashMap success(String msg, Object data) {
        HashMap 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 fail(int code, String msg) {
        HashMap 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 fail(int code, String msg, Object data) {
        HashMap result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }
}

2.定义一个将返回类, 返回数据进行统一处理的类 - ResponseBodyAdvice.java

@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) {
        // 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);
    }
}

统一 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;
    }
}

统一常量类

此处的常量类主要是用户登录成功后, 将其身份信息存储 session 时, 需要多次写字符串常量, 为了避免写错, 所以专门定义一个常量类, 就只需要写一次即可.

public class Constant {
    public static final String SESSION_USERINFO_KEY = "session_userinfo_key";
}

2.5 准备静态页面

对于学后端的我来说, 前端我是基本不懂, 所以静态页面也是通过各种学习网站, 以及从朋友那求助, 最终弄出来的一个还能凑合的页面, 此处就不将代码粘贴到这了, 代码都在 Gitee 上.

Gitee 链接 - https://gitee.com/xiaobite_hl

3. 实现相关功能

3.1 实现注册功能

注册功能是不需要用户登录效验的, 注册都还没注册, 谈何登录. 所以该接口是不需要拦截的.

(前端操作 reg.html)

第一步: 引入用于发送 ajax 请求的依赖

第二步 :约定前后端交互接口

url :"/user/reg"
type : "POST"
data : "username,password"
后端返回 data=1, code=200 表示代码执行成功, 返回 data != 1, code != 200 表示执行失败.

第三步 :写前端代码

1.分别给用户名, 密码, 确认密码加上 id 属性, 并给提交按钮加上点击事件.

2. 点击事件触发的 ajax 请求

function myreg() {
    //1. 非空效验
    var username = jQuery("#username");
    var password = jQuery("#password")
    var password2 = jQuery("#password2")
    if(username.val() == "") {
        alert("请先输入用户名!");
        username.focus();
        return false;
    }
    if(password.val() == "") {
        alert("请先输入密码!");
        password.focus();
        return false;
    }
    if(password2.val() == "") {
        alert("请先输入确认密码!");
        password2.focus();
        return false;
    }
    if(password.val() != password2.val()) {
        alert("两次密码输入不一致, 请重新输入!");
        password.focus();
        return false;
    }
    
    jQuery.ajax({
        url:"/user/reg",
        type:"post",
        data:{
            username:username.val(),
            password:password.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("该用户名已被使用, 请重新输入!");
            }
        }
    });
}

第四步: 写后端代码

注册功能涉及到的数据库操作就是新增功能.

1. 先从 mapper 层开始写 (UserMapper):

public int add(@Param("username") String username,
                   @Param("password") String password);

2. 写对应的 xml 实现 (UserMapper.xml):


    insert into userinfo(username,password)
    values(#{username},#{password})

3. 写 service 层代码 (UserService)

public int add(String username, String password) {
    return userMapper.add(username, password);
}

4. 写 controller 层代码 (UserController)

// 注册 【不拦截】
@RequestMapping("/reg")
public Object reg(String username, String password) {
    // 1. 非空效验
    if(!StringUtils.hasLength(username) && !StringUtils.hasLength(password)) {
        return AjaxResult.fail(-1, "非法的参数请求");
    }
    // 2. 进行添加操作
    int result = userService.add(username, password);
    if(result == 1) {
        return AjaxResult.success("注册成功!", 1);
    } else {
        return AjaxResult.fail(-1, "数据库添加出错!");
    }
}

此处先进行明文存储, 方便后续操作; 等全部功能基本实现时, 再将密码升级为加盐存储.

另外注册页面导航栏的右上角只有登录和主页两个按钮:

  • 登录 : 有些用户已经有账号了,, 就可以直接跳转到登录页面;

  • 主页 : 像掘金和 CSDN 这种网站吗没有登录也是可以访问主页别人写的博客的.

代码写到这, 注册功能就基本实现了, 然后通过浏览器访问接口, 如果注册失败了, 那么肯定是你的问题, 后面会给出项目中出错如何排查和解决问题的具体流程, 然后跟着流程一步一步排查错误即可.

页面效果 :


3.2 实现登录功能

登录功能也是不需要用户登录效验的, 我得先登录, 才能进行身份验证吧, 如果将登录功能也拦截了, 就相当于两个房间的钥匙分别放在对方的房间里, 且锁住了门.

(前端操作 login.html)

第一步:引入用于发送 ajax 请求的依赖

第二步: 约定前后端交互接口

url : "/user/login"
type : "POST"
data : "username, password"
后端返回 data=1, code=200 表示代码执行成功, 返回 data != 1, code != 200 表示执行失败.

第三步: 写前端代码

1. 给用户名和密码加上 id 属性, 并给提交按钮绑定点击事件.

2. 点击事件触发的 ajax 请求

function login() {
    // 1. 非空效验
    var username = jQuery("#username");
    var password = jQuery("#password");
    if(username.val() == "") {
        alert("请先输入用户名!");
        username.focus(); // 光标聚集
        return false;
    }
    if(password.val() == "") {
        alert("请先输入密码!");
        password.focus();
        return false;
    }
    // 2. 发送请求给客户端
    jQuery.ajax({
        url:"/user/login",
        type:"post",
        data:{
            // 前面的 username 可以加引号, 也可以不加
            username:username.val(),
            password:password.val()
        },
        success:function(body) {
            if(body.code == 200 && body.data == 1) {
                // alert("登录成功!");
                location.href = "myblog_list.html";
            } else {
                alert("用户名或密码错误, 请重新输入!");
                username.focus();
            }
        }
    });
}

第四步 : 写后端代码

登录功能设计到的数据库操作就是查询操作.

  1. 先从mapper 层开始写

public UserInfo login(@Param("username") String username,
                          @Param("password") String password);
  1. 再写对应的 xml 实现

  1. 写 Service 层代码

public UserInfo login(String username, String password) {
    return userMapper.login(username, password);
}
  1. 写 Controller 层代码

// 登录 【不拦截】
@RequestMapping("/login")
public int login(HttpServletRequest request, String username, String password) {
    // 1.非空效验
    if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
        return 0;
    }
    // 2.查询操作
    UserInfo userInfo = userService.login(username, password);
    if(userInfo == null || userInfo.getId() <= 0) {
        // 用户名或密码错误
        return -1;
    } else {
        // 用户名密码都正确, 将 userinfo 保存到 session
        HttpSession session = request.getSession();
        session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);
        return 1;
    }
}

此处的登录功能也是没有通过明文查询, 后面升级为加盐处理时, 会有对应的解密方法来处理, 后面再进行代码的大改.

另外登录页面导航栏的右上角只有注册和主页两个按钮:

  • 注册 : 有些用户需要注册, 但是不小心点到登录界面, 所以对于用户来说, 两个页面需要来回跳转.

  • 主页 : 像掘金和 CSDN 这种网站吗没有登录也是可以访问主页别人写的博客的.

此时登录功能也基本实现了, 下来自己通过浏览器访问对应的接口来进行验证.

页面效果 :

3.3 实现退出登录功能

该页面也是需要用户登录效验的, 只有登录了, 才会有退出登录.

前端操作 myblog_list.html 的导航栏

该功能是登录人的博客列表页中导航栏上的一个 a标签 . 因为需要用户登录效验, 所以需要发送 ajax 请求. 那么就需要给 a 标签添加点击事件, 而 a 标签的 onclick 事件支持兼容性是不好的, 有些浏览器是不认 a 标签的 onclick 时间的, 所以需要在 href 标签中写 js 代码, 具体写法如下 :
退出登录
  1. 引入用于发送 ajax 请求的依赖.

  1. 约定前后端交互接口

url : "/user/logout"
type : "POST"
data : 不传
后端只要返回 200 状态码, 就触发 success 分支, 如果返回 401 状态码, 就触发 error 分支, 两分支效果一样, 都是跳转到登录页面.
  1. 写前端代码

// 退出登录
function onExti() {
    if(confirm("确认退出? ")) {
        jQuery.ajax({
            url:"/user/logout",
            type:"POST",
            data:{},
            success:function(body) {
                // alert(JSON.stringify(body)); 将 json 转换成 String
                location.href = "/login.html";
            },
            // 非 200
            error:function(err) {
                if(err != null && err.status==401) {
                    // alert("用户未登录, 即将跳转到登录页!")
                    // 已经被拦截器拦截了 - 未登录
                    location.href = "/login.html";
                }
            }
        });
    }
}
  1. 写后端代码

// 退出登录 【拦截】
@RequestMapping("/logout")
public boolean logout(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {
        // 清除 session
        session.removeAttribute(Constant.SESSION_USERINFO_KEY);
    }
    // 默认返回 true, 如果不清除 session, session 也有一个过期时间
    return true;
}

【注意事项】

1. 跳转工作一般交给前端处理, 一般不建议后端去跳转
2. 而且此处如果想要改成后端跳转, 在拦截器中实现跳转到登录页面还行不通. 因为 ajax 是局部刷新技术, 它发送的请求无法控制整个页面进行跳转. 所以还是要在前端使用 location.href.

3.4 实现登录人的博客列表页

该页面是需要进行用户登录效验的, 所以该功能的后端接口需要被拦截器拦截.

(前端操作 myblog_list.html)

该页面要实现的功能:
1. 展现登录人写的所有博客, 如果此人没写博客, 就显示暂无数据.
2. 在左侧展示登录人的个人信息, 此处要动态展示的个人信息有用户名(username)和文章数量

引入需要发送 ajax 请求的依赖

实现获取个人文章列表页

  1. 约定前后端交互接口

url : "art/mylist"
type : "POST"
data : 不传, 让后端从 session 中拿, 避免出现张三传了李四的 id, 把李四的文章查出来了.
后端返回 code == 200, 如果 data 不为空, 且 data.length > 0 表示该作者写了文章, 否则表示该作者没有文章. 后端返回非 200 就走 error 分支.
  1. 写前端代码

var descLength = 80; // 文件简介长度
// 在 mylist 列表页截取我的文章的一部分显示
function contentSubstr(content) {
    if(content.length > descLength) {
        return content.substr(0, descLength);
    }
    return content;
}

// 获取个人文章列表
function initMyList() {
    jQuery.ajax({
        url:"/art/mylist",
        type:"POST",
        data:{}, 
        success:function(body) {
            // 获取右侧的容器
            var container = 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';
                    // 大于 80 个字符的文章, 截取前 80 个字符
                    var content = contentSubstr(blog.content);
                    descDiv.innerHTML = content;
                    blogDiv.appendChild(descDiv);

                    let div = document.createElement('div');
                    div.className = 'mydetail';
                    // 创建查看全文按钮
                    let a = document.createElement('a');
                    a.innerHTML = '查看详情';
                    a.style = "border: 2px solid black;";
                    a.href = 'blog_content.html?id=' + blog.id;
                    div.appendChild(a);
                    // 创建修改文章按钮
                    let a1 = document.createElement('a');
                    a1.innerHTML = '修改';
                    a1.style = "border: 2px solid black;";
                    a1.href = 'blog_update.html?id=' + blog.id;
                    div.appendChild(a1);
                    // 创建删除文章按钮
                    let a2 = document.createElement('a');
                    a2.innerHTML = '删除';
                    a2.style = "border: 2px solid black;";
                    a2.href = 'javascript:deleteClick('+ blog.id +')';
                    div.appendChild(a2);
                    blogDiv.appendChild(div);
                    // 把 blogDiv 加入外层元素
                    container.appendChild(blogDiv);
                }
            } else {
                // 未发表任何数据
                let blogDiv = document.createElement('div');
                blogDiv.innerHTML = "

暂无数据

"; blogDiv.style = "text-align: center"; container.appendChild(blogDiv); } }, error:function(err) { if(err!=null && err.status==401) { // alert("用户未登录, 即将跳转到登录页!") // 已经被拦截器拦截了 - 未登录 location.href = "/login.html"; } } }); } initMyList();

此处的文章列表页展示的内容只是博客的一部分, 这里的规定展示前 100 个字符只是拍脑门操作, 并且该接口在主页中所有人的列表页也需要调用, 所以将前面的内容截取函数封装在一个公共的 js 方法中.

  1. 写后端代码 (查询操作)

mapper 层:

// 获取我的博客列表页
public List getMyList(@Param("uid") Integer uid);

对应的 xml 实现:

service 层 :

// 获取文章列表页
public List getMyList(Integer uid) {
    return articleMapper.getMyList(uid);
}

controller 层 :

// 获取文章列表页 【拦截】
@RequestMapping("/mylist")
public List myList(HttpServletRequest request) {
    // 在 session 中验证登录状态
    UserInfo userInfo = SessionUtil.getLoginUser(request);
    if(userInfo != null) {
        return articleService.getMyList(userInfo.getId());
    }
    return null;
}

我的文章列表页要实现的功能还有很多, 先看看整体页面:

我的博客列表页实现的功能有 : 查看详情, 修改 , 删除, 以及左侧个人信息的 username, 以及文章数量. 后面会一一实现.

获取我的博客列表页的个人信息(username, 文章数量)

这里的操作, 都是需要被拦截器拦截的. 只有用户登录了, 才可以正常构造数据.

获取文章对应的 username (登录人)

  1. 约定前后端交互接口:

url : "/user/myinfo"
type : "POST"
data : 不传
当后端返回 200 , 并且 data 不为空, 就构造数据, 否则就是未登录, 直接跳转到登陆页面.
  1. 写前端代码 :

// 获取登录人的 username
function getMyInfo() {
    jQuery.ajax({
        url:"/user/myinfo",
        type:"POST",
        data:{},
        success:function(body) {
            if(body.code == 200 && body.data != null) {
                // jQuery('#username.text').text(body.data.username);
                let h3 = document.querySelector('.card h3');
                h3.innerHTML = body.data.username;
            }
        },
        // 非 200
        error:function(err) {
            location.href = "/login.html";
        }
    });
}
getMyInfo();
  1. 写后端代码

// 博客列表页的个人信息(登录人) 【拦截】
@RequestMapping("/myinfo")
public UserInfo myInfo(HttpServletRequest request) {
    return SessionUtil.getLoginUser(request);
}

获取登录人的文章数量:

  1. 约定前后端交互接口:

url : "/art/myartcount"
type : "POST"
data : 不传
后端返回 200 状态码并且 data 不为空就构造数据, 否则就是未登录.
  1. 写前端代码 :

// 获取文章数量
function getMyArtCount() {
    jQuery.ajax({
        url:"/art/myartcount",
        type:"POST",
        data:{},
        success:function(body) {
            if(body.code == 200 && body.data != null) {
                let artSpan = document.querySelector('#cnt');
                artSpan.innerHTML = body.data;
            }
        },
        // 非 200
        error:function(err) {
            location.href = "/login.html";
        }
    });
}
getMyArtCount();
  1. 写后端代码 (查询操作) :

mapper 层 :

// 登录人的文章数量
public int artCount(@Param(("uid")) Integer uid);

对应的 xml 实现 :

service 层:

// 获取登录人的文章数量
public int artCount(Integer uid) {
    return articleMapper.artCount(uid);
}

controller 层 :

// 获取我的文章数量 【拦截】
@RequestMapping("/myartcount")
public int myArtCount(HttpServletRequest request) {
    UserInfo userInfo = SessionUtil.getLoginUser(request);
    if(userInfo != null) {
        int result = articleService.artCount(userInfo.getId());
        return result;
    }
    return 0;
}

3.5 实现博客详情页

此处的博客详情页, 是不能被拦截器拦截的, 因为主页有一个没有登录就可以访问的所有人的分页列表页, 在用户没有登录的情况下也是可以查看别人的文章详情页的. 此处自己的详情页就是多了两个按钮 : 修改,删除. (前端操作 blog_content.html)

博客详情页要实现的功能:

1. 展示博客详情内容.
2. 展示博客的访问量. (不管是谁访问, 只要访问了该文章, 访问量就 + 1) 仅在后端实现.
2. 展示左侧博客对应的作者的 username
  1. 引入需要发送 ajax 请求的依赖

  1. 约定前后端交互接口

获取文章详情:

url : "/art/detail"
type : "POST"
data : aid - 文章 id (从URL中获取)
后端返回 200 , 并且 data 不为空就构造数据.

获取文章对应的作者 :

url : "/user/author"
type : "POST"
data : uid
后端返回状态码 200 以及 dta 不为空, 就构造数据.

获取文章对应作者的文章总数 :

url : "/art/artcount"
type : "POST"
data : uid
后端返回状态码 200 以及 dta 不为空, 就构造数据.

  1. 写前端代码 :

从 URL 中获取 id 的方法封装在公共的 js 中.(多处要使用)

// 获取当前 url 中某个参数的方法
function getURLParam(key) {
    var params = location.search;  // query string
    if(params.indexOf("?") >= 0) {
        params = params.substring(1);
        // 键值对之间使用 & 分割
        var paramArr = params.split('&');
        for(var i = 0; i < paramArr.length; i++) {
            // 键和值使用 = 分割
            var namevalues = paramArr[i].split("=");
            if(namevalues[0] == key) {
                return namevalues[1];
            }
        }
    } else {
        return "";
    }
}

获取博客详情:

var editormd;
function initEdit(md){
    editormd = editormd.markdownToHTML("editorDiv", {
    markdown : md,
    });
}
// 获取文章的详细信息 [不拦截]
function getArtDetail() {
    // 从 URL 中获取文章 id
    var aid = getURLParam("id");
    if(aid != null && aid > 0) {
        // 访问后端查寻文章详情
        jQuery.ajax({
            url:"/art/detail",
            type:"post",
            data:{"aid":aid},
            success:function(body) {
                if(body.code == 200 && body.data != null) {
                    // 填充标题
                    let title = document.querySelector('#title');
                    title.innerHTML = body.data.title;
                    // 填充日期
                    let date = document.querySelector('#date');
                    date.innerHTML = body.data.createtime;
                    // 填充访问量
                    let rcount = document.querySelector('#rcount');
                    rcount.innerHTML = body.data.rcount;
                    // 填充内容
                    editormd = editormd.markdownToHTML("editorDiv", {
                        markdown : body.data.content
                    });
                    // 左侧个人信息
                    authorInfo(body.data.uid);
                    getArtCount(body.data.uid);
                }
            }
        });
    }
}
getArtDetail();

获取博客对应的作者的用户名 :

// 获取当前博客作者的 username [不拦截]
function authorInfo(uid) {
    jQuery.ajax({
        url:"/user/author",
        type:"post",
        data:{uid:uid},
        success:function(body) {
            if(body.code == 200 && body.data != null) {
                let username = document.querySelector(".card h3");
                username.innerHTML = body.data.username;
            }
        }
    });
}

获取博客对应的作者的文章总数量 :

// 获取当前博客对应作者的文章数量 [不拦截]
function getArtCount(uid) {
    jQuery.ajax({
        url:"/art/artcount",
        type:"POST",
        data:{"uid":uid},
        success:function(body) {
            if(body.code == 200 && body.data != null) {
                let artSpan = document.querySelector('#cnt');
                artSpan.innerHTML = body.data;
            }
        }
    });
}
getArtCount();
  1. 写后端代码:

后端代码设计的操作有查询和修改.

查询 : 查询文章详情, 查询作者, 查询作者的文章总数
修改 : 每次访问, 都让访问量 + 1.

mapper 层 (ArticleMapper) :

// 获取文章详情页
public ArticleInfo getDetail(@Param("aid") Integer aid);
// 作者的文章数量
public int artCount(@Param(("uid")) Integer uid);
// 每次查看相详情页, 访问量 + 1
public int addRcount(@Param("rcount") Integer rcount, @Param("aid") Integer aid);

(UserMapper) :

// 获取博客对应作者的信息
public UserInfo getAuthorInfo(@Param("uid") Integer uid);

对应的 xml 实现 (ArticleMapper.xml):






    update articleinfo set rcount=#{rcount} where id=#{aid}

(UserMapper.xml) :

service 层 (ArticleService):

// 获取文章详情页
public ArticleInfo getDetail(Integer aid) {
    return articleMapper.getDetail(aid);
}
// 获取作者的文章数量
public int artCount(Integer uid) {
    return articleMapper.artCount(uid);
}
// 增加访问量
public int addRcount(Integer rcount, Integer aid) {
    return articleMapper.addRcount(rcount, aid);
}

(UserService):

// 获取博客对应作者的信息
public UserInfo getAuthorInfo(Integer uid) {
    return userMapper.getAuthorInfo(uid);
}

controller 层 (ArticleController):

// 获取文章详情页 【不拦截】
@RequestMapping("/detail")
public Object getDetail(Integer aid) {
    if(aid != null && aid > 0) {
        ArticleInfo articleInfo = articleService.getDetail(aid);
        // 在每次获取详情页的时候增加访问量
        int result = articleService.addRcount(articleInfo.getRcount() + 1, aid);
        if(result == 1) {
            return AjaxResult.success(articleInfo);
        }
    }
    return AjaxResult.fail(-1, "查询失败");
}

// 获取当前博客作者的文章数量
@RequestMapping("/artcount")
public int artCount(Integer uid) {
    return articleService.artCount(uid);
}

(UserController):

// 查询当前博客的个人信息 - username【不拦截】
@RequestMapping("/author")
public UserInfo authorInfo(Integer uid) {
    return userService.getAuthorInfo(uid);
}

3.6 实现删除博客

删除博客功能需要用户登录了才能进行操作, 所以需要过拦截器. 删除博客时需要弹窗是否要删除, 防止用户误点. 并且只有我的博客列比页才有删除功能. 主页的博客列表页是没有的.

(前端操作 myblog_list.html)

  1. 约定前后端交互接口

url : "/art/delete"
type : "POST"
data : aid
后端返回 code == 200 并且受影响的行数 > 0 就弹出删除成功. 否则弹出删除失败.
  1. 写前端代码

删除操作的点击事件的参数, 是在获取博客列表页获取到传递过来的:

SSM项目 - 博客系统_第5张图片
// 删除博客
function deleteClick(aid) {
    if(confirm("确认删除?")) {
        jQuery.ajax({
            url:"/art/delete",
            type:"post",
            data:{"aid":aid},
            success:function(body) {
                if(body.code == 200 && body.data > 0) {
                    alert("删除成功!");
                } else {
                    alert("删除失败, 请重试!");
                }
            }, 
            error:function(err) {
                if(err!=null && err.status==401) {
                    // alert("用户未登录, 即将跳转到登录页!")
                    // 已经被拦截器拦截了 - 未登录
                    location.href = "/login.html";
                }
            }
        });
    }
}
  1. 写后端代码

mapper 层 :

虽然前端只传递一个 aid, 但是在后端最好把 uid 也带上 (从 session 中获取), 删除功能只能是登录的人删除登录人自己的文章.

// 删除文章
public int delete(@Param("aid") Integer aid,
                  @Param("uid") Integer uid);

对应的 xml 实现 :


    delete from articleinfo where id=#{aid} and uid=#{uid}

service 层 :

// 删除文章
public int delete(Integer aid, Integer uid) {
    return articleMapper.delete(aid, uid);
}

controller 层

// 删除文章 【拦截】
@RequestMapping("/delete")
public int delete(HttpServletRequest request, Integer aid) {
    UserInfo userInfo = SessionUtil.getLoginUser(request);
    if(userInfo != null && userInfo.getId() > 0 && aid != null) {
        return articleService.delete(aid, userInfo.getId());
    }
    return -1;
}

3.7 实现修改博客

修改博客按钮也是在我的博客列表页才有, 主页的列表页没有修改功能. 并且修改功能是需要用户登录才能进行操作的, 所以需要过拦截器.

(前端操作 myblog_list.html)

实现修改博客有两个大的步骤 :
1. 从数据库中查询出文章的内容和标题, 然后初始化在 Markdown 编辑器上.
2. 修改文章内容/标题, 然后进行提交.

修改前先获取博客详情并初始化到编辑器上 (和前一个获取详情页不一样, 这个要过拦截器)

  1. 约定前后端交互接口

url : "/art/mydetail"
type : "POST"
data : aid
后端返回状态码 200 并且 data 不为空, 就构造数据, 如果是 401 就跳转至登录页面.
  1. 写前端代码

此处的 URL中的 aid, 是从列表页跳转过来时, 拼接上的:

SSM项目 - 博客系统_第6张图片
var aid; // 从 URL 中拿到文章 id 赋值给 aid
var editor;
function initEdit(md){
    // 编辑器设置
    editor = editormd("editorDiv", {
        // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. 
        width: "100%",
        // 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
        height: "calc(100% - 50px)",
        // 编辑器中的初始内容
        markdown: md,
        // 指定 editor.md 依赖的插件路径
        path: "editor.md/lib/",
        saveHTMLToTextarea: true // 
    });
}

// 获取文章详情并显示
function showArtDetail() {
    // 从 URL 中获取 aid
    aid = getURLParam("id");
    if(aid != null && aid > 0) {
        jQuery.ajax({
            url:"/art/mydetail",
            type:"post",
            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) {
                    // alert("用户未登录, 即将跳转到登录页!")
                    // 已经被拦截器拦截了 - 未登录
                    location.href = "/login.html";
                }
            }
        });
    }

}
showArtDetail();
  1. 写后端代码

获取详情页后端代码除了 controller 层不一样, 其他和前面都一样.

mapper 层 :

// 获取文章详情页
public ArticleInfo getDetail(@Param("aid") Integer aid);

对应的 xml 实现 :

service 层 :

 // 获取文章详情页
public ArticleInfo getDetail(Integer aid) {
    return articleMapper.getDetail(aid);
}

controller 层 :

// 修改文章 : 获取当前登录的人的文章详情页 【拦截】
@RequestMapping("/mydetail")
public Object getMyDetail(HttpServletRequest request, Integer aid) {
    if(aid != null && aid > 0) {
        // 查询自己写的的文章详情页
        ArticleInfo articleInfo = articleService.getDetail(aid);
        // 文章归属人验证
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        // 1. 是否登录,userId 是否大于 0;
        // 2. 文章是否为空 (没查到);
        // 3.文章作者和登录人是否是同一个人
        if(userInfo != null && userInfo.getId() > 0
                && articleInfo != null && userInfo.getId() == articleInfo.getUid()) {
            return AjaxResult.success(articleInfo);
        }
    }
    return AjaxResult.fail(-1, "查询失败");
}
修改文章时 controller 层在获取文章详情时, 不仅需要验证登录状态, 还需要文章归属人验证, 如果是登录状态, 并且该文章就是登录人的, 那么就可以获取, 否则返回查询失败.

修改文章并提交

  1. 约定前后端交互接口

url : "/art/update"
type : "POST"
data : aid, title, content
后端返回 code == 200 并且返回受影响的行数 > 0 就成功执行, 如果 401 就跳转至登录页面.
  1. 写前端代码

// 提交修改
function myupdate() {
    var title = jQuery("#title");
    var content = editor.getValue();  // 插件提供的 : 获取编辑器中的正文的方式
    // 非空效验
    if(title.val() == "") {
        title.focus();
        alert('请先输入标题!');
        return false;
    }
    if(content == "") {
        content.focus();
        alert('请先输入正文!');
        return false;
    }
    jQuery.ajax({
        url:"/art/update",
        type:"post",
        data:{
            "aid":aid,
  // 全局变量
            "title":title.val(),
            "content":content
        },
        success:function(body) {
            // 状态码 + 返回受影响的行数
            if(body.code == 200 && body.data > 0) {
                alert('修改成功!');
                location.href = "myblog_list.html";
            } else {
                alert('修改失败, 请重试!');
            }
        },
        error:function(err){
            if(err!=null && err.status==401){
                // alert("用户未登录,即将跳转到登录页!");
                // 已经被拦截器拦截了,未登录
                location.href = "/login.html";
            }
        }
    });
}
  1. 写后端代码

mapper 层 :

// uid 不是前端传的, 是从 session 中取得
public int update(@Param("aid") Integer aid,
                  @Param("uid") Integer uid,
                  @Param("title") String title,
                  @Param("content") String content);

对应的 xml 实现 :


    update articleinfo set title=#{title}, content=#{content}
    where id=#{aid} and uid=#{uid}
    

service 层 :

 // 修改自己的博客
public int update(Integer aid, Integer uid, String title, String content) {
    return articleMapper.update(aid, uid, title, content);
}

controller 层 :

// 修改自己的博客 【拦截】
@RequestMapping("/update")
public int update(HttpServletRequest request, Integer aid,
                  String title, String content) {
    // 非空效验
    if(aid != null && StringUtils.hasLength(title) && StringUtils.hasLength(content)) {
        // 从 session 中验证登录状态
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo != null && userInfo.getId() > 0) {
            // 虽然前端没给 uid, 但是我们最好将登录人的 uid 也传过去, 这样代码更健全
            return articleService.update(aid, userInfo.getId(), title, content);
        }
    }
    return 0;
}

3.8 实现发布博客

发布需要登录了才能进行发布博客的操作, 所以需要过拦截器.(前端操作 blog_edit.html)

页面布局 :

SSM项目 - 博客系统_第7张图片

  1. 约定前后端交互接口

url : "/art/submit"
type : "POST"
data : title, content
后端返回 code == 200 并且返回的受影响行数 > 0, 就表示发布成功. 如果 401, 则表示未登录.
  1. 写前端代码

首先给发布文章按钮添加点击事件. 然后再写 js 代码 :

var editor;
function initEdit(md){
    // 编辑器设置
    editor = editormd("editorDiv", {
        // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. 
        width: "100%",
        // 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
        height: "calc(100% - 50px)",
        // 编辑器中的初始内容
        markdown: md,
        // 指定 editor.md 依赖的插件路径
        path: "editor.md/lib/",
        saveHTMLToTextarea: true // 
    });
}
initEdit("# 在这里写下一篇博客"); // 初始化编译器的值

// 发布文章
function mysub(){
    // alert(editor.getValue()); // 获取值
    // editor.setValue("#123") // 设置值
    var title = jQuery("#title");
    // var title = document.querySelector('#title');
    var content = editor.getValue();  // 插件提供的 : 获取编辑器中的正文的方式
    // 非空效验
    if(title.val() == "") {
        title.focus();
        alert('请先输入标题!');
        return false;
    }
    if(content == "") {
        content.focus();
        alert('请先输入正文!');
        return false;
    }
    jQuery.ajax({
        url:"/art/submit",
        type:"post",
        data:{
            "title":title.val(),
            "content":content
        },
        success:function(body) {
            // 状态码 + 返回受影响的行数
            if(body.code == 200 && body.data > 0) {
                alert('发布成功!');
                location.href = "myblog_list.html";
            } else {
                alert('发布失败, 请重试!');
            }
        },
        error:function(err){
            if(err!=null && err.status==401){
                // alert("用户未登录,即将跳转到登录页!");
                // 已经被拦截器拦截了,未登录
                location.href = "/login.html";
            }
        }
    });
}
  1. 写后端代码 (数据库中对应新增操作)

mapper 层 :

// 发布文章
public int submit(@Param("title") String title,
                  @Param("content") String content,
                  @Param("uid") Integer uid); // 从 session 中获取

对应的 xml 实现 :


    insert into articleinfo(title,content,uid) values(
        #{title},#{content},#{uid}
    )

service 层 :

 // 发布文章
public int submit(String title, String content, Integer uid) {
    return articleMapper.submit(title, content, uid);
}

controller 层 :

// 发布文章 【拦截】
@RequestMapping("/submit")
public int submit(HttpServletRequest request, String title, String content) {
    // 非空效验
    if(StringUtils.hasLength(title) && StringUtils.hasLength(content)) {
        // 获取用户的 uid
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo != null && userInfo.getId() > 0) {
            return articleService.submit(title, content, userInfo.getId());
        }
    }
    return -1;
}

3.9 实现所有人的文章列表的分页功能

实现该功能之前, 先要知道一个公式, 下面推算一下这个公式 >>

分页的要素 :

1. 页码 (pIndex) : 要查询第几页的数据.
2. 每页显示的最大长度的数据 (pSize) : 每页显示多少条数据.

找规律 :

SSM项目 - 博客系统_第8张图片

第一页数据, 我们想要显示 id=1, id=2 的两条数据, 那么通过下图中的 SQL 语句就可以做到.

SSM项目 - 博客系统_第9张图片

第二页数据, 我们想要显示 id=3, id=4 的两条数据, 那么通过下图中的 SQL 语句就可以做到.

SSM项目 - 博客系统_第10张图片

第三页数据, 我们想要显示 id=5, id=6 的两条数据, 那么通过下图中的 SQL 语句就可以做到.

SSM项目 - 博客系统_第11张图片

由此可以得出 pSize 和 offset 偏移量的一个关系 :

分页公式 : (pIndex - 1) * pSize = offset
最终的一个分页语法 : limit pSize offset (pIndex - 1) * pSize

=============================

所有人的文章列表在没有登录的时候也是可以访问的, 因此不需要过拦截器. (blog_list.html)

页面布局 :

SSM项目 - 博客系统_第12张图片

先不管四个按钮, 先处理分页的数据. 处理分页的数据之前, 先初始化 psize 和 pindex. (这两参数可以从 URL 中传递)

// 分页功能的默认参数值
var psize = 3; // 每页显示的文章数量
var pindex = 1; // 当前页码

// 初始化分页功能的的参数,从 url 中获取 pindex 和 psize
function initPageParam() {
    var pagesize = getURLParam("psize");
    if(pagesize != "") {
        psize = pagesize;
    }
    var pageindex = getURLParam("pindex");
    if(pageindex != "") {
        pindex = pageindex;
    }
}
initPageParam();

构造页面数据

  1. 约定前后端交互接口

url : "/art/list"
type : "POST"
data : psize, pindex
后端返回 code == 200 并且 data != null 就构造数据.
  1. 写前端代码

// 查询分页数据
function initList() {
    jQuery.ajax({
        url:"/art/list",
        type:"post",
        data:{
            "psize":psize,
            "pindex":pindex
        },
        success:function(body) {
            let lisDiv = document.querySelector(".listDiv");
            if(body.code == 200 && body.data != null) {
                // 遍历 body 中的 data
                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.innerHTML = contentSubstr(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();
  1. 写后端代码

mapper 层 :

 // 获取分页列表页
public List getList(@Param("psize") Integer psize,
                                 @Param("offset") Integer offset);

注意此处的 offset 需要在 controller 层计算好再传递过来, 否则 (pindex - 1) * psize 出现在 SQL 中就会报错.

对应 xml 的实现 :

service 层

// 获取分页列表
public List getList(Integer psize, Integer offset) {
    return articleMapper.getList(psize, offset);
}

controller 层 :

// 获取分页列表  [不拦截]
@RequestMapping("/list")
public List getList(Integer psize, Integer pindex) {
    if(psize == null || pindex == null) {
        return null;
    }
    // 分页公式 : 计算偏移量
    int offset = psize * (pindex - 1);
    return articleService.getList(psize, offset);
}

=================

实现四个按钮, 给四个按钮添加点击事件 >>


实现首页按钮

// 首页
function firstClick() {
    location.href = "blog_list.html";
}

实现上一页按钮

// 上一页
function beforeClick() {
    if(pindex <= 1) {
        alert("当前已经是第一页了!");
        return false;
    }
    pindex = parseInt(pindex) - 1;
    location.href = "blog_list.html?pindex=" + pindex + "&psize=" + psize;
}

实现下一页按钮

在实现下一页按钮前, 需要统计数据库中总共有多少条数据, 然后计算出分页列表总共有多少页数据. 并且这个接口也是不能被拦截的. (分页涉及的所有ajax 请求都不能被拦截, 因为没有登录也可以访问)

总页数 : totalPage = Math.ceil (total * 1.0 / psize)

  1. 约定前后端交互接口

url : "/art/totalpage"
type : "GET"
data : psize
后端返回 code == 200 并且 data 不为空, 就构造数据.
  1. 写前端代码

var totalpage = 1; // 总共多少页

// 查询总共有多少页的数据
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;
            }
        }
    });
}
getTotalPage();
  1. 写后端代码

mapper 层 :

// 查询数据库总共有多少条数据
public int getTotalCount();

对应的 xml 实现 :

service 层 :

// 查询数据库总共有多少条数据
public int getTotalCount() {
    return articleMapper.getTotalCount();
}

controller 层 :

// 计算分页功能的总页数 【不拦截】
@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 nextClick() {
    if(parseInt(pindex) + 1 > totalpage) {
        alert("当前已经是最后一页了!");
        return false;
    }
    location.href = "blog_list.html?pindex=" + (parseInt(pindex) + 1) + "&psize=" + psize;
}

实现末页按钮

前端已经求得了总页数 totalPage, 那么末页跳转就只需要做前端工作即可.

// 末页
function lastClick() {
    pindex = totalpage;
    location.href = "blog_list.html?pindex=" + pindex + "&psize=" + psize;
}

3.10 实现密码的加盐加密

在了解加盐加密之前, 我们先来了解一下 md5 加密.

md5 加密其实和明文传输差不太多, 就像穿上皇帝的新装一样, 没有隐私可言, 只不过是每一个密码 (字符串) 都对应着一个固定的 md5 值. 例如下表:

MD5

明文

202cb962ac59075b964b07152d234b70

123

900150983cd24fb0d6963f7d28e17f72

abc

所以说单纯想通过 md5 对密码进行加密是不行的, 还需要在这上面加工一下 -> 加盐处理

什么是加盐处理 ??

加盐就是在每次加密之前, 给密码加上一个不同的盐值 (salt), 此时再对这个最终密码进行 md5 操作, 那么每次加密得到的 md5 值就不一样了. 于是乎, 想要解密就必须拿到最终的 md5 值和盐值, 才能解密出原始的密码.

示例

SSM项目 - 博客系统_第13张图片
SSM项目 - 博客系统_第14张图片
使用Java 提供的 UUID 每次生成不同的盐值. 然后和密码拼接在一起生成新的 md5 值, 那么每次的加密密码都不一样. 这就是加盐.

加盐是解决了, 但是加盐加密的难点不在于这, 而在于加密后的密码要与什么对照去解密出原始密码 >>

效验密码的有效性

1. 待验证密码
2. 盐值

既然知道了要将加密后的密码和盐值共同作用才能解密, 那么盐值就也需要存储数据库 >>

【问题】盐值如何存储 ??

1. 新起一个字段, 用来存储盐值.
2. 将盐值和密码存储在一个字段中. (正确做法)

为什么不把盐值重新作为一个字段 >>

  • 首先违背了数据库的第三范式, 因为盐值根咱们表的业务逻辑是没有半毛钱关系的.

  • 其次, 贸然的将盐值明目张胆的作为一列, 很容易被黑客猜测到这就是盐值.

因此多角度考虑后选择将盐值和密码存储在一个字段中, 最终通过某种特征来将加密密码和盐值区分.

【问题】此处的某种特征, 到底是何种特征 ??

SSM项目 - 博客系统_第15张图片
SSM项目 - 博客系统_第16张图片

从代码中及运行结果中可以看出如下特征 :

1. Java 提供的 UUID 生成的随机盐值, 如果剔除中间的 "-" 之后 (让别人看不出是 UUID 的特征), 始终都是 32 位数据.
2. md5 加密后的数据, 不论密码的长短, 生成的加密密码也始终是 32 位数据.

既然长度都是固定, 而且还是一样的, 那么就可以在长度上做文章了.

我们约定将盐值和最终的加密密码拼接在一起存储在数据库中. 也就是一个 64 位的数据. (盐值在前 , 密码在后)

【总结】

做到这种程度的加密后, 你的密码就已经足够安全了, 就算黑客进行一个密码的破解, 至少也要花上几个月. 对于校招阶段就没必要再对这个 64 位密码进行一个对称加密了. 因为这种做法也是参考公共类库, 而且网上这些类库都是公布的, 都是开源的, 比这种做法还要透彻的多, 所以说没有足够的安全, 当成本大于收益的时候, 黑客自然就没有动力去做这些事情了.

加盐加密的代码实现

public class SaltSecurityUtil {
    /**
     * 加盐处理
     * @param password 原始密码
     * @return 返回 (盐值 + 加盐加密后的密码)
     */
    public static String encrypt(String password) {
        // 使用 java 提供的 UUID, 每次生成内容不同的, 32位固定长度的盐值
        String salt = UUID.randomUUID().toString().replace("-", "");
        // 最终密码 = md5(盐值 + 原始密码)
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        return salt + finalPassword;
    }

    /**
     * 密码正确性的验证
     * @param password 待验证密码
     * @param finalPassword 最终正确的密码 (数据库中加盐的密码)
     * @return
     */
    public static boolean decrypt(String password, String finalPassword) {
        // 非空效验
        if(!StringUtils.hasLength(password) || !StringUtils.hasLength(finalPassword)) {
            return false;
        }
        // 最终密码不正确
        if(finalPassword.length() != 64) {
            return false;
        }
        // 盐值
        String salt = finalPassword.substring(0, 32);
        // 使用盐值 + 待确认的密码生成一个最终密码
        String securityPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 将得到的盐值 + 生成的加盐加密密码 和数据库中的最终密码进行比较, 判断是否相等
        return (salt + securityPassword).equals(finalPassword);
    }
}

加盐及密处理好了之后, 前面的注册和登录功能就都需要做出改变了.


3.11 加盐后 - 注册功能的调整

加盐处理后, 我们存储数据库的密码, 就需要存储进行加密后的最终密码和盐值拼接在一起的结果了. (约定盐值在前, 密码在后)

新的 Controller 代码 :

 // 注册 【不拦截】
@RequestMapping("/reg")
public Object reg(String username, String password) {
    // 1. 非空效验
    if(!StringUtils.hasLength(username) && !StringUtils.hasLength(password)) {
        return AjaxResult.fail(-1, "非法的参数请求");
    }
    // 2. 进行添加操作
    int result = userService.add(username, SaltSecurityUtil.encrypt(password)); // 加盐处理
    if(result == 1) {
        return AjaxResult.success("注册成功!", 1);
    } else {
        return AjaxResult.fail(-1, "数据库添加出错!");
    }
}

3.12 加盐后 - 登录功能的调整

注册的时候, 我们存储的是加盐处理后的 64 位密码, 那么登录的时候, 验证密码用户名密码正确性的时候, 就不能从数据库取出密码和前端传递过来的密码进行比较了. 做出调整 >>

1. 先根据用户名拿到 userinfo. (验证用户名的正确性)
2. 再拿前端传递过来的密码和最终的 64 位密码传过去解密, 得到一个布尔值, 根据这个布尔值来验证密码的正确性.

新的 Controller 代码 :

// 登录 【不拦截】
@RequestMapping("/login")
public int login(HttpServletRequest request, String username, String password) {
    // 1.非空效验
    if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
        return 0;
    }
    // 2.根据用户名查询用户 [为了拿到用户的密码, 然后调用 decrypt 方法进行验证]
    UserInfo userInfo = userService.getUserByName(username);
    if(userInfo == null || userInfo.getId() <= 0) {
        // 用户名错误
        return -1;
    } else {
        // 用户名正确, 密码需要验证 [传过来的密码, 和数据库中的密码经过解密进行判断]
        boolean result = SaltSecurityUtil.decrypt(password, userInfo.getPassword());
        if(result) {
            // 将 userinfo 保存到 session 中
            HttpSession session = request.getSession();
            session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);
            return 1;
        }
    }
    return -1;
}

原来的根据用户名和密码验证用户名密码的正确性的代码已经不适用了. 所以需要重新写一个接口 - 根据用户名查询 userinfo. 并且数据库中的 username 必须是唯一约束.

mapper 层 :

// 根据用户名查询 userinfo
public UserInfo getUserByName(@Param("username") String username);

对应的 xml 实现 :

service 层 :

public UserInfo getUserByName(String username) {
    return userMapper.getUserByName(username);
}

4. 项目中遇到问题如何冷静解决

此处主要偏向于前端问题, 因为我学的后端方向, 我觉得后端问题通过调试和打印日志的方式, 问题基本上可以得到妥善处理 , 而前段比较陌生, 所以前端解决问题的方式才是重点.

4.1 首先排查是否是缓存问题

前端 :

1. 右键查看源代码, 看看是旧的代码还是新的代码, 如果是旧的代码就可以尝试强制刷新或清空缓存再试.

1. ctrl + r - 强制刷新.
2. 清空浏览器的缓存.
3. 在 url中更改参数, 浏览器比较笨, 它都是以 url 地址做缓存的, 所以 url 发生改变, 一定能清除前端缓存.

如果确定是缓存问题., 并且上述三个步骤都没用, 那就是 IDEA 的 target 目录还没有更新 (社区办 IDEA 比较拉胯), 那就需要手动重启 IDEA 了.

后端 :

删除 target 目录, 重新运行程序.

4.2 打开浏览器开发者工具, 查看控制是否有 JS 报错

代码示例 :

SSM项目 - 博客系统_第17张图片

我们写出这样一个代码, 登录功能最终也是不会触发的, 当我们一打开控制台, 就能很容易定位到问题.


4.3 使用浏览器的开发者工具调试 JS

SSM项目 - 博客系统_第18张图片

此处的开发者工具的调试功能其实就和 IDEA 差不读, 我们只要会用 IDEA 的调试, 那么这个也是很容易上手.


4.4 使用浏览器开发者工具 / Fiddler 等工具抓包

SSM项目 - 博客系统_第19张图片

抓住几个主要的请求, 然后点击查看具体信息 >>

SSM项目 - 博客系统_第20张图片

4.5 启动 IDEA 调试后端 BUG

第 5 点是必会技能, 没有啥好说的.


本篇博客就到这里, 谢谢观看!!

你可能感兴趣的:(SSM,项目,spring,java,mybatis)