OAuth2 עבור ממשק API של REST באביב - התמודד עם אסימון הרענון בזווית

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

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

נשתמש בערמת OAuth ב- Spring Security 5. אם ברצונך להשתמש בערמת מורשת OAuth מדור קודם של Spring Security, עיין במאמר הקודם: OAuth2 עבור ממשק API של REST Spring - טפל באסימון הרענון ב- AngularJS (מחסנית OAuth מדור קודם)

2. תפוגת אסימון גישה

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

אסימון הגישה שלנו מאוחסן בקובץ cookie שתוקפו יפוג על סמך תאריך האסימון עצמו:

var expireDate = תאריך חדש (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate);

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

אך שימו לב כיצד אנו מגדירים זאת בפועל retrieveToken () פונקציה כדי להשיג את אסימון הגישה:

retrieveToken (code) {let params = 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 => התראה ('אישורים לא חוקיים')); }

אנו שולחים את הלקוח בסוד פאראמים, שהיא לא ממש דרך בטוחה להתמודד עם זה. בואו נראה כיצד אנו יכולים להימנע מלעשות זאת.

3. פרוקסי

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

לקוח החזית יתארח כעת כאפליקציית Boot כך שנוכל להתחבר בצורה חלקה לפרוקסי ה- Zuul המוטמע שלנו באמצעות Starter Spring Cloud Zuul.

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

עַכשָׁיו בואו נגדיר את מסלולי ה- proxy:

zuul: מסלולים: auth / code: path: / auth / code / ** sensitive כותרות: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / auth auth / token: path: / auth / token / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token auth / refresh: path: / auth / refresh / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token auth / redirect: path: / auth / redirect / ** sensitive כותרות: url: // localhost: 8089 / auth / resources: path: / auth / resources / ** sensitive כותרות: url: // localhost: 8083 / auth / resources /

קבענו מסלולים לטיפול בפעולות הבאות:

  • אימות / קוד - השג את קוד ההרשאה ושמור אותו בעוגיה
  • אימות / הפניה מחדש - לטפל בהפניה מחדש לדף הכניסה של שרת ההרשאה
  • אימות / משאבים - מפה לנתיב המתאים של שרת ההרשאה למשאבי דף הכניסה שלו (css ו js)
  • אימות / אסימון - קבל את אסימון הגישה, הסר רענון_אסימון מהמטען ושמור אותו בעוגיה
  • לאמת / לרענן - השג את אסימון הרענון, הסר אותו מהמטען ושמור אותו בעוגיה

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

לאחר מכן, בואו נסתכל על כל אלה בזה אחר זה.

4. קבל את הקוד באמצעות פילטר מקדים של Zuul

השימוש הראשון ב- proxy הוא פשוט - קבענו בקשה לקבלת קוד ההרשאה:

מחלקה ציבורית @Component CustomPreZuulFilter מרחיב את ZuulFilter {@Override Object Object run () {RequestContext ctx = RequestContext.getCurrentContext (); HttpServletRequest req = ctx.getRequest (); מחרוזת requestURI = req.getRequestURI (); אם (requestURI.contains ("auth / code")) {Map params = ctx.getRequestQueryParams (); אם (params == null) {params = Maps.newHashMap (); } params.put ("response_type", Lists.newArrayList (מחרוזת חדשה [] {"קוד"})); params.put ("היקף", Lists.newArrayList (מחרוזת חדשה [] {"קרא")); params.put ("client_id", Lists.newArrayList (מחרוזת חדשה [] {CLIENT_ID})); params.put ("redirect_uri", Lists.newArrayList (מחרוזת חדשה [] {REDIRECT_URL})); ctx.setRequestQueryParams (פאראמס); } להחזיר אפס; } @Override בוליאני ציבורי shouldFilter () {בוליאני shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); מחרוזת URI = ctx.getRequest (). GetRequestURI (); אם (URI.contains ("auth / code") || URI.contains ("auth / token") || URI.contains ("auth / refresh")) {shouldfilter = true; } להחזיר את המסנן; } @Override public int filterOrder () {return 6; } @Override ציבור מחרוזת filterType () {להחזיר "pre"; }}

אנו משתמשים בסוג מסנן מִרֹאשׁ לעבד את הבקשה לפני העברתה.

במסננים לָרוּץ() שיטה, אנו מוסיפים פרמטרים של שאילתה עבור תגובה_סוג, תְחוּם, client_id ו redirect_uri- כל מה ששרת ההרשאה שלנו צריך כדי להביא אותנו לדף הכניסה שלו ולשלוח קוד בחזרה.

שימו לב גם ל shouldFilter () שיטה. אנו מסננים בקשות רק עם שלושת ה- URI שהוזכרו, אחרים לא עוברים אל ה- לָרוּץ שיטה.

5. שים את הקוד בעוגיה באמצעות מסנן פוסט צוול

מה שאנחנו מתכננים לעשות כאן הוא לשמור את הקוד כקובץ cookie כדי שנוכל לשלוח אותו לשרת ההרשאה כדי לקבל את אסימון הגישה. הקוד קיים כפרמטר שאילתה בכתובת האתר של הבקשה אליה מפנה אותנו שרת ההרשאה לאחר הכניסה.

נקים פוסט פילטר של Zuul כדי לחלץ קוד זה ולהגדיר אותו בעוגיה. זו לא סתם עוגיה רגילה, אלא א עוגיית HTTP בלבד מאובטחת עם נתיב מוגבל מאוד (/ auth / token):

המחלקה הציבורית @Component CustomPostZuulFilter מרחיבה את ZuulFilter {mapper ObjectMapper פרטי = ObjectMapper חדש (); @Override הפעלת אובייקט ציבורי () {RequestContext ctx = RequestContext.getCurrentContext (); נסה {Map params = ctx.getRequestQueryParams (); if (requestURI.contains ("auth / redirect")) {cookie cookie = cookie חדש ("code", params.get ("code"). get (0)); cookie.setHttpOnly (נכון); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / token"); ctx.getResponse (). addCookie (cookie); }} לתפוס (חריג e) {logger.error ("אירעה שגיאה בפילטר zuul post", e); } להחזיר אפס; } @ Override בוליאני ציבורי shouldFilter () {בוליאני shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); מחרוזת URI = ctx.getRequest (). GetRequestURI (); אם (URI.contains ("auth / redirect") || URI.contains ("auth / token") || URI.contains ("auth / refresh")) {shouldfilter = true; } להחזיר את המסנן; } @Override public int filterOrder () {return 10; } @ עקוף ציבורי מחרוזת filterType () {להחזיר "פוסט"; }}

על מנת להוסיף שכבת הגנה נוספת מפני התקפות CSRF, נוסיף כותרת עוגיות של אותו אתר לכל העוגיות שלנו.

לשם כך ניצור מחלקת תצורה:

המחלקה הציבורית @Configuration SameSiteConfig מיישם את WebMvcConfigurer {@Bean הציבור TomcatContextCustomizer sameSiteCookiesConfig () {return context -> {final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor (); cookieProcessor.setSameSiteCookies (SameSiteCookies.STRICT.getValue ()); context.setCookieProcessor (cookieProcessor); }; }}

כאן אנו מגדירים את התכונה ל קַפְּדָנִי, כך שכל העברה של עוגיות חוצה אתרים נמנעת לחלוטין.

6. קבל והשתמש בקוד מהעוגיה

כעת, כשיש לנו את הקוד בקובץ ה- cookie, כאשר היישום Angular החזיתי מנסה להפעיל בקשת אסימון, הוא ישלח את הבקשה בכתובת / auth / token וכך הדפדפן, כמובן, ישלח את העוגיה הזו.

אז עכשיו יהיה לנו מצב אחר מִרֹאשׁ לסנן את ה- proxy ש יחלץ את הקוד מהעוגיה וישלח אותו יחד עם פרמטרים אחרים של הטופס לקבלת האסימון:

הפעלת אובייקט ציבורי () {RequestContext ctx = RequestContext.getCurrentContext (); ... אחרת אם (requestURI.contains ("auth / token"))) {נסה {String code = extractCookie (req, "code"); String formParams = String.format ("grant_type =% s & client_id =% s & client_secret =% s & redirect_uri =% s & code =% s", "קוד הרשאה", CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, קוד); בתים [] בתים = formParams.getBytes ("UTF-8"); ctx.setRequest (CustomHttpServletRequest חדש (req, בתים)); } לתפוס (IOException e) {e.printStackTrace (); }} ...} ExtractCookie פרטי (מחרוזת HttpServletRequest, שם מחרוזת) {Cookie [] cookies = req.getCookies (); אם (עוגיות! = null) {עבור (int i = 0; i <עוגיות.אורך; i ++) {אם (עוגיות [i] .getName (). equalsIgnoreCase (שם)) {עוגיות להחזיר [i] .getValue () ; }}} להחזיר null; }

והנה שלנוCustomHttpServletRequest - משמש לשליחת גוף הבקשה שלנו עם פרמטרי הטופס הנדרשים שהומרו לבתים:

מחלקה ציבורית CustomHttpServletRequest מרחיב HttpServletRequestWrapper {בתים פרטיים [] בתים; PublicHttpServletRequest ציבורי (HttpServletRequest בקשה, בתים [] בתים) {super (בקשה); this.bytes = בתים; } @Override ציבור ServletInputStream getInputStream () זורק IOException {להחזיר ServletInputStreamWrapper חדש (בתים); } @Override int ציבורי getContentLength () {להחזיר בתים.אורך; } @Override ציבורי ארוך getContentLengthLong () {return bytes.length; } @Override מחרוזת ציבורית getMethod () {להחזיר "POST"; }}

זה יביא לנו אסימון גישה משרת ההרשאה בתגובה. לאחר מכן נראה כיצד אנו משנים את התגובה.

7. שים את אסימון הרענון בעוגיה

על הדברים המהנים.

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

נוסיף לסינון פוסט-פילטר שלנו כדי לחלץ את אסימון הרענון מגוף JSON של התגובה ולהגדיר אותו בעוגיה. זו שוב קובץ cookie מאובטח HTTP בלבד עם נתיב מוגבל מאוד (/ אימות / רענון):

הפעלת אובייקט ציבורי () {... else if (requestURI.contains ("auth / token") || requestURI.contains ("auth / refresh")) {InputStream is = ctx.getResponseDataStream (); מחרוזת responseBody = IOUtils.toString (הוא, "UTF-8"); if (responseBody.contains ("refresh_token")) {Map responseMap = mapper.readValue (responseBody, TypeReference חדש() {}); מחרוזת refreshToken = responseMap.get ("refresh_token"). ToString (); responseMap.remove ("רענון_סימן"); responseBody = mapper.writeValueAsString (responseMap); קובץ Cookie = קובץ Cookie חדש ("refreshToken", refreshToken); cookie.setHttpOnly (נכון); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / refresh"); cookie.setMaxAge (2592000); // 30 יום ctx.getResponse (). AddCookie (קובץ cookie); } ctx.setResponseBody (responseBody); } ...}

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

ואז הסרנו רענון_אסימון מתגובת JSON כדי לוודא שהיא לעולם לא נגישה לקצה הקדמי מחוץ לעוגיה.

נקודה נוספת שיש לציין כאן היא שהגדרנו את הגיל המקסימלי של העוגיה ל -30 יום - מכיוון שזה תואם את זמן התפוגה של האסימון.

8. קבל והשתמש באסימון הרענון מהעוגיה

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

אז עכשיו יהיה לנו מצב נוסף מִרֹאשׁ סנן את ה- proxy שיוציא את אסימון הרענון מהעוגיה וישלח אותו קדימה כפרמטר HTTP - כך שהבקשה תקפה:

הפעלת אובייקט ציבורי () {RequestContext ctx = RequestContext.getCurrentContext (); ... אחרת אם (requestURI.contains ("auth / refresh"))) {נסה {String token = extractCookie (req, "token"); String formParams = String.format ("grant_type =% s & client_id =% s & client_secret =% s & refresh_token =% s", "refresh_token", CLIENT_ID, CLIENT_SECRET, token); בתים [] בתים = formParams.getBytes ("UTF-8"); ctx.setRequest (CustomHttpServletRequest חדש (req, בתים)); } לתפוס (IOException e) {e.printStackTrace (); }} ...}

זה דומה למה שעשינו כשהשגנו לראשונה את אסימון ה- Access. אבל שימו לב שגוף הצורה שונה. עכשיו אנחנו שולחים סוג_מענק שֶׁל רענון_אסימון במקום קוד אימות יחד עם האסימון ששמרנו בעבר בעוגיה.

לאחר קבלת התגובה, היא עוברת שוב את אותה טרנספורמציה ב מִרֹאשׁ מסנן כפי שראינו קודם בסעיף 7.

9. רענון אסימון הגישה מזוויתית

לבסוף, בואו ונשנה את היישום הקידמי הפשוט שלנו ובעצם נשתמש בריענון האסימון:

הנה התפקיד שלנו refreshAccessToken ():

refreshAccessToken () {let headers = HttpHeaders new ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8'}); this._http.post ('אימות / רענון', {}, {כותרות: כותרות}). מנוי (data => this.saveToken (נתונים), err => התראה ('אישורים לא חוקיים')); }

שים לב כיצד אנו פשוט משתמשים בקיים saveToken () פונקציה - ופשוט להעביר אליו תשומות שונות.

שימו לב גם לזה אנחנו לא מוסיפים פרמטרים של טופס עם רענון_אסימון את עצמנו - מכיוון שמסנן זול יטפל בזה.

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

מכיוון שלקוח Angular החזיתי שלנו מתארח כעת כאפליקציית Boot, הריצתו תהיה שונה מעט מבעבר.

הצעד הראשון זהה. עלינו לבנות את האפליקציה:

mvn נקי להתקין

זה יפעיל את תוסף frontend-maven מוגדר שלנו pom.xml לבנות את הקוד הזוויתי ולהעתיק את החפצים של ממשק המשתמש אל יעד / כיתות / סטטי תיקיה. תהליך זה מחליף כל דבר אחר שיש לנו ב src / main / resources מַדרִיך. לכן עלינו לוודא ולכלול את כל המשאבים הנדרשים מתיקיה זו, כגון application.yml, בתהליך ההעתקה.

בשלב השני עלינו להריץ את שלנו יישום SpringBoot מעמד UiApplication. אפליקציית הלקוחות שלנו תפעל ביציאה 8089 כמפורט ב application.yml.

11. מסקנה

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

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


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