本项目是基于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 > <dependency > <groupId > org.thymeleaf</groupId > <artifactId > thymeleaf-spring5</artifactId > <version > 3.0.12.RELEASE</version > </dependency > <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 > <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 > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 4.0.1</version > <scope > provided</scope > </dependency > <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 { @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 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 { }
Security配置类 接着我们需要再创建一个配置类用于配置SpringSecurity:
1 2 3 4 5 @Configuration @EnableWebSecurity public class SecurityConfiguration extends 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 public SpringTemplateEngine springTemplateEngine (@Autowired ITemplateResolver resolver) { SpringTemplateEngine engine = new SpringTemplateEngine (); engine.setTemplateResolver(resolver); engine.addDialect(new SpringSecurityDialect ()); 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 .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) .passwordEncoder(new 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() .antMatchers("/**" ).hasRole("user" ) }
首先我们需要配置拦截规则,也就是当用户未登录时,哪些路径可以访问,哪些路径不可以访问,如果不可以访问,那么会被自动重定向到登陆页面。
接着我们需要配置表单登陆和登录页面:
1 2 3 4 5 6 .and() .formLogin() .loginPage("/login" ) .loginProcessingUrl("/doLogin" ) .defaultSuccessUrl("/index" ,true ) .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 ())
前端修改 接着我们需要在前端修改一下记住我勾选框的名称,将名称修改与上面一致,如果上面没有配置名称,那么默认使用”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 .antMatchers("/register" ).permitAll()
数据上 :
在数据流程上,我们将数据输入前端,以表单的方式将数据传到Controller中
Controller将数据提取出来放到变量里面,调用Service中的方法,以存取了数据的变量为参数传入进去。
实际上Service也只是调用了mapper的方法,继续将变量的值继续传入mapper定义的接口方法中。
在Mapper中定义的传入的参数名与数据库中的变量名并没有什么必然的联系,因为可以使用@Param注解将变量的值传入sql语句中。
设计上 :
我们先设计前端对应的名字到Controller的post方法中,将变量值与之对应。
简单框架设计 重新设计数据库 我们重新设计数据库表主要是因为多了users这一张表,users关系着登陆权限和访问权限。
设置外键
我们让student表中的uid字段引用users的id字段。那么此时student就是从表,子表,外键。users表就是主表,父表,主键。
--创建时:先创建主键,再创建外键  
--删除时:先删除外键,再删建主键
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 @Override protected Filter[] getServletFilters() { return new Filter []{new CharacterEncodingFilter ("utf-8" ,true )}; }
正常情况下这样配置是没有问题的。但是没有什么用,因为SpringSecurity会把自己的Filter放在最前面,我们自己写的被放在后面,数据来的时候已经就乱码了。
所以我们应该弄个前置方法,在里面设置Filter
1 2 3 4 5 6 7 @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) ; @Options(useGeneratedKeys = true,keyColumn = "id",keyProperty = "id") @Insert("insert into users(name,role,password) values(#{username},#{role},#{password})") int registerUser (AuthUser user) ; @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 @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 .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() .anyRequest().hasRole("user" ) .and() .formLogin() .loginPage("/login" ) .loginProcessingUrl("/api/auth/login" ) .defaultSuccessUrl("/index" ,true ) .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 @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; 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" )
菜单栏 菜单栏我们并不会进行两个页面的书写,但是我们会不同的显示进行不同的划分。
管理员 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 (); repository.setDataSource(dataSource); repository.setCreateTableOnStartup(true ); return repository; }
1 2 3 4 5 .and() .rememberMe() .rememberMeParameter("remember" ) .tokenRepository(repository) .tokenValiditySeconds(60 * 60 * 24 * 7 )
出现了报错:
因为我们的记住我是在跳转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" ; }
页面内容和划分   已知管理员有两个页面(所有的借阅信息,增删改书籍),学生有两个界面(显示自己的借阅信息,借书还书)。
为了节省代码量,并且也没有必要将所有的信息进行展示然后再进行权限控制,我们决定设置一个头部模板,将前面相关的,相似的页面进行套用。
页面位置分配 页面模板 侧边栏和用户名放到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() .antMatchers("/page/user/**" ).hasRole("user" ) .antMatchers("/page/admin/**" ).hasRole("admin" ) .anyRequest().hasAnyRole("user" ,"admin" ) .and() .formLogin() .loginPage("/page/auth/login" ) .loginProcessingUrl("/api/auth/login" ) .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 ="" > <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  思路 我们的本意其实只是想输入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
借阅时间不能正常显示问题
因为在实体类中将时间定义的是
前端getDate()
正确应该定义成
前端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 >