30天完成家庭任务平台:第二十九天

我最近试着把家庭任务平台的前後端分离时,後端要开出API给前端来抓取资料,但因为家庭任务平台会有权限限制,例如只有建立计画的人才能看到计画,Laravel有提供一个方便的套件Sanctum来处理这样的状况。

Sanctum可以提供单页应用程序认证、手机应用程序、APIs的Token认证。单页应用程序认证和APIs的Token认证采取不同的机制:(1)单页应用程序认证: cookie based session;(2)APIs的Token认证:Bear Token。开发者可以根据自己的状况任选其中一个机制来使用,但如果可以适用单页应用程序认证,即前端和後端的顶级网域名称相同,则建议使用单页应用程序认证,因为其提供的防护如防CSRF更为周全。

由於前端并不拥有相同的顶级网域名称,我们使用APIs的Token认证:

  1. 安装Sanctum
    composer require laravel/sanctum
    php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

  2. 利用User的HasApiTokens来发Token

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}
  • Token会在杂凑处理後存入资料库,此时回传未经杂凑处理的Token给前端:
$token = $user->createToken('token-name');
return $token->plainTextToken;
  • 使Token无效的方法:
$user->tokens()->delete();

// Revoke the user's current token...
$request->user()->currentAccessToken()->delete();

// Revoke a specific token...
$user->tokens()->where('id', $id)->delete();

token-name:自取。

  1. 处理认证的AuthController
  • AuthController
class AuthController extends Controller
{
    public function register(RegisterRequest $request)
    {
        $validatedData = $request->validated();
        $validatedData['password'] = Hash::make(request('password'));
        $user = User::create($validatedData);
        return $this->userResponse($user, 201);
    }

    public function login(LoginRequest $request)
    {
        $validatedData = $request->validated();
        if (!Auth::attempt($validatedData)) {
            return response()->json(
                ["message" => "The credential was invalid."],
                401
            );
        }
        $user = User::where('email', $validatedData['email'])->first();
        return $this->userResponse($user, 200);
    }

    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json([],204);
    }
    protected function userResponse(User $user, int $status)
    {

        $token = $user->createToken('familyboard-apis');
        return response()->json(['accessToken' => $token->plainTextToken, 'type' => 'Bearer'], $status);
    }
}
  • RegisterRequest
class RegisterRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [  
                'name' => ['required', 'string', 'max:255'],
                'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
                'password' => ['required', 'string', 'min:8', 'confirmed'], 
        ];
    }
    public function messages()
{
    return [
        'name.required' => 'A name is required',
        'email.required' => 'A email is required',
        'email.unique'=>'The email already exists',
        'password.required' => 'A password is required',
    ];
}
}
  • LoginRequest
class LoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'email' => ['required', 'string', 'email', 'max:255', 'exists:users'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }
    public function messages()
{
    return [
        'email.required' => 'A email is required',
        'email.exists'=>'The email is not registered yet',
        'password.required' => 'A password is required',
    ];
}
}

  1. 测试我们的AuthController
class LoginAndRegisterTest extends TestCase
{
    use RefreshDatabase, WithFaker;
    /**
     * A basic feature test example.
     *
     * @test
     */
    public function a_registered_user_can_get_a_token()
    {
        $response = $this->post(
            route('register'),
            [
                'name' => 'jhao',
                'email' => '[email protected]',
                'password' => '12345678',
                'password_confirmation' => '12345678'
            ]
        );

        $this->assertDatabaseHas('users', ['email' => '[email protected]']);
        $this->assertDatabaseHas(
            'personal_access_tokens',
            [
                'tokenable_type' => 'App\Models\User',
                'tokenable_id' => User::where('name', 'jhao')->first()->id
            ]
        );
        $response->assertStatus(201);
    }

    /** @test */
    public function registering_twice_would_receive_status_422()
    {
        $user = User::factory()->create();
        $response = $this->post(
            route('register'),
            [
                'name' => $user->name,
                'email' => $user->email,
                'password' => '12345678',
                'password_confirmation' => '12345678'
            ],
        );

        $response->assertStatus(422);
        $response->assertExactJson(["message" => "The given data was invalid.", "errors" => ["email" => ["The email already exists"]]]);
    }

    /** @test */
    public function logged_in_user_must_have_a_registered_email()
    {
        $response = $this->post(route('login'), ['email' => '[email protected]', 'password' => '12345678']);
        $response->assertStatus(422);
        $response->assertExactJson(["message" => "The given data was invalid.", "errors" => ["email" => ["The email is not registered yet"]]]);
    }

    /** @test */
    public function logged_in_user_must_have_valid_credential()
    {
        $user = User::factory()->create(['password' => '1234567']);
        $response = $this->post(route('login'), ['email' => $user->email, 'password' => '1qaz2wsx']);
        $response->assertStatus(401);
        $response->assertExactJson(["message" => "The credential was invalid."]);
    }
    /** @test */
    public function logged_in_user_can_get_an_access_token()
    {
        $user = User::factory()->create(['password' => Hash::make('12345678'), 'name' => 'jhao']);
        $response = $this->post(route('login'), ['email' => $user->email, 'password' => '12345678']);
        $response->assertStatus(200);
    }

}

  1. 加入auth:sanctum的Middleware来保护需要认证的路由,使得我们可以利用$request->user()来辨识前端使用者的身份
Route::prefix('v1')->group(function () {
    Route::post('register', [AuthController::class, 'register'])->name('register');
    Route::post('login', [AuthController::class, 'login'])->name('login');
    
});


Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::get('logout', [AuthController::class, 'logout'])->name('logout');
    Route::apiResource('projects',ProjectController::class);
});

此外,为了让api路由都会在Header中加入['Accept', 'application/json'],做一个AddJsonHeader的Middleware。

class AddJsonHeader
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {

        $request->headers->set('Accept', 'application/json');

        return $next($request);
    }
}

'api' => [
            \App\Http\Middleware\AddJsonHeader::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
  1. 测试logout路径是否受到保护
class LoginAndRegisterTest extends TestCase
{
     ...
    /** @test */
    public function logged_out_user_can_not_access_website()
    {
        $user = Sanctum::actingAs(
            User::factory()->create(),
            ['*']
        );
        $this->get(route('projects.index'))->assertSuccessful();
        $this->get(route('logout'))->assertStatus(204);
        $this->assertDatabaseMissing(
            'personal_access_tokens',
            [
                'tokenable_id' => $user->id,
                'tokenable_type' => 'App\Models\User'
            ]
        );
       
    }
  ...
 }
  • Laravel提供经过验证使用者的机制:
    $user = Sanctum::actingAs( User::factory()->create(), ['*'] );

参考文章
Laravel Sanctum


<<:  菜鸟日记Day 30-用JSON-Server自建云端资料库

>>:  JavaScript 之旅 (29):Logical assignment operators ( &&=、||= 和 ??= )

{CMoney战斗营} 的第六周 # 游戏模组套用

好不容易拟定了游戏专题的方向,接下来是要奠基在上一届学长姐的模组上继续成长出自己的专案。 为期一个月...

有关Wscript.exe *.vbs 的中文字编码( utf-8)问题

(一) WSH script程序,utf-8的档案A 中文字抄至B时会变乱码。 inputFileP...

基於 SAML 的联合身份管理 (FIM) 以支持单点登录 (SSO)

来源:安全断言标记语言 (SAML) V2.0 技术概述 如上图所示: .一个用户可以在每个域中拥...

03 | 认识 WordPress「区块编辑器」的发展和简介

关於 Block Editor(区块编辑器)的各类延伸有很多,我们这篇文章尽量保持简单,但您可以从...

二元树之 IF 上策 - DAY 17

假如用人数去施打疫苗图表 人数是概略计算非准确值 算一下总触发 IF 次数 348.5万 * 1 +...