五、会话管理

五、会话管理

一、开发目标

本章将为应用开发一个完整用户的登录和退出功能,一般用户的登录认证流程操作流程如下:

  1. 访问登录页面,输入账号密码点击登录;

  2. 服务器对用户身份进行认证,认证通过后,记录登录状态并进行页面重定向;

  3. 登录成功后的用户,能够使用退出按钮来销毁当前登录状态;

除此之外,我们还会添加一个大部分网站都会实现的功能 - 『记住我』,来保证用户的登录状态能够长时间被浏览器记录着,而不是在登录了一段时间后自动掉线,需要再次填写账号密码才能进行登录,这同时也是出于增强用户体验方面的考虑。

二、会话

1、会话

由于 HTTP 协议是无状态的,我们无法在两个页面之间保证用户身份的同步,因此我们需要借助会话在浏览器中临时存储用户的身份信息,进而保证在同一浏览器中,用户在不同页面具有相同的登录状态。

接下来让我们来新建分支,开始进行登录和退出相关功能的开发。

$ git checkout master
$ git checkout -b login-logout

2、会话控制器

首先我们要新建一个会话控制器,该控制器将用于处理用户登录退出相关的操作。你可以把会话理解为我们之前谈到过的资源,当用户登录成功时,会话将被创建;当用户退出登录时,会话会被销毁。只是在这里会话并不会保存到数据库中,而是保存在浏览器上。让我们运行下面命令来生成会话控制器。

$ php artisan make:controller SessionsController

下面我们还需要对路由进行配置,添加一些接下来需要用到的路由,新增的路由分别对应会话控制器的三个动作:create, store, destroy。

routes/web.php

Route::get('login','SessionsController@create')->name('login');
Route::post('login','SessionsController@store')->name('login');
Route::delete('logout','SessionsController@destroy')->name('logout');

新增的路由功能如下。

HTTP 请求URL动作作用
GET/loginSessionsController@create显示登录页面
POST/loginSessionsController@store创建新会话(登录)
DELETE/logoutSessionsController@destroy销毁会话(退出登录)

你也可以使用 Laravel 提供的 route:list 命令来查看已添加的路由。

$ php artisan route:list

image.png

我们可以从上面的列表清晰的看到所有在 routes/web.php 中被定义好的路由,这将帮助我们更好的理解应用的基础架构。

接下来让我们先来完善会话控制器的 create 动作,为用户创建一个登录页面。

3、登录表单

在用户填写登录表单时,只需要用户提供个人邮箱账号和密码信息即可。由于我们前面给邮箱做了唯一性限制,因此能够保证所有的注册用户邮箱都不相同,为了确认登录者为邮箱拥有者本人,我们需要将邮箱与密码进行匹配,匹配成功的用户将通过认证并登录。

接下来让我们完善一开始创建的会话控制器,加入 create 动作,并返回一个指定的登录视图。

app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SessionsController extends Controller
{
    public function create()
    {
        return view('sessions.create');
    }
}

让我们新建一个登录视图,并加上表单信息。

resources/views/sessions/create.blade.php

@extends('layouts.default')
@section('title','登录')

@section('content')
    <div class="offset-md-2 col-md-8">
        <div class="card ">
            <div class="card-header">
                <h5>登录</h5>
            </div>
            <div class="card-body">
                @include('shared._errors')

                <form method="POST" action="{{ route('login') }}">
                    {{ csrf_field() }}

                    <div class="form-group">
                        <label for="email">邮箱:</label>
                        <input type="text" name="email" class="form-control" value="{{ old('email') }}">
                    </div>

                    <div class="form-group">
                        <label for="password">密码:</label>
                        <input type="password" name="password" class="form-control" value="{{ old('password') }}">
                    </div>

                    <button type="submit" class="btn btn-primary">登录</button>
                </form>

                <hr>

                <p>还没账号?<a href="{{ route('signup') }}">现在注册!</a></p>
            </div>
        </div>
    </div>
@stop

上面构建的登录表单有一行代码需要我们特别关注。

<form method="POST" action="{{ route('login') }}">

我们在前面新增的路由中,有两个路由的命名完全一致,但由于我们在表单中清楚的指明了使用 POST 动作来提交用户的登录信息,因此 Laravel 会自动将该请求映射到会话控制器的 store 动作上。

Route::get('login', 'SessionsController@create')->name('login');
Route::post('login', 'SessionsController@store')->name('login');

访问 http://weibo.test/login 页面已能够看到登录页面能够正常显示,但现在表单仍处于不可用状态,因为我们还没有对用户发送的登录请求做任何处理。

image.png


4、认证用户身份

Laravel 默认提供的内置认证控制器功能非常强大,只需要你做简单的几行代码配置即可完成整个登录功能的构建。但在本教程中,为了让新手用户对整个用户登录流程有个更加清楚的了解,为此我们将使用手动认证的方式来一步一步实现用户的登录功能。

在我们成功构建用户的登录表单之后,我们需要在会话控制器中创建 store 动作来对用户提交的数据进行验证。

app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SessionsController extends Controller
{
    public function create()
    {
        return view('sessions.create');
    }

    public function store(Request $request)
    {
        $credentials = $this->validate($request,[
            'email'=>'required|email|max:255',
            'password'=>'require'
        ]);
        return ;
    }
}

我们可以看到,在 store 动作中的数据验证与之前的有所不同,因为在这里只需要保证用户输入的值不为空且格式正确即可。

验证失败时的错误提示:

image.png


当用户填写的信息验证通过之后,我们还需要对用户提供的信息进行用户身份认证,因为验证通过只能说明用户提交的信息格式是正确的,并不能保证提交的用户信息存在于数据库中。

我们可以使用 Illuminate\Http\Request 实例来接收用户的所有输入数据,当我们需要取出 Request 实例的单个值时,可以使用以下方法:

$request->email;

当 $request 请求中包含 email 字段时,上面这行代码将返回邮箱信息。

借助 Laravel 提供的 Auth 的 attempt 方法可以让我们很方便的完成用户的身份认证操作,如下所示:

if (Auth::attempt(['email'=>$email,'password'=>$password])){
    //该用户存在于数据库,且邮箱和密码相符合
    
}

attempt 方法会接收一个数组来作为第一个参数,该参数提供的值将用于寻找数据库中的用户数据。因此在上面的例子中,attempt 方法执行的代码逻辑如下:

  1. 使用 email 字段的值在数据库中查找;

  2. 如果用户被找到:
    1). 先将传参的 password 值进行哈希加密,然后与数据库中 password 字段中已加密的密码进行匹配;
    2). 如果匹配后两个值完全一致,会创建一个『会话』给通过认证的用户。会话在创建的同时,也会种下一个名为 laravel_session 的 HTTP Cookie,以此 Cookie 来记录用户登录状态,最终返回 true
    3). 如果匹配后两个值不一致,则返回 false

  3. 如果用户未找到,则返回 false

结合 attempt 方法对用户身份进行认证的具体代码实现如下,使用 Auth 前需要对其进行引用(注意文件顶部引入 use Auth;):

app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class SessionsController extends Controller
{
    public function create()
    {
        return view('sessions.create');
    }

    public function store(Request $request)
    {
        $credentials = $this->validate($request,[
            'email'=>'required|email|max:255',
            'password'=>'required'
        ]);
        if (Auth::attempt($credentials)){
            //登录成功后的相关操作


        }else{
            //登录失败后的相关操作

            
        }

        return ;
    }
}

5、消息提示和页面重定向

现在 store 方法已经能够完成用户身份的认证操作了,接下来我们需要针对用户认证成功或失败的情况做不同处理。在用户登录失败时,我们需要在顶部显示提示信息,明确告诉用户登录失败的原因,并将页面重定向回登录页面。

在用户登录失败之后,我们使用以下代码来进行消息提示。

session()->flash('danger', '很抱歉,您的邮箱和密码不匹配');

由于 danger 在 Bootstrap 中有特殊含义,借助我们在前面章节中定义的消息提示局部视图,可以使得页面上的消息提示更加美观。

在用户登录成功之后我们还需要将用户重定向至其个人页面,让用户可以在第一时间查看到自己的个人信息。而当用户登录失败时,则需要将页面重定向回登录页面,让他尝试重新登录。接下来让我们接着完善 store 方法,加入消息提示和页面重定向操作。

app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class SessionsController extends Controller
{
    public function create()
    {
        return view('sessions.create');
    }

    public function store(Request $request)
    {
        $credentials = $this->validate($request,[
            'email'=>'required|email|max:255',
            'password'=>'required'
        ]);
        if (Auth::attempt($credentials)){
            //登录成功后的相关操作
            session()->flash('success','欢迎回来!');
            return redirect()->route('users.show',[Auth::user()]);
        }else{
            //登录失败后的相关操作
            session()->flash('danger','很抱歉,您的邮箱和密码不匹配');
            return redirect()->back()->withInput();
        }

        return ;
    }
}

我们在 store 方法内使用了 Laravel 提供的 Auth::user() 方法来获取 当前登录用户 的信息,并将数据传送给路由。

这时如果尝试输入错误密码则会显示登录失败的提示信息。使用 withInput() 后模板里 old('email') 将能获取到上一次用户提交的内容,这样用户就无需再次输入邮箱等内容:

image.png

登录成功后,重定向到用户个人页面:

image.png


Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "创建会话"

三、用户登录

1、修改布局中的链接


现在登录成功的用户显示的页面信息看起来跟未登录用户没有太大区别,我们需要对网站顶部导航进行调整,当用户登录成功时,在顶部导航显示当前用户的用户名,并添加退出登录的按钮让用户可以随时退出登录状态;当用户未登录时,则显示登录按钮。

Laravel 提供了 Auth::check() 方法用于判断当前用户是否已登录,已登录返回 true,未登录返回 false

下面让我们对顶部导航的页面结构进行调整,加上一些链接。

resources/views/layouts/_header.blade.php

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container ">
    <a class="navbar-brand" href="{{ route('home')}}">Weibo App</a>
    <ul class="navbar-nav justify-content-end">

      @if(Auth::check())
        <li class="nav-item"><a class="nav-link" href="#">用户列表</a></li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            {{ Auth::user()->name }}
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="{{ route('users.show', Auth::user()) }}">个人中心</a>
            <a class="dropdown-item" href="#">编辑资料</a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" id="logout" href="#">
              <form action="{{ route('logout') }}" method="POST">
                {{ csrf_field() }}
                {{ method_field('DELETE') }}
                <button class="btn btn-block btn-danger" type="submit" name="button">退出</button>
              </form>
            </a>
          </div>
        </li>
      @else
        <li class="nav-item"><a class="nav-link" href="{{ route('help') }}">帮助</a></li>
        <li class="nav-item" ><a class="nav-link" href="#">登录</a></li>
      @endif
    </ul>
  </div>
</nav>

从上面代码可以看到,我们添加了用于显示用户列表的链接,『用户列表』的链接我们将在本书后面章节讲解其功能时进行替换。编辑资料链接则在用户更新功能开发完成后添加。本章节我们先把注意力放在用户退出登录按钮的具体实现上:

<form action="{{ route('logout') }}" method="POST">
  {{ csrf_field() }}
  {{ method_field('DELETE') }}
  <button class="btn btn-block btn-danger" type="submit" name="button">退出</button>
</form>

可以看到用户退出登录的按钮实际上是一个表单的提交按钮,在点击退出按钮之后浏览器将向 /logout 地址发送一个 POST 请求。但由于 RESTful 架构中会使用 DELETE 请求来删除一个资源,当用户退出时,实际上相当于删除了用户登录会话的资源,因此这里的退出操作需要使用 DELETE 请求来发送给服务器。由于浏览器不支持发送 DELETE 请求,因此我们需要使用一个隐藏域来伪造 DELETE 请求。

在 Blade 模板中,我们可以使用 method_field 方法来创建隐藏域。

{{ method_field('DELETE') }}

其转化为 HTML 代码如下:

<input type="hidden" name="_method" value="DELETE">

2、集成 Bootstrap 的 JavaScript 库

如果我们现在进行登录,则可以看到顶部导航链接已经加上,但当我们尝试点击用户名时,理应弹出的下拉菜单却没有任何响应。这是因为我们还没有引入 Bootstrap 的 JavaScript 组件库。Laravel 5 默认已经在 resources/js/bootstrap.js 文件中为我们配置好了 jQuery 和 Bootstrap。

我们只需要在 app.js 中对其进行加载即可:

resources/js/app.js

require('./bootstrap');

完成之后,需要重启 npm run watch-poll 让其编译新增的 app.js 文件。可使用 ctrl + c 退出 watch-poll 任务。然后重新运行:

$ npm run watch-poll

在我们重新运行 watch-poll 任务之后,app.js 文件将会被编译到应用的 public 文件夹下。现在我们要在全局默认视图中引用编译后的 app.js 文件。

resources/views/layouts/default.blade.php

<!DOCTYPE html>
<html>
  <head>
    <title>@yield('title', 'Weibo App') - Laravel 入门教程</title>
    <link rel="stylesheet" href="{{ mix('css/app.css') }}">
  </head>
  <body>
    @include('layouts._header')

    <div class="container">
      <div class="offset-md-1 col-md-10">
          @include('shared._messages')
          @yield('content')
          @include('layouts._footer')
      </div>
    </div>
    <script src="{{ mix('js/app.js') }}"></script>
  </body>
</html>

现在尝试再次点击下拉菜单,能发现它已经能够正常工作。

image.png


这时我们 使用 Chrome 开发者工具 可以看到有个报错:

image.png

这是因为 resources/js/bootstrap.js 文件中有以下代码:

let token = document.head.querySelector('meta[name="csrf-token"]');

if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
    console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

Laravel 默认创建了一个 JavaScript 变量,此变量会从页面的元标签中取 CSRF 密钥,取不到就会报错。本教程中我们不需要使用到该变量,把以上代码删除即可。

3、注册后自动登录

现在的注册功能已经可以正常使用,但我们希望在用户注册成功后能够自动登录,这样的应用用户体验会更棒。在 Laravel 中,如果要让一个已认证通过的用户实例进行登录,可以使用以下方法:

Auth::login($user);

让我们接着对用户控制器的 store 方法进行更改,让用户注册成功后自动登录。

app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Auth;

class UsersController extends Controller
{
    public function create()
    {
        return view('users.create');
    }

    public function show(User $user)
    {
        return view('users.show',compact('user'));
    }

    public function store(Request $request)
    {
        $this->validate($request,[
            'name' => 'required|max:50',
            'email' => 'required|email|unique:users|max:255',
            'password'=>'required|confirmed|min:6'
        ]);
        $user = User::create([
            'name'=>$request->name,
            'email'=>$request->email,
            'password'=>bcrypt($request->password),
        ]);
        Auth::login($user);
        session()->flash('success','欢迎,您将在这里开启一段新的旅程~');
        return redirect()->route('users.show',[$user]);
    }
}

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "登录成功后导航逻辑"

四、退出

现在如果我们点击退出按钮,会出现以下报错:

那是因为我们的退出逻辑代码还未编写。Laravel 默认提供的 Auth::logout() 方法来实现用户的退出功能。接下来让我们加上会话控制器的最后一个动作 - destroy,并实现用户退出登录的相关逻辑:

app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;

class SessionsController extends Controller
{

    
    public function destroy()
    {
        Auth::logout();
        session()->flash('success','您已成功推出!');
        return redirect('login');
    }
}

我们在用户退出之后,会在顶部显示相关的消息提醒,并将用户重定向到登录页面。

image.png


Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "用户退出登录"

五、记住我

本章节最后要实现的一个功能是『记住我』功能,在 Laravel 的默认配置中,如果用户登录后没有使用『记住我』功能,则登录状态默认只会被记住两个小时。如果使用了『记住我』功能,则登录状态会被延长到五年。我们可以通过使用 Laravel 提供的『记住我』功能来保存一个记忆令牌,用于长时间记录用户登录的状态。Laravel 默认为用户生成的迁移文件中已包含 remember_token 字段,该字段将用于保存『记住我』令牌。

首先让我们修改登录视图,加上『记住我』复选框。

resources/views/sessions/create.blade.php

@extends('layouts.default')
@section('title','登录')

@section('content')
    <div class="offset-md-2 col-md-8">
        <div class="card ">
            <div class="card-header">
                <h5>登录</h5>
            </div>
            <div class="card-body">
                @include('shared._errors')

                <form method="POST" action="{{ route('login') }}">
                    {{ csrf_field() }}

                    <div class="form-group">
                        <label for="email">邮箱:</label>
                        <input type="text" name="email" class="form-control" value="{{ old('email') }}">
                    </div>

                    <div class="form-group">
                        <label for="password">密码:</label>
                        <input type="password" name="password" class="form-control" value="{{ old('password') }}">
                    </div>

                    <div class="form-group">
                        <div class="form-check">
                            <input type="checkbox" class="form-check-input" name="remember" id="exampleCheck1">
                            <label class="form-check-label" for="exampleCheck1">记住我</label>
                        </div>
                    </div>

                    <button type="submit" class="btn btn-primary">登录</button>
                </form>

                <hr>

                <p>还没账号?<a href="{{ route('signup') }}">现在注册!</a></p>
            </div>
        </div>
    </div>
@stop


image.png



前面我们介绍过的 Auth::attempt() 方法可接收两个参数,第一个参数为需要进行用户身份认证的数组,第二个参数为是否为用户开启『记住我』功能的布尔值。接下来让我们修改会话控制器中的 store 方法,为 Auth::attempt()添加『记住我』参数。

app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;

class SessionsController extends Controller
{
    public function create()
    {
        return view('sessions.create');
    }

    public function store(Request $request)
    {
        $credentials = $this->validate($request,[
            'email'=>'required|email|max:255',
            'password'=>'required'
        ]);
        if (Auth::attempt($credentials,$request->has('remember'))){
            //登录成功后的相关操作
            session()->flash('success','欢迎回来!');
            return redirect()->route('users.show',[Auth::user()]);
        }else{
            //登录失败后的相关操作
            session()->flash('danger','很抱歉,您的邮箱和密码不匹配');
            return redirect()->back()->withInput();
        }

        return ;
    }

    public function destroy()
    {
        Auth::logout();
        session()->flash('success','您已成功推出!');
        return redirect('login');
    }
}

至此,『记住我』功能已被成功添加,使用 Laravel 开发一切就是如此简单:

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "登录时记住我"

六、小结

现在,我们已完成整个用户注册、登录和退出功能。下面让我们需要将改动的代码进行提交并切换到主分支上进行合并。

$ git checkout master
$ git checkout .
$ git merge login-logout

我们修改了 app.scss 和 app.js ,如果切换到 master 上并且运行着 npm run watch-pull 的话,会引起文件冲突,git checkout . 是清空本地修改,也就是放弃切换到 master 后 npm run watch-pull 生成的内容。

最后再将代码推送到 GitHub 和 Heroku 上。

经过本章节的学习,我们学到了以下内容:

  • Laravel 强大的用户认证机制;

  • 了解了常用登录机制的具体实现;

  • 集成 Bootstrap JavaScript 组件;

  • 通过『记住我』来记住用户登录状态;


回复列表



回复操作

正在加载验证码......

请先拖动验证码到相应位置