ביטוי אבטחה מותאם אישית עם אבטחת אביב

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

במדריך זה נתמקד יצירת ביטוי אבטחה מותאם אישית עם Spring Security.

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

נדון תחילה כיצד ליצור מותאם אישית PermissionEvaluator, ואז ביטוי מותאם אישית לחלוטין - ולבסוף כיצד לבטל את אחד מביטויי האבטחה המובנים.

2. ישות משתמש

ראשית, בואו נכין את הבסיס ליצירת ביטויי האבטחה החדשים.

בואו נסתכל על שלנו מִשׁתַמֵשׁ ישות - שיש לה פריבילגיות ו אִרגוּן:

משתמש בכיתה ציבורית @Entity {@Id @GeneratedValue (אסטרטגיה = GenerationType.AUTO) פרטי מזהה ארוך; @Column (nullable = false, unique = true) שם משתמש מחרוזת פרטי; סיסמת מחרוזת פרטית; @ManyToMany (fetch = FetchType.EAGER) @JoinTable (name = "users_privileges", joinColumns = @JoinColumn (name = "user_id", referencedColumnName = "id"), inverseJoinColumn = @JoinColumn (name = "privilege_id", referensCol id ")) private Set privileges; @ManyToOne (fetch = FetchType.EAGER) @JoinColumn (name = "organization_id", referencedColumnName = "id") ארגון פרטי בארגון; // סטרים וקובעים סטנדרטיים}

והנה הפשוט שלנו זְכוּת:

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

ושלנו אִרגוּן:

@Entity Class Class Organization {@Id @GeneratedValue (אסטרטגיה = GenerationType.AUTO) פרטי מזהה ארוך; @Column (nullable = false, unique = true) שם מחרוזת פרטי; // סטרים וקובעים סטנדרטיים}

לבסוף - נשתמש במנהג פשוט יותר קֶרֶן:

מחלקה ציבורית MyUserPrincipal מיישם UserDetails {משתמש פרטי פרטי; MyUserPrincipal ציבורי (משתמש משתמש) {this.user = משתמש; } @ העבר לציבור מחרוזת getUsername () {return user.getUsername (); } @Override ציבורי מחרוזת getPassword () {return user.getPassword (); } @Override אוסף ציבורי getAuthorities () {רשויות רשימה = ArrayList חדש (); עבור (הרשאת הרשאות: user.getPrivileges ()) {autorities.add (חדש SimpleGrantedAuthority (privilege.getName ())); } להחזיר רשויות; } ...}

עם כל השיעורים האלה מוכנים, אנו נשתמש במנהג שלנו קֶרֶן בבסיסי UserDetailsService יישום:

המחלקה הציבורית @Service MyUserDetailsService מיישמת את UserDetailsService {@Autowired פרטי UserRepository userRepository; @Override UserDetails ציבורי loadUserByUsername (שם משתמש מחרוזת) {User user = userRepository.findByUsername (שם משתמש); אם (user == null) {זרוק UsernameNotFoundException חדש (שם משתמש); } להחזיר MyUserPrincipal חדש (משתמש); }}

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

3. הגדרת נתונים

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

@Component מחלקה ציבורית SetupData {@ UserRepository פרטי פרטי UserRepository; @ PrivilegeRepository פרטי פרטי מאוחסן; @Autowired פרטי OrganizationRepository organizationRepository; @PostConstruct בטל פומבי init () {initPrivileges (); initOrganizations (); initUsers (); }}

הנה שלנו init שיטות:

initprivileges () בטל פרטי () {Privilege privilege1 = Privilege new ("FOO_READ_PRIVILEGE"); privilegeRepository.save (privilege1); הרשאת פריבילגיה 2 = הרשאה חדשה ("FOO_WRITE_PRIVILEGE"); privilegeRepository.save (privilege2); }
initOrganizations חלל פרטי () {Organization org1 = ארגון חדש ("FirstOrg"); organizationRepository.save (org1); ארגון org2 = ארגון חדש ("SecondOrg"); organizationRepository.save (org2); }
initUsers חללים פרטיים () {Privilege privilege1 = privilegeRepository.findByName ("FOO_READ_PRIVILEGE"); פריבילגיות privilege2 = privilegeRepository.findByName ("FOO_WRITE_PRIVILEGE"); משתמש משתמש 1 = משתמש חדש (); user1.setUsername ("ג'ון"); user1.setPassword ("123"); user1.setPrivileges (HashSet חדש (Arrays.asList (privilege1))); user1.setOrganization (organizationRepository.findByName ("FirstOrg")); userRepository.save (user1); משתמש משתמש 2 = משתמש חדש (); user2.setUsername ("טום"); user2.setPassword ("111"); user2.setPrivileges (HashSet חדש (Arrays.asList (privilege1, privilege2))); user2.setOrganization (organizationRepository.findByName ("SecondOrg")); userRepository.save (user2); }

ציין זאת:

  • למשתמש "ג'ון" יש רק FOO_READ_PRIVILEGE
  • למשתמש "טום" יש את שניהם FOO_READ_PRIVILEGE ו FOO_WRITE_PRIVILEGE

4. מעריך הרשאות מותאם אישית

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

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

בוא נתחיל.

4.1. PermissionEvaluator

על מנת ליצור מעריך הרשאות מותאם אישית משלנו עלינו ליישם את ה- PermissionEvaluator מִמְשָׁק:

המחלקה הציבורית CustomPermissionEvaluator מיישמת PermissionEvaluator {@Override has the public boolean hasPermission (Authentication auth, Object targetDomainObject, Object object) {if ((auth == null) || (targetDomainObject == null) ||! (הרשאת מופע מחרוזת)) {החזר שקר ; } מחרוזת targetType = targetDomainObject.getClass (). GetSimpleName (). ToUpperCase (); להחזיר hasPrivilege (auth, targetType, permission.toString (). toUpperCase ()); } @Override ציבורי בוליאני hasPermission (אימות אימות, targetId ניתן לסידור, targetType מחרוזת, הרשאת אובייקט) {if ((auth == null) || (targetType == null) ||! (הרשאה למשל של מחרוזת)) {return false; } להחזיר hasPrivilege (auth, targetType.toUpperCase (), permit.toString (). toUpperCase ()); }}

הנה שלנו hasPrivilege () שיטה:

פרטי בוליאני hasPrivilege (אימות אימות, מחרוזת targetType, מחרוזת הרשאה) {עבור (GrantedAuthority givenAuth: auth.getAuthorities ()) {if (givenAuth.getAuthority (). startsWith (targetType)) {if (givenAuth.getAuthority (). הרשאה)) {להחזיר נכון; }}} להחזיר שקר; }

כעת יש לנו ביטוי אבטחה חדש זמין ומוכן לשימוש: יש הרשאה.

וכך, במקום להשתמש בגרסה המקודדת יותר:

@PostAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')")

אנו יכולים להשתמש בשימוש:

@PostAuthorize ("hasPermission (returnObject, 'קרא')")

אוֹ

@PreAuthorize ("hasPermission (#id, 'Foo', 'read')")

הערה: #תְעוּדַת זֶהוּת מתייחס לפרמטר השיטה ו 'פו'מתייחס לסוג אובייקט היעד.

4.2. שיטת תצורת אבטחה

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

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) מחלקה ציבורית MethodSecurityConfig מרחיב את GlobalMethodSecurityConfiguration {@Override MethodSecurityExpressionHandler createExpressionHandler () {DefaultMethodSecurityExpressionHandler expressionHandler = ExpressMethodSecurityHandler; expressionHandler.setPermissionEvaluator (חדש CustomPermissionEvaluator ()); ביטוי להחזיר מפעיל; }}

4.3. דוגמה בפועל

נתחיל כעת להשתמש בביטוי החדש - בכמה שיטות בקר פשוטות:

@Controller מחלקה ציבורית MainController {@PostAuthorize ("hasPermission (returnObject, 'read')") @GetMapping ("/ foos / {id}") @ResponseBody public Foo findById (@PathVariable id) {להחזיר Foo חדש ("דוגמה "); } @PreAuthorize ("hasPermission (#foo, 'write')") @PostMapping ("/ foos") @ResponseStatus (HttpStatus.CREATED) @ResponseBody ציבור Foo ליצור (@RequestBody Foo foo) {להחזיר foo; }}

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

4.4. המבחן החי

בואו נכתוב כעת בדיקות חי פשוטות - להכות את ה- API ולוודא שהכל תקין:

@ מבחן חלל ציבורי givenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). מכיל ("id")); } @Test ציבורי בטל שניתןUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden () {תגובה תגובה = givenAuth ("john", "123"). ContentType (MediaType.APPLICATION_JSON_VALUE). גוף (Foo חדש ("מדגם // /):). foos "); assertEquals (403, response.getStatusCode ()); } @Test ציבורי בטל givenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk () {תגובה תגובה = givenAuth ("tom", "111"). ContentType (MediaType.APPLICATION_JSON_VALUE). גוף (Foo חדש ("דוגמה")). פוסט ("/ / local / foos "); assertEquals (201, response.getStatusCode ()); assertTrue (response.asString (). מכיל ("id")); }

והנה שלנו givenAuth () שיטה:

RequestSpecification פרטי givenAuth (שם משתמש מחרוזת, סיסמת מחרוזת) {FormAuthConfig formAuthConfig = new FormAuthConfig ("// localhost: 8082 / login", "שם משתמש", "סיסמה"); החזר RestAssured.given (). auth (). טופס (שם משתמש, סיסמה, formAuthConfig); }

5. ביטוי אבטחה חדש

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

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

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

5.1. ביטוי אבטחה בשיטה מותאמת אישית

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

מחלקה ציבורית CustomMethodSecurityExpressionRoot מרחיב את SecurityExpressionRoot מיישם MethodSecurityExpressionOperations {public CustomMethodSecurityExpressionRoot (אימות אימות) {סופר (אימות); } ציבור בוליאני isMember (Long OrganizationId) {משתמש משתמש = ((MyUserPrincipal) this.getPrincipal ()). getUser (); להחזיר user.getOrganization (). getId (). longValue () == OrganizationId.longValue (); } ...}

עכשיו איך סיפקנו את הפעולה החדשה הזו ממש בהערת השורש כאן; isMember () משמש כדי לבדוק אם המשתמש הנוכחי חבר בנתון אִרגוּן.

שימו לב גם כיצד הרחבנו את SecurityExpressionRoot לכלול גם את הביטויים המובנים.

5.2. מטפל ביטוי בהתאמה אישית

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

מחלקה ציבורית CustomMethodSecurityExpressionHandler מרחיב DefaultMethodSecurityExpressionHandler {private AuthenticationTrustResolver trustResolver = AuthenticationTrustResolverImpl (); MethodSecurityExpressionOperations מוגן @Override createSecurityExpressionRoot (אימות אימות, קריאת MethodInvocation) {CustomMethodSecurityExpressionRoot root = חדש CustomMethodSecurityExpressionRoot (אימות); root.setPermissionEvaluator (getPermissionEvaluator ()); root.setTrustResolver (this.trustResolver); root.setRoleHierarchy (getRoleHierarchy ()); שורש החזרה; }}

5.3. שיטת תצורת אבטחה

עכשיו, אנחנו צריכים להשתמש שלנו CustomMethodSecurityExpressionHandler בתצורת אבטחת השיטה:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) מחלקה ציבורית MethodSecurityConfig מרחיב את GlobalMethodSecurityConfiguration {@Override MethodSecurityExpressionHandler createExpressionHandler () {CustomMethodSecurityExpressionHandler expressionHandler = ExpressMethodSecurityHandler; expressionHandler.setPermissionEvaluator (חדש CustomPermissionEvaluator ()); ביטוי להחזיר מפעיל; }}

5.4. שימוש בביטוי החדש

הנה דוגמה פשוטה לאבטחת שיטת הבקר שלנו באמצעות isMember ():

@PreAuthorize ("isMember (#id)") @GetMapping ("/ organisations / {id}") @ResponseBody הארגון הציבורי findOrgById (@PathVariable id) {return organizationRepository.findOne (id); }

5.5. מבחן חי

לבסוף, הנה מבחן חי פשוט למשתמש "ג'ון“:

@ מבחן חלל ציבורי givenUserMemberInOrganization_whenGetOrganization_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisations / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). מכיל ("id")); } @Test public void givenUserMemberNotInOrganization_whenGetOrganization_thenForbidden () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisations / 2"); assertEquals (403, response.getStatusCode ()); }

6. השבת ביטוי אבטחה מובנה

לבסוף, בואו נראה כיצד לעקוף ביטוי אבטחה מובנה - נדון בהשבתה hasAuthority ().

6.1. שורש ביטוי אבטחה בהתאמה אישית

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

מחלקה ציבורית MySecurityExpressionRoot מיישם MethodSecurityExpressionOperations {public MySecurityExpressionRoot (אימות אימות) {if (אימות == null) {זרוק IllegalArgumentException חדש ("אובייקט אימות לא יכול להיות ריק"); } this.authentication = אימות; } @Override הציבורי הסופי הבוליאני hasAuthority (מחרוזת מחרוזת) {לזרוק RuntimeException חדש ("שיטה hasAuthority () אסור"); } ...}

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

6.2. דוגמא - שימוש בביטוי

עכשיו, אם אנחנו רוצים להשתמש hasAuthority () כדי לאבטח שיטות - כדלקמן, זה יזרוק חריגת זמן ריצה כאשר אנו מנסים לגשת לשיטה:

@PreAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')") @GetMapping ("/ foos") @ResponseBody ציבור Foo findFooByName (@RequestParam שם מחרוזת) {להחזיר Foo חדש (שם); }

6.3. מבחן חי

לבסוף, הנה המבחן הפשוט שלנו:

@Test public void givenDisabledSecurityExpression_whenGetFooByName_thenError () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos? Name = sample"); assertEquals (500, response.getStatusCode ()); assertTrue (response.asString (). מכיל ("שיטה hasAuthority () אסור")); }

7. מסקנה

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

וכמו תמיד, ניתן למצוא את קוד המקור המלא ב- GitHub.


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