אביב REST API + OAuth2 + זוויתי

1. סקירה כללית

במדריך זה, אנו נאבטח REST API עם OAuth2 ונצרוך אותו מלקוח Angular פשוט.

היישום שאנחנו הולכים לבנות יורכב משלושה מודולים נפרדים:

  • שרת הרשאה
  • שרת משאבים
  • קוד הרשאת ממשק משתמש: יישום חזיתי המשתמש בזרימת קוד ההרשאה

נשתמש בערמת OAuth ב- Spring Security 5. אם ברצונך להשתמש בערמת מורשת OAuth מדור קודם של Spring Security, עיין במאמר הקודם: Spring REST API + OAuth2 + Angular (שימוש בערמת מורשת OAuth Legacy של Spring Security).

בוא נקפוץ ישר פנימה.

2. שרת ההרשאה OAuth2 (AS)

פשוט שים, שרת הרשאה הוא יישום שמנפיק אסימונים לאישור.

בעבר, ערימת ה- OAuth של Spring Security הציעה אפשרות להגדיר שרת הרשאות כ- Spring Application. אבל הפרויקט הוצא משימוש, בעיקר בגלל ש- OAuth הוא תקן פתוח עם ספקים מבוססים רבים כמו Okta, Keycloak ו- ForgeRock, עד כמה שם.

מבין אלה נשתמש בקייקלוק. זהו שרת קוד פתוח וניהול גישה המנוהל על ידי Red Hat, שפותח בג'אווה, על ידי JBoss. הוא תומך לא רק ב- OAuth2 אלא גם בפרוטוקולים סטנדרטיים אחרים כגון OpenID Connect ו- SAML.

להדרכה זו, נקים שרת Keycloak מוטבע באפליקציית Spring Boot.

3. שרת המשאבים (RS)

עכשיו בואו נדבר על שרת המשאבים; זה בעצם ה- REST API, שבסופו של דבר אנחנו רוצים להיות מסוגלים לצרוך.

3.1. תצורת Maven

ה- pom של שרת המשאבים שלנו זהה לזה של פום שרת ההרשאה הקודם, ללא החלק של Keycloak ועם נוסף spring-boot-starter-oauth2-resource-server תלות:

 org.springframework.boot spring-boot-starter-oauth2-resource-server 

3.2. תצורת אבטחה

מכיוון שאנחנו משתמשים באביב מגף, אנו יכולים להגדיר את התצורה הנדרשת מינימלית באמצעות מאפייני האתחול.

נעשה זאת תוך application.yml קוֹבֶץ:

שרת: יציאה: 8081 servlet: path-context: / resource-server spring: security: oauth2: resourceserver: jwt: issuer-uri: // localhost: 8083 / auth / realms / baeldung jwk-set-uri: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / certs

כאן ציינו שנשתמש באסימני JWT לצורך הרשאה.

ה jwk-set-uri המאפיין מצביע על ה- URI המכיל את המפתח הציבורי כדי ששרת המשאבים שלנו יוכל לאמת את תקינות האסימונים.

ה מנפיק-אורי המאפיין מייצג אמצעי אבטחה נוסף כדי לאמת את מנפיק האסימונים (שהוא שרת ההרשאה). עם זאת, הוספת מאפיין זה מחייבת כי שרת ההרשאה יפעל לפני שנוכל להפעיל את יישום Resource Server.

לאחר מכן, בואו נקבע א תצורת אבטחה עבור ה- API לאבטחת נקודות קצה:

@Configuration מחלקה ציבורית SecurityConfig מרחיב את WebSecurityConfigurerAdapter {@Override מוגן חלל להגדיר (HttpSecurity http) זורק חריג {http.cors () .and () .authorizeRequests () .antMatchers (HttpMethod.GET, "/ user / info", "/ api / foos / ** ") .hasAuthority (" SCOPE_read ") .antMatchers (HttpMethod.POST," / api / foos ") .hasAuthority (" SCOPE_write ") .anyRequest () .uthuthicated () .and () .oauth2ResourceServer ( ) .jwt (); }}

כפי שאנו רואים, בשיטות ה- GET שלנו אנו מאפשרים רק בקשות שיש לקרוא תְחוּם. לשיטת POST, על המבקש להיות בעל לִכתוֹב סמכות בנוסף ל לקרוא. עם זאת, עבור כל נקודת קצה אחרת, יש לאמת את הבקשה רק עם כל משתמש.

גַם, ה oauth2ResourceServer () השיטה מציינת שמדובר בשרת משאבים, עם jwt () -אסימונים מעוצבים.

נקודה נוספת שיש לציין כאן היא השימוש בשיטה cors () כדי לאפשר כותרות בקרת גישה על הבקשות. זה חשוב במיוחד מכיוון שאנו עוסקים בלקוח Angular והבקשות שלנו יגיעו מכתובת אתר מקור אחרת.

3.4. המודל והמאגר

לאחר מכן, בואו נגדיר א javax. התמדה. ישות עבור המודל שלנו, פו:

@Entity בכיתה ציבורית Foo {@Id @GeneratedValue (אסטרטגיה = GenerationType.IDENTITY) פרטי מזהה ארוך; שם מחרוזת פרטי; // קונסטרוקטור, גטרים וקובעים}

ואז אנחנו צריכים מאגר של פוס. נשתמש באביב PagingAndSortingRepository:

ממשק ציבורי IFooRepository מרחיב את PagingAndSortingRepository {} 

3.4. השירות והיישום

לאחר מכן נגדיר וניישם שירות פשוט עבור ה- API שלנו:

ממשק ציבורי IFooService {findById אופציונלי (מזהה ארוך); Foo save (Foo foo); Iterable findAll (); } @Service בכיתה ציבורית FooServiceImpl מיישם את IFooService {פרטי IFooRepository fooRepository; FooServiceImpl ציבורי (IFooRepository fooRepository) {this.fooRepository = fooRepository; } @Override public אופציונלי findById (מזהה ארוך) {החזר fooRepository.findById (id); } @Override ציבורי Foo save (Foo foo) {return fooRepository.save (foo); } @Override ציבור Iterable findAll () {return fooRepository.findAll (); }} 

3.5. בקר לדוגמא

עכשיו בואו נפעיל בקר פשוט שחושף את שלנו פו משאב באמצעות DTO:

@RestController @RequestMapping (value = "/ api / foos") FooController בכיתה ציבורית {IFooService פרטית fooService; FooController ציבורי (IFooService fooService) {this.fooService = fooService; } @CrossOrigin (origins = "// localhost: 8089") @ GetMapping (value = "/ {id}") FooD ציבורי כדי למצואOne (@PathVariable מזהה ארוך) {Foo entity = fooService.findById (id) .orElseThrow (() -> ResponseStatusException חדש (HttpStatus.NOT_FOUND)); להחזיר convertToDto (ישות); } @GetMapping אוסף ציבורי findAll () {Iterable foos = this.fooService.findAll (); רשימת fooDtos = ArrayList חדש (); foos.forEach (p -> fooDtos.add (convertToDto (p))); החזר fooDtos; } מוגן FooDto convertToDto (ישות Foo) {FooDto dto = FooDto חדש (entity.getId (), entity.getName ()); להחזיר dto; }}

שימו לב לשימוש ב- @ CrossOrigin מֵעַל; זהו התצורה ברמת הבקר שאנחנו צריכים כדי לאפשר ל- CORS מהאפליקציה Angular שלנו לפעול בכתובת האתר שצוינה.

הנה שלנו FooDto:

בכיתה ציבורית FooDto {מזהה פרטי ארוך; שם מחרוזת פרטי; }

4. חזית - התקנה

כעת אנו נסתכל על יישום פשוט זוויתי חזיתי עבור הלקוח, אשר ייגש לממשק ה- REST שלנו.

נשתמש תחילה ב- Angular CLI כדי ליצור ולנהל את המודולים הקדמיים שלנו.

ראשית, אנו מתקינים צומת ו- npm, מכיוון ש- CLI Angular הוא כלי npm.

ואז עלינו להשתמש ב- תוסף frontend-maven לבנות את פרויקט Angular שלנו באמצעות Maven:

   com.github.eirslett frontend-maven-plugin 1.3 v6.10.2 3.10.10 src / main / resources להתקין צומת ו- npm להתקין צומת ו- npm npm להתקין npm npm להריץ לבנות npm להריץ לבנות 

ולבסוף, ליצור מודול חדש באמצעות CLI Angular:

ng oauthApp חדש

בחלק הבא נדון בהיגיון האפליקציות Angular.

5. זרימת קוד הרשאה באמצעות זוויתית

אנו נשתמש בזרימת קוד ההרשאה של OAuth2 כאן.

מקרה השימוש שלנו: אפליקציית הלקוח מבקשת קוד משרת ה- Authorization ומוצגת לו דף כניסה. ברגע שמשתמש מספק את האישורים וההגשה שלהם, שרת ההרשאה נותן לנו את הקוד. ואז הלקוח החזיתי משתמש בו לרכישת אסימון גישה.

5.1. רכיב ביתי

בואו נתחיל עם המרכיב העיקרי שלנו, ה- HomeComponent, שם כל הפעולות מתחילות:

@Component ({selector: 'home-header', ספקים: [AppService], תבנית: `כניסה ברוך הבא !! התנתק

`}) מחלקת ייצוא HomeComponent {public isLoggedIn = false; בנאי (פרטי _שירות: AppService) {} ngOnInit () {this.isLoggedIn = this._service.checkCredentials (); תן ל- i = window.location.href.indexOf ('קוד'); אם (! this.isLoggedIn && i! = -1) {this._service.retrieveToken (window.location.href.substring (i + 5)); }} התחברות () {window.location.href = '// localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / auth? response_type = code & scope = openid% 20write% 20read & client_id = '+ this._service.clientId +' & redirect_uri = '+ this._service.redirectUri; } התנתקות () {this._service.logout (); }}

בהתחלה, כאשר המשתמש לא מחובר, מופיע רק כפתור הכניסה. בלחיצה על כפתור זה, המשתמש מועבר לכתובת ה- URL של ההרשאה של ה- AS שם הוא מקיש שם משתמש וסיסמה. לאחר כניסה מוצלחת, המשתמש מנותב חזרה עם קוד ההרשאה, ואז אנו מאחזרים את אסימון הגישה באמצעות קוד זה.

5.2. שירות אפליקציות

עכשיו בואו נסתכל AppService - ממוקם ב app.service.ts - המכיל את ההיגיון לאינטראקציות בין השרתים:

  • retrieveToken (): להשיג אסימון גישה באמצעות קוד הרשאה
  • saveToken (): כדי לשמור את אסימון הגישה שלנו בקובץ cookie באמצעות ספריית ng2-cookies
  • getResource (): להשיג אובייקט Foo מהשרת באמצעות המזהה שלו
  • אישורי צ'ק (): כדי לבדוק אם המשתמש מחובר או לא
  • להתנתק(): למחיקת קובץ cookie של אסימון גישה ולנתק את המשתמש
מחלקת ייצוא Foo {constructor (מזהה ציבורי: מספר, שם ציבורי: מחרוזת) {}} @ Injectable () מחלקת ייצוא AppService {public clientId = 'newClient'; redirectUri ציבורי = '// localhost: 8089 /'; קונסטרוקטור (פרטי _http: HttpClient) {} retrieveToken (קוד) {let params = new URLSearchParams (); params.append ('grant_type', 'authorisation code'); params.append ('client_id', this.clientId); params.append ('client_secret', 'newClientSecret'); params.append ('redirect_uri', this.redirectUri); params.append ('קוד', קוד); let headers = HttpHeaders new ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8'}); this._http.post ('// localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token', params.toString (), {headers: headers}). מנוי (data => this.saveToken ( נתונים), err => התראה ('אישורים לא חוקיים')); } saveToken (אסימון) {var expireDate = תאריך חדש (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); console.log ('אסימון גישה מושגת'); window.location.href = '// localhost: 8089'; } getResource (resourceUrl): נצפה {var headers = new HttpHeaders ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + Cookie.get ('אסימון גישה')}); להחזיר this._http.get (resourceUrl, {headers: headers}) .catch ((error: any) => Observable.throw (error.json (). error || 'שגיאת שרת')); } checkCredentials () {return Cookie.check ('access_token'); } התנתקות () {Cookie.delete ('access_token'); window.location.reload (); }}

בתוך ה retrieveToken בשיטה, אנו משתמשים בתעודות הלקוח שלנו וב- Auth Auth כדי לשלוח הודעה אל ה / openid-connect / token נקודת קצה כדי לקבל את אסימון הגישה. הפרמטרים נשלחים בפורמט מקודד URL. לאחר שנקבל את אסימון הגישה, אנחנו שומרים אותו בעוגיה.

אחסון העוגיות חשוב במיוחד כאן מכיוון שאנו משתמשים בעוגיה רק ​​למטרות אחסון ולא בכדי להניע את תהליך האימות ישירות. זה מסייע בהגנה מפני התקפות ופגיעות של CSRF (Cross-Site Request Forgery).

5.3. רכיב Foo

לבסוף, שלנו FooComponent כדי להציג את פרטי ה- Foo שלנו:

@Component ({selector: 'foo-details', ספקים: [AppService], תבנית: 'מזהה {{foo.id}} שם {{foo.name}} Foo חדש'}) מחלקת יצוא FooComponent {foo public = new Foo (1, 'foo sample'); פרטי foosUrl = '// localhost: 8081 / resource-server / api / foos /'; קונסטרוקטור (שירות _ פרטי: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id). מנוי (data => this.foo = data, error => this.foo.name = 'שְׁגִיאָה'); }}

5.5. רכיב האפליקציה

הפשוט שלנו AppComponent לפעול כמרכיב השורש:

@Component ({selector: 'app-root', תבנית: "Spring Security Oauth - קוד אישור"}) מחלקת ייצוא AppComponent {} 

וה AppModule שם אנו עוטפים את כל הרכיבים, השירותים והמסלולים שלנו:

@NgModule ({הצהרות: [AppComponent, HomeComponent, FooComponent], ייבוא: [BrowserModule, HttpClientModule, RouterModule.forRoot ([{{path: '', component: HomeComponent, pathMatch: 'full'}], {onSameUrlNavigation: ' })], ספקים: [], bootstrap: [AppComponent]}) מחלקת ייצוא AppModule {} 

7. הפעל את חזית הקצה

1. כדי להפעיל כל אחד ממודולי הקצה שלנו, עלינו לבנות את האפליקציה תחילה:

mvn נקי להתקין

2. אז אנחנו צריכים לנווט לספריית האפליקציות Angular שלנו:

cd src / main / resources

3. לבסוף, נפתח את האפליקציה שלנו:

npm התחלה

השרת יתחיל כברירת מחדל ביציאה 4200; כדי לשנות את היציאה של כל מודול, שנה:

"start": "ng serve"

ב package.json; לדוגמה, כדי להפעיל אותו ביציאה 8089, הוסף:

"start": "ng serve --port 8089"

8. מסקנה

במאמר זה למדנו כיצד לאשר את היישום שלנו באמצעות OAuth2.

ניתן למצוא את היישום המלא של מדריך זה בפרויקט GitHub.