ตรรกะการเข้าสู่ระบบ Angular Frontend
นี่คือการติดตามคำถามนี้Node.js ตรรกะแบ็กเอนด์เข้าสู่ระบบ ฉันเขียนตรรกะส่วนหน้าเชิงมุมสำหรับล็อกอินต่อไปนี้สำหรับแบ็กเอนด์ Node.js ของฉัน (ดูคำถามก่อนหน้านี้ด้านบน) การรักษาความปลอดภัยประสิทธิภาพการสร้าง async / sync การบันทึกเป็นสิ่งที่ดีหรือไม่? ความปลอดภัยเป็นปัญหาหลักของฉัน ในรูปแบบที่สวยกว่าคำถามคือ:
- การรักษาความปลอดภัย : เว็บไซต์ของฉันปลอดภัยในลักษณะรูปร่างหรือรูปแบบใด ๆ หรือไม่? ผมสงสัยว่าถ้าผมสามารถใช้มาตรการรักษาความปลอดภัยอื่น ๆ
Angularกว่าคนที่มีการสร้างขึ้นในวิธีการที่ให้บริการโดย การส่งรหัสผ่านเป็นข้อความธรรมดาไม่ใช่ปัญหาด้านความปลอดภัยใช่หรือไม่? แล้ว XSS และปัญหาที่คล้ายกันล่ะ? การเข้าสู่ระบบของฉันไม่สามารถหลีกเลี่ยงได้หรือไม่? นั่นจะเป็นความผิดพลาดอย่างร้ายแรง - ประสิทธิภาพ : การตรวจสอบชื่อผู้ใช้และรหัสผ่านมีประสิทธิภาพอย่างไร มีวิธีไหนที่ดีกว่านี้ไหม?
- BUILDING : ฉันสามารถโหลดเว็บไซต์ได้อย่างไร?
- ASYNC / SYNC : ฉันรู้ว่าฉัน preform
asyncและsyncโทรในเวลาเดียวกัน มีปัญหาอะไรไหม? - การบันทึก : ฉันบันทึกการเชื่อมต่อทั้งหมดไปยังเซิร์ฟเวอร์และความพยายามในการเข้าสู่ระบบทั้งหมด นี่เป็นแนวทางปฏิบัติที่ดีหรือฉันทำมากเกินไปในสิ่งที่ควรจะทำให้สำเร็จ
- MISC : มีข้อผิดพลาดในการเล่นระหว่างแบ็กเอนด์และส่วนหน้าหรือไม่? หากฉันลืมประเด็นสำคัญอื่น ๆ เกี่ยวกับรหัสฉันจะดีใจถ้าคุณพูดถึงพวกเขาด้วย (ที่มา: Login Server with Node.js )
รหัสของฉัน :
authentication.service.ts:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import { User } from '../models/user.model';
import { Router } from '@angular/router';
import { GlobalDataService } from './global-data.service';
@Injectable({ providedIn: 'root' })
export class AuthenticationService {
constructor(private http: HttpClient,
private router: Router, public DataService: GlobalDataService) {
this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
this.currentUser = this.currentUserSubject.asObservable();
this.LoggedIn = true;
}
public LoggedIn = true;
public get currentUserValue(): User {
return this.currentUserSubject.value;
}
private currentUserSubject: BehaviorSubject<User>;
public currentUser: Observable<User>;
getRedirectUrl() {
throw new Error('Method not implemented.');
}
isUserLoggedIn() {
throw new Error('Method not implemented.');
}
login(email: string, password: string) {
return this.http.post<any>(`${environment.apiUrl}/api/login`, { email, password }, {withCredentials: true})
.pipe(map(user => {
// login successful if there's a jwt token in the response
if (user && user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
// https://dev.to/rdegges/please-stop-using-local-storage-1i04
localStorage.setItem('currentUserToken', JSON.stringify(user));
this.currentUserSubject.next(user);
}
// set firstname & email of loggedin user
this.DataService.loggedinfirstname = user['firstname'];
this.DataService.loggedinemail = user['eMail'];
this.redirtoDashboard();
this.Toolbar();
this.DataService.prefillSenderData();
return user;
}));
}
redirtoDashboard() {
this.router.navigate(['order']);
}
Toolbar() {
this.LoggedIn = !this.LoggedIn;
}
}
login.component.ts:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AuthenticationService } from '../services/authentication.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
returnUrl: string;
loginForm: FormGroup;
submitted = false;
error = '';
loading = false;
public errorMsg = 'Please login to continue.';
public redirected: boolean;
public utm_source: string;
constructor(private router: Router, private formBuilder: FormBuilder,
private authenticationService: AuthenticationService, private activatedRoute: ActivatedRoute) {
if (this.authenticationService.currentUserValue) {
this.router.navigate(['order']);
}
this.activatedRoute.queryParams.subscribe(params => {
const param = params['utm_source'];
if (param === 'order' || param === 'work-document' || param === 'profile') {
this.redirected = true;
this.utm_source = param;
} else {
this.redirected = false;
}
});
}
ngOnInit(): void {
this.loginForm = this.formBuilder.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
// convenience getter for easy access to form fields
get f() { return this.loginForm.controls; }
onSubmit(loginsubmit) {
this.submitted = true;
// stop here if form is invalid
if (this.loginForm.invalid) {
return console.log('LoginForm Invalid');
}
this.loading = true;
this.authenticationService.login(this.f.email.value, this.f.password.value)
.pipe(first())
.subscribe(
data => {
if (this.redirected) {
this.router.navigate([this.utm_source]);
} else {
this.router.navigate(['order']);
}
},
error => {
console.log('Login->authservice->err: ', error);
this.error = error;
this.loading = false;
});
}
}
login.component.html:
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Login</h5>
<br>
<form [formGroup]="loginForm" class="form-signin" (ngSubmit)="onSubmit(this.loginForm.value)">
<div class="form-label-group">
<input #userName formControlName="email" type="text" id="inputUser" class="form-control" placeholder="E-Mail" required autofocus [ngClass]="{ 'is-invalid': submitted && f.email.errors }">
<div *ngIf="submitted && f['email'].errors" class="invalid-feedback">
<div *ngIf="f['email'].errors.required">E-Mail is required</div>
</div>
</div>
<br>
<div class="form-label-group">
<input #password type="password" formControlName="password" id="inputPassword" class="form-control" placeholder="Password" required [ngClass]="{ 'is-invalid': submitted && f.password.errors }">
<div *ngIf="submitted && f['password'].errors" class="invalid-feedback">
<div *ngIf="f['password'].errors.required">Password is required</div>
</div>
</div>
<br>
<div *ngIf="redirected">
<mat-error>
<p class="alert alert-danger">
{{errorMsg}}
</p>
</mat-error>
</div>
<button [disabled]="!loginForm.valid" class="btn btn-dark btn-block" id="loginSubmit" type="submit">Login</button>
<div class="forgot-password-link">
<a routerLink="/forgot-password">Forgot password</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
คำตอบ
ขออภัยฉันไม่ได้ดูโค้ดแบ็กเอนด์ของคุณดังนั้นนี่เป็นเพียงครึ่งรีวิว
สำหรับคำถามของคุณ:
ความปลอดภัย
ตามที่ Sam วิเคราะห์แล้วไม่มีปัญหาที่ชัดเจนในส่วนของส่วนหน้า (ตราบใดที่คุณใช้ HTTPS) ฉันคาดว่ารหัสผ่านถูกแฮช (ด้วยเกลือ) ในแบ็กเอนด์และมีเพียงแฮชเท่านั้นที่ถูกเก็บไว้ในฐานข้อมูลดังนั้นจึงไม่มีใครสามารถดึงรหัสผ่านจริงออกจากฐานข้อมูลได้ประสิทธิภาพ
ใช่ผู้ตรวจสอบความถูกต้องเป็นวิธีที่จะไปในเชิงมุม ในทางเทคนิคจะมีประสิทธิภาพมากกว่าเล็กน้อยในการใช้การตรวจสอบความถูกต้อง HTML แต่แฟร็กทัลของมิลลิวินาทีนั้นไม่คุ้มค่าที่จะสูญเสียความยืดหยุ่นของ Validators ไปอาคาร
Async / Sync
ปัญหาในการเรียกเมธอด async โดยไม่จัดการกับผลลัพธ์คือคุณจะไม่รู้ว่ามีบางอย่างไม่ได้ผลตามที่คาดไว้ ไม่เป็นไรถ้าคุณรู้ว่าโค้ดที่คุณใช้นั้นได้รับการพัฒนาและดูแลโดยนักพัฒนาที่เก่งกาจซึ่งอยู่เหนือข้อผิดพลาด หากนักพัฒนาเป็นมนุษย์คุณควรคาดหวังไว้เสมอว่าอาจมีปัญหา และหากคุณเรียกใช้วิธีการและคุณรู้ว่าปัญหาใด ๆ ที่ไม่มีปัญหาสำหรับรหัสของคุณให้กำหนดรหัสของคุณให้ชัดเจนเพื่อให้นักพัฒนาต่อไปนี้ (เช่นคุณในอีก 3 เดือน) จะรู้เช่นกัน :-)การบันทึก
คำถามคือสิ่งที่คุณตั้งใจกับการบันทึก หากคุณต้องการตรวจสอบทุกสิ่งเพื่อรับความรู้เกี่ยวกับผู้ใช้โครงสร้างพื้นฐานของคุณ ฯลฯ ยิ่งไปกว่านั้นก็ดี เอาล่ะฉันจะใช้หนึ่งในกรอบที่มีอยู่เพื่อทำสิ่งนั้นให้ฉันและไม่สร้างวงล้อขึ้นมาใหม่
หากคุณสนใจ แต่สิ่งที่ไม่ดีฉันจะบันทึกเฉพาะสิ่งเหล่านั้น (เช่นการพยายามเข้าสู่ระบบล้มเหลว)
เช่นเคยไม่มี "ใช่" หรือ "ไม่" มันขึ้นอยู่กับความตั้งใจของคุณ
ตามคำชี้แจงconsole.logไม่ใช่ "การบันทึก" สำหรับฉันเพราะผู้ใช้เท่านั้นที่มองเห็นได้และสำหรับเขาก็ต่อเมื่อเขาเปิดคอนโซลไว้MISC
ดูสิ่งต่อไปนี้
Refactoring
ก่อนอื่นฉันต้องการ refactor โค้ดเล็กน้อยเพื่อให้อ่านง่าย จากประสบการณ์ของฉันมันช่วยให้มองเห็นข้อผิดพลาดได้ง่ายขึ้น คุณสามารถเพิกเฉยและข้ามไปยังส่วนที่น่าสนใจได้หากต้องการ
โดยทั่วไปฉันชอบที่จะใช้เสมอprivateและpublicจำกัด ให้มากที่สุด แสดงให้ผู้อ่านเห็นว่าฉันคิดเกี่ยวกับขอบเขตของวิธีการ / ตัวแปร และช่วยลดโอกาสในการใช้ผิดประเภท privateถ้าผมไม่แน่ใจว่าแล้วฉันเริ่มต้นด้วย
หากไม่มีการใช้งานจะเป็นสาธารณะโดยค่าเริ่มต้น และในฐานะผู้อ่านฉันไม่รู้ว่าผู้พัฒนาเลือกสิ่งนั้นโดยตั้งใจหรือลืมไป
AuthenticationService
LoggedInถูกตั้งค่าเป็นจริงในเวลานิยาม ( public LoggedIn = true) และอีกครั้งในตัวสร้าง โดยส่วนตัวแล้วฉันชอบกำหนดค่าเริ่มต้นในเวลาที่กำหนด นอกจากนี้ค่านี้ยังใช้เป็น "true หมายถึงไม่ได้ล็อกอิน" นั่นเป็นเรื่องที่น่ารำคาญดังนั้นฉันจะเปลี่ยนชื่อisLogedInและเริ่มต้นด้วย false
DataService เป็นสาธารณะ แต่ดูเหมือนจะไม่ถูกใช้นอกชั้นเรียน
ฉันไม่ชอบสตริงคงที่ในโค้ดของฉันดังนั้นฉันจึงแยกมันออกเป็นค่าคงที่ ชอบ
private loginUrl: string = `${environment.apiUrl}/api/login`;
ฉันชอบใช้ตัวดำเนินการ RxJs "พูด" mapวิธีในloginไม่เปลี่ยนกระแสดังนั้นผมจะใช้tapแทน นั่นทำให้เห็นได้ชัดว่ามีเพียงผลข้างเคียง `นอกจากนี้เรายังสามารถข้ามreturnบรรทัดได้
ฉันชอบรหัสเร่งความเร็ว ดังนั้นถ้าฉันสามารถแยกโค้ดบางบรรทัดออกเป็นวิธีการพูดได้ฉันกำลังทำเช่นนั้นเพราะฉันต้องอ่านชื่อวิธีการเท่านั้นไม่ใช่โค้ดทั้งหมดที่อยู่ข้างหลังและสามารถตัดสินใจได้ว่าฉันต้องการดำน้ำลึกลงไปหรือเพียงแค่ดำเนินการต่อ
ดังนั้นฉันจะเปลี่ยนรหัสในmapของloginวิธีบิต
นอกจากนี้ดูเหมือนว่าเมธอด "Toolbar" จะใช้เพื่อเปลี่ยนLogedInสถานะเพียงครั้งเดียวเท่านั้น เราจึงสามารถกำหนดค่าโดยไม่ต้องสลับ
นอกจากนี้ยังมีปัญหาที่นี่ (ดูในบทปัญหาในภายหลัง) ดังนั้นฉันจะย้ายการเรียกใช้ฟังก์ชันนั้นไปยังคำสั่ง if
โดยปกติการเปลี่ยนเส้นทางเป็นสิ่งสุดท้ายที่ควรเกิดขึ้นดังนั้นฉันจึงย้ายไปที่ส่วนท้ายของการแตะ
export class AuthenticationService {
public isLoggedIn = false;
public currentUser: Observable<User>
public get currentUserValue(): User {
return this.currentUserSubject.value;
}
private currentUserSubject: BehaviorSubject<User> = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
private loginUrl: string = `${environment.apiUrl}/api/login`;
constructor(private DataService: GlobalDataService,
private http: HttpClient,
private router: Router) {
this.currentUser = this.currentUserSubject.asObservable();
}
private getRedirectUrl() {
throw new Error('Method not implemented.');
}
private isUserLoggedIn() {
throw new Error('Method not implemented.');
}
public login(email: string, password: string):Observable<User> {
return this.http.post<User>(loginUrl, { email, password }, {withCredentials: true})
.pipe(
tap(user => {
// login successful if there's a jwt token in the response
if (this.isLoginSuccessful(user)) {
this.setLogedInUser(user);
this.isLoggedIn = true;
}
this.setDataServiceForUser(user);
this.toggleLoginStatus();
this.redirectToDashboard();
})
);
}
private isLoginSuccessful(user:User):boolean{
return user && user.token;
}
private redirectToDashboard():void {
this.router.navigate(['order']);
}
private setLogedInUser(user: User):void{
// store user details and jwt token in local storage to keep user logged in between page refreshes
// https://dev.to/rdegges/please-stop-using-local-storage-1i04
localStorage.setItem('currentUserToken', JSON.stringify(user));
this.currentUserSubject.next(user);
}
private setDataServiceForUser(user:User):void{
// set firstname & email of loggedin user
this.DataService.loggedinfirstname = user['firstname'];
this.DataService.loggedinemail = user['eMail'];
this.DataService.prefillSenderData();
}
}
เข้าสู่ระบบส่วนประกอบ
ในonSubmitคุณไม่จำเป็นต้องมีfirst(). ด้านหลังthis.authenticationService.loginคือคำขอ http และสิ่งเหล่านั้นจะสิ้นสุดโดยอัตโนมัติหลังจากผลลัพธ์แรก ด้วยเหตุผลเดียวกันคุณจึงไม่จำเป็นต้องยกเลิกการสมัครสมาชิกเหล่านั้น
ปัญหา
AuthenticationService
ในloginวิธีนี้ดูเหมือนว่าแม้ในกรณีของการเข้าสู่ระบบที่ไม่สำเร็จ (ไม่มีผู้ใช้หรือไม่มีข้อมูลโทเค็น) ก็ยังพยายามส่งข้อมูลไปDataServiceยังสิ่งต่างๆ ที่นี่ฉันจะแยกสิ่งเหล่านี้ออกจากกันอย่างชัดเจนว่าควรเกิดขึ้นหลังจากพยายามเข้าสู่ระบบและสิ่งที่อาจเกิดขึ้นหลังจากการเข้าสู่ระบบสำเร็จเท่านั้น
โดยเฉพาะอย่างยิ่งตอนนี้จะเปลี่ยนไปLogedInแม้ว่าการเข้าสู่ระบบจะไม่สำเร็จก็ตาม
เข้าสู่ระบบส่วนประกอบ
ปฏิบัติที่ดีที่สุด
แนวทางปฏิบัติที่ดีที่สุด (อย่างน้อยก็ในสายตาของฉัน :-))
ในรหัสข้อมูลผู้ใช้ใช้สำหรับสองสิ่ง สำหรับข้อมูลโดยละเอียดเกี่ยวกับผู้ใช้และประการที่สองเป็น "ผู้ใช้ล็อกอิน" โดยปริยาย ใน AuthentificationService การเชื่อมต่อนี้ถูกต้อง แต่ภายนอกฉันจะให้ข้อมูลผู้ใช้และข้อมูล "isLogedIn" เพิ่มเติม ด้วยวิธีนี้นักพัฒนาไม่จำเป็นต้อง "รู้" ว่าข้อมูลผู้ใช้บ่งบอกเป็นนัยว่าผู้ใช้เข้าสู่ระบบ
เป็นนิสัยที่ดีในการยกเลิกการสมัครเมื่อคุณออกจากองค์ประกอบ ดังนั้นฉันมักจะทำอะไรแบบนี้
private subscriptions: Subscription() = new Subscription();
...
this.subscriptions.add(
sourceA.subscribe(...)
)
this.subscriptions.add(
sourceB.subscribe(...)
)
ngOnDestroy(){
this.subscriptions.unsubcribe();
}
ด้วยวิธีนี้ทันทีที่คอมโพเนนต์ถูกทำลายการสมัครสมาชิกเหล่านั้นทั้งหมดจะถูกยกเลิกโดยอัตโนมัติ โปรดทราบว่าส่วนประกอบจะถูกทำลายก็ต่อเมื่อถูกลบออกจาก DOM อย่างสมบูรณ์ ถ้ามันซ่อนอยู่แสดงว่ามันยังมีชีวิตอยู่
ฉันหวังว่าหนึ่งหรือสองสิ่งจะเป็นประโยชน์สำหรับคุณ
ขอแสดงความนับถือ
คำนำ
ฉันใช้ AngularJS เมื่อสองสามปีก่อน แต่ไม่ได้เข้าสู่ Angular2 + ดังนั้นความรู้ของฉันเกี่ยวกับมันจึงบางเฉียบ อย่างไรก็ตามฉันมีความคุ้นเคยกับ Javascript และเฟรมเวิร์กต่างๆพอสมควร
การตอบคำถาม
ความปลอดภัย การส่งรหัสผ่านเป็นข้อความธรรมดาไม่ใช่ปัญหาด้านความปลอดภัยใช่หรือไม่?
ฉันพบโพสต์เกี่ยวกับคำถามนี้ในเว็บไซต์ SE หลายแห่ง ตัวอย่างเช่นฉันพบว่าสามารถส่งรหัสผ่านข้อความธรรมดาผ่าน HTTPS ได้หรือไม่ [ซ้ำ] หากต้องการอ้างอิงคำตอบที่ยอมรับโดย Buffalo5ix :
เป็นแนวทางปฏิบัติมาตรฐานในการส่งรหัสผ่าน "ข้อความธรรมดา" ผ่าน HTTPS ในที่สุดรหัสผ่านจะไม่ใช่ข้อความธรรมดาเนื่องจากการสื่อสารไคลเอนต์ - เซิร์ฟเวอร์จะถูกเข้ารหัสตาม TLS
คำถามนั้นถูกทำเครื่องหมายว่าซ้ำกับโพสต์อื่นสองโพสต์ซึ่งรวมถึงคำถามนี้ฉันเพิ่งส่งชื่อผู้ใช้และรหัสผ่านผ่าน https นี่โอเคไหม? . มีสองคำตอบและคำตอบที่สองโดย Steveเสนอตัวเลือก:
สิ่งที่คุณทำได้เพิ่มเติมอีกอย่างหนึ่งคือการใช้ใบรับรองไคลเอ็นต์ เซิร์ฟเวอร์สามารถรับประกันตัวเองได้ว่าไม่มี MitM โดยต้องมีใบรับรองไคลเอ็นต์ มิฉะนั้นเขาจะต้องไว้วางใจลูกค้าในการตรวจสอบอย่างถูกต้องว่าไม่มี MitM นี่เป็นมากกว่าบริการมากมายที่ควรเต็มใจที่จะไว้วางใจ
ฉันไม่เคยได้ยินว่ามีใครทำแบบนั้น แต่บางทีทำเสร็จแล้วและเราก็ไม่รู้เรื่องนี้
แม้จะมีคำถาม Stack Overflow เกี่ยวกับคำถามโดยคำตอบที่ยอมรับนั้นคล้ายคลึงกับคำตอบที่ยอมรับของคำถามแรก (จาก Security SE) ที่กล่าวถึงข้างต้น
EFFICIENCYการตรวจสอบชื่อผู้ใช้และรหัสผ่านมีประสิทธิภาพอย่างไร มีวิธีไหนที่ดีกว่านี้ไหม?
ฉันไม่รู้วิธีที่ดีกว่าในการทำสิ่งนี้ แต่ฉันสังเกตเห็นAuthenticationService.login()ว่าบรรทัดเหล่านี้ควรจะสามารถใช้สัญกรณ์จุด:
this.DataService.loggedinfirstname = user['firstname']; this.DataService.loggedinemail = user['eMail'];
ชื่อวิธีการ
ฉันถามเกี่ยวกับการอัปเดตคุณสมบัติLoggedInและสังเกตว่ามีการตั้งค่าในตัวสร้างและแก้ไขในวิธีการToolbarแล้ว ชื่อที่Toolbarดูเหมือนว่าอาจเกี่ยวข้องกับการดึงแถบเครื่องมือ วิธีการอื่น ๆ ในระดับที่มีคำกริยา - เช่นการเข้าสู่ระบบ , redirtoDashboard ชื่อวิธีการที่เหมาะสมกว่าสำหรับวิธีการนั้นอาจเป็นToggleLoggedInหรือบางอย่างตามบรรทัดเหล่านั้น
ลดความซับซ้อนหรือเงื่อนไข
บรรทัดนี้ในLoginComponent::constructor():
if (param === 'order' || param === 'work-document' || param === 'profile') {
สามารถทำให้ง่ายขึ้นโดยใช้Array.prototype.includes()ซึ่งทำการเปรียบเทียบอย่างเข้มงวด1 2 :
if ([ 'order', 'work-document', 'profile'].includes(param)) {