手把手教你用Java实现一套简单的鉴权服务(SpringBoot,SSM)(万字长文1)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云解析DNS,个人版 1个月
简介: 手把手教你用Java实现一套简单的鉴权服务(SpringBoot,SSM)(万字长文)

一、何为鉴权服务

引用百度百科的话说


鉴权(authentication)是指验证用户是否拥有访问系统的权利。


鉴权包括两个方面:


用户鉴权,网络对用户进行鉴权,防止非法用户占用网络资源。

网络鉴权,用户对网络进行鉴权,防止用户接入了非法的网络,被骗取关键信息。

而我们这里的鉴权主要指用户鉴权,即如何确认“你是你”。最简单的体现便是平常用的用户登录登出。


现今大部分系统都会有自己的鉴权服务,它是用户与系统交互的第一步,系统需要一系列步骤明白你是谁,你可以做哪些事,明白了这些之后它才能更好的服务于你。


二、利用servlet+jdbc实现简单的用户登录程序

1.明确思路

首先,我们要仔细思考一下我们到底需要什么?


先让我们回想一下一般的登录是如何做的呢?


对于网页,首先会出现一个登录页面,然后呢,输入账号密码,点击登录,就会弹出成功/失败的页面。


那如何去判断成功/失败呢?


思考一下,最简单的方法便是拿到前端传来的数据之后便将其拿到数据中去查,看看密码是不是一样,然后给前端回复说——我找到了,他就是XXX或者我找不到他的记录,让他重新输入账号密码。


然后前端对此回复做出相应的操作,比如登录成功便跳转到首页,失败让用户重新输入。


2.手把手教你实现一个简单的web登录程序

出于某些原因,我这里手把手教你如何实现一个简单的web登录程序。


①创建web项目

打开idea,新建一个web项目



q5.png

这里为了方便jar包的管理,选择maven结构的项目(至于什么是maven结构,不懂的可以百度,了解概念即可),然后选择从原型创建,选择webapp(这里只是方便,你也可以选择空项目,不过会费点时间)。


q4.png


点击下一步,输入项目名称


q3.png


这里选择相应的maven,idea里有自带的maven和jar包仓库,不过我是自己去官网下了一个(不下也完全可以)。


q2.png


选择完成,这样一个最简单的项目结构就出来了。


q1.png


接下来需要配置一下pom.xml,因为要用到jdbc和tomcat的jar包(毕竟都是调用人家的接口(笑哭))


<dependencies>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
      <version>9.0.37</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.20</version>
    </dependency>


(加在project标签里就行),上面配置的意思就是导入两个第三方工具包


②编写简单的登录页面

这里我既想要好看,又想偷懒,所以用了layui框架的模板


<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>后台管理-登陆</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta http-equiv="Access-Control-Allow-Origin" content="*">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="format-detection" content="telephone=no">
    <link rel="stylesheet" href="../lib/layui-v2.6.3/css/layui.css" media="all">
    <!--[if lt IE 9]>
    <script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
    <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
    <style>
        .main-body {top:50%;left:50%;position:absolute;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);transform:translate(-50%,-50%);overflow:hidden;}
        .login-main .login-bottom .center .item input {display:inline-block;width:227px;height:22px;padding:0;position:absolute;border:0;outline:0;font-size:14px;letter-spacing:0;}
        .login-main .login-bottom .center .item .icon-1 {background:url(../images/icon-login.png) no-repeat 1px 0;}
        .login-main .login-bottom .center .item .icon-2 {background:url(../images/icon-login.png) no-repeat -54px 0;}
        .login-main .login-bottom .center .item .icon-3 {background:url(../images/icon-login.png) no-repeat -106px 0;}
        .login-main .login-bottom .center .item .icon-4 {background:url(../images/icon-login.png) no-repeat 0 -43px;position:absolute;right:-10px;cursor:pointer;}
        .login-main .login-bottom .center .item .icon-5 {background:url(../images/icon-login.png) no-repeat -55px -43px;}
        .login-main .login-bottom .center .item .icon-6 {background:url(../images/icon-login.png) no-repeat 0 -93px;position:absolute;right:-10px;margin-top:8px;cursor:pointer;}
        .login-main .login-bottom .tip .icon-nocheck {display:inline-block;width:10px;height:10px;border-radius:2px;border:solid 1px #9abcda;position:relative;top:2px;margin:1px 8px 1px 1px;cursor:pointer;}
        .login-main .login-bottom .tip .icon-check {margin:0 7px 0 0;width:14px;height:14px;border:none;background:url(../images/icon-login.png) no-repeat -111px -48px;}
        .login-main .login-bottom .center .item .icon {display:inline-block;width:33px;height:22px;}
        .login-main .login-bottom .center .item {width:288px;height:35px;border-bottom:1px solid #dae1e6;margin-bottom:35px;}
        .login-main {width:428px;position:relative;float:left;}
        .login-main .login-top {height:117px;background-color:#148be4;border-radius:12px 12px 0 0;font-family:SourceHanSansCN-Regular;font-size:30px;font-weight:400;font-stretch:normal;letter-spacing:0;color:#fff;line-height:117px;text-align:center;overflow:hidden;-webkit-transform:rotate(0);-moz-transform:rotate(0);-ms-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);}
        .login-main .login-top .bg1 {display:inline-block;width:74px;height:74px;background:#fff;opacity:.1;border-radius:0 74px 0 0;position:absolute;left:0;top:43px;}
        .login-main .login-top .bg2 {display:inline-block;width:94px;height:94px;background:#fff;opacity:.1;border-radius:50%;position:absolute;right:-16px;top:-16px;}
        .login-main .login-bottom {width:428px;background:#fff;border-radius:0 0 12px 12px;padding-bottom:53px;}
        .login-main .login-bottom .center {width:288px;margin:0 auto;padding-top:40px;padding-bottom:15px;position:relative;}
        .login-main .login-bottom .tip {clear:both;height:16px;line-height:16px;width:288px;margin:0 auto;}
        body {background:url(../images/loginbg.png) 0% 0% / cover no-repeat;position:static;font-size:12px;}
        input::-webkit-input-placeholder {color:#a6aebf;}
        input::-moz-placeholder {/* Mozilla Firefox 19+ */            color:#a6aebf;}
        input:-moz-placeholder {/* Mozilla Firefox 4 to 18 */            color:#a6aebf;}
        input:-ms-input-placeholder {/* Internet Explorer 10-11 */            color:#a6aebf;}
        input:-webkit-autofill {/* 取消Chrome记住密码的背景颜色 */            -webkit-box-shadow:0 0 0 1000px white inset !important;}
        html {height:100%;}
        .login-main .login-bottom .tip {clear:both;height:16px;line-height:16px;width:288px;margin:0 auto;}
        .login-main .login-bottom .tip .login-tip {font-family:MicrosoftYaHei;font-size:12px;font-weight:400;font-stretch:normal;letter-spacing:0;color:#9abcda;cursor:pointer;}
        .login-main .login-bottom .tip .forget-password {font-stretch:normal;letter-spacing:0;color:#1391ff;text-decoration:none;position:absolute;right:62px;}
        .login-main .login-bottom .login-btn {width:288px;height:40px;background-color:#1E9FFF;border-radius:16px;margin:24px auto 0;text-align:center;line-height:40px;color:#fff;font-size:14px;letter-spacing:0;cursor:pointer;border:none;}
        .login-main .login-bottom .center .item .validateImg {position:absolute;right:1px;cursor:pointer;height:36px;border:1px solid #e6e6e6;}
        .footer {left:0;bottom:0;color:#fff;width:100%;position:absolute;text-align:center;line-height:30px;padding-bottom:10px;text-shadow:#000 0.1em 0.1em 0.1em;font-size:14px;}
        .padding-5 {padding:5px !important;}
        .footer a,.footer span {color:#fff;}
        @media screen and (max-width:428px) {.login-main {width:360px !important;}
            .login-main .login-top {width:360px !important;}
            .login-main .login-bottom {width:360px !important;}
        }
    </style>
</head>
<body>
<div class="main-body">
    <div class="login-main">
        <div class="login-top">
            <span>LayuiMini后台登录</span>
            <span class="bg1"></span>
            <span class="bg2"></span>
        </div>
        <form class="layui-form login-bottom" action="/login" method="post">
            <div class="center">
                <div class="item">
                    <span class="icon icon-2"></span>
                    <input type="text" name="uname" lay-verify="required"  placeholder="请输入登录账号" maxlength="24"/>
                </div>
                <div class="item">
                    <span class="icon icon-3"></span>
                    <input type="password" name="pwd" lay-verify="required"  placeholder="请输入密码" maxlength="20">
                    <span class="bind-password icon icon-4"></span>
                </div>
            </div>
            <div class="tip">
                <span class="icon-nocheck"></span>
                <span class="login-tip">保持登录</span>
                <a href="javascript:" class="forget-password">忘记密码?</a>
            </div>
            <div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0px;">
                <button class="login-btn" type="submit" lay-submit="" lay-filter="login">立即登录</button>
            </div>
        </form>
    </div>
</div>
<div class="footer">
    ©版权所有 2014-2018 叁贰柒工作室<span class="padding-5">|</span><a target="_blank" href="http://www.miitbeian.gov.cn">粤ICP备16006642号-2</a>
</div>
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script>
    //原本想用json的post发送,结果发现后端数据得自己解析,为了降低难度,直接用form表单的post提交,这样后端直接拿数据即可(不然还得解析Json数据)
    // layui.use(['form','jquery'], function () {
    //     var $ = layui.jquery,
    //         form = layui.form,
    //         layer = layui.layer;
    //
    //     // 登录过期的时候,跳出ifram框架
    //     if (top.location != self.location) top.location = self.location;
    //
    //     $('.bind-password').on('click', function () {
    //         if ($(this).hasClass('icon-5')) {
    //             $(this).removeClass('icon-5');
    //             $("input[name='pwd']").attr('type', 'password');
    //         } else {
    //             $(this).addClass('icon-5');
    //             $("input[name='pwd']").attr('type', 'text');
    //         }
    //     });
    //
    //     $('.icon-nocheck').on('click', function () {
    //         if ($(this).hasClass('icon-check')) {
    //             $(this).removeClass('icon-check');
    //         } else {
    //             $(this).addClass('icon-check');
    //         }
    //     });
    //
    //     // 进行登录操作
    //     form.on('submit(login)', function (data) {
    //         data = data.field;
    //         if (data.uname == '') {
    //             layer.msg('用户名不能为空');
    //             return false;
    //         }
    //         if (data.pwd == '') {
    //             layer.msg('密码不能为空');
    //             return false;
    //         }
    //         $.ajax({
    //             url:'/login',
    //             method:'post',
    //             data:data,
    //             dataType:'JSON',
    //             success:function(res){
    //                 if (res.msg==='登录成功'){
    //                     layer.msg('登录成功', function () {
    //                         window.location = '../index.html';
    //                     });
    //                 }else {
    //                     layer.msg("登录失败");
    //                 }
    //             },
    //             error:function (data) {
    //             }
    //         }) ;
    //
    //
    //         return false;
    //     });
    // });
</script>
</body>
</html>



当然以上代码有一部分注释掉了,原因是如果用JSON格式发送post请求,后端的servlet(准确的说是Tomcat的解析)并没有帮我们解析封装这部分数据,所以我们无法直接get到,得自己另外解析数据,当然也有一些第三方的工具包可以帮我们做这些事情(如阿里的fastjson等),这里为了使其更加简单,所以采用表单提交post请求的方式,这样解析的工作就不用我们做了。


效果是这样的:

q1.png


如果你没学过layui或者对前端不太行,你也可以这样


<!DOCTYPE html>
<htmllang="en">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="uname">
    密码:<input type="password" name="pwd">
    <input type="submit" value="login">
</form>
</body>
</html>


一样的功能,不过看上去的效果就不怎么好了。


③编写servlet程序

当有了前端的页面,看上去好了很多,但实质校验的程序我们还没有写。


想象一下我们就是后端程序,当前端的数据历经艰险,从错综复杂的网络中到达我们的服务器,然后经过系统分发到相应端口,这时恰在此端口的tomcat程序接受到了HTTP请求并对其封装,经过一系列骚操作后分发到了我们手中,而我们要做的就是拿着这个封装好的请求进行校验操作,然后对返回对象进行相应修改。


而这也是servlet类所需要做的(如果你想更好的理解servlet,可以看看bravo1988的回答),


package com.dreamchaser.loginTest;
import com.dreamchaser.loginTest.mapper.UserMapper;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class LoginServlet extends HttpServlet {
    static UserMapper userMapper=UserMapper.getUserMapper();
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request,response);
    }
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String uname=req.getParameter("uname");
        String pwd=req.getParameter("pwd");
        ServletOutputStream outputStream = resp.getOutputStream();
        String result;
        if (pwd.equals(userMapper.getPwdByName(uname))){
            //响应
            result="登录成功";
        }else {
            result="登录失败";
        }
        outputStream.write(result.getBytes());
    }
}

你可能会疑惑这个UserMapper是什么,别急,后面会介绍。


④封装jdbc操作,编写简单的数据库连接池

在操作数据库之前,最好写个简单的数据库连接池。一个是简化我们的操作,一个是节省开销,提高性能(Connection是个非常耗费资源的对象,频繁的创建和回收将会是一笔巨大的开销)


package com.dreamchaser.loginTest.utils;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
 * 一个简单的数据库连接池
 */
public class Pool {
    private static Driver driver;
    static {
        try {
            driver = new com.mysql.cj.jdbc.Driver();
            DriverManager.registerDriver(driver);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
    private static Map<Connection,Integer> pool=new HashMap<>();
    private static String url="jdbc:mysql://localhost:3306/depository?serverTimezone=Asia/Shanghai";
    private static String user="root";
    private static String password="jinhaolin";
    /**
     * 从连接池中获取一个空闲连接,如果没有则创建一个新的连接返回
     * synchronized确保并发请求时,数据库连接的正确性
     * @return
     */
    public synchronized static Connection getConnection(){
        for (Map.Entry entry:pool.entrySet()){
            if (entry.getValue().equals(1)) {
                entry.setValue(0);
                return (Connection) entry.getKey();
            }
        }
        Connection connection=null;
        try {
            connection=DriverManager.getConnection(url,user,password);
            pool.put(connection,0);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return connection;
    }
    /**
     * 释放connection连接对象
     * @param connection
     */
    public synchronized static void releaseConnection(Connection connection){
        pool.put(connection,1);
    }
}


当然上述实现非常简陋,并发性能也不是很好,高并发时可能还会发生OOM,不过凑活着用吧(笑哭)。


⑤操作数据库

package com.dreamchaser.loginTest.mapper;
import com.dreamchaser.loginTest.utils.Pool;
import java.sql.*;
/**
 * 查询用户的Mapper
 */
public class UserMapper {
    static UserMapper userMapper=new UserMapper();
    //单例
    public static UserMapper getUserMapper(){
        return userMapper;
    }
    private UserMapper(){
    }
    //默认数据库中用户名唯一
    public String getPwdByName(String name){
        Connection connection= Pool.getConnection();
        try {
            PreparedStatement statement=connection.prepareStatement("select pwd from `user` where uname=?");
            statement.setString(1,name);
            ResultSet rs=statement.executeQuery();
            //resultSet初始下标无法访问,要调用next方法后移一位
            rs.next();
            return rs.getString(1);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return null;
    }
}


这里采用单例的设计模式,保证UserMapper对象只有一个。(非常简陋,实现也不优雅,看着自己的代码,突然感觉框架好方便啊(笑哭))


这里的作用就是根据用户名查询密码。


⑥配置web.xml

虽然写了servlet,但是tomcat并不知道你这个servlet的类在哪啊,所以必须让tomcat知道,配置web.xml的目的就是通知tomcat在哪(更准确的说是servlet容器)的一种方式(当然也可以用注解)。

配置如下:


<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.dreamchaser.loginTest.LoginServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/login</url-pattern>
  </servlet-mapping>
</web-app>

servlet-class里写你这个Servlet类的路径即可。


⑦idea运行配置

idea配置还是比较方便的。


点击编辑配置,


q3.png

点击+(添加按钮),选择tomcat服务器(选哪个都可以,我选了tomcat本地)


q2.png

然后选择相应的服务器程序,配置项目访问的端口,就是tomcat在哪个端口运行(注意不要占用已有端口,默认8080,我这里是因为8080被占了,所以用了9090)


q1.png

这里还得配置一下工件,因为项目要运行一般有两种方式:


一种是打成war包放在tomcat的webapps目录下

一种是打成jar包直接运行(SpringBoot就是用这种方式,因为它内置tomcat)


q5.png

这里工件的作用就是打成war包,至于每次运行部署?idea都会帮你搞定!是不是很方便?


这里那个应用程序上下文的作用就是给访问路径加个前缀

q4.png

一般直接写成"/"就行了,这样我们要访问login.html,只需访问http://localhost:9090/login.html就行了,是不是很方便?


⑧运行程序

点击运行


q3.png

访问localhost:9090/login.html(我因为是在login.html外面放了一个pages包,所以路径是http://localhost:9090/pages/login.html)


q2.png

访问成功,试试账号密码


q2.png

我现在数据库里只有root这一条数据,试试效果


输入错误的密码


q1.png

输入正确的密码



q2.png

到这里,我们松了一口气,终于完成了简单的登录功能。


三、回顾

别急,我们虽然实现了登录这个功能,但是这个实现是在太简陋了,各方各面都没考虑,返回页面也只登录成功,登录失败的提示。

我们回顾一下,仔细想想有哪些问题。


1.密码未加密裸奔

我们在做上面的登录时查询时,密码是查询出来直接比对的,也就是说数据库的密码是明文存储,而注册登录请求中密码都是明文传输,这样做的安全性极低,当黑客破解进入了你的数据库时,你的数据库的账户信息都在“裸奔”,比如前些年的csdn密码泄露事件

q1.png


如果我们存储在数据库的用户密码是加密过的,那么就算黑客进入了你的数据库,损失也不会像明文存储那样大。


2.登录信息未存储

对于这个登录操作,登录成功后并未做其他处理,也就是说每次访问都要登录(如果对请求进行了拦截),或者这个登录操作就是摆设,用户访问其他资源依旧畅通无阻。


3.对于其他资源并未进行权限管理

对于其他资源,如果不进行权限管理,那么登录认证便失去了意义,不如做成一个静态网页来的省事。


四、优化设计

针对上述缺点,我们可以进行以下改进:


1.密码加密存储

针对密码未加密裸奔的问题,我们可以选择在注册的时候对密码进行加密,然后存储;对于登录功能,我们对前端传过来的密码进行加密,再根据这个密码去数据库中取数据,这样我们就实现了对密码的加密存储


2.存储登录信息

对于登录操作,我们必须记录下此次登录状态,并在该用户继续访问其他资源时予以放行,避免用户多次进行登录操作


3.对资源进行管理

对于系统资源我们必须进行管理,在用户没有相应权限时拒绝用户的访问请求,这个可以用过滤器或者SpringBoot的拦截器实现。


相关文章
|
13天前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
46 0
|
14天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的母婴全程服务管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的母婴全程服务管理系统附带文章源码部署视频讲解等
18 1
基于springboot+vue.js+uniapp的母婴全程服务管理系统附带文章源码部署视频讲解等
|
12天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的志愿服务管理系统设计和实现(源码+LW+部署讲解)
基于SpringBoot+Vue的志愿服务管理系统设计和实现(源码+LW+部署讲解)
32 6
|
10天前
|
监控 Java 开发者
Spring Boot框架在java领域的优势
随着云计算、微服务架构的兴起,Java开发领域迫切需要一套高效、灵活且易于上手的框架来应对日益复杂的业务需求。正是在这样的背景下,Spring Boot应运而生,以其独特的魅力迅速成为了Java开发者手中的利器。
21 3
|
12天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的同城家政服务的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的同城家政服务的详细设计和实现(源码+lw+部署文档+讲解等)
|
12天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的中医学习服务管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的中医学习服务管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
|
12天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的博物馆展览与服务一体化平台的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的博物馆展览与服务一体化平台的详细设计和实现(源码+lw+部署文档+讲解等)
|
12天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的厨到家服务系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的厨到家服务系统的详细设计和实现(源码+lw+部署文档+讲解等)
|
12天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的家政服务预约系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的家政服务预约系统的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的小区家政服务预约平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的小区家政服务预约平台附带文章源码部署视频讲解等
14 0