יישום מסגרת ההרשאה OAuth 2.0 באמצעות ג'קרטה EE

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

במדריך זה אנו מספקים יישום למסגרת ההרשאה OAuth 2.0 באמצעות ג'קרטה EE ו- MicroProfile. והכי חשוב, אנו הולכים ליישם את האינטראקציה של תפקידי OAuth 2.0 באמצעות סוג המענק של קוד ההרשאה. המניע מאחורי כתיבה זו הוא לתת תמיכה בפרויקטים המיושמים באמצעות ג'קרטה EE מכיוון שזה עדיין לא מספק תמיכה ב- OAuth.

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

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

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

2. סקירה כללית של OAuth 2.0

בחלק זה נביא סקירה קצרה של תפקידי OAuth 2.0 וזרימת המענק של קוד ההרשאה.

2.1. תפקידים

מסגרת OAuth 2.0 מרמזת על שיתוף הפעולה בין ארבעת התפקידים הבאים:

  • בעל משאבים: בדרך כלל, זהו משתמש הקצה - הישות שיש לה כמה משאבים שכדאי להגן עליהם
  • שרת משאבים: שירות המגן על נתוני בעל המשאב, בדרך כלל מפרסם אותו באמצעות REST API
  • לָקוּחַ: יישום המשתמש בנתוני בעל המשאב
  • שרת הרשאה: יישום שמעניק הרשאה - או סמכות - ללקוחות בצורה של אסימונים שפג תוקפם

2.2. סוגי מענקי אישור

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

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

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

בנוסף, הלקוח יכול להשתמש ב- משתמע סוג מענק. עם זאת, בדרך כלל בטוח יותר להשתמש במענק קוד ההרשאה עם PKCE.

2.3. זרימת מענקי קוד הרשאה

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

יישום - לקוח - מבקש הרשאה על ידי הפניה מחדש לשרת ההרשאות /לְאַשֵׁר נקודת סיום. לנקודת קצה זו, היישום נותן התקשר חזרה נקודת סיום.

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

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

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

2.4. במה תומכת ג'קרטה EE?

עדיין לא הרבה. במדריך זה נבנה את רוב הדברים מהיסוד.

3. שרת הרשאות OAuth 2.0

ביישום זה נתמקד סוג המענקים הנפוץ ביותר: קוד אימות.

3.1. רישום לקוח ומשתמשים

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

למען הפשטות, נשתמש בלקוח מוגדר מראש:

הכנס ללקוחות (client_id, client_secret, redirect_uri, scope, authorized_grant_types) VALUES ('webappclient', 'webappclientsecret', '// localhost: 9180 / callback', 'resource.read resource.write', 'authorisation_code refresh_token');
@Entity @Table (name = "clients") לקוח בכיתה ציבורית {@ Id @Column (name = "client_id") clientId מחרוזת פרטי; @Column (name = "client_secret") פרטי מחרוזת clientSecret; @Column (name = "redirect_uri") redirectUri פרטי מחרוזת; @Column (name = "scope") טווח מחרוזות פרטי; // ...}

ומשתמש שהוגדר מראש:

הכנס למשתמשים (user_id, סיסמה, תפקידים, טווחים) VALUES ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity @Table (name = "משתמשים") מחלקה ציבורית משתמש ביישום הראשי {@Id @Column (name = "user_id") פרטי מחרוזת userId; @Column (name = "password") סיסמת מחרוזת פרטית; @Column (שם = "תפקידים") תפקידי מחרוזת פרטיים; @Column (name = "scopes") טווחי מחרוזת פרטיים; // ...}

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

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

3.2. נקודת סיום הרשאה

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

בהתאם להוראות המפרט OAuth2, נקודת קצה זו אמורה לתמוך בשיטת HTTP GET, אם כי היא יכולה לתמוך גם בשיטת HTTP POST. ביישום זה נתמוך רק בשיטת HTTP GET.

ראשון, נקודת הקצה של ההרשאה דורשת לאמת את המשתמש. המפרט אינו דורש דרך מסוימת כאן, אז בואו נשתמש באימות טפסים מממשק ה- API לאבטחה של ג'קרטה EE 8:

@FormAuthenticationMechanismDefinition (loginToContinue = @LoginToContinue (loginPage = "/ login.jsp", errorPage = "/ login.jsp"))

המשתמש ינותב אל /login.jsp לאימות ואז יהיה זמין כ- מנהל קורא דרך ה- SecurityContext ממשק API:

מנהל ראשי = securityContext.getCallerPrincipal ();

אנו יכולים להרכיב אותם באמצעות JAX-RS:

@FormAuthenticationMechanismDefinition (loginToContinue = @LoginToContinue (loginPage = "/login.jsp", errorPage = "/login.jsp")) @Path ("authorize") מחלקה ציבורית AuthorizationEndpoint {// ... @GET @Produces (MediaType. TEXT_HTML) תגובה ציבורית doGet (@Context HttpServletRequest בקשה, @Context HttpServletResponse תגובה, @Context UriInfo uriInfo) זורק ServletException, IOException {MultivaluedMap params = uriInfo.getQueryParameters (); מנהל ראשי = securityContext.getCallerPrincipal (); // ...}}

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

ה client_id צריך להיות לקוח תקף, במקרה שלנו מה- לקוחות טבלת מסד נתונים.

ה redirect_uri, אם צוין, צריך להתאים גם למה שאנו מוצאים ב- לקוחות טבלת מסד נתונים.

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

מאחר שההרשאה היא תהליך רב-שלבי, אנו יכולים לאחסן באופן זמני את הערכים הבאים באתר:

request.getSession (). setAttribute ("ORIGINAL_PARAMS", params);

ואז התכונן לשאול את המשתמש באילו הרשאות היישום עשוי להשתמש, ומפנה לדף זה:

מחרוזת מותר Scope = checkUserScopes (user.getScopes (), wantedScope); request.setAttribute ("סקופס", מותר סקופים); request.getRequestDispatcher ("/ authorize.jsp"). העבר (בקשה, תגובה);

3.3. אישור היקף משתמשים

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

@POST @Consumes (MediaType.APPLICATION_FORM_URLENCODED) @Produces (MediaType.TEXT_HTML) תגובה ציבורית doPost (@Context HttpServletRequest בקשה, @Context HttpServletResponse תגובה, MultivaluedMap params) זורק Exception {MultivaluedMap. מקורי. "ORIGINAL_PARAMS"); // ... אישור מחרוזת סטטוס = params.getFirst ("אישור_סטטוס"); // כן או לא // ... אם כן כן אושרה רשימת סקופים = params.get ("היקף"); // ...}

לאחר מכן, אנו מייצרים קוד זמני המתייחס ל- user_id, client_id, וredirect_uri, את כל היישומים ישתמש מאוחר יותר כאשר הוא יגיע לנקודת הקצה האסימונית.

אז בואו ניצור קוד אימות ישות JPA עם מזהה שנוצר אוטומטית:

@Entity @Table (שם) מחלקה ציבורית AuthorizationCode {@Id @GeneratedValue (אסטרטגיה = GenerationType.AUTO) @Column (name = "code") קוד מחרוזת פרטי; // ...}

ואז אכלס אותו:

AuthorizationCode authorCode = AuthorizationCode חדש (); authorizationCode.setClientId (clientId); authorizationCode.setUserId (userId); authorizationCode.setApprovedScopes (String.join ("", authorScopes)); authorizationCode.setExpirationDate (LocalDateTime.now (). plusMinutes (2)); authorizationCode.setRedirectUri (redirectUri);

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

appDataRepository.save (הרשאת קוד); קוד מחרוזת = authorizationCode.getCode ();

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

לאחר מכן אנו מפנים חזרה לאפליקציות redirect_uri, נותן לו את הקוד כמו גם כל אחד מהם מדינה פרמטר שהיישום ציין בו /לְאַשֵׁר בַּקָשָׁה:

StringBuilder sb = StringBuilder חדש (redirectUri); // ... sb.append ("? code ="). append (code); מצב מחרוזת = params.getFirst ("מדינה"); אם (state! = null) {sb.append ("& state ="). append (state); } מיקום URI = UriBuilder.fromUri (sb.toString ()). Build (); Return Response.seeOther (מיקום) .build ();

שים לב שוב redirectUri הוא כל מה שקיים ב לקוחות שולחן, לא את redirect_uri פרמטר בקשה.

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

3.4. נקודת סיום אסימון

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

@Path ("אסימון") מחלקה ציבורית TokenEndpoint {List supportedGrantTypes = Collections.singletonList ("קוד הרשאה"); @ הזרקת AppDataRepository פרטית appDataRepository; @Inject Instance authorGrantTypeHandlers; @POST @Produces (MediaType.APPLICATION_JSON) @Consumes (MediaType.APPLICATION_FORM_URLENCODED) אסימון תגובה ציבורי (Params MultivaluedMap, @HeaderParam (HttpHeaders.AUTHORIZATION) Autheader מחרוזת) זורק JOSEException // ...

נקודת הקצה של האסימון דורשת POST, כמו גם קידוד הפרמטרים באמצעות ה- יישום / x-www-form-urlencoded סוג המדיה.

כפי שדנו, אנו נתמוך רק ב- קוד אימות סוג מענק:

רשימה supportGrantTypes = Collections.singletonList ("קוד הרשאה");

אז, התקבל סוג_מענק כפרמטר נדרש יש לתמוך:

מחרוזת grantType = params.getFirst ("grant_type"); Objects.requireNonNull (grantType, "נדרש paramants_type"); אם (! supportedGrantTypes.contains (grantType)) {JsonObject error = Json.createObjectBuilder () .add ("error", "unsupported_grant_type") .add ("error_description", "סוג המענק צריך להיות אחד מ:" + SupportGrantTypes). לִבנוֹת(); החזר Response.status (Response.Status.BAD_REQUEST) .entity (error) .build (); }

לאחר מכן, אנו בודקים את אימות הלקוח באמצעות אימות HTTP בסיסי. כלומר, אנחנו בודקים אם התקבל client_id ו סוד לקוח, דרך ה הרשאה כּוֹתֶרֶת, תואם לקוח רשום:

מחרוזת [] clientCredentials = תמצית (authHeader); מחרוזת clientId = clientCredentials [0]; מחרוזת clientSecret = clientCredentials [1]; לקוח לקוח = appDataRepository.getClient (clientId); אם (client == null || clientSecret == null ||! clientSecret.equals (client.getClientSecret ())) {JsonObject error = Json.createObjectBuilder () .add ("error", "invalid_client") .build () ; החזר Response.status (Response.Status.UNAUTHORIZED) .entity (error) .build (); }

לבסוף, אנו מאצלים את הפקת ה- תגובת Token למטפל בסוג המענק המקביל:

ממשק ציבורי AuthorizationGrantTypeHandler {TokenResponse createAccessToken (מחרוזת clientId, Params MultivaluedMap) זורק חריג; }

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

@ Name ("קוד הרשאה")

בזמן הריצה, ולפי המתקבל סוג_מענק ערך, ההטמעה המתאימה מופעלת באמצעות מנגנון ה- CDI Instance:

מחרוזת grantType = params.getFirst ("grant_type"); // ... AuthorizationGrantTypeHandler authorGrantTypeHandler = autorisationGrantTypeHandlers.select (NamedLiteral.of (grantType)). Get ();

הגיע הזמן לייצר /אֲסִימוֹןהתגובה.

3.5. RSA מפתחות פרטיים וציבוריים

לפני יצירת האסימון, אנו זקוקים למפתח פרטי RSA לחתימת אסימונים.

לצורך כך נשתמש ב- OpenSSL:

# מפתח פרטי openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits: 2048

ה private-key.pem מסופק לשרת באמצעות התצורה של MicroProfile חתימת מפתח מאפיין באמצעות הקובץ META-INF / microprofile-config. נכסים:

חתימת מפתח = / META-INF / private-key.pem

השרת יכול לקרוא את הנכס באמצעות המוזרק תצורה לְהִתְנַגֵד:

מחרוזת חתימת מפתח = config.getValue ("מפתח חתימה", מחרוזת.קלאס);

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

# מפתח ציבורי openssl rsa -pubout -in private-key.pem -out public-key.pem

והשתמש בתצורת MicroProfile אימות מפתח לקרוא את זה:

מפתח אימות = / META-INF / public-key.pem

על השרת להפוך אותו לזמין עבור שרת המשאבים עבור מטרת האימות. זה גמור דרך נקודת סיום של JWK.

נימבוס JOSE + JWT היא ספרייה שיכולה להועיל כאן. בואו קודם להוסיף את נימבוס-ג'וזה-jwt תלות:

 com.nimbusds nimbus-jose-jwt 7.7 

ועכשיו נוכל למנף את תמיכת ה- JWK של נימבוס כדי לפשט את נקודת הסיום שלנו:

@Path ("jwk") @ApplicationScoped מחלקה ציבורית JWKEndpoint {@GET תגובה ציבורית getKey (@QueryParam ("פורמט") פורמט מחרוזת) זורק חריג {// ... מחרוזת verificationkey = config.getValue ("מפתח אימות", מחרוזת. מעמד); מחרוזת pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString (מפתח אימות); if (format == null || format.equals ("jwk")) {JWK jwk = JWK.parseFromPEMEncodedObjects (pemEncodedRSAPublicKey); החזר Response.ok (jwk.toJSONString ()). סוג (MediaType.APPLICATION_JSON) .build (); } אחרת אם (format.equals ("pem")) {return Response.ok (pemEncodedRSAPublicKey) .build (); } // ...}}

השתמשנו בפורמט פָּרָמֶטֶר כדי לעבור בין הפורמטים PEM ו- JWK. ה- MicroProfile JWT בו נשתמש ליישום שרת המשאבים תומך בשני הפורמטים הללו.

3.6. תגובת נקודת סיום אסימון

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

ליצירת אסימון בפורמט זה, נשתמש שוב בספריית Nimbus JOSE + JWT, אך ישנן גם ספריות JWT רבות אחרות.

אז כדי ליצור JWT חתום, ראשית עלינו לבנות את כותרת ה- JWT:

JWSHeader jwsHeader = JWSHeader.Builder חדש (JWSAlgorithm.RS256) .type (JOSEObjectType.JWT) .build ();

לאחר מכן אנו בונים את המטען שהוא א מַעֲרֶכֶת של תביעות סטנדרטיות ומותאמות אישית:

מיידי עכשיו = Instant.now (); זמן רב פג InMin = 30L; תאריך in30Min = Date.from (now.plus (expiresInMin, ChronoUnit.MINUTES)); JWTClaimsSet jwtClaims = חדש JWTClaimsSet.Builder () .issuer ("// localhost: 9080") .subject (authorisationCode.getUserId ()) .claim ("upn", authorisationCode.getUserId ()) .audience ("// localhost: 9280 ") .claim (" scope ", authorisationCode.getApprovedScopes ()) .claim (" groups ", Arrays.asList (authorisationCode.getApprovedScopes (). Split (" "))) .expirationTime (in30Min) .notBeforeTime (תאריך. מ (עכשיו)) .issueTime (Date.from (now)) .jwtID (UUID.randomUUID (). toString ()) .build (); SignedJWT חתום JWT = חדש SignedJWT (jwsHeader, jwtClaims);

בנוסף לתביעות JWT הרגילות, הוספנו שתי תביעות נוספות - upn ו קבוצות כפי שהם נדרשים על ידי MicroProfile JWT. ה upn ימופה לביטחון ה- EE בג'קרטה מנהל קורא וה קבוצות ימופה לג'קרטה EE תפקידים.

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

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

SignedJWT חתום JWT = חדש SignedJWT (jwsHeader, jwtClaims); // ... מחרוזת חתימת מפתח = config.getValue ("מפתח חתימה", מחרוזת.קלאס); מחרוזת pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString (חתימת מפתח); RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects (pemEncodedRSAPrivateKey);

הַבָּא, אנו חותמים ומסמנים את ה- JWT:

חתם JWT.sign (חדש RSASSASigner (rsaKey.toRSAPrivateKey ())); מחרוזת accessToken = חתום JWT.serialize ();

ולבסוף אנו בונים תגובה סמלית:

להחזיר את Json.createObjectBuilder () .add ("token_type", "Bearer") .add ("access_token", accessToken) .add ("expires_in", expiresInMin * 60) .add ("scope", authorisCode.getApprovedScopes ()) .לִבנוֹת();

שהוא, הודות ל- JSON-P, בסידרה לפורמט JSON ונשלח ללקוח:

{"access_token": "acb6803a48114d9fb4761e403c17f812", "token_type": "Bearer", "expires_in": 1800, "scope": "resource.read resource.write"}

4. לקוח OAuth 2.0

בחלק זה נהיה בניית לקוח OAuth 2.0 מבוסס אינטרנט באמצעות Servlet, MicroProfile Config ו- API של לקוח JAX RS.

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

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

4.1. פרטי לקוח OAuth 2.0

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

  • client_id: מזהה לקוח והוא בדרך כלל מונפק על ידי שרת ההרשאה במהלך תהליך הרישום.
  • client_secret: סוד הלקוח.
  • redirect_uri: המיקום שבו מקבלים את קוד ההרשאה.
  • תְחוּם: הלקוח ביקש אישורים.

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

  • הרשאה_אורי: מיקום נקודת הסיום של הרשאת שרת ההרשאה בה נוכל להשתמש כדי לקבל קוד.
  • token_uri: מיקום נקודת הקצה של אסימון שרת ההרשאה שנוכל להשתמש בה כדי לקבל אסימון.

כל המידע הזה מסופק דרך קובץ התצורה של MicroProfile, META-INF / microprofile-config. נכסים:

# רישום לקוח client.clientId = webappclient client.clientSecret = webappclientsecret client.redirectUri = // localhost: 9180 / callback client.scope = resource.read resource.write # Provider provider.authorizationUri = // 127.0.0.1:9080 / אישור ספק .tokenUri = // 127.0.0.1:9080/ אסימון

4.2. בקשת קוד הרשאה

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

בדרך כלל זה קורה כאשר המשתמש מנסה לגשת ל- API של משאבים מוגנים ללא הרשאה, או על ידי הפעלת הלקוח באופן מפורש /לְאַשֵׁר נָתִיב:

@WebServlet (urlPatterns = "/ authorize") מחלקה ציבורית AuthorizationCodeServlet מרחיב את HttpServlet {@Inject config config config; @Override מוגן ריק doGet (בקשת HttpServletRequest, תגובה HttpServletResponse) זורק ServletException, IOException {// ...}}

בתוך ה doGet () בשיטה, אנו מתחילים ליצור ולאחסן ערך מצב אבטחה:

מצב מחרוזת = UUID.randomUUID (). ToString (); request.getSession (). setAttribute ("CLIENT_LOCAL_STATE", מצב);

לאחר מכן אנו מאחזרים את פרטי תצורת הלקוח:

הרשאת מחרוזת Uri = config.getValue ("provider.authorizationUri", String.class); מחרוזת clientId = config.getValue ("client.clientId", String.class); מחרוזת redirectUri = config.getValue ("client.redirectUri", String.class); מחרוזת היקף = config.getValue ("client.scope", String.class);

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

מחרוזת authorLocation = authorUri + "? Response_type = code" + "& client_id =" + clientId + "& redirect_uri =" + redirectUri + "& scope =" + scope + "& state =" + state;

ולבסוף, ננתב את הדפדפן לכתובת אתר זו:

response.sendRedirect (authorisationlocation);

לאחר עיבוד הבקשה, נקודת הקצה של ההרשאה של שרת ההרשאה תיצור ותוסיף קוד, בנוסף לפרמטר המצב שהתקבל, ל- redirect_uri וינווט מחדש את הדפדפן // localhost: 9081 / callback? code = A123 & state = Y.

4.3. בקשת אסימון גישה

שירות השיחה החוזרת של הלקוח, /התקשר חזרה, מתחיל באימות התקבל מדינה:

מחרוזת localState = (מחרוזת) request.getSession (). GetAttribute ("CLIENT_LOCAL_STATE"); אם (! localState.equals (request.getParameter ("מדינה"))) {request.setAttribute ("שגיאה", "המאפיין state אינו תואם!"); משלוח ("/", בקשה, תגובה); לַחֲזוֹר; }

הַבָּא, נשתמש בקוד שקיבלנו בעבר כדי לבקש אסימון גישה דרך נקודת הקצה האסימונית של שרת ההרשאה:

קוד מחרוזת = request.getParameter ("קוד"); לקוח לקוח = ClientBuilder.newClient (); WebTarget target = client.target (config.getValue ("provider.tokenUri", String.class)); טופס טופס = טופס חדש (); form.param ("סוג_מענק", "קוד הרשאה"); form.param ("קוד", קוד); form.param ("redirect_uri", config.getValue ("client.redirectUri", String.class)); TokenResponse tokenResponse = target.request (MediaType.APPLICATION_JSON_TYPE) .header (HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue ()) .post (Entity.entity (טופס, MediaType.APPLICATION_FORM_URLENCODED_TYPE);

כפי שאנו רואים, אין שום אינטראקציה בדפדפן לשיחה זו, והבקשה נעשית ישירות באמצעות ממשק ה- API של הלקוח JAX-RS כ- HTTP POST.

מכיוון שנקודת הקצה האסימלית דורשת אימות לקוח, כללנו את אישורי הלקוח client_id ו סוד לקוח בתוך ה הרשאה כּוֹתֶרֶת.

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

4.4. גישה למשאבים מוגנים

בשלב זה, יש לנו אסימון גישה חוקי ואנחנו יכולים להתקשר לשרת המשאב /לקרוא ו /לִכתוֹב ממשקי API.

לעשות את זה, עלינו לספק את הרשאה כּוֹתֶרֶת. באמצעות ממשק ה- API של לקוח JAX-RS, זה פשוט נעשה באמצעות ה- כותרת Invocation.Builder () שיטה:

resourceWebTarget = webTarget.path ("משאב / קריאה"); Invocation.Builder invocationBuilder = resourceWebTarget.request (); תגובה = invocationBuilder .header ("הרשאה", tokenResponse.getString ("access_token")) .get (String.class);

5. שרת משאבים OAuth 2.0

בחלק זה נבנה יישום אינטרנט מאובטח המבוסס על JAX-RS, MicroProfile JWT ו- MicroProfile Config. ה- MicroProfile JWT דואג לאמת את ה- JWT שהתקבל ולמפות את תחומי ה- JWT לתפקידי EE בג'קרטה..

5.1. תלות Maven

בנוסף לתלות Java EE Web API, אנו זקוקים גם לממשקי ה- API של MicroProfile ו- MicroProfile JWT:

 javax javaee-web-api 8.0 סיפק org.eclipse.microprofile.config microprofile-config-api 1.3 org.eclipse.microprofile.jwt microprofile-jwt-auth-api 1.1 

5.2. מנגנון אימות JWT

ה- MicroProfile JWT מספק יישום של מנגנון האימות של Bearer Token. זה דואג לעבד את ה- JWT הנוכחי ב הרשאה כותרת, מנגיש מנהל אבטחה בג'קרטה EE כ- JsonWebToken המחזיקה בתביעות JWT וממפה את ההיקפים לתפקידי EE בג'קרטה. עיין בממשק ה- API לאבטחה של ג'קרטה לקבלת רקע נוסף.

כדי לאפשר את מנגנון אימות JWT בשרת, אנחנו צריכים תוסיף את ה LoginConfig ביאור ביישום JAX-RS:

@ApplicationPath ("/ api") @DeclareRoles ({"resource.read", "resource.write"}) @LoginConfig (authMethod = "MP-JWT") מחלקה ציבורית OAuth2ResourceServerApplication מרחיב את היישום {}

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

mp.jwt.verify.publickey.location = / META-INF / public-key.pem

לבסוף, ה- JWT של MicroProfile צריך לאמת את iss הטענה של ה- JWT הנכנס, שצריכה להיות נוכחת ולהתאים לערך המאפיין MicroProfile Config:

mp.jwt.verify.issuer = // 127.0.0.1:9080

בדרך כלל, זהו המיקום של שרת ההרשאה.

5.3. נקודות הקצה המאובטחות

למטרות הדגמה, נוסיף ממשק API עם שתי נקודות קצה. האחד הוא א לקרוא נקודת קצה הנגישה למשתמשים שיש להם resource.read היקף ועוד לִכתוֹב נקודת קצה עבור משתמשים עם resource.write תְחוּם.

ההגבלה על היקפים נעשית באמצעות @ RollesAllowed ביאור:

@Path ("/ resource") @RequestScoped מחלקה ציבורית ProtectedResource {@ הזריק מנהל JsonWebToken פרטי; @GET @ RollesAllowed ("resource.read") @ Path ("/ read") ציבורי לקרוא מחרוזת () {return "משאב מוגן אליו גישה:" + principal.getName (); } @ POST @ RollesAllowed ("resource.write") @ Path ("/ write") מכתב מחרוזת ציבורי () {return "משאב מוגן אליו גישה:" + principal.getName (); }}

6. הפעלת כל השרתים

כדי להפעיל שרת אחד, עלינו רק להפעיל את הפקודה Maven בספריה המתאימה:

חופש החבילה mvn: run-server

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

# שרת הרשאה // localhost: 9080 / # לקוח // localhost: 9180 / # שרת משאבים // localhost: 9280 / 

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

בהתאם להיקפים שהוענקו, שרת המשאבים יגיב בהודעה מוצלחת או שנקבל מעמד אסור ב- HTTP 403.

7. מסקנה

במאמר זה, סיפקנו יישום של שרת הרשאות OAuth 2.0 שיכול לשמש עם כל לקוח OAuth 2.0 תואם ושרת משאבים.

כדי להסביר את המסגרת הכוללת, סיפקנו גם יישום ללקוח ולשרת המשאבים. כדי ליישם את כל הרכיבים הללו, השתמשנו בשימוש בממשקי API של ג'קרטה EE 8, במיוחד CDI, Servlet, JAX RS, Jakarta EE Security. בנוסף, השתמשנו ב- Pseudo-Jakarta EE APIs של MicroProfile: MicroProfile Config ו- MicroProfile JWT.

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

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