基于SSM+Security的图书管理系统

本项目是基于SSM的图书管理系统,使用Thymeleaf作为前端模板,使用SpringSecurity进行用户登录权限相关的管理操作。

项目成果展示

需求分析

  • 自定义登录登出

  • 针对不同权限规划不同页面

  • 学生的注册

  • 管理员的功能:增删书籍、查看全部借阅信息

  • 学生的功能:借阅归还书籍、查看自己的借阅信息

开发环境测试

我们依然使用之前的前端模板来搭建图书管理系统项目。

导入依赖

我们导入以下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<dependencies>
<!--Thymeleaf视图解析器-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
<!-- Spring框架依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.14</version>
</dependency>

<!-- 持久层框架依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.14</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.5</version>
</dependency>

<!-- 其他工具框架依赖:Lombok、Slf4j -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.32</version>
</dependency>

<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

<!-- JUnit依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

删除无用文件夹

删除JSP,初始化servlet等

创建初始化包

接着创建Initializer(初始化包,专门来初始化的包)来配置Web应用程序

MVC初始化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MvcInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfiguration.class};
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{MvcConfiguration.class};
}

@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}

然后就是将两个没有写的初始化器进行完善

配置类

创建配置类

Mvc配置类

mvc扫描Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@ComponentScan("book.manager.controller")
@Configuration
@EnableWebMvc
public class MvcConfiguration implements WebMvcConfigurer {

//我们需要使用ThymeleafViewResolver作为视图解析器,并解析我们的HTML页面
@Bean
public ThymeleafViewResolver thymeleafViewResolver(@Autowired SpringTemplateEngine springTemplateEngine){
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setOrder(1);
resolver.setCharacterEncoding("UTF-8");
resolver.setTemplateEngine(springTemplateEngine);
return resolver;
}

//配置模板解析器
@Bean
public SpringResourceTemplateResolver templateResolver(){
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setSuffix(".html");
resolver.setPrefix("/WEB-INF/template/");
return resolver;
}

//配置模板引擎Bean
@Bean
public SpringTemplateEngine springTemplateEngine(@Autowired ITemplateResolver resolver){
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(resolver);
return engine;
}

//开启静态资源处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}

//静态资源路径配置
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/WEB-INF/static/");
}
}

Root配置类

扫描Service

1
2
3
4
5
6
7
@Configuration
@ComponentScan({
"book.manager.service"
})
public class RootConfiguration {

}

启动测试

Controller

最后创建一个专用于响应页面的PageController即可:

1
2
3
4
5
6
7
8
9
10
11
/**
* 专用于处理页面响应的控制器
*/
@Controller
public class PageController {

@RequestMapping("/index")
public String login(){
return "index";
}
}

配置前端相关文件夹

将static直接放进WEB-INF文件夹中,再创建一个template文件夹专门用于放前端模板

接着我们需要将前端页面放到对应的文件夹中,然后开启服务器并通过浏览器,成功访问。

网站前缀

修改Tomcat服务器部署URL和应用程序上下文

访问测试

在默认地址后面输入index

访问成功!

配置Security环境

Security初始化器

接着我们需要配置SpringSecurity,与Mvc一样,需要一个初始化器

1
2
3
4
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
//不用重写任何内容
//这里实际上会自动注册一个Filter,SpringSecurity底层就是依靠N个过滤器实现的,我们之后再探讨
}

Security配置类

接着我们需要再创建一个配置类用于配置SpringSecurity:

1
2
3
4
5
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//继承WebSecurityConfigurerAdapter,之后会进行配置
}

配置MVC初始化器

接着在mvc容器中添加此配置文件即可:

1
2
3
4
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfiguration.class, SecurityConfiguration.class};
}

这样,SpringSecurity的配置就完成了,我们再次运行项目,会发现无法进入的我们的页面中,无论我们访问哪个页面,都会进入到SpringSecurity为我们提供的一个默认登录页面,之后我们会讲解如何进行配置。

至此,项目环境搭建完成。

Security自动出现了一个登陆页面

添加Thymeleaf对Security的支持

开始之前我们需要先配置一下Thymeleaf的SpringSecurity扩展,它针对SpringSecurity提供了更多额外的解析:

导入依赖

1
2
3
4
5
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>

MVC配置类

1
2
3
4
5
6
7
8
//配置模板引擎Bean
@Bean
public SpringTemplateEngine springTemplateEngine(@Autowired ITemplateResolver resolver){
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(resolver);
engine.addDialect(new SpringSecurityDialect()); //添加针对于SpringSecurity的方言
return engine;
}

前端头部

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

登录登出记住我

登录功能实现

我们知道前端输入的密码会被Security翻译成密文,这就导致我们数据库中存储的必须是经过翻译的密文,否则经过转换之后密码不能与之对应。

像是这种向数据库中查询的操作,因为数据库中已经有了数据,我们可以先写mapper进行查询,然后从Service中进行直接调用以及逻辑处理(说是Service实际上相当于ServiceImpl,注意此时应给配置RootConfiguration对mapper和service进行扫描,应该是Security有与之对应的Service,所以负责相对应的Impl就可以了)

再将Service传入到相应的应用部分(Security或者Controller)

我们这次是将Service传入SecurityConfiguration进行逻辑处理。

配置数据库层

前面我们已经实现了直接认证的方式,那么如何将其连接到数据库,通过查询数据库中的内容来进行用户登录呢?

Test翻译密码为密文

首先我们需要将加密后的密码添加到数据库中作为用户密码(注意不同的项目的翻译出来的密文不同):

1
2
3
4
5
6
7
8
public class MainTest {

@Test
public void test(){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("123456"));
}
}

这里编写一个测试来完成。

查询密码Mapper

别忘了在配置类中进行扫描,将其注册为Bean,接着我们需要编写一个Mapper用于和数据库交互:

1
2
3
4
5
6
@Mapper
public interface UserMapper {

@Select("select password from users where name = #{username}")
String getPasswordByUsername(String username);
}

根容器配置

根容器用来扫描mapper和service,配置一下Mybatis和数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ComponentScans({
@ComponentScan("book.manager.service")
})
@MapperScan("book.manager.mapper")
@Configuration
public class RootConfiguration {
@Bean
public DataSource dataSource(){
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/book_manage_ssm");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(@Autowired DataSource dataSource){
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean;
}
}

这样,登陆就会从数据库中进行查询。

配置Service和Security逻辑

比对密码Service

然后我们需要创建一个Service实现,实现的是UserDetailsService,它支持我们自己返回一个UserDetails对象,我们只需直接返回一个包含数据库中的用户名、密码等信息的UserDetails即可,SpringSecurity会自动进行比对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class UserAuthService implements UserDetailsService {

@Resource
UserMapper mapper;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String password = mapper.getPasswordByUsername(s); //从数据库根据用户名获取密码
if(password == null)
throw new UsernameNotFoundException("登录失败,用户名或密码错误!");
return User //这里需要返回UserDetails,SpringSecurity会根据给定的信息进行比对
.withUsername(s)
.password(password) //直接从数据库取的密码
.roles("user") //用户角色
.build();
}
}

Security配置

最后再重新修改一下Security配置:

1
2
3
4
5
6
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(service) //使用自定义的Service实现类进行验证
.passwordEncoder(new BCryptPasswordEncoder()); //依然使用BCryptPasswordEncoder
}

登录页面自定义

首先我们关闭csrf,这个隐藏的框的作用是携带Token进入每一个网页,没携带的会自动被拦截。关闭了csrf之后可以不用继续在网页中携带Token,不过我们只是为了方便,在实际开发的时候不能本末倒置。

由于使用了SpringSecurity,我们所有的与登录以及权限相关的比如登录页,首页,登录跳转请求,等等都是在SecurityConfiguration中进行配置

在前端更换了Security对应的变量名,方法名,请求名之后,在Security中进行继续配置即可,虽然Security取代了部分的Controller的重定向跳转功能但是具体的网址对应的页面还是要Controller进行编写。

前端配置

添加Login.html页面

前端更改表单提交action

页面跳转链接我们都是通过Controller进行跳转,前端的按钮我们要在前端的action和method进行命名。

后端替换默认界面

SecurityConfiguration

接着我们就可以将我们自己的页面替换掉默认的页面了,我们需要重写另一个方法来实现:

1
2
3
4
5
6
7
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //首先需要配置哪些请求会被拦截,哪些请求必须具有什么角色才能访问
.antMatchers("/static/**").permitAll() //静态资源,使用permitAll来运行任何人访问(注意一定要放在前面)
.antMatchers("/**").hasRole("user") //所有请求必须登陆并且是user角色才可以访问(不包含上面的静态资源)
}

首先我们需要配置拦截规则,也就是当用户未登录时,哪些路径可以访问,哪些路径不可以访问,如果不可以访问,那么会被自动重定向到登陆页面。

接着我们需要配置表单登陆和登录页面:

1
2
3
4
5
6
.and()
.formLogin() //配置Form表单登陆
.loginPage("/login") //登陆页面地址(GET)
.loginProcessingUrl("/doLogin") //form表单提交地址(POST)
.defaultSuccessUrl("/index"true) //登陆成功后跳转的页面,也可以通过Handler实现高度自定义
.permitAll() //登陆页面也需要允许所有人访问

关闭csrf

由于我们在学习的过程中暂时用不到CSFR防护,因此可以将其关闭,这样直接使用get请求也可以退出登陆,并且登陆请求中无需再携带Token了,推荐关闭,因为不关闭后面可能会因为没考虑CSRF防护而遇到一连串的问题:

1
2
.and()
.csrf().disable();

这样就可以直接关闭此功能了,但是注意,这样将会导致您的Web网站存在安全漏洞。(这里为了之后省事,就关闭保护了,但是一定要记得在不关闭的情况下需要携带Token访问)

Controller

需要配置登陆页面的地址和登陆请求发送的地址,这里登陆页面填写为/login,登陆请求地址为/doLogin,登陆页面需要我们自己去编写Controller来实现,登陆请求提交处理由SpringSecurity提供,只需要写路径就可以了。

1
2
3
4
@RequestMapping("/login")
public String login(){
return "login";
}

登出功能实现

退出的逻辑比登录要简单一点,Security自动对退出操作进行了身份删除,我们只需要简单的对两个地方进行修改

①前端的按钮名称以及方法名称

②SecurityConfiguration的logout的请求地址名称和退出后返回的网址

SecurityConfiguration

配置好后,我们还需要配置一下退出登陆操作:

1
2
3
4
.and()
.logout()
.logoutUrl("/logout") //退出登陆的请求地址
.logoutSuccessUrl("/login"); //退出后重定向的地址

前端配置

注意这里的退出登陆请求也必须是POST请求方式

1
2
3
4
5
6
<body>
<form action="logout" method="post">
<button>退出登陆</button>
</form>
</body>
</html>

登陆成功后,点击退出登陆按钮,就可以成功退出并回到登陆界面了。

记住我

由于之前的Cookie存储用户名和密码存在一定的危险,我们选择使用Security的两种记住我的形式。

①将Token存在内存中,不过重启服务器会导致Token失效,重启浏览器倒是没有什么关系。

②将Token存在数据库中,重启服务器时Security相关的配置会消失

配置SecurityConfiguration

1
2
3
4
5
6
.and()
.rememberMe() //开启记住我功能
.rememberMeParameter("remember") //登陆请求表单中需要携带的参数,如果携带,那么本次登陆会被记住
.tokenRepository(new InMemoryTokenRepositoryImpl()) //这里使用的是直接在内存中保存的TokenRepository实现
//TokenRepository有很多种实现,InMemoryTokenRepositoryImpl直接基于Map实现的,缺点就是占内存、服务器重启后记住我功能将失效
//后面我们还会讲解如何使用数据库来持久化保存Token信息

前端修改

接着我们需要在前端修改一下记住我勾选框的名称,将名称修改与上面一致,如果上面没有配置名称,那么默认使用”remember”作为名称:

1
<input type="checkbox" name="remember" class="ad-checkbox">

现在我们启动服务器,在登陆时勾选记住我勾选框,观察Cookie的变化。

重启服务器失效

将Token存在内存中会导致服务器重启之后就消失

虽然现在已经可以实现记住我功能了,但是还有一定的缺陷,如果服务器重新启动(因为Token信息全部存在HashMap中,也就是存在内存中),那么所有记录的Token信息将全部丢失,这时即使浏览器携带了之前的Token也无法恢复之前登陆的身份。

学生注册

思路

首先我们找到一个注册页面对其进行内容相关的规划,进行数据库以及网页前端的统一。输入框的name。

显示上

我们设置一个前端链接get请求(实际上前端对method不需要有具体的限制)。

1
<a th:href="register">注册用户</a></p>

通过前端的get请求传到Controller

1
2
3
4
5
//注册页面
@RequestMapping("/register")
public String register(){
return "register";
}

这时候我们发现进入页面的时候不能进入,会重定向到主页,原因是我们在设置Security的时候没有对Registerye页面进行放行。(注意要在拦截的前面)

1
2
//register页面,匀速所有人进行访问
.antMatchers("/register").permitAll()

数据上

在数据流程上,我们将数据输入前端,以表单的方式将数据传到Controller中

Controller将数据提取出来放到变量里面,调用Service中的方法,以存取了数据的变量为参数传入进去。

实际上Service也只是调用了mapper的方法,继续将变量的值继续传入mapper定义的接口方法中。

在Mapper中定义的传入的参数名与数据库中的变量名并没有什么必然的联系,因为可以使用@Param注解将变量的值传入sql语句中。

设计上

我们先设计前端对应的名字到Controller的post方法中,将变量值与之对应。

简单框架设计

重新设计数据库

我们重新设计数据库表主要是因为多了users这一张表,users关系着登陆权限和访问权限。

设置外键

我们让student表中的uid字段引用users的id字段。那么此时student就是从表,子表,外键。users表就是主表,父表,主键。

--创建时:先创建主键,再创建外键 &#x20;

--删除时:先删除外键,再删建主键

https://blog.csdn.net/qq_61122628/article/details/123763603

前端配置

输入框的name

1
2
3
4
5
<input name="name" type="text" placeholder="姓名" class="ad-input">
<input name="sex" type="text" placeholder="性别" class="ad-input">
<input name="grade" type="text" placeholder="年级" class="ad-input">
<input name="password" type="password" placeholder="密码" class="ad-input">

提交键使用button

1
2
3
<div class="ad-auth-btn">
<button class="ad-btn ad-login-member">注册</button>
</div>

表单头使用doRegister连接,前面没有斜杠。注明请求方法

1
<form action="doRegister" method="post">

Controller的post方法

注意Controller类上要写出@Controller

1
2
3
4
5
6
7
8
//注册测试
@RequestMapping(value="/doRegister" , method = RequestMethod.POST)
public String register(@RequestParam("username") String username,
@RequestParam("sex") String sex,
@RequestParam("grade") String grade,
@RequestParam("password") String password){
return "login";
}

前端传值到Controller测试

报错;至少有一个JAR被扫描用于TLD但尚未包含TLD。?

j解决:在设置catalina.properties中,将/转换成 *.jar

报错:没有直接跳转到调试页面

解决:因为Security将doRegister这个Post方法挡住了!!!我们要对“/doRegister”方法进行放行

报错:前端传入的值“男”乱码

解决:在MvcInitializer中添加Filter

1
2
3
4
5
//配置前端传值乱码Filter(无效,因为SpringSecurity的Filter自动前置)
@Override
protected Filter[] getServletFilters() {
return new Filter[]{new CharacterEncodingFilter("utf-8",true)};
}

正常情况下这样配置是没有问题的。但是没有什么用,因为SpringSecurity会把自己的Filter放在最前面,我们自己写的被放在后面,数据来的时候已经就乱码了。

所以我们应该弄个前置方法,在里面设置Filter

1
2
3
4
5
6
7
//配置前端传值乱码Filter(领先于Security的Filters)
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addFilter("characterEncodingFilter",new CharacterEncodingFilter("UTF-8",true))
.addMappingForUrlPatterns(null,false,"/*");
super.onStartup(servletContext);
}

配置后端代码

AuthService

创建AuthService使用来专门负责与用户登录授权相关的Service操作

1
2
3
public interface AuthService {
boolean register(String username,String sex,String grade,String password);
}

Mapper

新增了回写实体类的操作:我们在插入了用户信息表之后需要获取到uid,在后续插入学生表的时候需要用到。这时候我们选择的就是插入之后回写到实体类的一个操作(因为如果反过来进行查询的话,可能会出现重名不同uid的情况,就算进行时间上的排序也是比较麻烦)

我们的思路流程是先将信息插入user表中,然后通过回写实体类的机制将uid返回到实体类,我们在获取实体类之后将uid输入到student表中

mapper新增了@Option注解,会将信息对实体类有所反馈.

将传入的参数改成了实体类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Mapper
public interface UserMapper {

@Select("select password from users where name = #{username}")
AuthUser getPasswordByUsername(String username);

//注册时插入users
//options表示的是一层对应关系。true的意思是主键是自动生成的,keyColomn表示的数据库的主键,keyProperty表示的时对应的实体类中的键
@Options(useGeneratedKeys = true,keyColumn = "id",keyProperty = "id")
@Insert("insert into users(name,role,password) values(#{username},#{role},#{password})")
int registerUser(AuthUser user);

//注册时插入student
@Insert("insert into student(uid,name,grade,sex) values(#{uid},#{name},#{grade},#{sex})")
int addStudentInfo(@Param("uid") int uid,@Param("name") String name,@Param("grade") String grade,@Param("sex") String sex);
}

完善AuthServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class AuthServiceImpl implements AuthService {

@Resource
UserMapper mapper;

@Override
public void register(String username, String sex, String grade, String password) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
AuthUser user = new AuthUser(0,username,encoder.encode(password),"user");
if(mapper.registerUser(user)<=0){
throw new RuntimeException("用户信息添加失败!");
}
if(mapper.addStudentInfo(user.getId(),username,grade,sex)<=0){
throw new RuntimeException("学生信息插入失败!");
}
}
}

Controller关联Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class AuthController {
@Resource
AuthService service;

//注册测试
@RequestMapping(value="/doRegister" , method = RequestMethod.POST)
public String register(@RequestParam("username") String username,
@RequestParam("sex") String sex,
@RequestParam("grade") String grade,
@RequestParam("password") String password){
service.register(username, sex, grade, password);
return "login";
}
}

将注册的Service中的返回值改成void

没必要写成boolean,因为我们直接抛出异常

报错:Controller类找不到Service bean

原因:我们应该给Service的Impl实现类加上@Service接口,来让RootConfiguration找到他。

users表中出现了数据,但是数据没有加入到student中。

2.我们在ServiceImpl中进行插入学生的操作时,将学生的sex写成了password,导致传值失败

我们应该做一个事务,让他们要不就都插入,要不就都失败。

student和users的事务操作

我们将插入uesrs和student表的操作设置成一个事务,这样就能保证原子性。

在Spring阶段深入Mybatis框架中,使用Spring进行事务管理的具体方法是三步①给RootConfiguration配置类加上注解②给RootConfiguration加上一个事务支持的方法③将要执行的事务放在Service的一个方法中,在方法上打上注解

RootConf设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@ComponentScans({
@ComponentScan("book.manager.service")
})
@MapperScan("book.manager.mapper")
@Configuration
//开启Spring事务
@EnableTransactionManagement
public class RootConfiguration {
@Bean
public DataSource dataSource(){
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/book_manage_ssm");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(@Autowired DataSource dataSource){
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean;
}

//开启事务管理
@Bean
public TransactionManager transactionManager(@Autowired DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}

Service注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Resource
UserMapper mapper;

@Transactional
@Override
public void register(String username, String sex, String grade, String password) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
AuthUser user = new AuthUser(0,username,encoder.encode(password),"user");
if(mapper.registerUser(user)<=0){
throw new RuntimeException("用户信息添加失败!");
}
if(mapper.addStudentInfo(user.getId(),username,grade,sex)<=0){
throw new RuntimeException("学生信息插入失败!");
}
}

关于test用户密码无效的问题

就算是相同的用户名和密码注册,生成的密码都是不同的,因为翻译的时候加了盐。所以不同的用户,相同的密码,密文不同也是很正常的事情。test换了个性别就可以正常登录了,应该是浏览器或则Security的奇怪缓存问题导致的。暂时没有更好的解决办法。

解决:其实是我们刚开始对数据进行字符串校验的时候,后来开始将字符串类型改成了AuthUser对象类型。在AuthUserService部分代码(String转换成AuthUser)没有进行修改。而且mapper语句也要继续修改成select * 因为我们现在要进行搜索的是整个对象了

思路:查密码主要分为两个思路,一个是通过用户名查找密码,但是此时我们也需要获得role相关的信息,所以我们通过整个实体类对象进行查找比较方便。

我们在前端设置了doLogin之后直接创建一个AuthUserService,继承自Security的UserDetailsService类。因为是Security的自动识别,我们没有Controller,也不用对Service进行引用。只需要继承Security的类就行。

Service类引用mapper,mapper查询的是对应名字的用户的所有信息,方便我们后面将信息封装成一个对象。

1
2
@Select("select * from users where name = #{username}")
AuthUser getPasswordByUsername(String username);

在Service创建对象为mapper的,Security将前端输入信息和返回的对象进行信息比对。

Security将前端输入信息和返回的对象进行信息比对。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
AuthUser user = mapper.getPasswordByUsername(s); //从数据库根据用户名获取密码
if(user == null) {
throw new UsernameNotFoundException("登录失败,用户名或密码错误!");
}
return User //这里需要返回UserDetails,SpringSecurity会根据给定的信息进行比对
.withUsername(user.getName())
.password(user.getPassword()) //直接从数据库取的密码
.roles(user.getRole()) //用户角色
.build();
}

User表中的名字设置不能重复

由于如果出现了重复的字段,我们进行密码验证就会出现错误,所以我们要给name进行unique限制。由于name不是主键不能设置主键unique,所以我们要在索引中对name进行限制。

统一Security对“/api/auth”路径的放行

我们给AuthController加上了/api/auth前缀,然后在Security中对此路径进行放行。

在Security中对注册和登录和注册提交的POST请求进行了前缀放行

在前端对命令进行了修改注意前端的action前面不带斜杠,但是Security中的命令都是带斜杠

然后修改Controller中的重定向命令,因为我们在整个类上面加上了前缀,所以我们要对重定向写上完整的也买你返回路径。(注意重定向的写法)

AuthController+重定向

给页面加上了个名字前缀

注意重定向写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
@RequestMapping("/api/auth")
public class AuthController {
@Resource
AuthService service;

//注册测试
@RequestMapping(value="/register" , method = RequestMethod.POST)
public String register(@RequestParam("username") String name,
@RequestParam("sex") String sex,
@RequestParam("grade") String grade,
@RequestParam("password") String password){
service.register(name, sex, grade, password);
return "redirect:/login";
}
}

SecurityConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http
//拦截放行
.authorizeRequests() //首先需要配置哪些请求会被拦截,哪些请求必须具有什么角色才能访问
.antMatchers("/static/**","/register","/login","/api/auth/**").permitAll() //静态资源,使用permitAll来运行任何人访问(注意一定要放在前面)
.anyRequest().hasRole("user") //所有请求必须登陆并且是user角色才可以访问(不包含上面的静态资源)

//自定义登陆页面
.and()
.formLogin() //配置Form表单登陆
.loginPage("/login") //登陆页面地址(GET)
.loginProcessingUrl("/api/auth/login") //form表单提交地址(POST)
.defaultSuccessUrl("/index",true) //登陆成功后跳转的页面,也可以通过Handler实现高度自定义
.permitAll() //登陆页面也需要允许所有人访问

//退出
.and()
.logout()
.logoutUrl("/api/auth/logout") //退出登陆的请求地址
.logoutSuccessUrl("/login") //退出后重定向的地址

前端对应进行修改

提交表单

1
2
3
4
5
6
7
8
9
<!--注册表单请求-->
<form action="api/auth/register" method="post">
<!--登录表达请求-->
<form action="api/auth/login" method="post">
<!--退出按钮请求-->
<a href="api/auth/logout">
<i class="fas fa-sign-out-alt"></i> logout
</a>

在登陆时出现了奇怪的bug,就是使用中文用户名的时候,不能登录,显示bug。但是调试的时候却可以进行登录。然后在我清除浏览器缓存之后,maven-clean,重新构建Maven项目,RebuildProject之后,可以正常登陆了,暂时归结为缓存问题。

主页搭建

主页总体框架搭建

中文乱码再次优化

我们发现中文乱码写在MvcInitializer中依旧不是最前面的

我们可以写在SecurityInitializer中,这里应该是最前面了。

1
2
3
4
5
6
//解决中文传入Security乱码的问题
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
servletContext.addFilter("characterEncodingFilter",new CharacterEncodingFilter("UTF-8",true))
.addMappingForUrlPatterns(null,false,"/*");
}

分析人物模块

管理员:书籍管理、借阅列表

同学:书籍借阅、自己的借阅列表

前端页面修改

用户信息读取+传入Thymeleaf

我们想在进入index界面的时候直接获取用户信息(包括用户名和用户身份)

在Controller进行用户信息读取有很多种方法。其中一种是利用SecurityContext,还有就是从Session获取信息。

我们在SecurityConfiguration登陆成功之后加了一部分操作。我们先从Session中获取用户信息,然后再通过重定向的方式进行主页的跳转。

SecurityConfigutation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取用户信息
@Resource
UserMapper mapper;

//lambda表达式获取用户信息
http.successHandler(this::onAuthenticationSuccess)

//获取用户信息
private void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
HttpSession session = httpServletRequest.getSession();
AuthUser user = mapper.getPasswordByUsername(authentication.getName());
session.setAttribute("user",user);
httpServletResponse.sendRedirect("/bookmanager/index");
}

PageController

1
2
3
4
5
@RequestMapping("/index")
public String index(@SessionAttribute("user") AuthUser user , Model model){
model.addAttribute("user",user);
return "index";
}

前端传入值

1
2
<h4 th:text="${user.getName()}">John Brown</h4>
<p th:text="${user.getRole().equals('user')?'普通学生' : '管理员'}">UI | UX Designer</p>

不同权限显示不同页面

我们制作一个主页,但是里面的内容划分根据不同的权限会有不同的消失效果,这就利用到了Security对Thymeleaf的支持。

数据库输入信息

由于主页只能注册学生相关信息,所以管理员信息只能通过数据库进行输入。

Security赋予管理员登陆权限

我们将hasRole()改成hasAnyRole()

1
.anyRequest().hasAnyRole("user","admin")     //所有请求必须登陆并且是user角色才可以访问(不包含上面的静态资源)

菜单栏

菜单栏我们并不会进行两个页面的书写,但是我们会不同的显示进行不同的划分。

管理员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--图书管理员菜单-->
<li sec:authorize="hasRole('admin')">
<a href="all-product.html">
<span class="icon-menu feather-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
</span>
<span class="menu-text">
借阅信息
</span>
</a>
</li>
<li sec:authorize="hasRole('admin')">
<a href="orders.html">
<span class="icon-menu feather-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-truck"><rect x="1" y="3" width="15" height="13"></rect><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon><circle cx="5.5" cy="18.5" r="2.5"></circle><circle cx="18.5" cy="18.5" r="2.5"></circle></svg>
</span>
<span class="menu-text">
图书管理
</span>
</a>
</li>

学生菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--学生菜单-->
<li sec:authorize="hasRole('user')">
<a href="all-product.html">
<span class="icon-menu feather-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
</span>
<span class="menu-text">
本人借阅信息
</span>
</a>
</li>
<li sec:authorize="hasRole('user')">
<a href="orders.html">
<span class="icon-menu feather-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-truck"><rect x="1" y="3" width="15" height="13"></rect><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon><circle cx="5.5" cy="18.5" r="2.5"></circle><circle cx="18.5" cy="18.5" r="2.5"></circle></svg>
</span>
<span class="menu-text">
图书借阅
</span>
</a>
</li>

想要每次进入都直接访问主页

思路一:让其登陆直接进入主页,先去session找,再去数据库找(找到了给session),如果都没有,就给你送回登陆页面。

思路二:登陆直接进入登陆页面,如果找到了登录信息(先session后数据库)那么给你送到主页。(本项目不回写思路二,因为在两个地方进行判断没有必要并且容易出现多次重定向)

①采用数据库保存Token的方法

②如果直接访问主页,如果访问主页没有权限,那么从数据库中寻找权限,否则跳转回登陆页面。

③如果直接访问登陆页面,且有 登录信息,那么直接跳转到主页。

设置数据库形式的记住我

SecurityConfiguration

1
2
3
4
5
6
7
8
9
10
@Resource
PersistentTokenRepository repository;

@Bean
public PersistentTokenRepository jdbcRepository(@Autowired DataSource dataSource){
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); //使用基于JDBC的实现
repository.setDataSource(dataSource); //配置数据源
repository.setCreateTableOnStartup(true); //启动时自动创建用于存储Token的表(建议第一次启动之后删除该行)
return repository;
}
1
2
3
4
5
.and()
.rememberMe()
.rememberMeParameter("remember")
.tokenRepository(repository)
.tokenValiditySeconds(60 * 60 * 24 * 7) //Token的有效时间(秒)默认为14天

出现了报错:

因为我们的记住我是在跳转index之前对Cookie进行了一个user值的存入。我们现在直接进入index,因为没有user的权限,所以被拦下来了。

配置index的Controller

我们对用户携带的Cookie进行了判断。

如果有user这个Session那么我们直接把他进行一个model设置。

如果没有,我们将获取被记住的登陆者的名字,通过名字查找用户的信息,将其封装在一个user对象中。然后对其进行session设置,最后再进行model设置。

model是给前端的user变量传值,方便进行身份判断。在此做出区别,model传给的是前端的参数,session传给的是Cookie中的信息。并不相等,但无论如何model都是要进行传值的,session可以进行传递,但是model只有在当前的controller才能进行赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Resource
UserMapper mapper;

@RequestMapping("/index")
public String index(HttpSession session, Model model){
AuthUser user = (AuthUser) session.getAttribute("user");
if(user == null){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
user = mapper.getPasswordByUsername(authentication.getName());
session.setAttribute("user",user);
}
model.addAttribute("user",user);
return "index";
}

页面内容和划分

&#x20; 已知管理员有两个页面(所有的借阅信息,增删改书籍),学生有两个界面(显示自己的借阅信息,借书还书)。

为了节省代码量,并且也没有必要将所有的信息进行展示然后再进行权限控制,我们决定设置一个头部模板,将前面相关的,相似的页面进行套用。

页面位置分配

页面模板

侧边栏和用户名放到template模板中

给其中的模板进行命名备注th:fragment

引用部分:

页面重组

我们对整个项目的页面进行结构重组。在页面中给页面分成admin和user两部分。在controller中将页面分成api和page两部分。方便页面进行访问,也方便Security进行控制划分。

注意:修改了Controller的前缀之后一定要在SecurityConfiguration中对路径进行修改。

修改SecurityConfiguration相关地址

我们给Controller加过前缀之后,必须要给Security配置了的地址转向进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//拦截放行
.authorizeRequests() //首先需要配置哪些请求会被拦截,哪些请求必须具有什么角色才能访问
.antMatchers("/static/**","/page/auth/**","/api/auth/**").permitAll() //静态资源,使用permitAll来运行任何人访问(注意一定要放在前面)
.antMatchers("/page/user/**").hasRole("user")
.antMatchers("/page/admin/**").hasRole("admin")
.anyRequest().hasAnyRole("user","admin") //所有请求必须登陆并且是user角色才可以访问(不包含上面的静态资源)

//自定义登陆页面
.and()
.formLogin() //配置Form表单登陆
.loginPage("/page/auth/login") //登陆页面地址(GET)
.loginProcessingUrl("/api/auth/login") //form表单提交地址(POST)
//lambda表达式获取用户信息
.successHandler(this::onAuthenticationSuccess)
.permitAll() //登陆页面也需要允许所有人访问

//退出
.and()
.logout()
.logoutUrl("/api/auth/logout") //退出登陆的请求地址
.logoutSuccessUrl("/page/auth/login") //退出后重定向的地址

静态文件调整

单独对文件夹外面的静态资源的地址进行配置

thymeleaf链接地址要加斜杠表示的是WEB-INF根目录,如果不加斜杠表示的就是当前目录。

本项目的一大工程就是将原文的链接换成thymeleaf版本的链接

包括html文件夹中的资源地址

MvcConfiguration

静态文件并不需要多做任何改变。

重定向index分类工作

登录POST请求报错

原来的请求登录doLogin请求换成Thymeleaf样式的连接并且加上斜杠。

1
<form method="post" th:action="@{/api/auth/login}">

页面搭建

Controller权限

我象征的东拼西凑了一下网页,结果发现点book按钮的时候,不能进去(网页一片白)?

结果发现是没有获取用户信息,所以给我拦住了所以我们新对Controller进行修改.

我们在写Controller的时候设置了两个参数,一个是Session,session让我们通过Security,model是为了向前端传入数值。

1
2
3
4
5
@RequestMapping("/book")
public String book(HttpSession session, Model model){
model.addAttribute("user",service.findUser(session));
return "/admin/book";
}

–管理员图书管理

思路

首先将前端页面配置好。再实现网页进入(包括点击侧边栏的Thymeleaf链接和Controller中的身份认证页面跳转)

页面上

可以设置图书展示和图书删除在一个页面,增加图书再另一个界面。

Controller

我们将展示放在admin页面controller中,将添加删除放在admin api controller中,将页面和操作明确区分开来。

查询图书列表

思路就是先写前端对应的输出位置,然后从后端实体类→mapper→service→Controller→前端

Book实体类

想要查询书籍列表我们首先要有实体类依托

1
2
3
4
5
6
7
@Data
public class Book {
int id;
String title;
String desc;
double price;
}

BookMapper

有了实体类我们可以通过List<实体类>进行查询图书列表合集

要打上@Mapper注释

1
2
3
4
5
@Mapper
public interface BookMapper {
@Select("select * from book")
List<Book> allBook();
}

BookService

1
2
3
public interface BookService {
List<Book> getAllBook();
}

BookServiceImpl

我们将mapper查到的东西原封不动的返回到Controller

Service调用mapper的方法

使用@Service注解

1
2
3
4
5
6
7
8
9
10
11
@Service
public class BookServiceImpl implements BookService {

@Resource
BookMapper mapper;

@Override
public List<Book> getAllBook() {
return mapper.allBook();
}
}

AdminPageController

Controller调用Service方法

我们通过model变量将查到的列表再一并传给前端页面,我们的目的是直接让Thymeleaf模板进行解析。

1
2
3
4
5
6
7
8
9
@Resource
BookService bookService;

@RequestMapping("/book")
public String book(HttpSession session, Model model){
model.addAttribute("user",service.findUser(session));
model.addAttribute("bookList", bookService.getAllBook());
return "/admin/book";
}

book.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--源代码-->
<tr th:each="abook : ${bookList}">
<td th:text="'#'+${abook.getId()}">#JH2033</td>
<td th:text="'《'+${abook.getTitle()}+'》'">#JH2033</td>
<td th:text="${abook.getDesc()}">22/06/2021</td>
<td th:text="'¥'+${abook.getPrice()}">$600</td>
<td class="relative">
<!--删除书籍-->
<a class="action-btn" href=""> <!--th:href="'delete-book?bid='+${book.getBid()}">-->
<svg class="default-size " viewBox="0 0 341.333 341.333 ">
<g>
<g>
<g>
<path d="M170.667,85.333c23.573,0,42.667-19.093,42.667-42.667C213.333,19.093,194.24,0,170.667,0S128,19.093,128,42.667 C128,66.24,147.093,85.333,170.667,85.333z "></path>
<path d="M170.667,128C147.093,128,128,147.093,128,170.667s19.093,42.667,42.667,42.667s42.667-19.093,42.667-42.667 S194.24,128,170.667,128z "></path>
<path d="M170.667,256C147.093,256,128,275.093,128,298.667c0,23.573,19.093,42.667,42.667,42.667s42.667-19.093,42.667-42.667 C213.333,275.093,194.24,256,170.667,256z "></path>
</g>

怎么书籍id都是0 呢

是没有映射好,我们数据库中的字段是bid,我们应该给实体类中的字段和前端页面改成bid,这样就可以了。中间不用改,因为中间没涉及到具体的参数。

删除图书

BookMapper

我们知道删除的依据是id

我们先在mapper中进行通过id删除书籍的操作

1
2
@Delete("delete from book where bid = #{bid}")
void deleteBook(int bid);

BookService

1
2
3
4
public interface BookService {
List<Book> getAllBook();
void deleteBook(int bid);
}

BookServiceImpl

1
2
3
4
@Override
public void deleteBook(int bid) {
mapper.deleteBook(bid);
}

Controller

Controller部分我们进行详细的划分,将删除操作划分到api/admin中,将展示和操作划分成两个部分

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
@RequestMapping("/api/admin")
public class AdminApiController {

@Resource
BookService service;

@RequestMapping(value = "/del-book",method = RequestMethod.GET)
public String deleteBook(@RequestParam("id") int id){
service.deleteBook(id);
return "redirect:/page/admin/book";
}
}

Security权限配置

将api/admin的权限分配给admin

1
2
.antMatchers("/page/user/**","/api/user/**").hasRole("user")
.antMatchers("/page/admin/**","/api/admin/**").hasRole("admin")

触发器配置

删除书之前,删除借阅信息

所以:触发器设置给book表,在删除操作之前从borrow中删除bid=old.bid

book.html

路径极其容易出错。

1
<a class="action-btn" th:href="@{/api/admin/del-book?id=}+${abook.getBid()}">

增加图书

按钮进入页面page

前端按钮

1
<a class="ad-btn" th:href="@{/page/admin/add-book}">添加图书</a>

AdminPageController

1
2
3
4
5
6
@RequestMapping(value = "/add-book",method = RequestMethod.GET)
public String addBook(HttpSession session, Model model){
model.addAttribute("user",service.findUser(session));
model.addAttribute("bookList", bookService.getAllBook());
return "/admin/add-book";
}

报错

我象征的东拼西凑了一下网页,结果发现点book按钮的时候,不能进去(网页一片白)?

结果发现是没有获取用户信息,所以给我拦住了所以我们新对Controller进行修改.

我们在写Controller的时候设置了两个参数,一个是Session,session让我们通过Security,model是为了向前端传入数值。

添加图书api

简单写一下Controller

简单写一下add-book的apiController,将前端的数值传入Controller

1
2
3
4
5
6
7
@RequestMapping(value = "/add-book",method = RequestMethod.POST)
public String addBook(@RequestParam("title") String title,
@RequestParam("desc") String desc,
@RequestParam("price") String price){

return "redirect:/page/admin/add-book";
}

BookMapper

desc是关键字,所以我们要加上飘

1
2
3
@Insert("insert into book(title,`desc`,price) values(#{title},#{desc},#{price})")
void addBook(@Param("title") String title, @Param("desc") String desc,@Param("price") double price);

BookService

1
void addBook(String title,String desc,double price);

BookServiceImpl

1
2
3
4
@Override
public void addBook(String title, String desc, double price) {
mapper.addBook(title,desc,price);
}

Controller

1
2
3
4
5
6
7
@RequestMapping(value = "/add-book",method = RequestMethod.POST)
public String addBook(@RequestParam("title") String title,
@RequestParam("desc") String desc,
@RequestParam("price") double price){
service.addBook(title,desc,price);
return "redirect:/page/admin/book";
}

–学生借阅管理

显示在馆书籍list

思路

可以通过sql语句进行筛选,我们在这里选择的是通过Service进行筛选。

先通过bookmapper查到所有书籍,再通过borrowmapper查到所有的借阅信息。

怎么才能进行筛选呢?

我们在Service中利用实体类获取了所有的borrow的bid成了一个数字列表。

再将books中bid再数字列表中的book对象删掉。

Mapper

bookmapper

1
2
@Select("select * from book")
List<Book> allBook();

borrowmapper

1
2
@Select("select * from borrow")
List<Borrow> borrowList();

Service

1
2
3
4
5
//获取全部书籍信息
List<Book> getAllBook();

//获取在馆书籍
public List<Book> getAllBookWithoutBorrow();

ServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Resource
BookMapper mapper;
@Resource
BorrowMapper borrowmapper;

@Override
public List<Book> getAllBook() {
return mapper.allBook();
}
@Override
public List<Book> getAllBookWithoutBorrow() {
List<Book> books = mapper.allBook();
List<Integer> borrows = borrowmapper.borrowList()
.stream()
.map(Borrow::getBid)
.collect(Collectors.toList());
return books
.stream()
.filter(book -> !borrows.contains(book.getBid()))
.collect(Collectors.toList());
}

Controller

1
2
3
4
5
6
@RequestMapping("/index")
public String index(HttpSession session, Model model){
model.addAttribute("user",service.findUser(session));
model.addAttribute("bookList", bookService.getAllBookWithoutBorrow());
return "/user/index";
}

前端页面

1
2
3
4
5
<tr th:each="bbook:${bookList}">
<td th:text="'#'+${bbook.getBid()}">书籍id</td>
<td th:text="'《'+${bbook.getTitle()}+'》'">可借书籍</td>
<td th:text="${bbook.getDesc()}">书籍描述</td>
<td th:text="${bbook.getPrice()}">书籍价格</td>

图书借阅api&#x20;

思路

我们的本意其实只是想输入sid和bid直接进行添加,可是虽然bid我们能获取到,但是sid我们并不能从页面中直接获取。(但是我们可以获得uid)

我们之前在student表中通过uid查找到sid,把sid传到service,由service调用mapper的添加操作。

Mapper

borrowmapper

1
2
@Insert("insert into borrow(bid,sid,`time`) values(#{bid},#{sid},NOW())")
void addBorrow(@Param("bid") int bid,@Param("sid") int sid);

usermapper

1
2
3
@Select("select sid from student where uid = #{uid}")
Integer getSidByUserId(int uid);

Service

1
2
//借书
void borrowBook(int bid,int sid);

ServiceImpl

1
2
3
4
5
6
7
8
9
10
11
@Resource
BorrowMapper mapper;
@Resource
UserMapper usermapper;
//借书
@Override
public void borrowBook(int bid,int id) {
Integer sid = usermapper.getSidByUserId(id);
if(sid==null){return;}
mapper.addBorrow(bid,sid);
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
@RequestMapping("/api/user")
public class UserApiController {

@Resource
BorrowService service;

@RequestMapping("/borrow-book")
public String borrowBook(@RequestParam("id") int bid,
@SessionAttribute("user")AuthUser user){
service.borrowBook(bid, user.getId());
return "redirect:/page/user/book";
}
}

前端页面

1
<a class="action-btn" th:href="@{/api/user/borrow-book?id=}+${bbook.getBid()}">

–借阅信息展示

所有借阅信息

思路

在查询所有书籍时逻辑较为简单,我们定义一个比较全面的borrow实体类,想要通过多表联查的方式将列表(List<Borrow>)直接查到并放到前端进行解析。在Controller层,除了让页面可以正常展示的user授权过程,另加了一个列表赋值的过程。

Borrow实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class Borrow {
int id;

int bid;
String title;
String desc;
double price;

int sid;
String name;
String sex;
String grade;

Date time;
}

BorrowMapper

1
2
3
//查看所有借阅信息
@Select("SELECT * FROM (SELECT * FROM borrow LEFT JOIN book USING (bid)) e LEFT JOIN student USING (sid)")
List<Borrow> allBorrowList();

Service

1
2
//显示所有借阅信息
List<Borrow> allBorrowList();

ServiceImpl

1
2
3
4
5
6
//查看所有借阅信息
@Override
public List<Borrow> allBorrowList() {
return mapper.allBorrowList();
}

Controller

1
2
3
4
5
6
@RequestMapping("/index")
public String index(HttpSession session, Model model){
model.addAttribute("user",service.findUser(session));
model.addAttribute("allBorrowList",borrowservice.allBorrowList());
return "/admin/index";
}

前端页面

1
2
3
4
5
6
7
<tr th:each="borrow:${allBorrowList}">
<td th:text="'#'+${borrow.getId()}">借阅ID</td>
<td th:text="'《'+${borrow.getTitle()}+'》'">书籍名称</td>
<td th:text="${borrow.getName()}">借阅人</td>
<td th:text="'#'+${borrow.getSid()}">借阅人学号</td>
<td th:text="${borrow.getTime()}">借阅时间</td>
</tr>

学生借阅信息

思路

我们获得个人借阅信息后,可以通过多表联查的方式获取到当前学生的借阅列表,将列表传到service,再传到controller,controller直接传到Thymeleaf进行列表解析。

此过程比查询全部的列表就多在要将sid从前端传到mapepr中。可是我们没有什么直接的post请求,确实,但是我们发现我们可以从id入手。我们在Service中通过id查找sid,再在service中将sid放入查询所用的mapper中,这样就可以完美的查到了。

前端的id传到service,由service变成sid传给mapper,mapper查到的再返回给service,通过service返回给Controller,可见service在其中的重要作用。

BorrowMapper

1
2
3
//查看个人借阅信息
@Select("SELECT * FROM (SELECT * FROM borrow LEFT JOIN book USING (bid)) e LEFT JOIN student USING (sid) WHERE sid = #{sid}")
List<Borrow> ownBorrowList(int sid);

Service

1
2
//显示个人借阅信息
List<Borrow> ownBorrowList(HttpSession session);

ServiceImpl

1
2
3
4
5
6
7
//显示个人借阅信息
@Override
public List<Borrow> ownBorrowList(HttpSession session) {
AuthUser user = (AuthUser) session.getAttribute("user");
Integer sid = usermapper.getSidByUserId(user.getId());
return mapper.ownBorrowList(sid);
}

Controller

1
2
3
4
5
6
@RequestMapping("/book")
public String book(HttpSession session, Model model){
model.addAttribute("user",service.findUser(session));
model.addAttribute("borrowList", borrowService.ownBorrowList(session));
return "/user/book";
}

前端页面

1
2
3
4
5
6
<tr th:each="borrow:${borrowList}">
<td th:text="'#'+${borrow.getBid()}">书籍ID</td>
<td th:text="'《'+${borrow.getTitle()}+'》'">书籍名称</td>
<td th:text="${borrow.getDesc()}">简介</td>
<td th:text="${borrow.getPrice()}">价格</td>
<td th:text="${borrow.getTime()}">借阅时间</td>

报错部分

mapper传多个值要有@Param

报错说del-borrow不可用

解决:果然是前端页面和mapper网址相关部分写错了。borrow写成了borrpw

借阅时间不能正常显示问题

因为在实体类中将时间定义的是

1
Date date;

前端getDate()

正确应该定义成

1
Date time;

前端getTime()

项目优化

四小块

思路

我们创建了一个实体类,将数据存储在其中。在我们service中创建实体类对象在其中设置值。因为我们将这个对象返回到Controller,所以我们可以直接调用其中的属性进行前端解析。

实体类

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
public class ShowCount {
int bookCount;
int studentCount;
int borrowCount;
}

ShowService

1
2
3
public interface ShowService {
ShowCount showCount();
}

ShowServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class ShowServiceImpl implements ShowService {

@Resource
BookMapper bookMapper;
@Resource
BorrowMapper borrowMapper;
@Resource
UserMapper userMapper;

@Override
public ShowCount showCount() {
return new ShowCount(userMapper.getStudentCount(),
bookMapper.getBookCount(),
borrowMapper.getBorrowCount());
}
}

Controller

1
2
3
4
5
6
7
@RequestMapping("/index")
public String index(HttpSession session, Model model){
model.addAttribute("user",service.findUser(session));
model.addAttribute("allBorrowList",borrowservice.allBorrowList());
model.addAttribute("bookCounts",showService.showCount());
return "/admin/index";
}

html代码

1
2
3
4
5
6
<h5 class="ad-title">学生数量</h5>
<h4 class="ad-card-title" th:text="${bookCounts.studentCount}">66k</h4>
<h5 class="ad-title">书籍数量</h5>
<h4 class="ad-card-title" th:text="${bookCounts.bookCount}">15k</h4>
<h5 class="ad-title">借阅数量</h5>
<h4 class="ad-card-title" th:text="${bookCounts.borrowCount}">420k</h4>

基于SSM+Security的图书管理系统
http://wahoyu.xyz/2022/07/26/BookManagerSSM/
作者
Wahoyu
发布于
2022年7月26日
许可协议