비밀번호 없이 이메일로 로그인하기 free

2019-08-30

트위터 창업자가 만든 글쓰기 플랫폼 미디엄은 해외에선 완전 대세 서비스입니다. 카카오 브런치, 네이버 포스트 등이 미디엄의 영향을 많이 받았죠.


미디엄은 이메로 로그인할 때 비밀번호를 요구하지 않고, 로그인할 수 있는 매직 링크를 보내주는 방을 사용하고 있습니다.


스크린샷 2019-08-30 오후 8.21.04.png


요즘 1 1 벨의 지난 글들을 볼 수 있는 웹사이트를 짬짬이 만들고 있는데, 이번 참에 저도 한 번 비밀번호 없이 이메로 로그인하는 시스템으로 만들어보고 싶더고요.


찾아보니 매트 스타우퍼가 2016년에 잘 정리해 둔 글이 있더군요. 글이 길긴 하지만 코드 위주로 따가면 크게 어렵진 않습니다만, 작성 시점이 2016년인 만큼 아주 조금은 현재에 맞지 않는 내용도 있습니다. 매트 스타우퍼의 글 속 예제를 벨 5.8 기준으로 따 할 수 있게 코드 위주로 정리해봤습니다.


새 앱 준비


laravel new medium-login
cd medium-login
php artisan key:generate
php artisan make:auth

데이터베이스 생성하고 접속 정보를 .env에 적습니다.


password 컬럼에 값이 입력되지 않아도 에러가 나지 않도록 마이그레이션을 수정하거나, 새 마이그레이션을 추가합니다. 여기서는 그냥 마이그레이션을 수정하겠습니다.


//  database/migrations/2014_10_12_000000_create_users_table.php
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable(); // <-- nullable()을 추가해줬습니다.
$table->rememberToken();
$table->timestamps();
});
}

마이그레이션을 실행합니다.


php artisan migrate

로그인과 회원가입 페이지에서 패스워드 입력란 제거


resources/views/auth/login.<wbr>blade.phpresources/views/auth/register.<wbr>blade.php에서 password, password-confirm 입력란을 제거하고, 비밀번호 찾기와 관련된 코드도 제거합니다.


RegisterController 수정


유효성 검사에서 password 관련 항목 제거


// app/http/Controllers/Auth/RegisterController.php
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
]);
}

DB에 추가할 때 password 항목 제거


// app/http/Controllers/Auth/RegisterController.php
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
]);
}

LoginController


AuthenticatesUsers 트레이트에있는 login() 매서드를 오버이드 합니다. 기존의 로그인 프로세스를 재활용하지 않을 것이기 때문에 내용은 그냥 비워둡니다.


// app/http/Controllers/Auth/LoginController.php
public function login(Request $request)
{
// 로그인시 입력한 이메일이 이메일 형식인지 검사
// 로그인 메일 전송
// 이메일 확인하라는 뷰 표현
}

로그인시 입력한 이메이 이메인지 검사


// app/http/Controllers/Auth/LoginController.php
public function login(Request $request)
{
$this->validate($request, ['email' => 'required|email|exists:users']);
}

로그인 메 전송


회용 토큰을 생성해서 이메로 보내고, 한 번 쓰이면 폐기하는 방을 사용합니다.


테이블 만들기


토큰을 저장할 테이블을 만듭니다.


php artisan make:migration create_email_logins_table --create=email_logins

마이그레이션 파은 아래와 같이 작성합니다.


Schema::create('email_logins', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token')->index();
$table->timestamps();
});

EmailLogin 모델 작성


email_logins 테이블과 상호작용하고, User 클래스와 1:1 관계를 맺을 모델을 작성합니다.


php artisan make:model EmailLogin

class EmailLogin extends Model
{
public $fillable = ['email', 'token'];

public function user()
{
return $this->hasOne(\App\User::class, 'email', 'email');
}
}

email_logins 테이블에 id를 안만들었기 때문에 email로 연결합니다.


EmailLogin에 토큰 생성 기능 추가


class EmailLogin extends Model
{
...
public static function createForEmail($email)
{
return self::create([
'email' => $email,
'token' => Str::random(20)
]);
}
}

로그인용 URL 생성


LoginController::login()을 아래와 같이 작성합니다.


public function login()
{
$this->validate($request, ['email' => 'required|email|exists:users']);

$emailLogin = EmailLogin::createForEmail($request->input('email'));

$url = route('auth.email-authenticate', [
'token' => $emailLogin->token
]);
}

위의 코드에 언급된 auth.email-authenticate 우트를 추가합니다.


// routes/web.php
Route::get('auth/email-authenticate/{token}', 'Auth\LoginController@authenticateEmail')->name('auth.email-authenticate');

로그인 컨트롤러에 authenticateEmail 매서드를 추가합니다.


class LoginController
{
...
public function authenticateEmail($token)
{
$emailLogin = EmailLogin::validFromToken($token);

Auth::login($emailLogin->user);

return redirect('home');
}
}

EmailLogin 모델에 validFromToken() 매서드를 추가합니다.


class EmailLogin
{
...
public static function validFromToken($token)
{
return self::where('token', $token)
->where('created_at', '>', Carbon::parse('-15 minutes'))
->firstOrFail();
}

이메 전송


LoginController::login() 매서드에 이메 전송 코드를 추가합니다.


public function login()
{
$this->validate($request, ['email' => 'required|email|exists:users']);

$emailLogin = EmailLogin::createForEmail($request->input('email'));

$url = route('auth.email-authenticate', [
'token' => $emailLogin->token
]);

Mail::send('auth.emails.email-login', ['url' => $url], function ($m) use ($request) {
$m->from('noreply@myapp.com', 'MyApp');
$m->to($request->input('email'))->subject('MyApp Login');
});
}

이메로 보내질 auth.emails.email-login 뷰를 만듭니다.



Log in to MyApp here: <a href="{{ $url }}">{{ $url }}</a>

이메 확인하는 뷰 표현


LoginController::login() 매서드에 이메을 확인하는 메시지를 출력하는 코드를 추가합니다. 실전에선 뷰를 만들어서 리턴하지만 예제니까 간단하게 ^^


public function login()
{
$this->validate($request, ['email' => 'required|email|exists:users']);

$emailLogin = EmailLogin::createForEmail($request->input('email'));

$url = route('auth.email-authenticate', [
'token' => $emailLogin->token
]);

Mail::send('auth.emails.email-login', ['url' => $url], function ($m) use ($request) {
$m->from('noreply@myapp.com', 'MyApp');
$m->to($request->input('email'))->subject('MyApp Login');
});

return '로그인 이메일을 발송했습니다. 확인해주세요.';
}

확인


이제 원하는대로 잘 동작하는지 확인해봅시다. 그 전에 이메이 실제로 발송되지 않도록 처리합니다.


실제 이메을 발송하도록 설정하려면 다소 귀찮습니다. 그래서 개발할 때는 실제로 메을 보내지 않고 보내는 척만 합니다. 이에 대해서는 공 매뉴얼의 & 로컬 설정을 참고하세요.


.env' 파<span class="il">일</span>에서MAIL_DRIVER` 항목을 log로 변경합니다.


MAIL_DRIVER=log

그러면 메이 실제로 발송되지 않고 로그에 기록됩니다.


이제 앱을 구동하고 회원 가입 후 로그인을 시도해봅시다.


storage/logs/laravel-날짜.log을 열어보면 아래와 같이 메이 로그에 기록되어있을 겁니다.


스크린샷 2019-08-30 오후 9.46.42.png


링크를 복사해서 브우저로 접근해보면 로그인이 될 것입니다.


마치며


비밀번호 없이 이메로 로그인하기를 적용해놓고 보니, 이메을 누락없이 발송하는 것, 이메이 전송되는 동안 기다리지 않도록 처리하는 것 등 손 대야할 곳이 많네요. 괜히 했나… 하핫


9월호 모집이 시작됐습니다. 웹사이트 만든다고 정신 팔려서 모집하는 걸 깜빡하고 있었네요.


8월호가 괜찮았다면 9월호는 https://forms.gle/Ymrsrfe7cnm1MFMbA 에서 신청 부탁드리고, 주변에도 권유 좀 부탁드려요! (굽신굽신)


1 1 벨 44호

2019년 8월 30



이현석

바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 2020년에 출간될 Laravel Up & Running 2nd Edition을 번역하고 있습니다.