אביב ביטחון מול אפאצ'י שירו

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

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

במדריך המהיר הזה, נשווה בין שתי מסגרות אבטחה פופולריות של Java - Apache Shiro ו- Spring Security.

2. רקע קטן

אפאצ'י שירו ​​נולד בשנת 2004 כ- JSecurity והתקבל על ידי קרן אפאצ'י בשנת 2008. עד היום היא ראתה מהדורות רבות, האחרונה לכתיבת שורות אלה היא 1.5.3.

Spring Security החלה את דרכה ב- Acegi בשנת 2003 ושולבה במסגרת Spring Framework עם פרסומה הציבורי הראשון בשנת 2008. מאז הקמתה, היא עברה מספר חזרות וגרסת ה- GA הנוכחית נכון לכתיבת שורות אלה היא 5.3.2.

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

בחלקים הקרובים נראה דוגמאות לאופן שבו שתי הטכנולוגיות מטפלות באימות והרשאה. כדי לשמור על הפשטות, נשתמש ביישומי MVC בסיסיים מבוססי Spring Boot עם תבניות FreeMarker.

3. קביעת תצורה של אפאצ'י שירו

ראשית, בואו נראה כיצד תצורות שונות בין שתי המסגרות.

3.1. תלות Maven

מכיוון שנשתמש ב- Shiro באפליקציית Boot Boot, נצטרך את המתנע שלו ואת ה- שירו-ליבה מודול:

 org.apache.shiro shiro-spring-boot-web-starter 1.5.3 org.apache.shiro shiro-core 1.5.3 

ניתן למצוא את הגרסאות האחרונות ב- Maven Central.

3.2. יצירת ממלכה

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

מחלקה ציבורית CustomRealm מרחיבה את JdbcRealm {פרטי כניסה למפה פרטית = HashMap חדש (); תפקידי מפה פרטיים = HashMap חדש (); הרשאות מפה פרטיות = HashMap חדש (); {credentials.put ("טום", "סיסמה"); credentials.put ("ג'רי", "סיסמה"); roller.put ("ג'רי", HashSet חדש (Arrays.asList ("מנהל"))); roller.put ("טום", HashSet חדש (Arrays.asList ("משתמש"))); permissions.put ("ADMIN", HashSet חדש (Arrays.asList ("READ", "WRITE"))); permissions.put ("USER", HashSet חדש (Arrays.asList ("READ")); }}

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

@Override מוגן AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) זורק AuthenticationException {UsernamePasswordToken userToken = (UsernamePasswordToken) אסימון; אם (userToken.getUsername () == null || userToken.getUsername (). isEmpty () ||! credentials.containsKey (userToken.getUsername ())) {זרוק UnknownAccountException חדש ("המשתמש לא קיים"); } להחזיר SimpleAuthenticationInfo חדש (userToken.getUsername (), credentials.get (userToken.getUsername ()), getName ()); } @Override מוגן AuthorizationInfo doGetAuthorizationInfo (ראשי PrincipalCollection) {הגדר תפקידים = HashSet חדש (); הגדר הרשאות = HashSet חדש (); עבור (משתמש אובייקט: עיקרי) {נסה {roller.addAll (getRoleNamesForUser (משתמש null, (מחרוזת))); permissions.addAll (getPermissions (null, null, תפקידים)); } לתפוס (SQLException e) {logger.error (e.getMessage ()); }} SimpleAuthorizationInfo authInfo = SimpleAuthorizationInfo חדש (תפקידים); authInfo.setStringPermissions (הרשאות); החזר authInfo; } 

השיטה doGetAuthorizationInfo משתמש בכמה שיטות עוזרות לקבלת תפקידי המשתמש והרשאותיו:

מוגן על ידי @Override הגדר getRoleNamesForUser (חיבור חיבור, שם משתמש מחרוזת) זורק SQLException {אם (! Role.containsKey (שם משתמש)) {זורק SQLException חדש ("המשתמש לא קיים"); } להחזיר role.get (שם משתמש); } @Override מוגן הגדר getPermissions (חיבור חיבור, שם משתמש מחרוזת, תפקידי אוסף) זורק SQLException {הגדר userPermissions = חדש HashSet (); עבור (תפקיד מחרוזת: תפקידים) {if (! permissions.containsKey (role)) {זרוק SQLException חדש ("התפקיד לא קיים"); } userPermissions.addAll (permissions.get (role)); } להחזיר userPermissions; } 

לאחר מכן, עלינו לכלול זאת CustomRealm כשעועית ביישום האתחול שלנו:

@Bean Public Realm customRealm () {להחזיר CustomRealm חדש (); }

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

@Bean ציבורי ShiroFilterChainDefinition shiroFilterChainDefinition () {DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition (); filter.addPathDefinition ("/ home", "authc"); filter.addPathDefinition ("/ **", "אנון"); מסנן החזרה; }

כאן, באמצעות א DefaultShiroFilterChainDefinition למשל, ציינו כי שלנו /בית ניתן לגשת לנקודת קצה רק על ידי משתמשים מאומתים.

זה כל מה שאנחנו צריכים עבור התצורה, שירו ​​עושה את השאר בשבילנו.

4. קביעת תצורת אבטחת האביב

עכשיו בואו נראה איך להשיג את אותו הדבר באביב.

4.1. תלות Maven

ראשית, התלות:

 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security 

ניתן למצוא את הגרסאות האחרונות ב- Maven Central.

4.2. מחלקת תצורה

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

@EnableWebSecurity המחלקה הציבורית SecurityConfig מרחיב את WebSecurityConfigurerAdapter {@Override מוגן ריק להגדיר (HttpSecurity http) זורק חריג {http .authorizeRequests (authorize -> authorize .antMatchers ("/ index", "/ login"). PermitAll () .antMatchers ("/ home "," / logout "). מאומת () .antMatchers (" / admin / ** "). hasRole (" ADMIN ")) .formLogin (formLogin -> formLogin .loginPage (" / login ") .failureUrl (" /שגיאת התחברות")); } התצורה הריקה המוגנת על ידי @Override (AuthenticationManagerBuilder auth) זורקת חריג {auth.inMemoryAuthentication () .withUser ("Jerry"). סיסמה (passwordEncoder (). קידוד ("סיסמה")). הרשויות ("READ", "WRITE") .roles ("ADMIN"). ו- () .withUser ("Tom"). סיסמה (passwordEncoder (). קידוד ("סיסמה")) .אוטוריות ("READ") .roles ("USER"); } @Bean סיסמא ציבורית Encoder סיסמא Encoder () {להחזיר BCryptPasswordEncoder חדש (); }} 

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

Spring Security מספק לנו גם את שלו HttpSecurity אובייקט לתצורות נוספות. לדוגמא שלנו, אפשרנו:

  • לכולם לגשת ל אינדקס ו התחברות עמודים
  • רק משתמשים מאומתים להיכנס ל- בית עמוד ו להתנתק
  • רק משתמשים עם תפקיד ADMIN לגישה ל- מנהל עמודים

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

5. בקרים ונקודות קצה

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

5.1. נקודות קצה לעיבוד תצוגה

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

@GetMapping ("/") אינדקס מחרוזות ציבורי () {return "index"; } @ GetMapping ("/ login") ציבורי מחרוזת showLoginPage () {החזר "כניסה"; } @GetMapping ("/ home") מחרוזת ציבורית getMeHome (מודל מודל) {addUserAttributes (מודל); לחזור הביתה"; }

גם יישומי הבקר שלנו, שירו, כמו גם אבטחת אביב, מחזירים את index.ftl בנקודת הקצה הבסיסית, login.ftl בנקודת הסיום של הכניסה, ו- home.ftl על נקודת הקצה הביתית.

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

שירו מספק א SecurityUtils # getSubject כדי לאחזר את הזרם נושאותפקידיה והרשאותיה:

ריק ריק addUserAttributes (מודל מודל) {נושא currentUser = SecurityUtils.getSubject (); אישור מחרוזת = ""; אם (currentUser.hasRole ("ADMIN")) {model.addAttribute ("תפקיד", "ADMIN"); } אחרת אם (currentUser.hasRole ("USER")) {model.addAttribute ("role", "USER"); } אם (currentUser.isPermitted ("READ")) {הרשאה = הרשאה + "READ"; } אם (currentUser.isPermitted ("WRITE")) {הרשאה = הרשאה + "WRITE"; } model.addAttribute ("שם משתמש", currentUser.getPrincipal ()); model.addAttribute ("הרשאה", הרשאה); }

מצד שני, Spring Security מספקת אימות אובייקט משלו SecurityContextHolderההקשר למטרה זו:

ריק ריק addUserAttributes (מודל מודל) {Authentication auth = SecurityContextHolder.getContext (). getAuthentication (); אם (auth! = null &&! auth.getClass (). שווה ל- (AnonymousAuthenticationToken.class)) {User user = (User) auth.getPrincipal (); model.addAttribute ("שם משתמש", user.getUsername ()); רשויות איסוף = user.getAuthorities (); עבור (סמכות הרשות הניתנת: רשויות) {אם (סמכות. getAuthority (). מכיל ("USER")) {model.addAttribute ("תפקיד", "USER"); model.addAttribute ("הרשאות", "READ"); } אחרת אם (Authority.getAuthority (). מכיל ("ADMIN")) {model.addAttribute ("תפקיד", "ADMIN"); model.addAttribute ("הרשאות", "READ WRITE"); }}}}

5.2. נקודת סיום להתחברות POST

ב- Shiro אנו ממפים את האישורים שהמשתמש מזין ל- POJO:

מחלקה ציבורית UserCredentials {שם משתמש פרטי מחרוזת; סיסמת מחרוזת פרטית; // גטרים וקובעים}

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

@ PostMapping ("/ login") ציבורי מחרוזת doLogin (HttpServletRequest req, UserCredentials credentials, RedirectAttributes attr) {Subject subject = SecurityUtils.getSubject (); if (! subject.isAuthenticated ()) {UsernamePasswordToken token = new UsernamePasswordToken (credentials.getUsername (), credentials.getPassword ()); נסה את {subject.login (אסימון); } לתפוס (AuthenticationException ae) {logger.error (ae.getMessage ()); attr.addFlashAttribute ("שגיאה", "אישורים לא חוקיים"); להחזיר "redirect: / login"; }} להחזיר "redirect: / home"; }

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

@PostMapping ("/ login") ציבורי מחרוזת doLogin (HttpServletRequest req) {return "redirect: / home"; }

5.3. נקודת סיום למנהל בלבד

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

בואו נראה איך לעשות זאת בשירו:

@GetMapping ("/ admin") ציבורי מחרוזת adminOnly (ModelMap modelMap) {addUserAttributes (modelMap); נושא currentUser = SecurityUtils.getSubject (); אם (currentUser.hasRole ("ADMIN")) {modelMap.addAttribute ("adminContent", "רק admin יכול להציג זאת"); } לחזור הביתה"; }

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

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

@GetMapping ("/ admin") ציבורי מחרוזת adminOnly (ביקורת HttpServletRequest, מודל מודל) {addUserAttributes (מודל); model.addAttribute ("adminContent", "רק admin יכול להציג זאת"); לחזור הביתה"; }

5.4. נקודת סיום של יציאה

לבסוף, בוא ניישם את נקודת הסיום של התנתקות.

בשירו פשוט נתקשר נושא # התנתקות:

@PostMapping ("/ התנתק") ציבורי מחרוזת () נושא נושא = SecurityUtils.getSubject (); subject.logout (); להחזיר "redirect: /"; }

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

6. אפאצ'י שירו ​​מול אבטחת האביב

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

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

לגבי תיעוד, אביב שוב הוא המנצח.

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

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

7. מסקנה

במדריך זה, השווינו את אפאצ'י שירו ​​עם Spring Security.

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

כמו תמיד, קוד המקור זמין ב- GitHub.


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