Spring Boot Security

Spring Boot Security

1. 关于Spring Boot Security

Spring Boot Security是在Spring Boot中使用的,基于Spring Security的依赖项,其本质就是Spring Security框架加上了应用于Spring Boot工程的自动配置。

Spring Security是一款主要解决了认证和授权相关处理的安全框架。

认证:验证用户的身份,例如在登录过程中验证用户名与密码是否匹配。

授权:使得用户允许访问服务器端的某些资源,或禁止访问某些资源,例如管理员可以执行一些数据删除操作,而普通用户则不可以删除关键数据。

2. 添加依赖项

提示:关于此框架的案例仍使用boot-demo

在项目的pom.xml需要添加的依赖代码为:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

当添加此依赖后,启动项目,在访问所有资源时,都是要求先登录的(未退出之前只需要登录1次)。

默认的用户名是user,会在启动日志中看到默认的密码,例如:

Using generated security password: 276ff4c1-32da-4e23-adad-c926f33e37c6

当登录成功后,会返回此前尝试访问的页面。

也可以在application.properties中定义用户名和密码:

spring.security.user.name=admin
spring.security.user.password=1234

当通过以上方式自定义用户名和密码后,启动项目时将不再生成临时密码,原user用户也不可用。

3. 使用数据库中的用户名和密码

3.1. 关于Bcrypt算法

Spring Security推荐使用Bcrypt算法来实现对密码原文的加密处理,在框架中有BcryptPasswordEncoder类,此类可以实现加密、判断密码是否匹配等功能。

在密码加密器(PasswordEncoder)中需要关注的方法有:

// 使用原文作为参数,将返回密文
public String encode(CharSequence rawPassword);

// 使用原文作为第1个参数,使用密文作为第2个参数,将返回是否匹配
public boolean matches(CharSequence rawPassword, String encodedPassword)

提示:Bcrypt算法对于同一个原文反复做加密处理时,每次得到的密文都是不同的,是因为在加密处理过程中使用了随机盐,所以可以得到不同的密文,同时,由于盐值被保存在密文中了,所以也是可以正常验证的。

简单的使用示例:

package cn.tedu.boot.demo;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BcryptTests {

    @Test
    public void testEncode() {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String rawPassword = "1234";
        for (int i = 0; i < 10; i++) {
            String encodedPassword = passwordEncoder.encode(rawPassword);
            System.out.println("原密码=" + rawPassword + ",密文=" + encodedPassword);
        }
    }

// 原密码=1234,密文=$2a$10$lQ/BuRDZG6h1GGZLWv/Rx.TjXEJer/668SKVeoFxNs65DMKxeVAmO
// 原密码=1234,密文=$2a$10$jqB0o3.bTzdPLuWVxgfIweZKFXC9qDU15Hzf6vpUSaPifmvdQr4je
// 原密码=1234,密文=$2a$10$NJlBJLl9lp0hAA.UfsvwheborCArK58iJF96te83jbL7J4wq079H6
// 原密码=1234,密文=$2a$10$Jm6fOxg4g4mH.FZ8xXO2OOeTqHAVe6fN/6IATox0NXO1wcW3wq.Q6
// 原密码=1234,密文=$2a$10$uzmvN3IU0yx4cVSBU49jEe2de217l7u2I84NIqiSpMCF2kQefmCZe
// 原密码=1234,密文=$2a$10$EbSsOZUg7F3EMEbgXTHXxuRSbjSxOker.aTjdI1h/zz4SCRNVuDvm
// 原密码=1234,密文=$2a$10$577X/D13svsDuT9cKNSjs.M2JUeqxF7ok9uHqCBYgdqv7pqvlOEM6
// 原密码=1234,密文=$2a$10$WeuikNbKuRCkqr8JrpC75uIa6lYhJ5fuBtWGnGMpJK7MQ8EKLaICa
// 原密码=1234,密文=$2a$10$b25uEZlqDB.koP2iY.RaKueImhdVS9Aeu5fgZmDJglqtpjIXd3UN.
// 原密码=1234,密文=$2a$10$rjTE6LmSC8Z5FBm1TQpmE.OVErbLy1xpkYPlO6pP2T2BoMclr3bTu

    @Test
    public void testMatch() {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String rawPassword = "1234";
        String[] encodedPasswords = {
                "$2a$10$WeuikNbKuRCkqr8JrpC75uIa6lYhJ5fuBtWGnGMpJK7MQ8EKLaICa",
                "$2a$10$Jm6fOxg4g4mH.FZ8xXO2OOeTqHAVe6fN/6IATox0NXO1wcW3wq.Q6",
                "$2a$10$EbSsOZUg7F3EMEbgXTHXxuRSbjSxOker.aTjdI1h/zz4SCRNVuDvm"
        };
        for (int i = 0; i < encodedPasswords.length; i++) {
            boolean result = passwordEncoder.matches(rawPassword, encodedPasswords[i]);
            System.out.println("原密码=" + rawPassword + ",密文=" + encodedPasswords[i] + ",验证结果=" + result);
        }
    }


}

为了便于后续的操作都使用了统一的密码处理机制,先在cn.tedu.boot.demo.config下创建SecurityConfiguration类,并在此类中使用@Bean方法返回BcryptPasswordEncoder对象:

@Configuration
public class SecurityConfiguration {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

接下来,调整GlobalPasswordEncoder,使得该类是通过自动装配的PasswordEncoder处理加密的:

@Component
public class GlobalPasswordEncoder {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 执行加密
     *
     * @param rawPassword 明文密码(原文)
     * @return 密文密码(加密后的结果)
     */
    public String encode(String rawPassword) {
        return passwordEncoder.encode(rawPassword);
    }

}

接下来,应该在AdminServiceImpl中自动装配以上GlobalPasswordEncoder的对象,并在原加密的代码处调整为使用装配的对象来调用方法进行加密处理!最后,执行测试,并在数据表中保留至少1个有效管理员信息。

3.2. 实现:根据用户名查询管理员登录时所需的详情

为了保证在处理登录认证时能获取到必要的管理员,且不包含非必要信息,应该先梳理所需要信息,并将它们封装在自定义类,此类将作为查询的返回结果类型!

则在cn.tedu.boot.demo.vo包中先创建PermissionSimpleVO类,声明查询时涉及的权限信息中的属性:

@Data
public class PermissionSimpleVO implements Serializable {
    private Long id;
    private String name;
    private String value;
}

然后,再创建AdminLoginVO类,在类中声明必要属性:

@Data
public class AdminLoginVO implements Serializable {
    private Long id;
    private String username;
    private String password;
    private Integer isEnable;
    private List<PermissionSimpleVO> permissions;
}

在编写代码之前,仍应分析需要执行的SQL语句:

select id, username, password, is_enable from ams_admin where username=?

然后 ,在AdminMapper接口中添加抽象方法:

AdminLoginVO getLoginInfoByUsername(String username);

并在AdminMapper.xml中配置以上抽象方法映射的SQL语句:

<select id="getLoginInfoByUsername" resultMap="LoginResultMap">
    select 
        ams_admin.id,
        ams_admin.username,
        ams_admin.password,
        ams_admin.is_enable,

        ams_permission.id AS permission_id,
        ams_permission.name AS permission_name,
        ams_permission.value AS permission_value
    from 
        ams_admin
    left join ams_admin_role 
        on ams_admin.id=ams_admin_role.admin_id
    left join ams_role_permission 
        on ams_admin_role.role_id=ams_role_permission.role_id
    left join ams_permission 
        on ams_role_permission.permission_id=ams_permission.id
    where 
        username=#{username}
</select>

<resultMap id="LoginResultMap" type="cn.tedu.boot.demo.vo.AdminLoginVO">
    <id column="id" property="id" />
    <result column="username" property="username" />
    <result column="password" property="password" />
    <result column="is_enable" property="isEnable" />
    <collection property="permissions" ofType="cn.tedu.boot.demo.vo.PermissionSimpleVO">
        <id column="permission_id" property="id" />
        <result column="permission_name" property="name" />
        <result column="permission_value" property="value" />
    </collection>
</resultMap>

完成后,在AdminMapperTests中进行测试:

@Test
// 脚本:清空,插入
// 脚本(测试之后):清空
public void testGetLoginInfoByUsernameSuccessfully() {
    // 测试数据
    String username = "admin001";
    // 执行测试
    AdminLoginVO admin = mapper.getLoginInfoByUsername(username);
    // 断言查询结果不为null
    Assertions.assertNotNull(admin);
}

@Test
// 脚本:清空
public void testGetLoginInfoByUsernameFailBecauseNotExist() {
    // 测试数据
    String username = "admin001";
    // 执行测试
    AdminLoginVO admin = mapper.getLoginInfoByUsername(username);
    // 断言查询结果为null
    Assertions.assertNull(admin);
}

当测试获取数据成功时,应该可以获取以下数据:

AdminLoginVO(
	id=1, 
	username=admin001, 
	password=123456, 
	isEnable=0, 
	permissions=[
		PermissionSimpleVO(id=1, name=商品-商品管理-读取, value=/pms/product/read), 
		PermissionSimpleVO(id=2, name=商品-商品管理-编辑, value=/pms/product/update), 
		PermissionSimpleVO(id=3, name=商品-商品管理-删除, value=/pms/product/delete), 
		PermissionSimpleVO(id=4, name=后台管理-管理员-读取, value=/ams/admin/read), 
		PermissionSimpleVO(id=5, name=后台管理-管理员-编辑, value=/ams/admin/update), 
		PermissionSimpleVO(id=6, name=后台管理-管理员-删除, value=/ams/admin/delete)
	])

管理员 管理员与角色 角色 角色与权限 权限

truncate ams_admin;
truncate ams_role;
truncate ams_permission;
truncate ams_admin_role;
truncate ams_role_permission;
insert into ams_admin (username, password) values ('user001', '1234');
insert into ams_role (name) values ('商品管理员'), ('积分管理员');
insert into ams_admin_role (admin_id, role_id) values (1, 1), (1, 2);
insert into ams_permission (name, value) values ('创建商品', '/product/create'), ('删除商品', '/product/delete'), ('修改库存', '/product/stock/update'), ('查看积分', '/reward-point/show'), ('扣减积分', '/reward-point/reduce'), ('查看订单', '/order/show');
insert into ams_role_permission (role_id, permission_id) values (1, 1), (1, 2), (1, 3), (2, 4), (2, 5), (1, 6), (2, 6);

select 
	ams_admin.id,
	ams_admin.username,
	ams_admin.password,
	ams_admin.is_enable,
	
	ams_permission.id,
	ams_permission.name,
	ams_permission.value
from 
	ams_admin
left join ams_admin_role 
	on ams_admin.id=ams_admin_role.admin_id
left join ams_role_permission 
	on ams_admin_role.role_id=ams_role_permission.role_id
left join ams_permission 
	on ams_role_permission.permission_id=ams_permission.id
where 
	username='user001';

3.3. 关于UserDetailsService

在Spring Security中,定义了UserDetailsService接口,此接口中有抽象方法:

UserDetails loadUserByUsername(String username);

Spring Security会在处理登录认证时自动根据尝试登录的用户名调用此接口实现类的此方法,并获得UserDetails对象,此对象应该包含用户的密码、权限等信息,接下来,Spring Security会自动判断密码正确,如果不正确,将返回错误信息,如果正确,会将此用户信息(包含权限)保存下来(默认保存在Session中)。

则在cn.tedu.boot.demo下创建security包,并在其下创建UserDetailsServiceImpl类,实现UserDetailsService接口:

package cn.tedu.boot.demo.security;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private AdminMapper adminMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);
        if (admin == null) {
            throw new BadCredentialsException("登录失败,用户名不存在!!!!!");
        }
        return User.builder()
                .username(admin.getUsername())
                .password(admin.getPassword())
                .disabled(admin.getIsEnable() == 0)
                .accountExpired(false)
                .accountLocked(false)
                .credentialsExpired(false)
                .authorities("权限待定")
                .build();
    }

}

完成后,Spring Boot Security会自动使用以上类来实验登录过程中的认证!所以,可以直接启动项目进行测试。

注意:在数据表中必须存在有效的管理员信息,且必须保证数据表中的密码是经过Bcrypt算法处理后的密文,且允许登录的账号的is_enable字段的值必须是1

3.4. 关于授权

在处理认证过程中,需要向响应的UserDetails对象中转入权限数据!则将UserDetailsServiceImpl中的loadUserByUsername()方法内部调整为:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("尝试登录,用户名={}", s);
    AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);
    log.debug("根据用户名查询的结果为:{}", admin);
    if (admin == null) {
        log.debug("登录失败,用户名不存在!!!!!");
        throw new BadCredentialsException("登录失败,用户名不存在!!!!!");
    }

    // --------- 以下是处理授权的相关代码 ---------
    // 将用户的权限信息转换为Spring Security要求的List<GrantedAuthority>类型的数据
    // ----------------------------------------
    List<PermissionSimpleVO> permissions = admin.getPermissions();
    log.debug("此用户的权限信息为:{}", permissions);
    List<GrantedAuthority> authorities = new ArrayList<>();
    for (PermissionSimpleVO permission : permissions) {
        String permissionValue = permission.getValue();
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
        authorities.add(authority);
    }

    return User.builder()
            .username(admin.getUsername())
            .password(admin.getPassword())
            .disabled(admin.getIsEnable() == 0)
            .accountExpired(false)
            .accountLocked(false)
            .credentialsExpired(false)
            .authorities(authorities) // 将以上得到的用户权限信息封装到UserDetails对象中
            .build();
}

关于Spring Security的配置类:

package cn.tedu.boot.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 添加此注解才可以在控制器方法上配置权限
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 提示:本方法中的配置可以一次性写完,每写一段都可以调用 and() 方法再拼接下一段配置

        // 关闭跨域
        http.csrf().disable();

        // authorizeRequests():需要对请求进行认证和授权
        // antMatchers():匹配某些路径,取值可以是字符串数组表示的URL,每个URL必须使用 / 作为第1个字符
        // permitAll():许可所有(不需要认证授权即可访问),此方法之前必须是先匹配了路径的
        // anyRequest():任何请求(除开以前已经配置的请求)
        // authenticated():已经认证的(要求已经登录)
        String[] urls = {
                "/favicon.ico",
                "/doc.html",
                "/**/*.js",
                "/**/*.css",
                "/swagger-resources",
                "/v2/api-docs"
        };
        http.authorizeRequests()
                .antMatchers(urls).permitAll()
                .anyRequest().authenticated();

        // 当需要登录时,启用表单登录
        http.formLogin();
    }

}

当需要控制权限时,可以在控制器类中处理请求的方法上添加注解,并配置需要哪个权限值,例如:

@GetMapping("/{id:[0-9]+}")
@PreAuthorize("hasAuthority('/ams/admin/read')") // 要求权限中具有 /ams/admin/read 这一条才允许访问此资源
public JsonResult<Admin> getById(@PathVariable Long id) {
    throw new RuntimeException("此功能尚未实现");
}

提示:以上做法可适用于通过Knife4j的调试功能发出的GET请求,如果需要测试POST请求,可能会出现403错误,则应该通过浏览器的调试面板中的“网络”来观察当前的Session ID,例如JSESSIONID=07C874111475B2CD1A41E64EA268059E,然后,在Knife4j的API文档中,在“文档管理”的“全局参数设置”中添加名为Cookie的参数,值为例如JSESSIONID=07C874111475B2CD1A41E64EA268059E,参数类型为header,则通过Knife4j进行调试时,会自动携带此信息,需要注意的是,如果重新登录、服务器端重启等原因导致Session ID发生变化,在Knife4j调试之前也应该更换Knife4j中配置的Session ID。

提示:为了保证较好的演示效果,请事先准备好数据表中的数据。

当某个已经登录的用户无权限访问资源时,将响应**403(Forbidden)**错误。

第1次响应时
Set-Cookie
JSESSIONID=FDED4F9C1FA8020FA16FF7BF4D07587B

Set-Cookie
JSESSIONID=07C874111475B2CD1A41E64EA268059E

后续请求时
Cookie
JSESSIONID=FDED4F9C1FA8020FA16FF7BF4D07587B

附:关于Session

HTTP协议是一种无状态协议,同一个用户在同一台设备上多次对同一个服务器端进行访问时,默认在服务器端并不保存此用户的相关信息,所以,无论访问多少次,服务器端都无法识别用户的身份!

为了解决此问题,最简单直接的方式是使用Session(会话),当某个客户端第1次向服务器端发送请求后,服务器端会在服务器端的内存中保存此用户的信息,并且此信息会关联到一个唯一的Session ID,当服务器端进行响应时,会将此Session ID响应到客户端,后续,客户端应该在每次请求时都携带此Session ID,则服务器可以根据后续请求头中的Session ID对应到此前保存的数据,从而识别用户的身份!

关于使用Session保存的数据,通常是:

  • 用户身份的唯一标识,例如用户的id
  • 高频率使用的数据,例如用户的权限
    • 可能存在数据不一致的风险
  • 不便于使用其它存储技术进行处理的数据

关于Session消失的机制:

  • 超时,如果某客户端长时间未向服务器端发起任何请求,则在服务器端上,此客户端对应的Session数据会被清除,常见的设置值是15分钟或30分钟
  • 更换客户端(包括更换浏览器、关开浏览器),也会无法访问此前的Session数据,并且,此前的Session数据将会根据超时机制被清理
  • 服务器端设备关机或重启
  • 服务器端程序调用了清除Session数据的方法,例如调用了HttpSession对象的invalidate()方法
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
《Vue和SpringBoot打造假日旅社管理系统》课程,将讲解如何使用Vue和SpringBoot开发这个项目,手把手演示开发流程!附赠源码、文档、数据库脚本等全部资料,提供售后答疑。 课程简介本课程讲解的是《基于 Vue 和 SpringBoot 的假日旅社管理系统》,该系统支持民宿档案、民宿新闻、民宿预定、民宿评论这四大核心业务,适用于乡村民宿企业的民宿预定业务。系统给每个民宿档案提供一个唯一标识,对民宿预定、评论等各个环节进行快速批量的数据采集,确保游客及时掌握景区民宿的真实数据,方便游客进行民宿预定。另外系统还包括员工管理、组织机构管理、文件管理、权限管理功能,给旅社企业提供更个性化的民宿管理模式。假日旅社管理系统采用了基于角色的访问控制,角色和菜单关联,一个角色可以配置多个菜单权限;然后再将用户和角色关联,一位用户可以赋予多个角色。这样用户就可以根据角色拿到该有的菜单权限,更方便旅社企业的管理人员进行权限管控。   软件技术选型前端Vue:Vue 是构建前端界面的核心框架,本系统采用 2.6.14 版本。View UI:基于 Vue.js2.0 的组件库,本系统采用 4.7.0 版本。后端Spring Boot:构建系统核心逻辑的后端框架,本系统采用 2.7.0 版本。MyBatis / MyBatis Plus:后端连接数据库的框架,本系统采用 3.5.2 版本。数据库MySQL:本项目的主数据库,本系统采用 8.0.29 版本。Redis:本系统采用基于 Windows 版本的 Redis,用于图形验证码和用户菜单权限的临时存储,采用了 5.0.14 版本。开发环境VsCode:项目前端的开发工具,使用版本为 1.68.0。IntelliJ IDEA :项目后端的开发工具,使用版本为 2021.3.2。Jdk:Java 的开发环境,使用版本为 17.0.3.1。Maven:后端项目的打包工具,使用版本为 3.6.2。NodeJs:前端项目的开发环境,使用版本为 16.13.0。 软件架构分析基于 Vue 和 SpringBoot 的假日旅社管理系统包括了系统基础模块、民宿档案模块、民宿新闻模块、民宿预定模块、民宿评论模块这五大功能模块,其架构如下图所示。  接下来,分别对五大模块进行详细介绍。系统基础模块系统基础模块,是用于支撑假日旅社管理系统的正常运行,这个模块包括了登陆注册模块、员工部门管理、菜单权限管理等。假日旅社管理系统支持用户使用账号、密码和图形验证码登陆,操作界面如下图所示。  假日旅社管理系统支持用户使用手机号、姓名、密码和图形验证码注册,操作界面如下图所示。 用户成功进入系统后,可进入到基于 Vue 和 SpringBoot 的假日旅社管理系统的首页,首页展示了当前登陆的地址、现在的时间和用户配置的常用模块,如下图所示。 接着用户点击左侧的用户管理,即可进入用户管理模块,用户管理模块的首页如下图所示。 用户可以在这个模块对系统登陆用户的档案进行维护,包括添加新用户、删除用户、编辑用户、根据姓名/部门查询用户。用户可以进入部门管理模块,管理旅社的部门数据,如下图所示。 同理用户可以进入到菜单管理模块,对系统的菜单进行管理,菜单管理模块的操作界面如下图所示。 民宿档案模块第二个模块是民宿档案模块,民宿档案就是用来管理民宿的数据,民宿档案包括民宿的名称、面积、房号、房间类型、早餐情况、价格、备注等,以下是民宿档案模块的主界面。用户可以点击顶部的“新增”按钮,进入民宿档案添加界面,添加民宿档案数据,如下图所示。 其中房间类型为下拉框单项选择,如下图所示。还有早餐情况也是下拉单选,如下图所示。 用户可以对现有的民宿档案数据进行编辑更新,只需点击每一行民宿档案数据的“编辑”按钮,即可进入民宿档案数据的编辑界面,如下图所示。 用户也可以对不需要的民宿数据进行删除操作,用户点击删除时,系统会弹出二次确认弹框,如下图所示。  民宿新闻模块第三个模块是民宿新闻模块,民宿新闻就是用来管理民宿的新闻资讯,包含的功能如下所示。 民宿新闻包括民宿的名称、面积、房号、房间类型、早餐情况、价格、备注等,以下是民宿新闻模块的主界面,其中的图片仅供测试样例使用。用户可以点击顶部的“新增”按钮,进入民宿新闻添加界面,添加民宿新闻数据,如下图所示。 新闻描述字段采用了 ueditor 富文本编辑器,这是由百度 web 前端研发部开发的所见即所得的开源富文本编辑器,具有轻量、可定制、用户体验优秀等特点,基于 MIT 开源协议,所有源代码可自由修改和使用。 用户可以对现有的民宿新闻数据进行编辑更新,只需点击每一行民宿新闻数据的“编辑”按钮,即可进入民宿新闻数据的编辑界面,如下图所示。 民宿预定模块第四个模块是民宿预定模块,旅客可以在民宿预定模块中预定民宿,达到旅客的住宿目的,民宿预定模块包含的功能如下所示。民宿预定包括了预定民宿 ID、预定民宿名称、预定日期、下单时间、下单人 ID、下单人姓名、价格、是否付款、下单备注等字段,旅客首先进入民宿档案模块,可以看到每一行民宿数据都有一个预约按钮,如下图所示。 如用户点击 1 幢 102 民宿的预约按钮后,会弹出预约确认框,需要输入预约的日期,日期表单默认选择今日,如下图所示。 旅客需要点击“确认预约”按钮,完成预约操作,系统给与“预约成功”提示,如下图所示。 预约成功后,旅客可以从民宿预定模块中进行查询,如下图所示。 最后旅客进行付款操作,点击每一行右侧的付款按钮,如下图所示。支付完成后,系统将预定单的支付状态改为付款,预定流程结束,如下图所示。 民宿评论模块 第五个模块是民宿预定模块,旅客可以在民宿预定结束后评论民宿,以帮助更多的人了解民宿,民宿评论模块包含的功能如下所示。 民宿评论包括了民宿名称、民宿 ID、评论时间、评论内容、评论人 ID、评论人姓名等字段,旅客首先进入民宿档案模块,可以看到每一行民宿数据都有一个评论按钮,如下图所示。 旅客点击评论后,系统给与弹框反馈,如下图所示。  用户输入评论内容后,点击确认评论按钮,即可完成评论操作,如下图所示。  旅客评论后,即可在民宿评论模块中查看此评论数据,如下图所示。 也可以在民宿模块中,双击民宿数据查看评论信息,如下图所示。 项目总结本软件是基于 Vue 和 SpringBoot 的假日旅社管理系统,包含了民宿档案、民宿新闻、民宿预定、民宿评论这四个功能模块。 开发本系统的目的,就是为了帮助旅游景点的民宿企业提高民宿管理效率,降低人力成本,让旅游景点的民宿企业获得更多的经济效益。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bromide-0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值