发布时间:2022-10-08 14:00
在淘宝上买的课程的一个例子,看了视频,抄了一遍代码,那时候刚开始学springboot,所以感觉没什么用,然后就又学习了一段时间。最近回想起来有这样的一个系统符合我现阶段的学习程度,然后就又写了一遍。其中css和js实在没法自己搞,所以就直接把课程里的拿来用。所有做完后的样式确实不好看,但我自己觉得看的过去。
今天下午可以算是完工了(也就是增删改查),又想起大佬的话,也怕自己忘了,所以写博客记录一下。
springboot+thymeleaf+mybatis+shiro
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.4.RELEASE
com.example
demo
0.0.1-SNAPSHOT
bill
Demo project for Spring Boot
1.8
com.alibaba
druid
1.1.13
org.springframework.boot
spring-boot-starter-data-jdbc
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.apache.shiro
shiro-spring
1.4.1
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.1
org.springframework.boot
spring-boot-maven-plugin
pom依赖差不多用的最新的版本,自己学习用的,应该没什么大的影响。
springboot使用thymeleaf我觉的比较方便,在HTML中引入,就能使用thymeleaf的功能(不知道这样说对不对)。thymeleaf特性的使用还是要多学习的,能够灵活应用的感觉应该非常不错。
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录(百度)。
刚接触springboot的时候就是学的mybatis,所以可能情有独钟吧。
本来的教程上没有这个技术点,但我在学习的过程中接触到了shiro,有一篇springboot+Vue的教学博客让我记忆犹新,所以我决定试一下。在这里给大家推荐一下 Evan-Nightly大佬的这篇博客 Vue + Spring Boot 项目实战,也给自己插个眼。
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序(百度)。
java类大概就是这些。
WebMvcConfig.java是拦截器、过滤器和视图控制器的配置文件。
package com.example.config;
import com.example.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MySpringMvcConfig implements WebMvcConfigurer {
//添加视图控制器
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//main.html是显示在浏览器网址上的,main/main是项目main下面的main/html
registry.addViewController("main.html").setViewName("main/main");
registry.addViewController("index.html").setViewName("main/index");
}
//配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
//拦截所有请求
.addPathPatterns("/**")
//不拦截
.excludePathPatterns("/", "/login", "/index.html")
//测试用的请,不拦截,可在项目上线时删掉
.excludePathPatterns("/test", "/thymeleaf");
}
}
我在刚开始学springboot的时候,拦截器、过滤器和视图控制器还挺难理解的,虽然看了几遍概念介绍但感觉还是无法理解这到底是个什么东西。在做这个项目的时候我自己多试了几遍。改一个编一次看一下什么情况,慢慢的就知道是什么作用了。自学,笨鸟先飞,是这样的,没办法。
ShiroConfig.java是shiro配置类
package com.example.config;
import com.example.realm.MyRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
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;
/**
* Create by Administrator on 2020/2/24.
*/
@Configuration
public class ShiroConfig {
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getMyRealm());
return securityManager;
}
@Bean
public MyRealm getMyRealm() {
MyRealm wjRealm = new MyRealm();
wjRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return wjRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
shiro的配置具体是什么意思我还真说不出来,感觉自己也就知道的大概,知其然而不知所以然。
我只用到了shiro的用户信息加密和认证登录功能,就是给用户注册时的密码加密,然后在登录时再比对加密的密码。
MyRealm.java
package com.example.realm;
/**
* Create by Administrator on 2020/2/24.
*/
public class MyRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//简单重写获取授权信息方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
return s;
}
//获取认证信息,即根据token中的用户名从数据库中获取密码和盐等信息并返回
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName = authenticationToken.getPrincipal().toString();
User user = userService.getUserByName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt), getName());
return authenticationInfo;
}
}
MyRealm和ShiroConfig.java配合使用,其中的奥秘我还没懂,希望有懂的大佬帮忙教一下。
package com.example.controller;
@Controller
public class LoginController {
@Autowired
UserService userService;
//跳转到用户登录界面
@RequestMapping("/index")
public String index(){
return "main/index";
}
//在页面点击登录按钮后执行登录
@RequestMapping("/login")
//username和password是前端传递过来的,这就是前后传递的一种方式
public String login(HttpSession httpSession, String username, String password){
System.out.println("username="+username+",password="+password);
//判断是否为空,这个可以改到前端独立判断
if(username.equals("") || password.equals("")){
//如果为空就跳转到登录页
return "redirect:/index";
}
//根据用户名获取用户
User loginuser = userService.getUserByName(username);
if(loginuser == null){
System.out.println("user==null");
return "redirect:/index";
}
//简单的判断密码是否正确
// if(!loginuser.getPassword().equals(password)){
// System.out.println("error password");
// return "redirect:/index";
// }
//shiro获取subject
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
try {
subject.login(usernamePasswordToken);
//存储到session中,拦截器中判断是否登录
httpSession.setAttribute("loginUser", loginuser);
return "redirect:/main.html";
}catch (AuthenticationException e){
return "redirect:/index";
}
// httpSession.setAttribute("loginUser", loginuser);
// return "redirect:/main.html";
}
//跳转到主页面
@GetMapping("/main/main")
public String mainMain(HttpServletRequest httpServletRequest){
System.out.println("mainMain method="+httpServletRequest.getMethod());
return "redirect:/main.html";
}
//退出登录
@GetMapping("/logout")
//public String logout(HttpSession httpSession){
public String logout(HttpServletRequest httpServletRequest){
System.out.println("logout method="+httpServletRequest.getMethod());
Subject subject = SecurityUtils.getSubject();
subject.logout();
//httpSession.removeAttribute("loginUser");
//httpSession.invalidate();
return "redirect:/index";
}
}
我不知道怎么讲,所以把该说的都用注释写出来
index.html
login
提交后,后台判断用户是否存在,用户的密码是否正确,然后跳转到主界面main.html,然后点击菜单就会跳转到相应的界面。
登录之后自然会有一些操作,但大多都基于对数据库的增删改查。
User.java
package com.example.bean;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* Create by Administrator on 2020/2/19.
*/
public class User {
private Integer id;
private String username;
private String realName;
private String password;
private String salt;
//性别 1:女 2:男
private int gender;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
//1管理员 2经理 3普通用户
private int userType;
public User(Integer id, String username, String realName, String password, String salt, int gender, Date birthday, int userType) {
this.id = id;
this.username = username;
this.realName = realName;
this.password = password;
this.salt = salt;
this.gender = gender;
this.birthday = birthday;
this.userType = userType;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", realName='" + realName + '\'' +
", password='" + password + '\'' +
", salt='" + salt + '\'' +
", gender=" + gender +
", birthday=" + birthday +
", userType=" + userType +
'}';
}
getter() and setter()...
在创建user类时要注意id的类型最好用Integer,否则在增加用户时会有问题,应为id在数据库中是自增的,而在增加用户时页面上没有,会有问题。
还有在建user表时password和salt的大小要大一点,我当时用25个字节结果加了shiro加密后有报错提示(具体的报错就不回放了)。
还有用户的生日也会有问题,要注意加上@DateTimeFormat(pattern = “yyyy-MM-dd”)注解。如果不加从页面保存时(增加或者更新)会有类型转换的错误。
然后重点说一下和shiro有关的内容,就是在注册时密码加密的过程。
@PostMapping("/user/add")
public String add(HttpServletRequest httpServletRequest, User user){
System.out.println("add method="+httpServletRequest.getMethod());
String password = user.getPassword();
String username = user.getUsername();
username = HtmlUtils.htmlEscape(username);
user.setUsername(username);
User user1 = userService.getUserByName(username);
if(null != user1){
return "redirect:/list/users";
}
//生成盐,默认长度16位
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
//设置hash迭代次数
int times = 2;
//获取hash后的密码
String encodingPassword = new SimpleHash("md5", password, salt, times).toString();
user.setSalt(salt);
user.setPassword(encodingPassword);
userService.addUser(user);
return "redirect:/list/users";
}
大概都写到注释里了。
再说一下在页面上展示所有用户,list.html
用户名
真实姓名
性别
出生日期
用户类型
操作
xxx
xx
x
xxx
xx
后台从数据库中查找出所有用户传递给前端
//列出所有用户
@RequestMapping("/list/users")
public String listUser(Map map){
List users = userService.uListAll();
map.put("users", users);
return "user/list";
}
修改用户页update.html
在修改页需要显示用户原本的信息,所以前后端需要用id来绑定一个user。
//查看用户详细信息
@GetMapping("/user/{id}")
//参数type来区分前端是要查看用户详细信息还是要修改用户信息
public String view(HttpServletRequest httpServletRequest, @PathVariable("id") int id,
@RequestParam(value = "type", defaultValue = "view") String type,
Map map){
System.out.println("view method="+httpServletRequest.getMethod());
//通过前端传来的id获取到用户
User user = userService.getUserById(id);
//返回给前端用于页面显示用户原本的信息
map.put("user", user);
return "user/"+type;
}
点击保存按钮好提交修改的用户信息
//更新用户信息
@PostMapping("/user")
public String update(HttpServletRequest httpServletRequest, User user){
System.out.println("update method="+httpServletRequest.getMethod());
userService.updateUser(user);
return "redirect:/list/users";
}
删除我就简单做了一下点击删除按钮直接从数据库删除
//删除用户
@GetMapping("/delete/user/{id}")
public String delete(HttpServletRequest httpServletRequest, @PathVariable(value = "id") int id){
System.out.println("delete method="+httpServletRequest.getMethod());
userService.deleteUserById(id);
return "redirect:/list/users";
}
现在就总结好像有点早,但我写着写着发现好像没什么东西能写,或许可能功能比较少吧,Bill和Provider都和user类似,也是对数据库的增删改查操作。所以就不再赘述。
之后应该还会去完善一下,加上log系统和druid后台监控。
最后分享一下GitHub链接
https://github.com/azermu-milk/SpringbootTest.git
供大家clone。近期应该还会更新吧。
在config文件夹下创建DruidConfig.java类,配置druid后台管理类和druid的filter类,当然还需要在application.yml中补全druid配置。
DruidConfig.java
package com.example.config;
/**
* Create by Administrator on 2020/2/25.
*/
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid(){
return new DruidDataSource();
}
//1.配置一个后台管理servlet
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
//设置初始化参数
Map initParam = new HashMap<>();
initParam.put(StatViewServlet.PARAM_NAME_USERNAME, "root");
initParam.put(StatViewServlet.PARAM_NAME_PASSWORD, "root");
//如果不写 则默认所有ip都能访问
initParam.put(StatViewServlet.PARAM_NAME_ALLOW, "");
//写入本地电脑的IP地址
initParam.put(StatViewServlet.PARAM_NAME_DENY, "192.xxx.11.1");
bean.setInitParameters(initParam);
return bean;
}
//2.配置druid的filter
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean<>();
bean.setFilter(new WebStatFilter());
Map initParam = new HashMap<>();
initParam.put(WebStatFilter.PARAM_NAME_EXCLUSIONS, "*.js,*.css,/druid/*");
bean.setInitParameters(initParam);
//设置拦截请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}
大概就是这样,如注释所示。
application.yml配置
#数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
#间隔多久进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
#配置有一个连接在连接池中的最小生存时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
maxPoolPreparedStatementPerConnectionSize: 25
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
刚开始我在statViewServlet()上少写了个@Bean,结果怎么改都没有后台的登录界面,全被我的拦截器拦截了,不过经过我再三检查终于被我发现了这个问题,哈哈。
看了半天日志框架什么也没看明白,就把自己想用到的加进来了。
logging:
level:
com.example.mapper: debug
#path和name二选一,name的优先级会高于path
file:
path: C:\Users\Administrator\IdeaProjects\mybill\
#name: BillLog.log
#如果使用yml做配置文件,格式要用'',如果是properties则不用
pattern:
#console: '%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger- %msg%n'
#把控制台的日志输出到文件的格式
file: '%d{yyyy/MM/dd-HH:mm:ss} ===== [%thread] ===== %-5level ===== %logger ===== %msg%n'
这个改动也在GitHub上提交了。整个项目是完整的,拉下来加到IDEA中就能启动成功。
在web项目当中引用了 spring-boot-starter-web 依赖
org.springframework.boot
spring-boot-starter-web
spring-boot-starter-web 中引入了 spring-boot-starter 启动器
org.springframework.boot
spring-boot-starter
2.0.6.RELEASE
compile
spring-boot-starter 中引入了 spring-boot-starter-logging 日志启动器
org.springframework.boot
spring-boot-starter-logging
spring-boot-starter-logging 日志启动器 采用的是 logback 日志框架
ch.qos.logback
logback-classic
1.2.3
compile
SpringBoot中默认日志启动器为 spring-boot-starter-logging ,默认采用的是 logback日志框架
logback的默认配置
这个项目到这里应该结束了,我不会再动它了。
又该去找新的项目学习去了。。。