אביב REST API + OAuth2 + זוויתי (באמצעות מחסנית מורשת OAuth של Spring Security)

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

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

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

  • שרת הרשאה
  • שרת משאבים
  • UI implicit - אפליקציית ממשק קצה המשתמשת ב- Flow Implicit
  • סיסמת ממשק משתמש - אפליקציית ממשק קצה המשתמשת בזרימת הסיסמה

הערה: מאמר זה משתמש בפרויקט מורשת Spring OAuth. לגרסה של מאמר זה המשתמש בערימה החדשה של Spring Security 5, עיין במאמר שלנו Spring REST API + OAuth2 + Angular.

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

2. שרת ההרשאה

ראשית, נתחיל בהגדרת שרת הרשאות כיישום Spring Boot פשוט.

2.1. תצורת Maven

נגדיר את קבוצת התלות הבאה:

 org.springframework.boot spring-boot-starter-web org.springframework spring-jdbc mysql mysql-connector-java runtime org.springframework.security.oauth spring-security-oauth2 

שים לב שאנחנו משתמשים ב- spring-jdbc וב- MySQL מכיוון שנשתמש ביישום מגובה JDBC של חנות האסימונים.

2.2. @EnableAuthorizationServer

עכשיו נתחיל להגדיר את תצורת שרת ההרשאה האחראי על ניהול אסימוני גישה:

@Configuration @EnableAuthorizationServer המחלקה הציבורית AuthServerOAuth2Config מרחיב את AuthorizationServerConfigurerAdapter {@Autowired @Qualifier ("authenticationManagerBean") AuthenticationManager AuthenticationManager; הגדרת תצורה חלולה ציבורית של @Override (AuthorizationServerSecurityConfigurer oauthServer) זורקת חריג {oauthServer .tokenKeyAccess ("permitAll ()") .checkTokenAccess ("isAuthenticated ()"); } @ קביעת תצורת הריק הציבורי של @Override (לקוחות ClientDetailsServiceConfigurer) זורקת חריגה {clients.jdbc (dataSource ()) .withClient ("sampleClientId") .authorizedGrantTypes ("implicit") .scopes ("read") .autoApprove (true) .and () ) .withClient ("clientIdPassword") .secret ("סוד") .authorizedGrantTypes ("סיסמה", "קוד הרשאה", "רענון_סימן") .scopes ("read"); } @ קביעת תצורה ריקה ציבורית של @Override (AuthorizationServerEndpointsConfigurer נקודות קצה) זורק Exception {endpoints .tokenStore (tokenStore ()) .authenticationManager (authenticationManager); } @Bean TokenStore ציבורי tokenStore () {להחזיר JdbcTokenStore חדש (dataSource ()); }}

ציין זאת:

  • על מנת להחזיק מעמד באסימונים, השתמשנו ב- JdbcTokenStore
  • רשמנו לקוח עבור "משתמעסוג מענק
  • רשמנו לקוח אחר והסמכנו את "סיסמה“, “קוד אימות"ו"רענון_אסימון”סוגי מענקים
  • על מנת להשתמש ב"סיסמהסוג המענק שאנחנו צריכים לחבר ולהשתמש ב- AuthenticationManager אפונה

2.3. תצורת מקור נתונים

לאחר מכן, בואו להגדיר את מקור הנתונים שלנו שישמש את ה- JdbcTokenStore:

@Value ("classpath: schema.sql") schemaScript משאבים פרטיים; @Bean DataSourceInitializer ציבורי dataSourceInitializer (DataSource dataSource) {DataSourceInitializer initializer = new DataSourceInitializer (); initializer.setDataSource (dataSource); initializer.setDatabasePopulator (databasePopulator ()); אתחול חזרה; } פרטי DatabasePopulator databasePopulator () {ResourceDatabasePopulator אוכלוס = חדש ResourceDatabasePopulator (); populator.addScript (schemaScript); אכלוס החזרה; } @Bean DataSource ציבורי DataSource () {DriverManagerDataSource dataSource = חדש DriverManagerDataSource (); dataSource.setDriverClassName (env.getProperty ("jdbc.driverClassName")); dataSource.setUrl (env.getProperty ("jdbc.url")); dataSource.setUsername (env.getProperty ("jdbc.user")); dataSource.setPassword (env.getProperty ("jdbc.pass")); להחזיר dataSource; }

שים לב שכמו שאנחנו משתמשים JdbcTokenStore עלינו לאתחל את סכימת מסד הנתונים, לכן השתמשנו DataSourceInitializer - וסכמת ה- SQL הבאה:

שחרר טבלה אם קיים oauth_client_details; צור טבלה oauth_client_details (client_id VARCHAR (255) KEY PRIMARY, resource_ids VARCHAR (255), client_secret VARCHAR (255), scope VARCHAR (255), authorized_grant_types VARCHAR (255), web_server_redirect_uri VARCH5 (25), 25 סמכויות (VARCHAR) , refresh_token_validity INTEGER, מידע נוסף VARCHAR (4096), אישור אוטומטי VARCHAR (255)); שחרר טבלה אם קיים oauth_client_token; צור טבלה oauth_client_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) KEY PRIMARY, user_name VARCHAR (255), client_id VARCHAR (255)); שחרר טבלה אם קיים oauth_access_token; צור טבלה oauth_access_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMARY KEY, user_name VARCHAR (255), client_id VARCHAR (255), authentication LONG VARBINARY, refresh_token VARCHAR; שחרר טבלה אם קיים oauth_refresh_token; צור טבלה oauth_refresh_token (token_id VARCHAR (255), token LONG VARBINARY, authentication LONG VARBINARY); שחרר טבלה אם קיים oauth_code; צור טבלת oauth_code (קוד VARCHAR (255), אימות LONG VARBINARY); שחרר טבלה אם קיימת oauth_provisals; צור טבלאות oauth_approvals (userId VARCHAR (255), clientId VARCHAR (255), scope VARCHAR (255), status VARCHAR (10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP); שחרר טבלה אם קיימת ClientDetails; צור טבלה ClientDetails (appId VARCHAR (255) מפתח ראשוני, resourceIds VARCHAR (255), appSecret VARCHAR (255), scope VARCHAR (255), grantTypes VARCHAR (255), redirectUrl VARCHAR (255), הרשויות VARCHAR (255), גישה , refresh_token_validity INTEGER, מידע נוסף VARCHAR (4096), autoApproveScopes VARCHAR (255));

שים לב שאנחנו לא בהכרח צריכים את המפורש DatabasePopulator אפונה - נוכל פשוט להשתמש ב- schema.sql - באיזה Spring Boot נעשה שימוש כברירת מחדל.

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

לבסוף, בואו לאבטח את שרת ההרשאה.

כאשר אפליקציית הלקוח צריכה לרכוש אסימון גישה, היא תעשה זאת לאחר תהליך אימות מונע כניסה פשוט:

@Configuration מחלקה ציבורית ServerSecurityConfig מרחיב את WebSecurityConfigurerAdapter {@Override מוגן חלל להגדיר (AuthenticationManagerBuilder auth) זורק חריג {auth.inMemoryAuthentication () .withUser ("john"). סיסמה ("123"). תפקידים ("USER"); } @Override @Bean AuthenticationManager public authenticationManagerBean () זורק חריג {להחזיר super.authenticationManagerBean (); } תצורת הריק המוגנת על ידי @Override (HttpSecurity http) זורקת חריגה {http.authorizeRequests () .antMatchers ("/ login"). PermitAll () .anyRequest (). מאומת () .and () .formLogin (). PermitAll () ; }}

הערה מהירה כאן היא ש תצורת הכניסה לטופס אינה הכרחית עבור זרימת הסיסמה - רק עבור הזרימה המרומזת - כך שתוכל לדלג עליה תלוי באיזה זרימת OAuth2 אתה משתמש.

3. שרת המשאבים

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

3.1. תצורת Maven

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

3.2. תצורת חנות אסימונים

לאחר מכן, נגדיר את שלנו TokenStore כדי לגשת לאותו מסד נתונים בו משתמש שרת ההרשאה לאחסון אסימוני גישה:

@ סביבה פרטית אוטומטית מאושרת; @Bean DataSource ציבורי DataSource () {DriverManagerDataSource dataSource = חדש DriverManagerDataSource (); dataSource.setDriverClassName (env.getProperty ("jdbc.driverClassName")); dataSource.setUrl (env.getProperty ("jdbc.url")); dataSource.setUsername (env.getProperty ("jdbc.user")); dataSource.setPassword (env.getProperty ("jdbc.pass")); להחזיר dataSource; } @Bean TokenStore ציבורי tokenStore () {להחזיר JdbcTokenStore חדש (dataSource ()); }

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

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

3.3. שירות אסימונים מרחוק

במקום להשתמש ב- TokenStore בשרת המשאבים שלנו נוכל להשתמש RemoteTokeServices:

@Primary @Bean RemoteTokenServices ציבורי tokenService () {RemoteTokenServices tokenService = RemoteTokenServices חדש (); tokenService.setCheckTokenEndpointUrl ("// localhost: 8080 / spring-security-oauth-server / oauth / check_token"); tokenService.setClientId ("fooClientIdPassword"); tokenService.setClientSecret ("סוד"); חזור tokenService; }

ציין זאת:

  • זֶה RemoteTokenService אשתמש CheckTokenEndPoint בשרת הרשאות לאמת את AccessToken ולהשיג אימות חפץ ממנו.
  • ניתן למצוא באתר AuthorizationServerBaseURL + ”/ oauth / check_token
  • שרת ההרשאה יכול להשתמש בכל סוג TokenStore [JdbcTokenStore, JwtTokenStore, ...] - זה לא ישפיע על ה- RemoteTokenService או שרת משאבים.

3.4. בקר לדוגמא

לאחר מכן, בואו נפעיל בקר פשוט שחושף א פו מַשׁאָב:

@Controller מחלקה ציבורית FooController {@PreAuthorize ("# oauth2.hasScope ('read')") @RequestMapping (method = RequestMethod.GET, value = "/ foos / {id}") @ResponseBody ציבור Foo findById (@PathVariable ארוך id) {להחזיר Foo חדש (Long.parseLong (randomNumeric (2)), randomAlphabetic (4)); }}

שים לב כיצד הלקוח זקוק ל "לקרוא" היקף לגשת למשאב זה.

עלינו לאפשר גם אבטחת שיטות גלובליות והגדרת תצורה MethodSecurityExpressionHandler:

@Configuration @EnableResourceServer @EnableGlobalMethodSecurity (prePostEnabled = true) מחלקה ציבורית OAuth2ResourceServerConfig מרחיב את GlobalMethodSecurityConfiguration {@Override מוגן MethodSecurityExpressionHandler createExpressionHandler () {להחזיר OAuthecM2; }}

והנה הבסיסי שלנו פו מַשׁאָב:

מחלקה ציבורית Foo {מזהה פרטי ארוך; שם מחרוזת פרטי; }

3.5. תצורת אינטרנט

לבסוף, בואו להגדיר תצורת אינטרנט בסיסית מאוד עבור ה- API:

@Configuration @EnableWebMvc @ComponentScan ({"org.baeldung.web.controller"}) מחלקה ציבורית ResourceWebConfig מיישם את WebMvcConfigurer {}

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

כעת נבחן יישום זוויתי פשוט חזיתי עבור הלקוח.

ראשית, נשתמש ב- 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 - ולכן זו רק הוכחת מושג, ולא יישום מוכן לייצור. תוכלו להבחין כי אישורי הלקוח נחשפים לממשק הקצה - וזה משהו שנעסוק בו במאמר עתידי.

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

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

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

  • obtainAccessToken (): כדי להשיג אישורי משתמש עם אסימון גישה
  • saveToken (): כדי לשמור את אסימון הגישה שלנו בקובץ cookie באמצעות ספריית ng2-cookies
  • getResource (): להשיג אובייקט Foo מהשרת באמצעות המזהה שלו
  • אישורי צ'ק (): כדי לבדוק אם המשתמש מחובר או לא
  • להתנתק(): למחיקת קובץ cookie של אסימון גישה ולנתק את המשתמש
מחלקת ייצוא Foo {קונסטרוקטור (מזהה ציבורי: מספר, שם ציבורי: מחרוזת) {}} @ Injectable () מחלקת ייצוא AppService {קונסטרוקטור (פרטי _router: נתב, פרטי _http: Http) {} להשיגAccessToken (loginData) {let params = new URLSearchParams (); params.append ('שם משתמש', loginData.username); params.append ('סיסמה', loginData.password); params.append ('grant_type', 'password'); params.append ('client_id', 'fooClientIdPassword'); let headers = new headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Basic' + btoa ("fooClientIdPassword: secret")}); let options = RequestOptions חדשים ({headers: headers}); this._http.post ('// localhost: 8081 / spring-security-oauth-server / oauth / token', params.toString (), options) .map (res => res.json ()). subscribe (data => this.saveToken (נתונים), err => התראה ('אישורים לא חוקיים')); } saveToken (אסימון) {var expireDate = תאריך חדש (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); this._router.navigate (['/']); } getResource (resourceUrl): נצפה {var כותרות = כותרות חדשות ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + Cookie.get ('אסימון גישה')}); var options = חדש RequestOptions ({headers: headers}); להחזיר this._http.get (resourceUrl, אפשרויות) .map ((res: Response) => res.json ()). catch ((error: any) => Observable.throw (error.json (). error || 'שגיאת שרת')); } checkCredentials () {if (! Cookie.check ('access_token')) {this._router.navigate (['/ login']); }} התנתקות () {Cookie.delete ('access_token'); this._router.navigate (['/ כניסה']); }}

ציין זאת:

  • כדי לקבל אסימון גישה אנו שולחים א הודעה אל ה "/ oauth / tokenנקודת קצה
  • אנו משתמשים בתעודות הלקוח וב- Auth Auth כדי להגיע לנקודת הקצה הזו
  • לאחר מכן אנו שולחים את אישורי המשתמש יחד עם מזהה הלקוח ופרמטרים מסוג ה- URL המקודדים
  • לאחר שנשיג את אסימון הגישה - אנחנו שומרים אותו בעוגיה

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

5.2. רכיב כניסה

לאחר מכן, בואו נסתכל על שלנו LoginComponent האחראי על טופס הכניסה:

@Component ({סלקטור: 'טופס כניסה', ספקים: [AppService], תבנית: `כניסה`}) מחלקת ייצוא LoginComponent {public loginData = {שם משתמש:" ", סיסמה:" "}; קונסטרוקטור (_service פרטי: AppService) {} כניסה () {this._service.obtainAccessToken (this.loginData); }

5.3. רכיב ביתי

הבא, שלנו HomeComponent שאחראית על הצגת דף הבית שלנו ומניפולציות שלו:

@Component ({selector: 'home-header', ספקים: [AppService], תבנית: 'ברוך הבא !! התנתק'}) מחלקת ייצוא HomeComponent {constructor (פרטי _שירות: AppService) {} ngOnInit () {this._service.checkCredentials (); } התנתקות () {this._service.logout (); }}

5.4. רכיב Foo

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

@Component ({selector: 'foo-details', ספקים: [AppService], תבנית: "מזהה {{foo.id}} שם {{foo.name}} Foo חדש}}) מחלקת ייצוא FooComponent {foo public = new Foo (1, 'דוגמה foo'); פרטי foosUrl = '// localhost: 8082 / spring-security-oauth-resource / 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', תבנית: "}) ייצוא מחלקה AppComponent {}

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

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

6. זרימה מרומזת

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

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

באופן דומה, נתחיל בשירות שלנו, אך הפעם נשתמש בספרייה angular-oauth2-oidc במקום לקבל בעצמנו אסימון גישה:

@Injectable () מחלקת ייצוא AppService {constructor (פרטי _router: נתב, פרטי _http: Http, oauthService פרטי: OAuthService) {this.oauthService.loginUrl = '// localhost: 8081 / spring-security-oauth-server / oauth / authorize '; this.oauthService.redirectUri = '// localhost: 8086 /'; this.oauthService.clientId = "sampleClientId"; this.oauthService.scope = "קרא כתוב סרגל foo"; this.oauthService.setStorage (sessionStorage); this.oauthService.tryLogin ({}); } להשיגAccessToken () {this.oauthService.initImplicitFlow (); } getResource (resourceUrl): נצפה {var כותרות = כותרות חדשות ({'Type-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + this.oauthService .getAccessToken ()}); var options = חדש RequestOptions ({headers: headers}); להחזיר this._http.get (resourceUrl, אפשרויות) .map ((res: Response) => res.json ()). catch ((error: any) => Observable.throw (error.json (). error || 'שגיאת שרת')); } isLoggedIn () {if (this.oauthService.getAccessToken () === null) {return false; } להחזיר נכון; } התנתקות () {this.oauthService.logOut (); location.reload (); }}

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

6.2. רכיב ביתי

שֶׁלָנוּ HomeComponent לטפל בדף הבית הפשוט שלנו:

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

`}) מחלקת ייצוא HomeComponent {public isLoggedIn = false; קונסטרוקטור (פרטי _שירות: AppService) {} ngOnInit () {this.isLoggedIn = this._service.isLoggedIn (); } התחבר () {this._service.obtainAccessToken (); } התנתקות () {this._service.logout (); }}

6.3. רכיב Foo

שֶׁלָנוּ FooComponent זהה לחלוטין למודול זרימת הסיסמה.

6.4. מודול אפליקציה

לבסוף, שלנו AppModule:

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

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

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

mvn נקי להתקין

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

cd src / main / resources

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

npm התחלה

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

"start": "ng serve"

ב package.json כדי לגרום לו לרוץ ביציאה 8086 לדוגמא:

"start": "ng serve --port 8086"

8. מסקנה

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

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


$config[zx-auto] not found$config[zx-overlay] not found