Blogs
Đăng nhập không cần password, tại sao không?
Bước xác minh, trong nhiều năm qua, đã có những bước tiến mạnh mẽ. Chúng ta đã chứng kiến sự thay đổi từ tổ hợp email-password sang xác minh mạng xã hội, và cuối cùng là xác minh lược bỏ password (mà thực ra lại giống kiểu xác minh “chỉ email” hơn). Trong trường hợp login lược bỏ password, ứng dụng sẽ giả định bạn nhận login link từ inbox nếu email được cung cấp đúng là của bạn.
Quy trình thường thấy của một hệ thống login không password diễn ra như sau:
- Người dùng truy cập vào login page
- Nhập địa chỉ email và xác nhận
- Một đường link được gửi đến email
- Khi click vào link, họ được chuyển hướng trở lại ứng dụng và đăng nhập
- Đường link bị vô hiệu hóa
Đây là một cách xác minh tiện lợi nếu bạn không tài nào nhớ được password cho ứng dụng đó, nhưng bạn lại nhớ email khai báo lúc đăng ký. Một điểm thú vị là thận chí cả Slack cũng dùng đến kỹ thuật này.
Trong bài viết này, chúng ta sẽ tìm cách tích hợp hệ thống trên vào ứng dụng Laravel. Đoạn code hoàn chỉnh có thể được tìm thấy ở đây.
Tạo ứng dụng
Hãy bắt đầu bằng cách tạo ứng dụng Laravel mới. Tôi sẽ sử dụng Laravel 5.2 trong bài viết này:
1
2
3
|
composer create - project laravel / laravel passwordless - laravel 5.2. *
|
Nếu bạn đã có sẵn một Laravel project với users và passwords, đừng lo lắng – chúng ta sẽ không động đến lớp xác minh đã có, mà tạo thêm một lớp mới chồng lên. User vẫn sẽ có tùy chọn đăng nhập thông qua password.
Database Setup
Kế tiếp, chúng ta sẽ phải set up MySQL database trước khi chạy bất cứ migration nào.
Mở file .env
trong thư mục root và nhập vào hostname, username, và database name:
1
2
3
4
5
6
7
8
9
|
[ . . . ]
DB_CONNECTION = mysql
DB_HOST = localhost
DB_DATABASE = passwordless - app
DB_USERNAME = username
DB_PASSWORD =
[ . . . ]
|
Nếu bạn đang dùng Homestead Improved box, tổ hợp database/username/password sẽ là homestead
, homestead
, secret
.
Scaffolding Auth
Laravel 5.2 có một tính năng cực kỳ hay, đó là khả năng thêm lớp xác minh đã tạo sẵn với một dòng lệnh duy nhất. Hãy làm thử nhé
1
2
3
|
php artisan make : auth
|
Lệnh này sẽ giúp ta lên khung cho mọi thứ cần có ở bước xác minh như Views, Controllers, và Routes.
Migrations
Nếu nhìn vào database/migration
, ta sẽ nhận thấy rằng ứng dụng Laravel được tạo đi kèm với migration để tạo user
table và password_resets
table.
Chúng ta sẽ không thay đổi gì cả vì ta vẫn muốn ứng dụng có bước xác minh thông thường.
Để tạo table, chạy:
1
2
3
|
php artisan migrate
|
Giờ đây ta đã có thể đưa ứng dụng vào hoạt động, và có thể đăng ký/đăng nhập bằng link trong nav.
Thay đổi Login Link
Kế tiếp, chúng ta muốn thay đổi đường link login để chuyển hướng người dùng sang custom login view, tại đây người dùng sẽ cung cấp địa chỉ email mà không phải nhập password.
Đi đến resources/views/layouts/app.blade.php
. Taị đây chúng ta sẽ tìm nav partial. Thay đổi dòng có login link (ngay dưới phần điều khiện để kiểm tra xem người dùng đã log out chưa) với:
1
2
3
4
5
6
7
|
[ . . . ]
@ if ( Auth :: guest ( ) )
< li > < a href = "{{ url('/login/magiclink') }}" > Login < / a > < / li >
< li > < a href = "{{ url('/register') }}" > Register < / a > < / li >
[ . . . ]
|
Khi người dùng cố gắng truy cập một route được bảo vệ mà chưa login, họ cần được đưa đến custom login view mới thay vì view thông thường. Hành vi này được xác định trong authenticate middleware. Nên chúng ta sẽ thay đổi chính chỗ đó:
app/Http/Middleware/Authenticate.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class Authenticate
{
[ . . . ]
public function handle ( $ request , Closure $ next , $ guard = null )
{
if ( Auth :: guard ( $ guard ) -> guest ( ) ) {
if ( $ request -> ajax ( ) || $ request -> wantsJson ( ) ) {
return response ( 'Unauthorized.' , 401 ) ;
} else {
return redirect ( ) -> guest ( 'login/magiclink' ) ;
}
}
return $ next ( $ request ) ;
}
[ . . . ]
|
Hãy để ý, trong else block
chúng ta đã chuyển địa điểm chuyển hướng đến login/magiclink
thay vì login
thông thường.
Tạo Magic Login Controller, View, và Routes
Bước tiếp theo, chúng ta sẽ tạo MagicLoginController
bên trong thư mục Auth
:
1
2
3
|
php artisan make : controller Auth \ \ MagicLoginController
|
Rồi sau đó thực hiện định tuyết để hiển thị custom login page của chúng ta:
app/Http/routes.php
1
2
3
4
|
[ . . . ]
Route :: get ( '/login/magiclink' , 'Auth\MagicLoginController@show' ) ;
|
Hãy cập nhật MagicLoginController
để kèm theo show action:
app/Http/Controllers/Auth/MagicLoginController.php
1
2
3
4
5
6
7
8
9
10
11
|
class MagicLoginController extends Controller
{
[ . . . ]
public function show ( )
{
return view ( 'auth.magic.login' ) ;
}
[ . . . ]
}
|
Để có login view, chúng ta sẽ mượn tạm login view thường và bỏ trường password đi. Chúng ta cũng sẽ thay đổi post URL của form đến \login\magiclink
.
Hãy tạo một thư mục magic
trong views/auth
để giữ view mới này:
1
2
3
4
|
< span class = "token function" > mkdir < / span > resources / views / auth / magic
< span class = "token function" > touch < / span > resources / views / auth / magic / login . blade . php
|
Và cập nhật view vừa tạo thành:
resources/views/auth/magic/login.blade.php
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
46
47
48
49
50
51
52
53
54
|
@ extends ( 'layouts.app' )
@ section ( 'content' )
< div class = "container" >
< div class = "row" >
< div class = "col-md-8 col-md-offset-2" >
< div class = "panel panel-default" >
< div class = "panel-heading" > Login < / div >
< div class = "panel-body" >
< form class = "form-horizontal" role = "form" method = "POST" action = "{{ url('/login/magiclink') }}" >
{ { csrf_field ( ) } }
< div class = "form-group{{ $errors->has('email') ? ' has-error' : '' }}" >
< label for = "email" class = "col-md-4 control-label" > E - Mail Address < / label >
< div class = "col-md-6" >
< input id = "email" type = "email" class = "form-control" name = "email" value = "{{ old('email') }}" required autofocus >
@ if ( $ errors -> has ( 'email' ) )
< span class = "help-block" >
< strong > { { $ errors -> first ( 'email' ) } } < / strong >
< / span >
@ endif
< / div >
< / div >
< div class = "form-group" >
< div class = "col-md-6 col-md-offset-4" >
< div class = "checkbox" >
< label >
< input type = "checkbox" name = "remember" > Remember Me
< / label >
< / div >
< / div >
< / div >
< div class = "form-group" >
< div class = "col-md-8 col-md-offset-4" >
< button type = "submit" class = "btn btn-primary" >
Send magic link
< / button >
< a href = "{{ url('/login') }}" class = "btn btn-link" > Login with password instead < / a >
< / div >
< / div >
< / form >
< / div >
< / div >
< / div >
< / div >
< / div >
@ endsection
|
Chúng ta sẽ vẫn để lại tùy chọn login với password vì người dùng vẫn có thể thích dùng password login hơn. Vậy nếu người dùng click login từ nav, họ sẽ được đưa đến login view trông như sau:
Tạo Tokens và liên kết chúng với
Bước tiếp theo của chúng ta là tạo token và liên kết chúng với người dùng. Điều này xảy ra khi ai đó nhập email để đăng nhập.
Hãy bắt đầu với bước tạo route để xử lý posting action của form đăng nhập.
app/Http/routes.php
1
2
3
4
|
[ . . . ]
Route :: post ( '/login/magiclink' , 'Auth\MagicLoginController@sendToken' ) ;
|
Sau đó, chúng ta sẽ thêm controller method tên sendToken
trong MagicLoginController
. Method này sẽ chứng thực địa chỉ email, liên kết token với user, gửi đi email đăng nhập và thông báo người dùng check email của mình:
app/Http/Controllers/Auth/MagicLoginController.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class MagicLoginController extends Controller
{
[ . . . ]
/**
* Validate that the email has a valid format and exists in the users table
* in the email column
*/
public function sendToken ( Request $ request )
{
$ this -> validate ( $ request , [
'email' = > 'required|email|max:255|exists:users,email'
] ) ;
//will add methods to send off a login email and a flash message later
}
[ . . . ]
}
|
Sau khi đã có địa chỉ email đạt chuẩn, chúng ta có thể gửi email đăng nhập đến người dùng. Nhưng trước khi email được gửi đi, chúng ta phải tạo trước token cho người dùng đang tìm cách đăng nhập này. Tôi không muốn phải để tất cả method trong MagicLoginController
nên ta sẽ tạo model user-token
dể xử lý chỉ một phần các method này.
1
2
3
4
|
php artisan make : model UserToken - m
|
Lệnh này sẽ giúp chúng ta có cả model và migration. Chúng ta vẫn phải tinh chỉnh migration một chút và thêm user_id
và cột token
. Mở file migration vừa tạo và thay đổi method up
thành thế này:
database/migrations/{timestamp}_create_user_tokens_table.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[ . . . ]
public function up ( )
{
Schema :: create ( 'user_tokens' , function ( Blueprint $ table ) {
$ table -> increments ( 'id' ) ;
$ table -> integer ( 'user_id' ) ;
$ table -> string ( 'token' ) ;
$ table -> timestamps ( ) ;
} ) ;
}
[ . . . ]
|
Sau đó chạy lệnh migrate
Artisan:
1
2
3
4
|
php artisan migrate
|
Trong model UserToken
, chúng ta cần thêm user_id
và token
dưới dạng thông số gán được (assignable attributes). Chúng ta cũng nên xác định mối quan hệ giữa model này với model User
và ngược lại:
App/UserToken.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
[ . . . ]
class UserToken extends Model
{
protected $ fillable = [ 'user_id' , 'token' ] ;
/**
* A token belongs to a registered user.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user ( )
{
return $ this -> belongsTo ( User :: class ) ;
}
}
|
Sau đó trong App/User.php
xác định rằng một User
chỉ có thể có một token liên kết với chúng:
App/User.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class User extends Model
{
[ . . . ]
/**
* A user has only one token.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function token ( )
{
return $ this -> hasOne ( UserToken :: class ) ;
}
}
|
Đến đây, hãy tạo token. Trước hết, chúng ta cần phải lấy user object thông qua email trước khi tạo token. Tạo method trong User
model với tên getUserByEmail
để xử lý tính năng này:
App/User.php
1
2
3
4
5
6
7
8
9
10
11
12
|
class User extends Model
{
[ . . . ]
protected static function getUserByEmail ( $ value )
{
$ user = self :: where ( 'email' , $ value ) -> first ( ) ;
return $ user ;
}
[ . . . ]
}
|
Chúng ta phải thêm namespace cho các class User
và UserToken
vào MagicLoginController
của chúng ta để có thể call các method trong những class này từ controller.
app/Http/Controllers/Auth/MagicLoginController.php
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
|
[ . . . ]
use App \ User ;
use App \ UserToken ;
[ . . . ]
class MagicLoginController extends Controller
{
[ . . . ]
public function sendToken ( Request $ request )
{
//after validation
[ . . . ]
$ user = User :: getUserByEmail ( $ request -> get ( 'email' ) ) ;
if ( ! user ) {
return redirect ( '/login/magiclink' ) -> with ( 'error' , 'User not foud. PLease sign up' ) ;
}
UserToken :: create ( [
'user_id' = > $ user -> id ,
'token' = > str_random ( 50 )
] ) ;
}
[ . . . ]
}
|
Trong khối code trên, chúng ta đang truy xuất user object dựa trên email được cung cấp. Trước khi đến bước này, các bạn cần nhớ là ta phải chứng thực sự tồn tại của địa chỉ email được cung cấp trong user
table. Nhưng nếu có ai đó vượt qua được bước chứng thực và cung cấp email chưa có trong kho lưu trữ của chúng ta, ta sẽ báo họ phải đăng ký trước.
Khi đã có user object, ta sẽ tạo token cho họ.
Gửi Token qua email
Chúng ta đến đây đã có thể email Token vừa tạo đến người dùng dưới dạng URL. Trước hết, chúng ta sẽ phải yêu cầu Facade Mail
trong model để hỗ trợ chức năng gửi email.
Tuy nhiên, trong phạm vi bài viết, chúng ta sẽ không gửi bất cứ email thật nào cả, mà chỉ để xác nhận là ứng dụng có thể gửi được email trong logs thôi. Để thực hiện, hãy tìm đến file .env
của bạn, dưới phần mail chỉnh thành MAIL_DRIVER=log
. Hơn nữa, chúng ta sẽ không tạo email view; chỉ tạo một email thuần từ class UserToken
.
Hãy tạo thêm một method nữa trong model UserToken
mang tên sendEmail
để xử lý chức năng này. URL tổ hợp của token
, email address
và giá trị remember me
sẽ được tạo trong method này.
app/UserToken.php
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
|
[ . . . ]
use Illuminate \ Support \ Facades \ Mail ;
[ . . . ]
class UserToken extends Model
{
[ . . . ]
public static function sendMail ( $ request )
{
//grab user by the submitted email
$ user = User :: getUserByEmail ( $ request -> get ( 'email' ) ) ;
if ( ! $ user ) {
return redirect ( '/login/magiclink' ) -> with ( 'error' , 'User not foud. PLease sign up' ) ;
}
$ url = url ( '/login/magiclink/' . $ user -> token -> token . '?' . http_build_query ( [
'remember' = > $ request -> get ( 'remember' ) ,
'email' = > $ request -> get ( 'email' ) ,
] ) ) ;
Mail :: raw (
"<a href='{$url}'>{$url}</a>" ,
function ( $ message ) use ( $ user ) {
$ message -> to ( $ user -> email )
-> subject ( 'Click the magic link to login' ) ;
}
) ;
}
[ . . . ]
}
|
Khi tạo URL, chúng ta sẽ dùng hàm http_build_query
của PHP để giúp một query từ array options được pass. Trong trường hợp của chúng ta là email, và giá trị remember me
.
Đã đến lúc cập nhật MagicLoginController
và call method sendEmail
:
app/Http/Controllers/Auth/MagicLoginController.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class MagicLoginController extends Controller
{
[ . . . ]
public function sendToken ( Request $ request )
{
$ this -> validate ( $ request , [
'email' = > 'required|email|max:255|exists:users,email'
] ) ;
UserToken :: storeToken ( $ request ) ;
UserToken :: sendMail ( $ request ) ;
return back ( ) -> with ( 'success' , 'We\'ve sent you a magic link! The link expires in 5 minutes' ) ;
}
[ . . . ]
}
|
Chúng ta cũng sẽ tích hợp một số trình tin nhắn đơn giản để gửi noti. Trong resources/views/layouts/app.blade.php
, hãy thêm đoạn code này vào hay trên content
của bạn vì flash message sẽ hiện trên bất kỳ nội dung nào khác:
resources/views/layouts/app.blade.php
1
2
3
4
5
6
7
8
9
10
11
12
|
[ . . . ]
< div class = "container" >
< div class = "row" >
< div class = "col-md-8 col-md-offset-2" >
@ include ( 'layouts.partials._notifications' )
< / div >
< / div >
< / div >
@ yield ( 'content' )
[ . . . ]
|
Sau đó tạo notifications partial:
resources/views/layouts/partials/_notifications.blade.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@ if ( session ( 'success' ) )
< div class = "alert alert-success" >
{ { session ( 'success' ) } }
< / div >
@ endif
@ if ( session ( 'error' ) )
< div class = "alert alert-danger" >
{ { session ( 'error' ) } }
< / div >
@ endif
|
Sau đó tạo notifications partial:
resources/views/layouts/partials/_notifications.blade.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@ if ( session ( 'success' ) )
< div class = "alert alert-success" >
{ { session ( 'success' ) } }
< / div >
@ endif
@ if ( session ( 'error' ) )
< div class = "alert alert-danger" >
{ { session ( 'error' ) } }
< / div >
@ endif
|
Trong partial, chúng ta đã dùng helper session
để giúp chúng với những màu noti khác nhay dựa trên các tình trạng session như success
hoặc error
.
Tại thời điểm này, chúng ta đã có thể gửi email. Bạn có thể thử trước bằng cách đăng nhập với một địa chỉ email hợp lệ, sau đó tìm đến file laravel.log
. Chúng ta sẽ có thể thấy email có chứa URL ở cuối file log.
Kế tiếp, chúng ta muốn chứng thực token là đăng nhập người dùng. Chúng ta cũng không muốn những trường hợp mà token đã được gửi 3 ngày trước vẫn dùng được để đăng nhập.
Chứng thực Token Validation và Authentication
Giờ thì chúng ta đã có URL, hãy tạo route và controller action để xử lý chuỗi sự kiện xảy ra khi người dùng click vào URL từ email:
app/Http/routes.php
1
2
3
4
|
[ . . . ]
Route :: get ( '/login/magiclink/{token}' , ' Auth \ MagicLoginController @ auth
|
Hãy tạo action authenticate
trong MagicLoginController
. Chúng ta sẽ xác minh người dùng ngay trong method này. Chúng ta sẽ kéo token vào method authenticate
thông qua Route Model Binding. Chúng ta sau đó sẽ grab người dùng từ token. Chú ý rằng chúng ta phải triệu tập Auth facade
trong controller để có thể dùng method Auth
:
app/Http/Controllers/Auth/MagicLoginController.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[ . . . ]
use Auth ;
[ . . . ]
class MagicLoginController extends Controller
{
[ . . . ]
public function authenticate ( Request $ request , UserToken $ token )
{
Auth :: login ( $ token -> user , $ request -> remember ) ;
$ token -> delete ( ) ;
return redirect ( 'home' ) ;
}
[ . . . ]
}
|
Sau đó, trong UserToken class
, đặt route key name chúng ta muốn. Trong trường hợp này, chính là token:
App/UserToken.php
1
2
3
4
5
6
7
8
|
[ . . . ]
public function getRouteKeyName ( )
{
return 'token' ;
}
[ . . . ]
|
Và xong rồi. Người dùng có thể đăng nhập được. Lưu ý, sau khi đăng nhập người dùng, chúng ta phải xóa token vì ta không muốn quá tải table user_tokens
với các token đã qua sử dụng.
Bước tiếp theo là kiểm tra độ xác thực của token. Với ứng dụng này, chúng ta sẽ thiết đặt magic link hết hạn sau 5 phút. Ta sẽ cần đến thư viện Carbon để theo dõi thời gian giữa lúc tạo token và thời gian hiện tại.
Trong model UserToken
, chúng ta sẽ tạo hai method: isExpired
và belongstoEmail
để kiểm tra độ xác thực của token. Chú ý, bước chứng thực belongsToEmail
chỉ là bước dự phòng để đảm bảo token thực sự thuộc về địa chỉ email đó:
App/UserToken.php
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
|
[ . . . ]
use Carbon \ Carbon ;
[ . . . ]
class UserToken extends Model
{
[ . . . ]
//Make sure that 5 minutes have not elapsed since the token was created
public function isExpired ( )
{
return $ this -> created_at -> diffInMinutes ( Carbon :: now ( ) ) > 5 ;
}
//Make sure the token indeed belongs to the user with that email address
public function belongsToUser ( $ email )
{
$ user = User :: getUserByEmail ( $ email ) ;
if ( ! $ user || $ user -> token == null ) {
//if no record was found or record found does not have a token
return false ;
}
//if record found has a token that matches what was sent in the email
return ( $ this -> token === $ user -> token -> token ) ;
}
[ . . . ]
}
|
Hãy call các method trên token instance trong method authenticate
trong MagicLoginController
:
app/Http/Controllers/Auth/MagicLoginController.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class MagicLoginController extends Controller
{
[ . . . ]
public function authenticate ( Request $ request , UserToken $ token )
{
if ( $ token -> isExpired ( ) ) {
$ token -> delete ( ) ;
return redirect ( '/login/magiclink' ) -> with ( 'error' , 'That magic link has expired.' ) ;
}
if ( ! $ token -> belongsToUser ( $ request -> email ) ) {
$ token -> delete ( ) ;
return redirect ( '/login/magiclink' ) -> with ( 'error' , 'Invalid magic link.' ) ;
}
Auth :: login ( $ token -> user , $ request -> get ( 'remember' ) ) ;
$ token -> delete ( ) ;
return redirect ( 'home' ) ;
}
[ . . . ]
}
|
Lời kết
Như vậy, chúng ta đã thành công với lớp đăng nhập không password bên cạnh cách xác minh truyền thống. Nhiều người cho rằng cách này sẽ mất nhiều thời gian hơn cách đăng nhập bằng password thông thường, nhưng dùng password manager liệu có nhanh hơn chăng?
Tuy vậy, hệ thống không-password không phải ở đâu cũng hoạt động được, nếu bạn có session timeout periods ngắn hoặc đòi hỏi người dùng phải đăng nhập thường xuyên, cách này tỏ ra khá dùng dằng. Cũng thật may, có rất ít trang đi theo hướng này.
- Làm app hot thế nào để không… phá sản?
- Khắc phục iPhone bị lỗi notification
- Web developer, HTTP/2 ảnh hưởng tới công việc của bạn như thế nào?
- Kỹ thuật quay phim trên smartphone bạn nên biết
- Lập trình viên đừng ở trong bóng tối?
- Rút tiền từ ATM bằng điện thoại thông minh
- Cách mạng ngành y tế bằng công nghệ từ xa và di động
- Quản lý môi trương phát triển web bằng Vagrant + Cheft
- 12 tính năng 'cực độc' trên smartphone ít người biết
- Ai là bạn thân trong lập trình của bạn?
- Đổi DNS trên Blackberry
- Xuất hiện các tay chơi mới trong lĩnh vực truyền hình
DVMS chuyên:
- Tư vấn, xây dựng, chuyển giao công nghệ Blockchain, mạng xã hội,...
- Tư vấn ứng dụng cho smartphone và máy tính bảng, tư vấn ứng dụng vận tải thông minh, thực tế ảo, game mobile,...
- Tư vấn các hệ thống theo mô hình kinh tế chia sẻ như Uber, Grab, ứng dụng giúp việc,...
- Xây dựng các giải pháp quản lý vận tải, quản lý xe công vụ, quản lý xe doanh nghiệp, phần mềm và ứng dụng logistics, kho vận, vé xe điện tử,...
- Tư vấn và xây dựng mạng xã hội, tư vấn giải pháp CNTT cho doanh nghiệp, startup,...
Vì sao chọn DVMS?
- DVMS nắm vững nhiều công nghệ phần mềm, mạng và viễn thông. Như Payment gateway, SMS gateway, GIS, VOIP, iOS, Android, Blackberry, Windows Phone, cloud computing,…
- DVMS có kinh nghiệm triển khai các hệ thống trên các nền tảng điện toán đám mây nổi tiếng như Google, Amazon, Microsoft,…
- DVMS có kinh nghiệm thực tế tư vấn, xây dựng, triển khai, chuyển giao, gia công các giải pháp phần mềm cho khách hàng Việt Nam, USA, Singapore, Germany, France, các tập đoàn của nước ngoài tại Việt Nam,…
Quý khách xem Hồ sơ năng lực của DVMS tại đây >>
Quý khách gửi yêu cầu tư vấn và báo giá tại đây >>