我最近试着把家庭任务平台的前後端分离时,後端要开出API给前端来抓取资料,但因为家庭任务平台会有权限限制,例如只有建立计画的人才能看到计画,Laravel有提供一个方便的套件Sanctum来处理这样的状况。
Sanctum可以提供单页应用程序认证、手机应用程序、APIs的Token认证。单页应用程序认证和APIs的Token认证采取不同的机制:(1)单页应用程序认证: cookie based session;(2)APIs的Token认证:Bear Token。开发者可以根据自己的状况任选其中一个机制来使用,但如果可以适用单页应用程序认证,即前端和後端的顶级网域名称相同,则建议使用单页应用程序认证,因为其提供的防护如防CSRF更为周全。
由於前端并不拥有相同的顶级网域名称,我们使用APIs的Token认证:
安装Sanctum
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
利用User的HasApiTokens来发Token
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
$token = $user->createToken('token-name');
return $token->plainTextToken;
$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:自取。
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);
}
}
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',
];
}
}
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',
];
}
}
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);
}
}
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,
],
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'
]
);
}
...
}
$user = Sanctum::actingAs( User::factory()->create(), ['*'] );
参考文章
Laravel Sanctum
<<: 菜鸟日记Day 30-用JSON-Server自建云端资料库
>>: JavaScript 之旅 (29):Logical assignment operators ( &&=、||= 和 ??= )
好不容易拟定了游戏专题的方向,接下来是要奠基在上一届学长姐的模组上继续成长出自己的专案。 为期一个月...
(一) WSH script程序,utf-8的档案A 中文字抄至B时会变乱码。 inputFileP...
来源:安全断言标记语言 (SAML) V2.0 技术概述 如上图所示: .一个用户可以在每个域中拥...
关於 Block Editor(区块编辑器)的各类延伸有很多,我们这篇文章尽量保持简单,但您可以从...
假如用人数去施打疫苗图表 人数是概略计算非准确值 算一下总触发 IF 次数 348.5万 * 1 +...