一個帶有用戶系統的應用最基本登錄方式是站內賬號登錄,但這種方式往往不能滿足我們的需求。現在的應用基本都有站內賬號、email、手機和一堆第三方登錄,那么如果需要支持這么多種登錄方式,或者還有銀行卡登錄、身份證登錄等等更多的登錄方式,我們的數據表應該怎么設計才更合理呢?
需求分析
實現多種登錄方式,并且除了站內賬號登錄方式以外的登錄方式,都能夠進行綁定和解綁或者更換綁定。
如果按照傳統的數據表設計,我們用戶表會存儲用戶的賬號和密碼等授權相關的字段,類似下面:
id
username
password
nickname
sex
...
1
2
3
4
5
6
id
username
password
nickname
sex
...
但是如果登錄方式非常多的情況下,這種數據表結構不再適用。那么應該怎么設計呢?在查閱了一些資料后,本渣渣終于有了一個自我感覺很合理的設計方式。
首先,一個用戶不管有多少種登錄方式,用戶還是只有那一個用戶,但登錄方式卻有多種。這就形成了一對多的關系:一個用戶對應多個登錄方式。
所以,我們就可以把用戶表拆分成2張表,一張表存儲用戶基本的數據,另一張表存儲登錄授權相關的數據。我們可以向下面這樣設計:
users
id
nickname
sex
age
mobile
status
...
1
2
3
4
5
6
7
8
id
nickname
sex
age
mobile
status
...
user_auths
id # 自增id
user_id # users表對應的id
identity_type # 身份類型(站內username 郵箱email 手機mobile 或者第三方的qq weibo weixin等等)
identifier # 身份唯一標識(存儲唯一標識,比如賬號、郵箱、手機號、第三方獲取的唯一標識等)
credential # 授權憑證(比如密碼 第三方登錄的token等)
verified # 是否已經驗證(存儲 1、0 來區分是否已經驗證通過)
1
2
3
4
5
6
id#自增id
user_id#users表對應的id
identity_type#身份類型(站內username郵箱email手機mobile或者第三方的qqweiboweixin等等)
identifier#身份唯一標識(存儲唯一標識,比如賬號、郵箱、手機號、第三方獲取的唯一標識等)
credential#授權憑證(比如密碼第三方登錄的token等)
verified#是否已經驗證(存儲1、0來區分是否已經驗證通過)
這樣我們創建一個用戶,首先需要創建一條 users 表的用戶基礎信息記錄和一條或者多條 user_auths 表的授權記錄。注意修改密碼時也需要同時修改多條 user_auths 記錄,保證需要密碼的登錄方式憑證需要同步更新。而第三方的授權憑證和需要密碼的授權憑證則不需要同步。
代碼實現
這里我使用 laravel 來實現簡單的用戶注冊、登錄、修改密碼等操作,僅供參考。
首先創建2張數據表,結構如下:
users
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('nickname', 30)->default('寶寶')->comment('昵稱');
$table->string('say')->nullable()->comment('心情寄語');
$table->string('avatar', 50)->default('uploads/user/avatar.jpg')->comment('頭像');
$table->string('mobile', 11)->nullable()->comment('手機號碼');
$table->string('email', 50)->nullable()->comment('郵箱');
$table->tinyInteger('sex')->default(0)->comment('性別 0女 1男');
$table->tinyInteger('status')->default(1)->comment('狀態 1可用 0 不可用');
$table->tinyInteger('is_admin')->default(0)->comment('是否是管理員');
$table->tinyInteger('qq_binding')->default(0)->comment('QQ登錄是否綁定');
$table->tinyInteger('weixin_binding')->default(0)->comment('微信登錄是否綁定');
$table->tinyInteger('weibo_binding')->default(0)->comment('微博登錄是否綁定');
$table->tinyInteger('email_binding')->default(0)->comment('郵箱登錄是否綁定');
$table->tinyInteger('phone_binding')->default(0)->comment('手機登錄是否綁定');
$table->timestamps();
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
publicfunctionup()
{
Schema::create('users',function(Blueprint$table){
$table->increments('id');
$table->string('nickname',30)->default('寶寶')->comment('昵稱');
$table->string('say')->nullable()->comment('心情寄語');
$table->string('avatar',50)->default('uploads/user/avatar.jpg')->comment('頭像');
$table->string('mobile',11)->nullable()->comment('手機號碼');
$table->string('email',50)->nullable()->comment('郵箱');
$table->tinyInteger('sex')->default(0)->comment('性別 0女 1男');
$table->tinyInteger('status')->default(1)->comment('狀態 1可用 0 不可用');
$table->tinyInteger('is_admin')->default(0)->comment('是否是管理員');
$table->tinyInteger('qq_binding')->default(0)->comment('QQ登錄是否綁定');
$table->tinyInteger('weixin_binding')->default(0)->comment('微信登錄是否綁定');
$table->tinyInteger('weibo_binding')->default(0)->comment('微博登錄是否綁定');
$table->tinyInteger('email_binding')->default(0)->comment('郵箱登錄是否綁定');
$table->tinyInteger('phone_binding')->default(0)->comment('手機登錄是否綁定');
$table->timestamps();
});
}
user_auths
public function up()
{
Schema::create('user_auths', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->index()->comment('用戶id');
$table->string('identity_type')->comment('登錄類型(手機號phone 郵箱email 用戶名username)或第三方應用名稱(微信weixin 微博weibo 騰訊QQqq等)');
$table->string('identifier')->unique()->index()->comment('標識(手機號 郵箱 用戶名或第三方應用的唯一標識)');
$table->string('credential')->nullable()->comment('密碼憑證(站內的保存密碼,站外的不保存或保存token)');
$table->tinyInteger('verified')->default(0)->comment('是否已經驗證');
$table->timestamps();
});
}
1
2
3
4
5
6
7
8
9
10
11
12
publicfunctionup()
{
Schema::create('user_auths',function(Blueprint$table){
$table->increments('id');
$table->integer('user_id')->index()->comment('用戶id');
$table->string('identity_type')->comment('登錄類型(手機號phone 郵箱email 用戶名username)或第三方應用名稱(微信weixin 微博weibo 騰訊QQqq等)');
$table->string('identifier')->unique()->index()->comment('標識(手機號 郵箱 用戶名或第三方應用的唯一標識)');
$table->string('credential')->nullable()->comment('密碼憑證(站內的保存密碼,站外的不保存或保存token)');
$table->tinyInteger('verified')->default(0)->comment('是否已經驗證');
$table->timestamps();
});
}
實現注冊功能,創建站內賬號,一個用戶 + 一個站內賬號登錄授權。
public function register(Request $request)
{
// 已經登錄則直接跳轉
if (Session::has('user')) {
return redirect()->route('admin.index');
}
if ($request->method() === 'GET') {
return view('admin.user.register');
}
// 驗證表單
$validator = Validator::make($request->all(), [
'identifier' => ['required', 'between:6,16', 'unique:user_auths'],
'credential' => ['required', 'between:6,16', 'confirmed'],
], [
'identifier.required' => '用戶名為必填項',
'identifier.unique' => '用戶名已經存在',
'identifier.between' => '用戶名長度必須是6-16',
'credential.required' => '密碼為必填項',
'credential.between' => '密碼長度必須是6-16',
'credential.confirmed' => '兩次輸入的密碼不一致',
]);
if ($validator->fails()) {
return back()->withErrors($validator);
}
// 創建用戶
$user = new User();
$user->save();
// 創建授權
$userAuth = new UserAuth();
$userAuth->user_id = $user->id;
$userAuth->identity_type = 'username';
$userAuth->identifier = $request->identifier;
$userAuth->credential = bcrypt($request->credential);
$userAuth->save();
return redirect()->route('admin.login');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
publicfunctionregister(Request$request)
{
// 已經登錄則直接跳轉
if(Session::has('user')){
returnredirect()->route('admin.index');
}
if($request->method()==='GET'){
returnview('admin.user.register');
}
// 驗證表單
$validator=Validator::make($request->all(),[
'identifier'=>['required','between:6,16','unique:user_auths'],
'credential'=>['required','between:6,16','confirmed'],
],[
'identifier.required'=>'用戶名為必填項',
'identifier.unique'=>'用戶名已經存在',
'identifier.between'=>'用戶名長度必須是6-16',
'credential.required'=>'密碼為必填項',
'credential.between'=>'密碼長度必須是6-16',
'credential.confirmed'=>'兩次輸入的密碼不一致',
]);
if($validator->fails()){
returnback()->withErrors($validator);
}
// 創建用戶
$user=newUser();
$user->save();
// 創建授權
$userAuth=newUserAuth();
$userAuth->user_id=$user->id;
$userAuth->identity_type='username';
$userAuth->identifier=$request->identifier;
$userAuth->credential=bcrypt($request->credential);
$userAuth->save();
returnredirect()->route('admin.login');
}
實現登錄,站內賬號、郵箱、手機號碼登錄方式。
public function login(Request $request)
{
// 已經登錄則直接跳轉
if (Session::has('user')) {
return redirect()->route('admin.index');
}
if ($request->method() === 'GET') {
return view('admin.user.login');
}
// 驗證表單
$validator = Validator::make($request->all(), [
'identifier' => ['required', 'exists:user_auths'],
'credential' => ['required', 'between:6,16'],
], [
'identifier.exists' => '用戶不存在',
'identifier.required' => '用戶名為必填項',
'credential.required' => '密碼為必填項',
'credential.between' => '密碼長度必須是6-16',
]);
if ($validator->fails()) {
return back()->withErrors($validator);
}
// 查詢授權記錄 - 查詢3種登錄方式的授權記錄
$userAuth = UserAuth::where('identifier' , $request->identifier)
->whereIn('identity_type', ['username', 'phone', 'email'])
->first();
if (isset($userAuth) && Hash::check($request->credential, $userAuth->credential)) {
// 查詢用戶表
$user = User::find($userAuth->user_id);
if ($user->status == 0) {
return back()->with('errors', '用戶已經被禁用');
}
if ($user->is_admin == 0) {
return back()->with('errors', '普通用戶禁止登陸后臺');
}
Session::put('user', $user);
return redirect()->route('admin.index');
} else {
return back()->with('errors', '管理員密碼錯誤');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
publicfunctionlogin(Request$request)
{
// 已經登錄則直接跳轉
if(Session::has('user')){
returnredirect()->route('admin.index');
}
if($request->method()==='GET'){
returnview('admin.user.login');
}
// 驗證表單
$validator=Validator::make($request->all(),[
'identifier'=>['required','exists:user_auths'],
'credential'=>['required','between:6,16'],
],[
'identifier.exists'=>'用戶不存在',
'identifier.required'=>'用戶名為必填項',
'credential.required'=>'密碼為必填項',
'credential.between'=>'密碼長度必須是6-16',
]);
if($validator->fails()){
returnback()->withErrors($validator);
}
// 查詢授權記錄 - 查詢3種登錄方式的授權記錄
$userAuth=UserAuth::where('identifier',$request->identifier)
->whereIn('identity_type',['username','phone','email'])
->first();
if(isset($userAuth)&&Hash::check($request->credential,$userAuth->credential)){
// 查詢用戶表
$user=User::find($userAuth->user_id);
if($user->status==0){
returnback()->with('errors','用戶已經被禁用');
}
if($user->is_admin==0){
returnback()->with('errors','普通用戶禁止登陸后臺');
}
Session::put('user',$user);
returnredirect()->route('admin.index');
}else{
returnback()->with('errors','管理員密碼錯誤');
}
}
實現修改密碼,站內登錄、郵箱登錄、手機登錄同步修改。
public function modifyPassword(Request $request)
{
if ($request->method() === 'GET') {
return view('admin.user.modify');
}
// 驗證輸入字段
$validator = Validator::make($request->all(), [
'credential' => 'required|between:6,20|confirmed',
], [
'credential.required' => '新密碼不能為空!',
'credential.between' => '新密碼必須在6-20位之間',
'credential.confirmed' => '新密碼和確認密碼不一致',
]);
if ($validator->fails()) {
return back()->withErrors($validator);
}
// 判斷當前Session里的用戶是否還有效
$user = Session::get('user');
if (! isset($user)) {
return redirect()->route('admin.login')->with('errors', '登錄超時');
}
// 查詢用戶權限表,修改密碼
$userAuths = UserAuth::where('user_id', $user->id)
->whereIn('identity_type', ['username', 'email', 'phone'])
->get();
if (count($userAuths) && Hash::check($request->credential_o, $userAuths[0]->credential)) {
UserAuth::where('user_id', $user->id)
->whereIn('identity_type', ['username', 'email', 'phone'])
->update(['credential' => bcrypt($request->credential)]);
return back()->with('errors', '修改密碼成功!');
}
return back()->with('errors', '原密碼錯誤!');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
publicfunctionmodifyPassword(Request$request)
{
if($request->method()==='GET'){
returnview('admin.user.modify');
}
// 驗證輸入字段
$validator=Validator::make($request->all(),[
'credential'=>'required|between:6,20|confirmed',
],[
'credential.required'=>'新密碼不能為空!',
'credential.between'=>'新密碼必須在6-20位之間',
'credential.confirmed'=>'新密碼和確認密碼不一致',
]);
if($validator->fails()){
returnback()->withErrors($validator);
}
// 判斷當前Session里的用戶是否還有效
$user=Session::get('user');
if(!isset($user)){
returnredirect()->route('admin.login')->with('errors','登錄超時');
}
// 查詢用戶權限表,修改密碼
$userAuths=UserAuth::where('user_id',$user->id)
->whereIn('identity_type',['username','email','phone'])
->get();
if(count($userAuths)&&Hash::check($request->credential_o,$userAuths[0]->credential)){
UserAuth::where('user_id',$user->id)
->whereIn('identity_type',['username','email','phone'])
->update(['credential'=>bcrypt($request->credential)]);
returnback()->with('errors','修改密碼成功!');
}
returnback()->with('errors','原密碼錯誤!');
}
例子中路由相關代碼直接無視!如果后期需要新增或刪除登錄方式,只需要新增或刪除 user_auths 表中的記錄。如果是判斷郵箱、手機是否已經驗證,也只是操作 user_auths 表中的?verified 字段即可。