ביטוי אבטחה מותאם אישית עם אבטחת אביב
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; }}
ושם אנחנו הולכים - כולנו מסודרים ומשתמשים בביטוי החדש בפועל. בואו נכתוב כעת בדיקות חי פשוטות - להכות את ה- API ולוודא שהכל תקין: והנה שלנו givenAuth () שיטה: בעזרת הפיתרון הקודם הצלחנו להגדיר ולהשתמש ב- יש הרשאה ביטוי - שיכול להיות די שימושי. עם זאת, אנחנו עדיין מוגבלים במקצת על ידי השם והסמנטיקה של הביטוי עצמו. וכך, בחלק זה, נעבור מנהג מלא - ונממש ביטוי ביטחוני שנקרא isMember () - בדיקה אם המנהל חבר בארגון. על מנת ליצור ביטוי מותאם אישית חדש זה, עלינו להתחיל ביישום הערת השורש בה מתחילה הערכת כל ביטויי האבטחה: עכשיו איך סיפקנו את הפעולה החדשה הזו ממש בהערת השורש כאן; isMember () משמש כדי לבדוק אם המשתמש הנוכחי חבר בנתון אִרגוּן. שימו לב גם כיצד הרחבנו את SecurityExpressionRoot לכלול גם את הביטויים המובנים. לאחר מכן, עלינו להזריק את שלנו CustomMethodSecurityExpressionRoot במטפל ההבעה שלנו: עכשיו, אנחנו צריכים להשתמש שלנו CustomMethodSecurityExpressionHandler בתצורת אבטחת השיטה: הנה דוגמה פשוטה לאבטחת שיטת הבקר שלנו באמצעות isMember (): לבסוף, הנה מבחן חי פשוט למשתמש "ג'ון“: לבסוף, בואו נראה כיצד לעקוף ביטוי אבטחה מובנה - נדון בהשבתה hasAuthority (). נתחיל באופן דומה על ידי כתיבת משלנו SecurityExpressionRoot - בעיקר בגלל שהשיטות המובנות הן סופי וכך אנו לא יכולים לעקוף אותם: לאחר הגדרת הערת שורש זו, נצטרך להזרים אותה למטפל הביטוי ואז לחבר את המטפל לתצורה שלנו - בדיוק כפי שעשינו לעיל בסעיף 5. עכשיו, אם אנחנו רוצים להשתמש hasAuthority () כדי לאבטח שיטות - כדלקמן, זה יזרוק חריגת זמן ריצה כאשר אנו מנסים לגשת לשיטה: לבסוף, הנה המבחן הפשוט שלנו: במדריך זה ערכנו צלילה מעמיקה בדרכים השונות בהן אנו יכולים ליישם ביטוי אבטחה מותאם אישית ב- Spring Security, אם אלה הקיימים אינם מספיקים. וכמו תמיד, ניתן למצוא את קוד המקור המלא ב- GitHub.4.4. המבחן החי
@ מבחן חלל ציבורי 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")); }
RequestSpecification פרטי givenAuth (שם משתמש מחרוזת, סיסמת מחרוזת) {FormAuthConfig formAuthConfig = new FormAuthConfig ("// localhost: 8082 / login", "שם משתמש", "סיסמה"); החזר RestAssured.given (). auth (). טופס (שם משתמש, סיסמה, formAuthConfig); }
5. ביטוי אבטחה חדש
5.1. ביטוי אבטחה בשיטה מותאמת אישית
מחלקה ציבורית CustomMethodSecurityExpressionRoot מרחיב את SecurityExpressionRoot מיישם MethodSecurityExpressionOperations {public CustomMethodSecurityExpressionRoot (אימות אימות) {סופר (אימות); } ציבור בוליאני isMember (Long OrganizationId) {משתמש משתמש = ((MyUserPrincipal) this.getPrincipal ()). getUser (); להחזיר user.getOrganization (). getId (). longValue () == OrganizationId.longValue (); } ...}
5.2. מטפל ביטוי בהתאמה אישית
מחלקה ציבורית 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. שיטת תצורת אבטחה
@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) מחלקה ציבורית MethodSecurityConfig מרחיב את GlobalMethodSecurityConfiguration {@Override MethodSecurityExpressionHandler createExpressionHandler () {CustomMethodSecurityExpressionHandler expressionHandler = ExpressMethodSecurityHandler; expressionHandler.setPermissionEvaluator (חדש CustomPermissionEvaluator ()); ביטוי להחזיר מפעיל; }}
5.4. שימוש בביטוי החדש
@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. השבת ביטוי אבטחה מובנה
6.1. שורש ביטוי אבטחה בהתאמה אישית
מחלקה ציבורית MySecurityExpressionRoot מיישם MethodSecurityExpressionOperations {public MySecurityExpressionRoot (אימות אימות) {if (אימות == null) {זרוק IllegalArgumentException חדש ("אובייקט אימות לא יכול להיות ריק"); } this.authentication = אימות; } @Override הציבורי הסופי הבוליאני hasAuthority (מחרוזת מחרוזת) {לזרוק RuntimeException חדש ("שיטה hasAuthority () אסור"); } ...}
6.2. דוגמא - שימוש בביטוי
@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. מסקנה