Error message here!

Hide Error message here!

忘记密码?

Error message here!

请输入正确邮箱

Hide Error message here!

密码丢失?请输入您的电子邮件地址。您将收到一个重设密码链接。

Error message here!

返回登录

Close

SpringBoot2.1.6 + Shiro1.4.1 + Thymeleaf + Jpa整合练习

搬块砖可以吗 2019-07-22 21:36:00 阅读数:178 评论数:0 点赞数:0 收藏数:0

首先,添加maven依赖,完整的pom文件如下:

 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hui</groupId>
<artifactId>SpringBoot22</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBoot22</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

接着,我们先编写自定义的Realm类(MyJbdcRealm)

 package com.hui.SpringBoot22.realm;

import org.apache.shiro.authc.*;
 import org.apache.shiro.authz.AuthorizationException;
 import org.apache.shiro.authz.AuthorizationInfo;
 import org.apache.shiro.authz.SimpleAuthorizationInfo;
 import org.apache.shiro.realm.AuthorizingRealm;
 import org.apache.shiro.subject.PrincipalCollection;
 import org.apache.shiro.util.ByteSource;
 import org.apache.shiro.util.JdbcUtils;

import javax.sql.DataSource;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashSet;
 import java.util.Set;

public class MyJdbcRealm extends AuthorizingRealm {

protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
 protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
 protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
 protected DataSource dataSource;
 protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
 protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;
 protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
 protected boolean permissionsLookupEnabled = false;

public void setDataSource(DataSource dataSource) {
 this.dataSource = dataSource;
  }

public void setAuthenticationQuery(String authenticationQuery) {
 this.authenticationQuery = authenticationQuery;
  }

public void setUserRolesQuery(String userRolesQuery) {
 this.userRolesQuery = userRolesQuery;
  }

public void setPermissionsQuery(String permissionsQuery) {
 this.permissionsQuery = permissionsQuery;
  }

public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) {
 this.permissionsLookupEnabled = permissionsLookupEnabled;
  }

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 UsernamePasswordToken upToken = (UsernamePasswordToken) token;
 String username = upToken.getUsername();
 if (username == null) {
 throw new AccountException("Null usernames are not allowed by this realm.");
  }
 Connection conn = null;
 SimpleAuthenticationInfo info = null;
 try {
 conn = dataSource.getConnection();
 String password = null;
 password = getPasswordForUser(conn, username);
 if (password == null) {
 throw new UnknownAccountException("No account found for user [" + username + "]");
  }
 info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
 } catch (SQLException e) {
 final String message = "There was a SQL error while authenticating user [" + username + "]";
 throw new AuthenticationException(message, e);
 } finally {
  JdbcUtils.closeConnection(conn);
  }
 return info;
  }

private String getPasswordForUser(Connection conn, String username) throws SQLException {
 String result = null;
 PreparedStatement ps = null;
 ResultSet rs = null;
 try {
 ps = conn.prepareStatement(authenticationQuery);
 ps.setString(1, username);
 rs = ps.executeQuery();
 boolean foundResult = false;
 while (rs.next()) {
 if (foundResult) {
 throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
  }
 result = rs.getString(1);
 foundResult = true;
  }
 } finally {
  JdbcUtils.closeResultSet(rs);
  JdbcUtils.closeStatement(ps);
  }
 return result;
  }

 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
 if (principals == null) {
 throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
  }
 String username = (String) getAvailablePrincipal(principals);
 Connection conn = null;
 Set<String> roleNames = null;
 Set<String> permissions = null;
 try {
 conn = dataSource.getConnection();
 roleNames = getRoleNamesForUser(conn, username);
 if (permissionsLookupEnabled) {
 permissions = getPermissions(conn, username, roleNames);
  }
 } catch (SQLException e) {
 final String message = "There was a SQL error while authorizing user [" + username + "]";
 throw new AuthorizationException(message, e);
 } finally {
  JdbcUtils.closeConnection(conn);
  }
 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
  info.setStringPermissions(permissions);
 return info;
  }

protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
 PreparedStatement ps = null;
 ResultSet rs = null;
 Set<String> roleNames = new LinkedHashSet<String>();
 try {
 ps = conn.prepareStatement(userRolesQuery);
 ps.setString(1, username);
 rs = ps.executeQuery();
 while (rs.next()) {
 String roleName = rs.getString(1);
 if (roleName != null) {
  roleNames.add(roleName);
  }
  }
 } finally {
  JdbcUtils.closeResultSet(rs);
  JdbcUtils.closeStatement(ps);
  }
 return roleNames;
  }

protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
 PreparedStatement ps = null;
 Set<String> permissions = new LinkedHashSet<String>();
 try {
 ps = conn.prepareStatement(permissionsQuery);
 for (String roleName : roleNames) {
 ps.setString(1, roleName);
 ResultSet rs = null;
 try {
 rs = ps.executeQuery();
 while (rs.next()) {
 String permissionString = rs.getString(1);
 String[] permissionNames = permissionString.split(",");
  permissions.addAll(Arrays.asList(permissionNames));
  }
 } finally {
  JdbcUtils.closeResultSet(rs);
  }
  }
 } finally {
  JdbcUtils.closeStatement(ps);
  }
 return permissions;
  }
 }

MyJdbcRealm类就是从shiro中原有的JdbcRealm类copy来的,去掉了与密码盐(salt)有关的部分。

我在数据库的权限表中添加的权限字段值为 “user:add,user:delete,user:update,user:select,user:updateRole” 的格式,而shiro中原有的JdbcRealm中会将这一长串当成一种权限来看,在 MyJdbcRealm 类中改写了 JdbcRealm中的 getPermissions(Connection conn, String username, Collection<String> roleNames) 方法(具体看159-161行)来使权限字段的值为 5种 不同的权限。

    当然,你也可以在自定义的 Realm 类中重写 doGetAuthorizationInfo(PrincipalCollection principals) 方法,来实现自己的权限管理。

 

接着,编写shiro的配置类(ShiroConfiguration)

 

 package com.hui.SpringBoot22;

import com.hui.SpringBoot22.realm.MyJdbcRealm;
 import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
 import org.apache.shiro.mgt.SecurityManager;
 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.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import javax.sql.DataSource;
 import java.util.*;

@Configuration
 public class ShiroConfiguration {

 @Autowired
 private DataSource dataSource;

@Bean(name = "shiroFilter")
 public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager){
 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  shiroFilterFactoryBean.setSecurityManager(securityManager);
 //配置login页、登陆成功页、没有权限页
shiroFilterFactoryBean.setLoginUrl("/");
 shiroFilterFactoryBean.setSuccessUrl("/index");
 shiroFilterFactoryBean.setUnauthorizedUrl("/403");

//配置访问权限(顺序执行拦截)
 // “/**” 放到最下面,如果将("/**","authc")放到("/userLogin","anon")的上面
 // 则“/userLogin”可能会被拦截
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
 filterChainDefinitionMap.put("/logout","logout");
 filterChainDefinitionMap.put("/userLogin","anon");
 filterChainDefinitionMap.put("/403","roles");
 filterChainDefinitionMap.put("/**","authc");
  shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
 return shiroFilterFactoryBean;
  }

@Bean(name = "securityManager")
 public SecurityManager securityManager(@Qualifier("myJdbcRealm")MyJdbcRealm myJdbcRealm){
 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  securityManager.setRealm(myJdbcRealm);
 return securityManager;
  }

@Bean(name = "myJdbcRealm")
 public MyJdbcRealm myJdbcRealm(@Qualifier("credentialsMatcher") HashedCredentialsMatcher credentialsMatcher,
 @Qualifier("dataSource") DataSource dataSource){
 MyJdbcRealm myJdbcRealm = new MyJdbcRealm();
 //打开shiro的权限 (默认为false) (不开启则不会检查权限 --> 点击“修改”,不管有没有权限都能进行跳转)
myJdbcRealm.setPermissionsLookupEnabled(true);
 //设置datasource
 myJdbcRealm.setDataSource(dataSource);
 //设置密码加密器
 myJdbcRealm.setCredentialsMatcher(credentialsMatcher);
 //设置登陆验证sql语句
String sql = "select password from test_user where username = ?";
  myJdbcRealm.setAuthenticationQuery(sql);
 //设置权限验证sql语句
String permissionSql = "select permission from permissions where role_name = ?";
  myJdbcRealm.setPermissionsQuery(permissionSql);
 return myJdbcRealm;
  }

//设置加密算法为MD5。加密次数为1
@Bean(name = "credentialsMatcher")
 public HashedCredentialsMatcher credentialsMatcher(){
 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
 credentialsMatcher.setHashAlgorithmName("md5");
 credentialsMatcher.setHashIterations(1);
 return credentialsMatcher;
  }

/**
 * 开启aop注解支持 -- 借助SpringAOP扫描使用shiro注解的类
  * (不开启则不能扫描到shiro的@RequiresPermissions等注解)
  * @param securityManager
  * @return
*/
 @Bean
 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
  authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
 return authorizationAttributeSourceAdvisor;
  }

//配置无权限异常处理,跳转到403
@Bean(name="simpleMappingExceptionResolver")
 public SimpleMappingExceptionResolver
  createSimpleMappingExceptionResolver() {
 SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
 Properties mappings = new Properties();
 mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
mappings.setProperty("UnauthorizedException", "403");
 r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
return r;
  }

}

 

上面代码中有几个需要注意的点:

第一点:是再定义的名为“securityManager”的 Bean 中,使用的是 DefaultWebSecurityManager 这个类,而不是 DefaultSecurityManager(使用DefaultSecurityManager类会报错),前者是 org.apache.shiro.web.mgt 包下的,与web有关;后者是 org.apache.shiro.mgt 包下的。

第二点:开启aop注解支持 -- 借助SpringAOP扫描使用shiro注解的类,开启之后可以扫描到 Controller 类上的shiro注解(例如:@RequiresPermissions、@RequiresRoles等

第三点:配置无权限异常处理,这样就会拦截到没有权限的用户,然后跳转到403页面(

这里配置无权限异常处理是为了配合shiro注解。

如果不想使用shiro注解,也可以不配置该异常处理,直接在拦截链“filterChainDefinitionMap”中配置 -- 例如:/userList= roles["admin","admin1"] --> 表明访问路径 /userList 需要同时具备“admin”和“admin1”的角色,不合条件则403;             另一种与角色拦截相似:权限拦截 -- /userList= perms["user:select"]

第四点:在名为 “myJdbcRealm” Bean中,设置登陆验证与权限验证的sql查询语句,方法分别是 setAuthenticationQuery("select password from test_user where username = ?") 、 setPermissionsQuery("select permission from permissions where role_name = ?")  

之所以执行这两个setXxx()方法,是因为我这里的实体类对应生成的表名、字段名与shiro默认的不一致(如果你想使用shiro默认的,那么你就需要按照shiro源码中的sql语句来设置实体生成的表名、字段名与shiro默认的一致即可)

 

接下来,编写实体类

User类

 package com.hui.SpringBoot22.pojo;

import javax.persistence.*;

@Entity
 @Table(name = "test_user")
 public class User {
  @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)//默认为AUTO,这里设置为自增
private Long id;
 @Column(name = "username",length = 50)
 private String username;
 @Column(name = "password",length = 50)
 private String password;

public Long getId() {
 return id;
  }

public void setId(Long id) {
 this.id = id;
  }

public String getUsername() {
 return username;
  }

public void setUsername(String username) {
 this.username = username;
  }

public String getPassword() {
 return password;
  }

public void setPassword(String password) {
 this.password = password;
  }
 }

 

Role类

 package com.hui.SpringBoot22.pojo;

import javax.persistence.*;

@Entity
 @Table(name = "user_roles")
 public class Role {
  @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 @Column(name = "username",length = 50)
 private String username;
 @Column(name = "role_name",length = 50)
 private String roles;

public Long getId() {
 return id;
  }

public void setId(Long id) {
 this.id = id;
  }

public String getUsername() {
 return username;
  }

public void setUsername(String username) {
 this.username = username;
  }

public String getRoles() {
 return roles;
  }

public void setRoles(String roles) {
 this.roles = roles;
  }
 }
Permission类
 package com.hui.SpringBoot22.pojo;

import javax.persistence.*;

@Entity
 @Table(name = "permissions")
 public class Permission {
  @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 @Column(name = "role_name",length = 50)
 private String roleName;
 @Column(name = "permission",length = 120)
 private String permissions;

public Long getId() {
 return id;
  }

public void setId(Long id) {
 this.id = id;
  }

public String getRoleName() {
 return roleName;
  }

public void setRoleName(String roleName) {
 this.roleName = roleName;
  }

public String getPermissions() {
 return permissions;
  }

public void setPermissions(String permissions) {
 this.permissions = permissions;
  }
 }

这里就不多说了,主要注意的就是对应的表名、字段名要与 ShiroConfiguration 类中的sql语句的表名、字段名一致。

 

接下来是Repository编写,直接看代码好了

 

 package com.hui.SpringBoot22.repository;

import com.hui.SpringBoot22.pojo.User;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Modifying;
 import org.springframework.data.jpa.repository.Query;

import javax.transaction.Transactional;

public interface UserRepository extends JpaRepository<User,Long> {
  User findUserById(Long id);

 @Transactional
  @Modifying
 @Query("update User set username=?2,password=?3 where id=?1")
 int updateUserById(Long id,String username,String password);

 @Transactional
  @Modifying
 @Query("delete from User where id=?1")
 void deleteUserById(Long id);
 }

 

这里继承了JpaRepository类,就不用再类上加Spring注解来将其注入(因为 JpaRepository 类上有一个@NoRepositoryBean注解,原理咱不懂!!!

select、delete之类的语句 Jpa 已经封装了一部分方法,我们可以直接调用,如 save(S entity)、delete(T entity)等

如果Jpa中封装的不能满足需求,那就自己写啦

  像上面的在 UserRepository 类中添加一个方法,然后再方法上加上@Query注解,里面有个value属性,用来指定编写的sql语句  -->  如:@Query(value="update ...")

值得注意的是,在 @Query 注解中的 sql 语句对应的表名应写 实体类名(上面代码中本人写的就是实体类 User );关于sql中的字段是不是需要用实体类属性名,有兴趣的朋友可以自己试一下。

如果觉得别扭,可以在@Query注解中编写 nativeQuery 属性,使其值为 true ,这样 Jpa 就能识别原生 sql 了   -->    

例子: @Query(nativeQuery = true,
value="select r.id,r.username,r.role_name from user_roles u left join user_roles r on u.username=r.username where u.id=?1")
Role findRoleByUserid(Long id);

 

最后,如果是insert、delete、update之类的语句,还要在方法上面加上@Modifying和@Transactional注解等大佬帮我解惑ing...

 

接着,再贴一下 controller 的代码

 

 package com.hui.SpringBoot22.controller;

import com.hui.SpringBoot22.pojo.User;
 import com.hui.SpringBoot22.repository.UserRepository;
 import com.hui.SpringBoot22.utils.Md5;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.authc.UsernamePasswordToken;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.apache.shiro.subject.Subject;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
 import java.util.Map;

@Controller
 public class UserController {
  @Autowired
 private UserRepository userRepository;

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

@RequestMapping("/userLogin")
 public String userLogin(String username, String password, Map<String,Object> map){
 Subject subject = SecurityUtils.getSubject();
 UsernamePasswordToken token = new UsernamePasswordToken(username,password);
 try{
  subject.login(token);
 map.put("loginName",username);
 }catch(Exception e){
 map.put("msg","登陆失败");
 return "login";
  }
 return "forward:userList";
  }

@RequestMapping("/userList")
 @RequiresPermissions("user:select")
 public String list(Map<String,Object> map){
 List<User> users = userRepository.findAll();
 map.put("userList",users);
 return "userList";
  }

@RequestMapping("/toUserAdd")
 public String toAdd(){
 return "userAdd";
  }

@RequestMapping("/userAdd")
 @RequiresPermissions("user:add")
 public String userAdd(User user){
  user.setPassword(Md5.md5(user.getPassword()));
  userRepository.save(user);
 return "forward:userList";
  }

@RequestMapping("/toUserEdit")
 public String toEdit(Long id,Map<String,Object> map){
 User user = userRepository.findUserById(id);
 map.put("user",user);
 return "userEdit";
  }

@RequestMapping("/userEdit")
 @RequiresPermissions("user:update")
 public String userEdit(Long id,String username,String password){
 password = Md5.md5(password);
  userRepository.updateUserById(id,username,password);
 return "forward:userList";
  }

@RequestMapping("/userDelete")
 @RequiresPermissions("user:delete")
 public String deleteUser(Long id){
  userRepository.deleteUserById(id);
 return "forward:userList";
  }
 }

@RequiresPermissions注解是验证用户权限

@RequiresRoles注解是验证用户角色(这个在RoleController中用到,这里没有贴出来

 

然后,再看一部分使用 thymeleaf 的HTML代码

 

 <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="margin-left: 30%">
<form action="/roleUpdate" method="post">
<input type="hidden" name="id" th:value="${role.id}"/>
用 户 名:<input name="username" type="text" th:value="${role.username}"/><br/><br/>
选择角色:<input style="margin-left: 8px;" type="radio" name="roles"
 th:each="roleName,roleNameStat:${roleNameList}"
 th:value="${roleName}"
 th:text="${roleName}"
 th:attr="checked=${roleName==role.roles?true:false}"
/><br/><br/>
<input type="submit" value="提交"/>
</form>
</div>
</body>
</html>

 

老实说,第一次使用 thymeleaf 真的不大习惯,总是写成常规的 HTML 代码

 

上面也没什么好说的,也就一个下拉框的遍历(

th:text 表示文本值, th:value 表示value值,th:attr 表示是否选中状态,

th:each 就是遍历后台传来的 list 集合  -->  roleName 代表每一个 list 集合元素,roleNameStat.index 代表着该元素的下标

  )

 

最后,再看一下配置文件 application.properties

 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 spring.datasource.url=jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
 spring.datasource.username=xxx
 spring.datasource.password=xxx

spring.jpa.hibernate.ddl-auto=create-drop
 spring.jpa.show-sql=true
 spring.jpa.database=mysql

spring.thymeleaf.cache=false
 spring.thymeleaf.mode=HTML

这个地方有一个坑,就是如果我们设置 spring.jpa.hibernate.ddl-auto=update,就不会执行 resources 目录下的 import.sql 文件等

根据官方文档来说,如果需要执行 resources 目录下的 import.sql 文件,就必须设置 spring.jpa.hibernate.ddl-auto 的值为 create 或者 create-drop

还一种办法就是不使用 spring.jpa.hibernate.ddl-auto ,直接在 resources 目录下添加 schema.sql 和 data.sql 文件(schema.sql用来执行DDL语句,data.sql用来执行DML语句)

 

还有少部分代码和 HTML 就不贴出来了,有兴趣的可以去下载源代码看看。

项目默认登陆用户   ==>   用户名:lmh,密码:123

项目GitHub地址https://github.com/Lmh115/SpringBoot

 

版权声明
本文为[搬块砖可以吗]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/java-hui/p/11228601.html