SpringBoot開發簡單的書籍管理系統

GitHub:https://github.com/kzj666/bookstore

1.新建Spring Initializr項目

Spring Boot 版本2.2.5

勾選相關架構支持
Lombok
Spring Web
Thymeleaf
MySQL Driver

先新建一個TestController測試啓動

@RestController
public class TestController {
    @RequestMapping("/test")
    public String test(){
        return "success";
    }
}

springboot約定大於配置,默認是集成的thymleaf。

thymleaf約定
1.默認靜態文件(js,css,jpg等)放在resources下面的static文件夾下面
2頁面文件放在templates文件夾下面

默認情況下:
spring-boot項目靜態文件目錄:/src/java/resources/static (比如:js、css、img等靜態資源)
spring-boot項目模板文件目錄:/src/java/resources/templates 

新建一個application.yml配置文件,用於配置

將官網下載的整個layui包導入static文件夾下

此時項目目錄如下(這裏的html頁面,經過thymeleaf改寫後放入templates,然後編寫相應的controller訪問)

配置thymeleaf

spring:
  application:
    name: myspringboot
  output:
    ansi:
      enabled: always   #高亮輸入日誌
  profiles:
    active: dev
  thymeleaf:
    encoding: UTF-8
    prefix: classpath:/templates/    默認到classpath:templates下檢索頁面
    cache: false    #關閉thymeleaf的緩存,不然在開發過程中修改頁面不會立刻生效需要重啓,生產可配置爲true

 

改寫的頁面都放在resource下的templates下

書寫login頁面,利用th重新改寫css和js的引入

<!DOCTYPE html>
<html class="x-admin-sm" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>KK書屋後臺登錄</title>
    <meta name="renderer" content="webkit|ie-comp|ie-stand">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport"
          content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi"/>
    <meta http-equiv="Cache-Control" content="no-siteapp"/>
    <link rel="stylesheet" th:href="@{/css/font.css}">
    <link rel="stylesheet" th:href="@{/css/login.css}">
    <link rel="stylesheet" th:href="@{/css/xadmin.css}">
    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
    <script th:src="@{lib/layui/layui.js}" charset="utf-8"></script>
    <!--[if lt IE 9]>
    <script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
    <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>
<body class="login-bg">

<div class="login layui-anim layui-anim-up">
    <div class="message">KK書屋管理系統</div>
    <div id="darkbannerwrap"></div>
    <div align="center"><p th:text="${msg}" style="color: red; font-size: 17px; font-weight: bold"></p></div>
    <hr class="hr15">
    <form method="post" class="layui-form" th:action="tologin">
        <input name="username" placeholder="用戶名" type="text" lay-verify="required" class="layui-input">
        <hr class="hr15">
        <input name="password" lay-verify="required" placeholder="密碼" type="password" class="layui-input">
        <hr class="hr15">
        <input value="登錄" lay-submit lay-filter="login" style="width:100%;" type="submit">
        <hr class="hr20">
    </form>
</div>
<div align="center" style="margin-top: 300px;">
    <p>© 2020 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.layui.com/" target="_blank">湖人總冠軍</a></p>
    <br>
    <span><a href="https://blog.csdn.net/K_kzj_K" target="_blank">CSDN</a></span>
    <span style="padding-left:30px;"><a href="https://github.com/kzj666" target="_blank">GitHub</a></span>
    <span style="padding-left:30px;"><a href="https://blog.csdn.net/K_kzj_K/article/details/104878242" target="_blank">開發文檔</a></span>
</div>

</body>
</html>

然後在controller包下創建一個LoginController控制類,編寫login請求映射,returnlogin,即跳轉到login頁面

Controller
public class LoginController {
    @GetMapping({"/login","/"})
    public String login(){
        return "login";
    }
}

請求localhost:8080/或者localhost:8080/login即可訪問

再寫一個index後臺頁面,同理也要利用th重新改寫css和js的引入

<!doctype html>
<html class="x-admin-sm" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>KKBOOKSTORE後臺</title>
    <meta name="renderer" content="webkit|ie-comp|ie-stand">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport"
          content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi"/>
    <meta http-equiv="Cache-Control" content="no-siteapp"/>
    <link rel="stylesheet" th:href="@{/css/font.css}">
    <link rel="stylesheet" th:href="@{/css/xadmin.css}">
    <!-- <link rel="stylesheet" href="./css/theme5.css"> -->
    <script th:src="@{/lib/layui/layui.js}" charset="utf-8"></script>
    <script type="text/javascript" th:src="@{/js/xadmin.js}"></script>
    <!-- 讓IE8/9支持媒體查詢,從而兼容柵格 -->
    <!--[if lt IE 9]>
      <script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
      <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
    <script>
        // 是否開啓刷新記憶tab功能
        // var is_remember = false;
    </script>
</head>
<body class="index">
<!-- 頂部開始 -->
<div class="container">
    <div class="logo">
        <a th:href="@{/index}">KK書屋後臺</a></div>
    <div class="left_open">
        <a><i title="展開左側欄" class="iconfont">&#xe699;</i></a>
    </div>
    <ul class="layui-nav left fast-add" lay-filter="">
        <li class="layui-nav-item">
            <a href="javascript:;">+新增</a>
            <dl class="layui-nav-child">
                <!-- 二級菜單 -->
                <dd>
                    <a onclick="xadmin.open('最大化','http://www.baidu.com','','',true)">
                        <i class="iconfont">&#xe6a2;</i>彈出最大化</a></dd>
                <dd>
                    <a onclick="xadmin.open('彈出自動寬高','http://www.baidu.com')">
                        <i class="iconfont">&#xe6a8;</i>彈出自動寬高</a></dd>
                <dd>
                    <a onclick="xadmin.open('彈出指定寬高','http://www.baidu.com',500,300)">
                        <i class="iconfont">&#xe6a8;</i>彈出指定寬高</a></dd>
                <dd>
                    <a onclick="xadmin.add_tab('在tab打開','member-list.html')">
                        <i class="iconfont">&#xe6b8;</i>在tab打開</a></dd>
                <dd>
                    <a onclick="xadmin.add_tab('在tab打開刷新','member-del.html',true)">
                        <i class="iconfont">&#xe6b8;</i>在tab打開刷新</a></dd>
            </dl>
        </li>
    </ul>
    <ul class="layui-nav right" lay-filter="">
        <li class="layui-nav-item">
            <a href="javascript:;" th:text="${session.user.username}"></a>
            <dl class="layui-nav-child">
                <!-- 二級菜單 -->
                <dd>
                    <a th:value="${session.user.id}" onclick="xadmin.open2('個人信息','/user/to-admin-edit/','','',false,this)">個人信息</a></dd>
                <dd>
                    <a onclick="xadmin.open('切換帳號','http://www.baidu.com')">切換帳號</a></dd>
                <dd>
                    <a th:href="@{/logout}">退出</a></dd>
            </dl>
        </li>
        <li class="layui-nav-item to-index">
            <a href="/">前臺首頁</a></li>
    </ul>
</div>
<!-- 頂部結束 -->
<!-- 中部開始 -->
<!-- 左側菜單開始 -->
<div class="left-nav">
    <div id="side-nav">
        <ul id="nav">
            <li shiro:hasPermission="U">
                <a href="javascript:;">
                    <i class="iconfont left-nav-li" lay-tips="管理員管理">&#xe726;</i>
                    <cite>管理員管理</cite>
                    <i class="iconfont nav_right">&#xe697;</i></a>
                <ul class="sub-menu">
                    <li>
                        <a onclick="xadmin.add_tab('管理員列表','user/selectList')">
                            <i class="iconfont">&#xe6a7;</i>
                            <cite>管理員列表</cite></a>
                    </li>
                </ul>
            </li>
            <li>
                <a href="javascript:;">
                    <i class="iconfont left-nav-li" lay-tips="書架">&#xe723;</i>
                    <cite>書架</cite>
                    <i class="iconfont nav_right">&#xe697;</i></a>
                <ul class="sub-menu">
                    <li>
                        <a onclick="xadmin.add_tab('所有書籍','book/selectList')">
                            <i class="iconfont">&#xe6a7;</i>
                            <cite>所有書籍</cite></a>
                    </li>
                </ul>
            </li>
        </ul>
    </div>
</div>
<!-- <div class="x-slide_left"></div> -->
<!-- 左側菜單結束 -->
<!-- 右側主體開始 -->
<div class="page-content">
    <div class="layui-tab tab" lay-filter="xbs_tab" lay-allowclose="false">
        <ul class="layui-tab-title">
            <li class="home">
                <i class="layui-icon">&#xe68e;</i>我的桌面
            </li>
        </ul>
        <div class="layui-unselect layui-form-select layui-form-selected" id="tab_right">
            <dl>
                <dd data-type="this">關閉當前</dd>
                <dd data-type="other">關閉其它</dd>
                <dd data-type="all">關閉全部</dd>
            </dl>
        </div>
        <div class="layui-tab-content">
            <div class="layui-tab-item layui-show">
                <iframe th:src="@{./welcome.html}" frameborder="0" scrolling="yes" class="x-iframe"></iframe>
            </div>
        </div>
        <div id="tab_show"></div>
    </div>
</div>
<div class="page-content-bg"></div>
<style id="theme_style"></style>
<!-- 右側主體結束 -->
<!-- 中部結束 -->

</body>

</html>

在LoginController寫一個請求處理方法

@PostMapping("/index")
public String tologin(){
    return "index";
}

訪問localhost:8080/tologin,跳轉到index頁面

2.設計表結構

腳本內容

/*
Navicat MySQL Data Transfer

Source Server         : localhost_3306
Source Server Version : 80018
Source Host           : localhost:3306
Source Database       : bookstore

Target Server Type    : MYSQL
Target Server Version : 80018
File Encoding         : 65001

Date: 2020-03-24 10:21:25
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for book
-- ----------------------------
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `bookname` varchar(30) NOT NULL,
  `pub` varchar(30) NOT NULL,
  `price` decimal(10,0) NOT NULL,
  `date` datetime NOT NULL,
  `count` int(11) NOT NULL,
  `kind` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of book
-- ----------------------------
INSERT INTO `book` VALUES ('1', '霸道總裁愛上我', '總裁出版社', '58', '2020-03-23 20:01:27', '20', '言情類');
INSERT INTO `book` VALUES ('2', '霸道總裁迷上我', '總裁出版社', '58', '2020-03-23 06:01:27', '10', '言情類');
INSERT INTO `book` VALUES ('5', '西遊記', '西遊出版社', '66', '2020-02-02 06:00:00', '40', '文學類');
INSERT INTO `book` VALUES ('6', '三國演義', '三國出版社', '68', '2019-12-31 16:00:00', '45', '文學類');
INSERT INTO `book` VALUES ('7', '紅樓夢', '紅樓出版社', '58', '2020-01-01 06:00:00', '23', '言情類');
INSERT INTO `book` VALUES ('8', '水滸傳', '水壺出版社', '76', '2024-08-03 06:00:00', '50', '文學類');
INSERT INTO `book` VALUES ('9', '人生', '神祕出版社', '66', '2020-02-22 16:00:00', '50', '文學類');
INSERT INTO `book` VALUES ('10', '貓生', '神祕出版社', '38', '2020-02-01 16:00:00', '10', '文學類');
INSERT INTO `book` VALUES ('11', '狗生', '神祕出版社', '33', '2020-02-01 16:00:00', '20', '文學類');
INSERT INTO `book` VALUES ('12', '蛋生', '神祕出版社', '45', '2020-02-01 16:00:00', '32', '文學類');
INSERT INTO `book` VALUES ('13', '鳥生', '神祕出版社', '45', '2020-02-01 16:00:00', '32', '文學類');
INSERT INTO `book` VALUES ('14', '龍生', '神祕出版社', '45', '2020-02-02 06:00:00', '32', '文學類');
INSERT INTO `book` VALUES ('15', '雞生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('16', '鴨生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('17', '花生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('18', '強生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('19', '醫生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('20', '鵝生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('21', '豬生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('22', '餘生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('23', '獅生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('24', '豹生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('25', '狒生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('26', '鹿生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('27', '象生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('28', '馬生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('29', '牛生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('30', '羊生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');
INSERT INTO `book` VALUES ('31', '學生', '神祕出版社', '50', '2020-02-01 16:00:00', '12', '文學類');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL,
  `password` varchar(20) DEFAULT NULL,
  `phone` varchar(11) DEFAULT NULL,
  `email` varchar(50) DEFAULT NULL,
  `sex` char(5) DEFAULT NULL,
  `perm` varchar(20) DEFAULT NULL,
  `role` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'kk', '123123', '13531174771', '[email protected]', 'm', 'A-C-U-R-D', 'super-administrator');
INSERT INTO `user` VALUES ('2', 'zz', '123456', '13531174771', '[email protected]', 'f', 'C-R-U', 'adminstrator');
INSERT INTO `user` VALUES ('3', 'jj', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');
INSERT INTO `user` VALUES ('4', 'aa', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');
INSERT INTO `user` VALUES ('5', 'bb', '123456', '13531174771', '[email protected]', 'f', 'R', 'guest');
INSERT INTO `user` VALUES ('7', 'dd', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');
INSERT INTO `user` VALUES ('8', 'ee', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');
INSERT INTO `user` VALUES ('9', 'ff', '123456', '13531174771', '[email protected]', 'f', 'R', 'guest');
INSERT INTO `user` VALUES ('10', 'gg', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');
INSERT INTO `user` VALUES ('12', 'ii', '123456', '13531174771', '[email protected]', 'f', 'R', 'guest');
INSERT INTO `user` VALUES ('14', 'mm', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');
INSERT INTO `user` VALUES ('15', 'nn', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');
INSERT INTO `user` VALUES ('16', 'oo', '123456', '13531174771', '[email protected]', 'f', 'R', 'guest');
INSERT INTO `user` VALUES ('17', 'pp', '123456', '13531174771', '[email protected]', 'f', 'R', 'guest');
INSERT INTO `user` VALUES ('26', 'rr', '123456', '13531174771', '[email protected]', 'm', 'R', 'guest');

3.生成各層代碼

利用idea的easycode插件 ,生成相應代碼

在dao接口上加上@Mapper註解,在主啓動類上加上@MapperScan(com.kk.dao)

爲UserDao接口新增一個根據用戶名檢索的查詢(用於shiro認證)

/**
 * 通過username查詢單挑數據
 * @param username 用戶名
 * @return 實例對象
 */
@Select("select * from user where username = #{username}")
User queryByName(@Param("username") String username);

同樣在UserService接口額UserServiceImpl下實現

4.集成mybatis和mysql

選擇一個數據源,這裏選擇druid數據源

後面還要實現分頁功能,所以導入pagehelper的依賴,分頁的依賴jar包依賴於mybatis,會自動導入mybats依賴,所以只需導入如下兩個依賴

<!--druid數據源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>
<!-- 分頁插件 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.3</version>
</dependency>

追加mybatis和數據源的配置

server:
  tomcat:
    uri-encoding: UTF-8
    max-connections: 500
    min-spare-threads: 25
    max-threads: 300
    accept-count: 200
  port: 9090

mybatis:
  type-aliases-package: com.kk.entity
  mapper-locations: classpath:mapper/*.xml

pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql

logging:
    level:
      com.kk.entity: debug

---

#開發配置
#autoReconnect        mysql超過8小時沒有操作,會自動斷開連接,該屬性可配置是否重新連接?
#failOverReadOnly     自動重連成功後,連接是否設置爲只讀?
#useSSL=false         MySQL在高版本需要指明是否進行SSL連接?

spring:
  profiles: dev
  datasource:
    url: jdbc:mysql://localhost:3306/kk_book?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&autoReconnect=true&failOverReadOnly=false&useSSL=false
    username: root
    password: admin
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    filters: stat
    #最大連接個數
    maxActive: 20
    #初始化連接個數
    initialSize: 1
    #獲取連接時最大等待時間,單位毫秒。
    maxWait: 60000
    #最小空閒連接個數
    minIdle: 1
    #配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    #配置一個連接在池中最小生存的時間,單位是毫秒
    minEvictableIdleTimeMillis: 300000
    #用來檢測連接是否有效的sql,要求是一個查詢語句。
    validationQuery: select 'x'
    #建議配置爲true,不影響性能,並且保證安全性。如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
    testWhileIdle: true
    #申請連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能
    testOnBorrow: false
    #歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能
    testOnReturn: false
    #是否緩存preparedStatement,也就是PSCache。PSCache對支持遊標的數據庫性能提升巨大,比如說oracle。在mysql下建議關閉。
    poolPreparedStatements: true
    #最大緩存數
    maxOpenPreparedStatements: 20

5.實現index頁面的書籍列表

EasyCode在UserDao接口中有自動生成queryAll(User)的方法是根據User字段查詢記錄,我們加入無參數的查詢所有記錄的方法queryListl(),並且在UserService和UserServiceImpl中實現一下,然後再在UserController中對請求進行處理

/**
 * 查詢出所有記錄
 * @return 實例對象列表
 */
@Select("select * from user")
List<User> queryList();

在userController中添加對請求的處理

@GetMapping("selectList")
public String selectList(Model model, @RequestParam(value = "pn", defaultValue = "1") int pn, @RequestParam(value = "size", defaultValue = "5") int size) {
    PageHelper.startPage(pn, size);
    List<User> users = userService.queryList();
    PageInfo<User> page = new PageInfo<>(users);
    model.addAttribute("users", users);
    model.addAttribute("page", page);
    return "user/admin-list";
}

跳轉到自己改寫的admin-list頁面,頁面中的各種增刪改查操作,都是通過改寫js事件實現觸發,發出指定請求。(頁面的改寫就不在這裏碼出來了,主要是一些按鈕的觸發事件改寫,如點擊【添加】和【編輯】按鈕,彈出添加和編輯的頁面,點擊【刪除】,發送刪除請求。這些請求都要在controller中進行映射處理)

<!DOCTYPE html>
<html class="x-admin-sm" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

<head>
    <meta charset="UTF-8">
    <title>歡迎頁面-X-admin2.2</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport"
          content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi"/>
    <link rel="stylesheet" th:href="@{/css/font.css}">
    <link rel="stylesheet" th:href="@{/css/xadmin.css}">
    <script th:src="@{/lib/layui/layui.js}" charset="utf-8"></script>
    <script type="text/javascript" th:src="@{/js/xadmin.js}"></script>
    <!--[if lt IE 9]>
    <script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
    <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>
<body>
<div class="x-nav">
          <span class="layui-breadcrumb">
            <a href="">首頁</a>
            <a href="">演示</a>
            <a>
              <cite>導航元素</cite></a>
          </span>
    <a class="layui-btn layui-btn-small" style="line-height:1.6em;margin-top:3px;float:right"
       onclick="location.reload()" title="刷新">
        <i class="layui-icon layui-icon-refresh" style="line-height:30px"></i></a>
</div>
<div class="layui-fluid">
    <div class="layui-row layui-col-space15">
        <div class="layui-col-md12">
            <div class="layui-card">
                <div class="layui-card-body ">
                    <form class="layui-form layui-col-space5">
                        <div class="layui-inline layui-show-xs-block">
                            <input class="layui-input" autocomplete="off" placeholder="開始日" name="start" id="start">
                        </div>
                        <div class="layui-inline layui-show-xs-block">
                            <input class="layui-input" autocomplete="off" placeholder="截止日" name="end" id="end">
                        </div>
                        <div class="layui-inline layui-show-xs-block">
                            <input type="text" name="username" placeholder="請輸入用戶名" autocomplete="off"
                                   class="layui-input">
                        </div>
                        <div class="layui-inline layui-show-xs-block">
                            <button class="layui-btn" lay-submit="" lay-filter="sreach"><i
                                    class="layui-icon">&#xe615;</i></button>
                        </div>
                    </form>
                </div>
                <div class="layui-card-header">
                    <button shiro:hasPermission="D" class="layui-btn layui-btn-danger" onclick="delAll()"><i class="layui-icon"></i>批量刪除
                    </button>
                    <!--這裏的url有點奇怪,本來應該是請求user/to-admin-add從而跳到admin-add頁面的,
                    但是報404,發現是user/user/to-admin-add,去掉user,反而能正確加載頁面,不明所以,尚未深究-->
                    <button shiro:hasPermission="C" class="layui-btn" onclick="xadmin.open('添加用戶','to-admin-add',600,540)"><i
                            class="layui-icon"></i>添加
                    </button>
                </div>
                <div class="layui-card-body ">
                    <table class="layui-table layui-form">
                        <thead>
                        <tr >
                            <th>
                                <input type="checkbox" name="" lay-skin="primary">
                            </th>
                            <th style="text-align: center" width="16%">ID</th>
                            <th width="16%">登錄名</th>
                            <th width="16%">手機</th>
                            <th width="16%">郵箱</th>
                            <th width="16%">性別</th>
                            <th width="16%">角色</th>
                            <!--<th>加入時間</th>-->
                            <!--<th>狀態</th>-->
                            <th>操作</th>
                        </thead>
                        <tbody>
                        <tr th:each="user:${page.list}">
                            <td>
                                <input type="checkbox" name="" lay-skin="primary">
                            </td>
                            <td th:text="${user.id}"></td>
                            <td th:text="${user.username}"></td>
                            <td th:text="${user.phone}"></td>
                            <td th:text="${user.email}"></td>
                            <td th:text="${user.sex}"></td>
                            <td th:text="${user.role}"></td>
                            <td class="td-manage">
                                <!--<a onclick="member_stop(this,'10001')" href="javascript:;" title="啓用">-->
                                    <!--<i class="layui-icon">&#xe601;</i></a>-->
                                <!--改寫xadmin.open()方法,傳this過去,就能根據id查詢然後回顯數據,注意不要重名-->
                                <a shiro:hasPermission="A" title="編輯" th:value="${user.id}" onclick="xadmin.open2('編輯','/user/to-admin-edit/','','',false,this)" href="javascript:;">
                                    <i class="layui-icon">&#xe642;</i>
                                </a>
                                <span>&nbsp;&nbsp;&nbsp;</span>
                                <!--有Delete權限的用戶才顯示刪除按鈕-->
                                <!--onclick,將本標籤元素this傳入-->
                                <a shiro:hasPermission="D" title="刪除" th:value="${user.id}" onclick=member_del(this)>
                                    <i class="layui-icon">&#xe640;</i>
                                </a>
                            </td>
                        </tr>
                        </tbody>
                    </table>
                </div>
                <div class="layui-card-body ">
                    <div class="page">
                        <!--當前頁數小於等於1時,上一頁還是顯示第一頁的信息-->
                        <div th:if="${page.pageNum} le 1">
                            <a class="prev" th:href="@{/user/selectList(pn=1)}">&lt;&lt;</a>
                            <a class="num" th:href="@{/user/selectList(pn=${page.pageNum})}">上一頁</a>
                            <span class="current" th:href="@{/user/selectList(pn=${page.pageNum})}" th:text="${page.pageNum}"></span>
                            <a class="num" th:if="${page.pageNum} lt ${page.pages}" th:href="@{/user/selectList(pn=${page.pageNum+1})}">下一頁</a>
                            <a class="next" th:href="@{/user/selectList(pn=${page.pages})}">&gt;&gt;</a>
                        </div>
                        <!--當前頁數大於等於最後一頁時,點擊下一頁還是顯示最尾頁頁的信息-->
                        <div th:if="${page.pageNum} != 1 and ${page.pageNum} ge ${page.pages}">
                            <a class="prev" th:href="@{/user/selectList(pn=1)}">&lt;&lt;</a>
                            <a class="num" th:href="@{/user/selectList(pn=${page.pageNum-1})}">上一頁</a>
                            <span class="current" th:href="@{/user/selectList(pn=${page.pageNum})}" th:text="${page.pageNum}"></span>
                            <a class="num" th:href="@{/user/selectList(pn=${page.pageNum})}">下一頁</a>
                            <a class="next" th:href="@{/user/selectList(pn=${page.pages})}">&gt;&gt;</a>
                        </div>
                        <!--非首尾頁-->
                        <div th:if="${page.pageNum} gt 1 and ${page.pageNum} lt ${page.pages}">
                            <a class="prev" th:href="@{/user/selectList(pn=1)}">&lt;&lt;</a>
                            <a class="num" th:href="@{/user/selectList(pn=${page.pageNum-1})}">上一頁</a>
                            <span class="current" th:href="@{/user/selectList(pn=${page.pageNum})}" th:text="${page.pageNum}"></span>
                            <a class="num" th:href="@{/user/selectList(pn=${page.pageNum+1})}">下一頁</a>
                            <a class="next" th:href="@{/user/selectList(pn=${page.pages})}">&gt;&gt;</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
<script>
    layui.use(['laydate', 'form'], function () {
        var laydate = layui.laydate;
        var form = layui.form;

        //執行一個laydate實例
        laydate.render({
            elem: '#start' //指定元素
        });

        //執行一個laydate實例
        laydate.render({
            elem: '#end' //指定元素
        });
    });

    /*用戶-停用*/
    function member_stop(obj, id) {
        layer.confirm('確認要停用嗎?', function (index) {

            if ($(obj).attr('title') == '啓用') {

                //發異步把用戶狀態進行更改
                $(obj).attr('title', '停用')
                $(obj).find('i').html('&#xe62f;');

                $(obj).parents("tr").find(".td-status").find('span').addClass('layui-btn-disabled').html('已停用');
                layer.msg('已停用!', {icon: 5, time: 1000});

            } else {
                $(obj).attr('title', '啓用')
                $(obj).find('i').html('&#xe601;');

                $(obj).parents("tr").find(".td-status").find('span').removeClass('layui-btn-disabled').html('已啓用');
                layer.msg('已啓用!', {icon: 5, time: 1000});
            }

        });
    }

    /*用戶-刪除*/
    function member_del(obj) {
        //拿到元素的value(即id),傳給異步刪除的請求
        var id = obj.getAttribute('value');
        // console.log("///////////////////////////////////////////////"+id);
        layer.confirm('確認要刪除嗎?', function (index) {
            //發異步刪除數據
            $(obj).parents("tr").remove();
            //在確認刪除後,發送刪除請求
            $.ajax({
                type: 'GET',
                url: "/user/deleteById",
                data: {'id':id},
            });
            layer.msg('已刪除!', {icon: 1, time: 1000});
        });
    }


    function delAll(argument) {

        var data = tableCheck.getData();

        layer.confirm('確認要刪除嗎?' + data, function (index) {
            //捉到所有被選中的,發異步進行刪除
            layer.msg('刪除成功', {icon: 1});
            $(".layui-form-checked").not('.header').parents('tr').remove();
        });
    }
</script>
</html>

同理實現書籍列表的增刪改查頁面

在書籍列表中有一個根據類別查詢,改變本來的form提交方式(因爲form好像會被自動監聽,發送一個我不期望的請求,所以乾脆不用form標籤包裹),自定義一個function()監聽搜索按鈕的點擊,然後提交輸入的內容,拼接成url,發送搜索請求

【傳參數進行查詢時,後臺接收參數後,利用StringUtils.deleteWhitespace(kind)進行去空格

這個工具類需要導入commons-lang依賴

<!--commons-lang依賴-->
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

6.shiro實現認證、授權

導入相關依賴

<!--shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.2</version>
</dependency>
<!--thymeleaf-extras-shiro依賴-->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

在con.kk.config包下新建一個名爲ShiroConfig的java文件

用@Configuration聲明此類爲一個配置類,配置shiro相關配置

此配置類旨在注入ShiroFilterFactoryBean對象,所以寫一個返回值爲ShiroFilterFactoryBean的方法並注入容器(@Bean),該方法中需要注入安全管理器DefaultWebSecurityManager,所以先就new一個管理器放入容器,然而manager需要注入自定義的Realm,所以需要自定義Realm。邏輯如下

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration	
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        return bean;

    }

    @Bean
    public DefaultWebSecurityManager securityManager(MyRealm myRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm);
        return  manager;
    }

    @Bean
    public MyRealm myRealm(){
        return  new MyRealm();
    }

}

那就在同包下新建一個繼承AuthorizingRealm的MyRealm類,實現doGetAuthenticationInfo認證方法和DoGetAuthorizationInfo授權方法

package com.kk.config;

/*
@author kzj
@date 2020/3/18 - 17:43
*/

import com.kk.entity.User;
import com.kk.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Arrays;
import java.util.HashSet;

public class MyRealm extends AuthorizingRealm {

    private static final Logger logger = LoggerFactory.getLogger(MyRealm.class);

    @Autowired
    UserService userService;


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("------------------------------->執行了授權");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //獲取當前登錄的對象
        User user = (User)principals.getPrimaryPrincipal();
        //拿到登錄對象的權限,用-分離出每一個權限,放入數組
        String[] perm = user.getPerm().split("-");
        //將整個perm數組轉成集合放入HashSet中
        HashSet<String> set = new HashSet<>(Arrays.asList(perm));

        info.setStringPermissions(set);
        //添加權限
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("------------------------------->執行了認證");
        //userToken:是一個簡單的用戶名/密碼身份驗證令牌,以支持使用最廣泛的身份驗證機制。
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        String username = userToken.getUsername();
        //驗證用戶名是否在數據庫中存在
        User user = userService.queryByName(username);
        if (user == null) {
            return null;
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    }
}

寫完doGetAuthenticationInfo邏輯後,回到LoginController實現登錄認證

@Controller
public class LoginController {

    @GetMapping({"login", "/"})
    public String login() {
        return "login";
    }

    @PostMapping("tologin")
    public String tologin(User user, Model model) {
        String username = user.getUsername();
        String password = user.getPassword();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            //login認證通過後,便可拿到shiro保存的用戶對象
            User user1 = (User) subject.getPrincipal();
            subject.getSession().setAttribute("user", user1);
            return "redirect:/index";

        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "用戶名錯誤");
            return "login";
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密碼錯誤");
            return "login";
        } catch (ExcessiveAttemptsException e) {
            model.addAttribute("msg", "登錄失敗次數過多");
            return "login";
        } catch (LockedAccountException e) {
            model.addAttribute("msg", "帳號已被鎖定. The account for username " + token.getPrincipal() + " was locked.");
            return "login";
        } catch (DisabledAccountException e) {
            model.addAttribute("msg", "帳號已被禁用. The account for username " + token.getPrincipal() + " was disabled.");
            return "login";
        } catch (ExpiredCredentialsException e) {
            model.addAttribute("msg", "帳號已過期. the account for username " + token.getPrincipal() + "  was expired.");
            return "login";
        } catch (UnauthorizedException e) {
            model.addAttribute("msg", "您沒有得到相應的授權!" + e.getMessage());
            return "login";
        }
    }

接着到shiroconfig配置類下的ShiroFilterFactoryBean下配置攔截鏈

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);


    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        /*
            anon:   無需認證就可以訪問
            authc:  必須認證了才能訪問
            user:   必須擁有 記住我 功能才能使用
            perms:  擁有某個資源的權限才能訪問
            role:   擁有某個角色權限才能訪問
        */
        //定義攔截鏈
        LinkedHashMap<String, String> filtermap = new LinkedHashMap<>();

        // 配置不會被攔截的鏈接 順序判斷
        filtermap.put("/*/*.js", "anon");
        filtermap.put("/*/*.css", "anon");
        filtermap.put("*.html", "anon");
        filtermap.put("/tologin", "anon");
        filtermap.put("/login", "anon");
        filtermap.put("/index", "authc");
        // 配置退出過濾器,其中的具體的退出代碼Shiro已經替我們實現了
        filtermap.put("/logout", "logout");

        //攔截所有請求,一般放最後面
        filtermap.put("/**", "authc");

        bean.setFilterChainDefinitionMap(filtermap);

        //設置登錄要跳轉的鏈接
        //如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
        bean.setLoginUrl("/login");
        //設置成功後要跳轉的鏈接
        bean.setSuccessUrl("/index");
        //設置未授權頁面
        bean.setUnauthorizedUrl("/noauth");

        return bean;

    }

最後在開啓shiro的註解功能,最終的ShirConfig文件如下

package com.kk.config;

/*
@author kzj
@date 2020/3/18 - 17:38
*/

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);


    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        /*
            anon:   無需認證就可以訪問
            authc:  必須認證了才能訪問
            user:   必須擁有 記住我 功能才能使用
            perms:  擁有某個資源的權限才能訪問
            role:   擁有某個角色權限才能訪問
        */
        //定義攔截鏈
        LinkedHashMap<String, String> filtermap = new LinkedHashMap<>();

        // 配置不會被攔截的鏈接 順序判斷
        filtermap.put("/*/*.js", "anon");
        filtermap.put("/*/*.css", "anon");
        filtermap.put("*.html", "anon");
        filtermap.put("/tologin", "anon");
        filtermap.put("/login", "anon");
        filtermap.put("/index", "authc");
        // 配置退出過濾器,其中的具體的退出代碼Shiro已經替我們實現了
        filtermap.put("/logout", "logout");


        //攔截所有請求,一般放最後面
        filtermap.put("/**", "authc");


        bean.setFilterChainDefinitionMap(filtermap);


        //設置登錄要跳轉的鏈接
        //如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
        bean.setLoginUrl("/login");
        //設置成功後要跳轉的鏈接
        bean.setSuccessUrl("/index");
        //設置未授權頁面
        bean.setUnauthorizedUrl("/noauth");


        return bean;

    }

    @Bean
    public DefaultWebSecurityManager securityManager(MyRealm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    @Bean
    public MyRealm myRealm() {
        return new MyRealm();
    }

    /**
     * 整合shiroDialect:用來整合shiro和thymeleaf
     *
     * @return
     */
    @Bean
    public ShiroDialect getShiroDialect() {
        return new ShiroDialect();
    }

    /**
     * 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
     * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可實現此功能
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 開啓aop註解支持
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

便可實現登錄認證,對於授權:在相應需要權限的位置利用shiro命名空間(如只有super-administrator才能delete,則在delete按鈕標籤添加shiro命名空間,shiro:hasPermission="D"),實現不同權限不同操作。

 

這裏再說明shiro的執行過程

進行了上述的shiro配置後,在LoginController中,

//拿到前臺傳來的user對象,拿到用戶名和密碼,放入令牌
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
//SecurityUtils拿到當前的登錄對象
Subject subject = SecurityUtils.getSubject();
//登錄
subject.login(token);
執行subject.login(token);會跳轉到Realm中的doGetAuthenticationInfo()方法中,進行認證,如果認證不通過,會拋出異常;如果認證通過,方法中return new SimpleAuthenticationInfo(user, user.getPassword(), getName());返回了認證通過的用戶信息,則LoginController中可以通過(User) subject.getPrincipal();拿到通過認證的用戶,然後可放入session等等其他操作。

Shiro詳解

最後在template下創建error目錄,以各種對應錯誤的響應碼命名自定義的錯誤頁面(如:404.html),當發生相應錯誤時,會自動顯示自定義的錯誤頁面(約定大於配置)

請求列表

 

GitHub:https://github.com/kzj666/bookstore

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章