Spring boot Security Jwt

说明 : Spring Security 做系统权限的,引入轻松,涵盖各种粒度的权限控制,可以自定义各种处理器,拦截器等.

5.7版本之前,整合Spring Security

引入 security 启动器,默认拦截所有资源,启动项目会生成一个默认密码,账号 user

 <!-- 引入Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

不前后分离,单线开发时 👇

  • 实现 UserDetailsService, 配置 SecurityUser或者返回 security 的User,自定义用户登录逻辑
  • 继承 WebSecurityConfigurerAdapter,定义加密方式,添加自定义拦截器各种Handler、登录异常、权限不足等…
@Data
@Accessors(chain = true)
public class SecurityUser implements UserDetails {
    private static final long serialVersionUID = -2211380247224432737L;

    private static final String DEFAULT_AUTH="ADMIN";
    private Long userId;
    private String userName;
    private String account;
    private String password;

    /**
     * 用户角色,权限集合
     */
    private List<String> rolesAuthorities;
    /**
     * 是否可用
     */
    private boolean enabled = true;
    /**
     * 是否冻结
     */
    private boolean locked = true;

    /**
     * 设置登录用户信息
     * @param user user 登录用户信息
     * @return SecurityUser
     */
    public SecurityUser setUser(User user,List<String> rolesAuthorities){
        this.userId=user.getId();
        this.userName=user.getUserName();
        this.account=user.getPhone();
        this.password=user.getPassWord();
        this.rolesAuthorities=rolesAuthorities;
        return this;
    }

    /**
     * 登录用户角色
     * @return List<GrantedAuthority>
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new LinkedList<>();
        if (rolesAuthorities!=null && rolesAuthorities.size()>0){
            for (String role : rolesAuthorities) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
        }else {
            authorities=AuthorityUtils.commaSeparatedStringToAuthorityList(DEFAULT_AUTH);
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }
    @Override
    public String getUsername() {
        return this.account;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return locked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }

}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Resource
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //username :前面提交登录账号
        User user = userMapper.selectOne(new User().setPhone(username));
        if (user==null){
            throw new UsernameNotFoundException("账号不存在");
        }
        //模拟数据查询用户角色集合,权限集合,角色用ROLE_ 开头
        List<String> rolesAuthorities = Arrays.asList("vip1", "vip2","ROLE_test","ROLE_vip");
        return new SecurityUser().setUser(user,rolesAuthorities);
    }

}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //登录配置 ,如果不前后分离,重新这方法就要配置登录跳转地址了,不然就404
        http.formLogin()
                //自定义:登录接口
                .loginProcessingUrl("/login")
                //定义登录请求参数
                .usernameParameter("username")
                .passwordParameter("password")
                //自定义:登录页面地址
                .loginPage("/toLogin")
                //登录成功跳转地址
                .loginProcessingUrl("/main")
                //登录失败跳转地址
                .failureForwardUrl("/toError");

        //注销配置
        http.logout()
                //注销地址
                .logoutUrl("/logout");
                //退出登录跳转页面
                //.logoutSuccessUrl("/login.html");
                
		//拦截配置
        //http.addFilter();
        
        //授权配置
        http.authorizeRequests()
                //访问 /user 需要权限标识 vip1
                .antMatchers("/user").hasAuthority("vip1")
                //访问 /admin 需要角色 admin
                .antMatchers("/admin").hasAnyRole("admin")
                //添加用户post请求 需要角色 admin
                .regexMatchers(HttpMethod.POST,"/addUser").hasAnyRole("admin")
                //放行静态资源
                .antMatchers("/js/**","/css/**","/images/**").permitAll()
                //放行 option 请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                //放行 /test 所有请求
                .antMatchers("/test/**").permitAll()
                //登录相关请求
                .antMatchers("/login/**").permitAll()
                //之外所有请求需要认证
                .anyRequest().authenticated();

        //自定义异常处理
        http.exceptionHandling()
                //403异常
                .accessDeniedHandler(myAccessDeniedHandler);

        //关闭scrf防护
        // http.csrf().disable();


    }

    /**
     * 配置加密方式
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

整合 Jwt,前后分离开发时

  • 配置,登录拦截 LoginFilter extends UsernamePasswordAuthenticationFilter
  • 配置,验证拦截 AuthenticationFilter extends BasicAuthenticationFilter
  • 自定义 异常处理,403 RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler
    ,失效令牌 AuthEntryPoint implements AuthenticationEntryPoint
  • 相当于 token校检、失效丢给 Jwt 处理,接口鉴权,忽略认证等用Security处理

整合代码:

//自定义 403异常
public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    public RestAuthenticationAccessDeniedHandler() {}

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        ResponseUtil.write(response, LoginResult.error("没有权限"));
    }
}
//无效令牌异常
public class AuthEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e){
        ResponseUtil.write(response, LoginResult.error("令牌无效"));
    }

}
//配置类
@Data
@Component
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
    /**
     * 忽略拦截路径
     */
    private List<String> httpIgnore;
    /**
     * 请求头名称
     */
    private String tokenHeader;
    /**
     * 请求头前缀
     */
    private String tokenPrefix;
    /**
     * 令牌加密密匙
     */
    private String secret;
    /**
     * 失效时间 /分钟 - 默认1小时
     */
    private Long expiration;

}
//token生成工具
@Component
public class JwtTokenUtil {

    @Resource
    private SecurityProperties securityProperties;

    public JwtTokenUtil() {}

    /**
     * token生成
     * @param user
     * @return
     */
    public String createToken(SecurityUser user) {
        String secret = this.securityProperties.getSecret();
        if (secret==null){
            secret="secret";
        }
        Long expiration = this.securityProperties.getExpiration();
        if (expiration==null){
            expiration=60L;
        }
        long time = expiration * 60L;
        HashMap<String, Object> map = new HashMap<>(1);
        map.put("user", user);
        return Jwts.builder()
                .setClaims(map)
                .setSubject(user.getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + time * 1000L))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    public String getUserName(String token) {
        return this.generateToken(token).getSubject();
    }

    private Claims generateToken(String token) {
        String secret = this.securityProperties.getSecret();
        if (secret==null){
            secret="secret";
        }
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    public SecurityUser getSecurityUser(String token){
        Claims claims = this.generateToken(token);
        Map map = claims.get("user", Map.class);
        return JSON.parseObject(JSON.toJSONString(map), SecurityUser.class);
    }

}
//登录拦截
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private static final String REQUEST_METHOD="POST";
    private static final String DATA_FORMAT="json";

    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Resource
    private SecurityProperties securityProperties;

    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!REQUEST_METHOD.equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String contentType = request.getHeader("Content-Type");
            SecurityUser user = null;
            //json提交
            if (contentType.contains(DATA_FORMAT)) {
                user = this.getSecurityUser(request);
                if (user == null || user.getUsername() == null || user.getPassword() == null) {
                    throw new AuthenticationServiceException("Authentication failure: username or password can't be null.");
                }
                logger.info("账号登录:"+user.getUsername());
            } else {
                //表单提交
                String username = this.obtainUsername(request);
                String password = this.obtainPassword(request);
                if (username == null || password == null) {
                    throw new AuthenticationServiceException("Authentication failure: username or password can't be null.");
                }
                logger.info("账号登录:"+username);
                user = (new SecurityUser()).setUsername(username.trim()).setPassword(password);
            }
            request.setAttribute("account",user.getUsername());
            return this.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList()));
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityUser user = (SecurityUser)authResult.getPrincipal();
        user.setPassword("");
        String token = this.jwtTokenUtil.createToken(user);
        String tokenPrefix = this.securityProperties.getTokenPrefix();
        if (!StringUtils.hasLength(tokenPrefix)){
            tokenPrefix="Bearer";
        }
        String tokenHeader = this.securityProperties.getTokenHeader();
        if (!StringUtils.hasLength(tokenHeader)){
            tokenHeader="Authorization";
        }
        response.addHeader(tokenHeader, tokenPrefix + token);
        logger.info("登录成功,用户: "+user.getUsername());
        ResponseUtil.write(response, LoginResult.login(tokenPrefix + token));
    }

    //对应 ->JwtUserDetailsServiceImpl loadUserByUsername()
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        String msg;
        String account = (String)request.getAttribute("account");
        if (failed instanceof UsernameNotFoundException) {
            msg="账号不存在";
        }else if ( failed instanceof BadCredentialsException){
            msg="密码输入错误";
        }else if (failed instanceof DisabledException) {
            msg="用户账号已被禁用";
        } else if (failed instanceof LockedException) {
            msg="抱歉您的账户已被锁定";
        } else if (failed instanceof AccountExpiredException) {
            msg="账户过期";
        }  else {
            msg="登录失败";
        }
        logger.info("登录失败,账号: "+account);
        ResponseUtil.write(response, LoginResult.error(msg));
    }


    private SecurityUser getSecurityUser(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        try {
            InputStream is = request.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String s = "";
            while((s = br.readLine()) != null) {
                sb.append(s);
            }
            return JSON.parseObject(sb.toString(), SecurityUser.class);
        } catch (IOException var7) {
            return null;
        }
    }

}
//鉴权拦截
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private SecurityProperties securityProperties;

    public static final List<String> HTTP_IGNORE = new LinkedList<>(Arrays.asList("/doc.html", "/swagger-resources", "/v3/api-docs/**", "/webjars/**","/logout"));

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        //忽略拦截路径放行
        if (this.ignore(request)) {
            chain.doFilter(request, response);
        } else {
            String tokenHeader = this.securityProperties.getTokenHeader();
            if (!StringUtils.hasLength(tokenHeader)){
                tokenHeader="Authorization";
            }
            //请求头获取
            String header = request.getHeader(tokenHeader);
            //前缀匹配
            String tokenPrefix = this.securityProperties.getTokenPrefix();
            if (!StringUtils.hasLength(tokenPrefix)){
                tokenPrefix="Bearer";
            }
            if (header != null && header.startsWith(tokenPrefix)) {
                UsernamePasswordAuthenticationToken authenticationToken = null;
                try {
                    authenticationToken = this.getAuthentication(header);
                } catch (Exception e) {
                    ResponseUtil.write(response,LoginResult.error("非法token"));
                    return;
                }
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                chain.doFilter(request, response);
            } else {
                ResponseUtil.write(response, LoginResult.error("无效令牌"));
            }
        }
    }

    /**
     * 这里从token中获取用户信息并新建一个token
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String header) {
        String tokenPrefix = this.securityProperties.getTokenPrefix();
        if (!StringUtils.hasLength(tokenPrefix)){
            tokenPrefix="Bearer";
        }
        String token = header.replace(tokenPrefix, "");
        String principal = this.jwtTokenUtil.getUserName(token);
        if (principal != null) {
            SecurityUser user = this.jwtTokenUtil.getSecurityUser(token);
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        } else {
            return null;
        }
    }

    /**
     * 校检是否忽略路径,默认放行路径 /logOut
     * @param request request
     * @return boolean
     */
    private boolean ignore(HttpServletRequest request) {
        for (String ignore : HTTP_IGNORE) {
            if (new AntPathRequestMatcher(ignore).matches(request)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 初始化忽略认证
     */
    @PostConstruct
    public void initIgnore(){
        List<String> curIgnore = securityProperties.getHttpIgnore();
        HTTP_IGNORE.addAll(curIgnore);
    }
}
//自定义密码解析
public class MD5PasswordEncoder implements PasswordEncoder {
    //TODO 加盐
    private static String salt="xiaoshu@730!@#$/";
    @Override
    public String encode(CharSequence charSequence) {
        return DigestUtils.md5Hex(charSequence +salt);
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        String s1 = DigestUtils.md5Hex(charSequence +salt);
        return s1.equals(s);
    }
}
//配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService loginUserDetailsService;

    @Resource
    private SecurityProperties properties;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //拦截配置
        //登录拦截
        http.addFilter(this.jwtLoginFilter())
                //认证拦截
                .addFilter(this.jwtAuthenticationFilter())
                .exceptionHandling()
                //自定义403
                .accessDeniedHandler(new RestAuthenticationAccessDeniedHandler())
                //自定义令牌失效
                .authenticationEntryPoint(new AuthEntryPoint());

        //默认放行配置
        http.authorizeRequests().antMatchers(HttpMethod.GET,"/js/**","/css/**","/images/**").permitAll()
        .antMatchers("/doc.html", "/swagger-resources", "/v3/api-docs/**", "/webjars/**","/logout").permitAll()
        .antMatchers(HttpMethod.OPTIONS,"/**").permitAll()

        //项目放行配置
        .antMatchers(properties.getHttpIgnore().toArray(new String[]{})).permitAll().anyRequest().authenticated();

        //关闭csrf
        http.csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(this.authenticationProvider());
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public JwtLoginFilter jwtLoginFilter() throws Exception {
        return new JwtLoginFilter(authenticationManager());
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JwtAuthenticationFilter(authenticationManager());
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setHideUserNotFoundExceptions(false);
        provider.setUserDetailsService(loginUserDetailsService);
        provider.setPasswordEncoder(new MD5PasswordEncoder());
        return provider;
    }

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

}
/**
 * 自定义登录逻辑
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!username.equals("admin")){
            throw  new UsernameNotFoundException("用户不存在");
        }
        String password = passwordEncoder.encode("123");
        return new SecurityUser().setUsername(username).setPassword(password).setRoles(Arrays.asList("ROLE_admin,","ROLE_vip","menu_del","auth"));
    }

}

5.7 版本整合,待整理…

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《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 的假日旅社管理系统,包含了民宿档案、民宿新闻、民宿预定、民宿评论这四个功能模块。 开发本系统的目的,就是为了帮助旅游景点的民宿企业提高民宿管理效率,降低人力成本,让旅游景点的民宿企业获得更多的经济效益。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值