במהלך כתיבת הבקשה שלי, נתקלתי בחוסר מאמרים ברורים כיצד לגרום למשתמש להירשם הן באמצעות האימייל והן ברשתות החברתיות. היו הדרכות טובות על הגדרת טופס הכניסה הקלאסי. היו הדרכות טובות על OAuth2 . היה מעט מידע מבחינה פלילית כיצד לשלב את שתי השיטות. במהלך תהליך החיפוש, הצלחנו למצוא פתרון ישים. היא לא מתיימרת להיות האמת האולטימטיבית, אבל היא ממלאת את תפקידה. במאמר זה אראה כיצד ליישם שירות אחסון פתקים עם תצורת Spring Security דומה מאפס. הערה: טוב אם הקורא עבר לפחות כמה מדריכים על Spring, כי תשומת הלב תתמקד רק ב-Spring Security, ללא הסברים מפורטים על מאגרים, בקרים וכו'. אחרת, מאמר די גדול כבר יתברר ל להיות ענק. תוֹכֶן
אנו רואים שמשתמש חדש הופיע במסד הנתונים. הסיסמה מוצפנת.
ההערות נשמרות במסד הנתונים.
אנו רואים שהפרויקט יצא לדרך בהצלחה. בשביל אושר מוחלט, אנחנו צריכים רק את היכולת להיכנס דרך רשתות חברתיות. ובכן, בואו נתחיל!
כעת גם User וגם OAuth2Authentication יכולים לפעול כמנהל. מכיוון שלקחנו בחשבון ב-UserService מראש את טעינת המשתמש דרך נתוני גוגל, האפליקציה תעבוד על שני סוגי המשתמשים. אנו משנים את בקר הדף הראשי של הפרויקט כך שיפנה מחדש משתמשים המחוברים באמצעות OAuth2 לדף ההערות.
המשתמש נכנס בהצלחה הן דרך הטופס הרגיל והן דרך חשבון גוגל. זה מה שרצינו! אני מקווה שמאמר זה הבהיר כמה נקודות לגבי יצירת יישום אינטרנט, אבטחתו באמצעות Spring Security ושילוב שיטות כניסה שונות. עם קוד הפרויקט המלא אתה יכול
- יצירת פרויקט
- יצירת ישויות והיגיון יישומים
- הגדרת Spring Security עבור התחברות קלאסית
- הגדרת OAuth2 באמצעות Google כדוגמה ב-Spring Security
- תצורת סינון ויישום.מאפיינים
- דגשים ברישום אפליקציה ב-Google Cloud Platform
- CustomUserInfoTokenServices
- השקה סופית של הפרויקט
יצירת פרויקט
אנחנו הולכים אל start.spring.io ומהווים את הבסיס לפרויקט:- אינטרנט - הפעלת אפליקציה על Tomcat המובנה, מיפוי כתובות אתרים וכדומה;
- JPA - חיבור למסד נתונים;
- שפם הוא מנוע תבנית המשמש ליצירת דפי אינטרנט;
- אבטחה - הגנה על אפליקציות. בשביל זה נוצר המאמר הזה.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
תצורת application.properties היא כעת כדלקמן:
spring.datasource.url=jdbc:mysql://localhost:3306/springsectut?createDatabaseIfNotExist=true&useSSL=false&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=yourUsername
spring.datasource.password=yourPassword
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.connection.characterEncoding=utf-8
spring.jpa.properties.connection.CharSet=utf-8
spring.jpa.properties.connection.useUnicode=true
spring.mustache.expose-request-attributes=true
יצירת ישויות והיגיון יישומים
ישויות
בואו ניצור חבילהentities
בה נמקם את ישויות מסד הנתונים. המשתמש יתואר על ידי מחלקה User
המיישמת את הממשק UserDetails
, אשר יידרש עבור תצורת Spring Security. למשתמש יהיה מזהה, שם משתמש (זהו דוא"ל), סיסמה, שם, תפקיד, דגל פעילות, שם חשבון Google ואימייל ( googleName
ו googleUsername
).
@Entity
@Table(name = "user")
public class User implements UserDetails
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private String name;
private boolean active;
private String googleName;
private String googleUsername;
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
@Enumerated(EnumType.STRING)
private Set<Role> roles;
//Геттеры, сеттеры, toString(), equals(), hashcode(), имплементация UserDetails
}
תפקידי משתמש משמשים להסדרת גישה ב-Spring Security. האפליקציה שלנו תשתמש רק בתפקיד אחד:
public enum Role implements GrantedAuthority
{
USER;
@Override
public String getAuthority()
{
return name();
}
}
בואו ניצור מחלקה הערה עם מזהה, כותרת הערה, גוף הערה ומזהה המשתמש שאליו היא שייכת:
@Entity
@Table(name = "note")
public class Note
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private String note;
private Long userId;
//Геттеры, сеттеры, toString(), equals(), hashcode()
}
מאגרים
כדי לשמור ישויות למסד הנתונים, אנחנו צריכים מאגרים שיעשו עבורנו את כל העבודה המלוכלכת. בואו ניצור חבילהrepos
, בה ניצור ממשקים UserRepo
שהועברו בירושה NoteRepo
מהממשק JpaRepository<Entity, Id>
.
@Service
@Repository
public interface UserRepo extends JpaRepository<User, Long>
{}
@Service
@Repository
public interface NoteRepo extends JpaRepository<Note, Long>
{
List<Note> findByUserId(Long userId);
}
בקרים
לשירות ההערות שלנו יהיו הדפים הבאים:- בית;
- הַרשָׁמָה;
- כְּנִיסָה;
- רשימת הערות משתמש.
controllers
המכילה מחלקה IndexController
המכילה את המיפוי הרגיל של העמוד הראשי. הכיתה RegistrationController
אחראית על רישום המשתמש. פוסט מיפוי לוקח נתונים מהטופס, שומר את המשתמש למסד הנתונים ומפנה לדף הכניסה. PasswordEncoder
יתואר בהמשך. הוא משמש להצפנת סיסמאות.
@Controller
public class RegistrationController
{
@Autowired
private UserRepo userRepo;
@Autowired
private PasswordEncoder passwordEncoder;
@GetMapping("/registration")
public String registration()
{
return "registration";
}
@PostMapping("/registration")
public String addUser(String name, String username, String password)
{
User user = new User();
user.setName(name);
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
userRepo.save(user);
return "redirect:/login";
}
הבקר האחראי על דף רשימת ההערות מכיל כרגע פונקציונליות פשוטה, שתהפוך מורכבת יותר לאחר יישום Spring Security.
@Controller
public class NoteController
{
@Autowired
private NoteRepo noteRepo;
@GetMapping("/notes")
public String notes(Model model)
{
List<Note> notes = noteRepo.findAll();
model.addAttribute("notes", notes);
return "notes";
}
@PostMapping("/addnote")
public String addNote(String title, String note)
{
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
noteRepo.save(newNote);
return "redirect:/notes";
}
}
לא נכתוב בקר עבור דף הכניסה מכיוון שהוא משמש את Spring Security. במקום זאת, נצטרך תצורה מיוחדת. כרגיל, בואו ניצור חבילה נוספת, נקרא לה config
ונמקם את הכיתה שם MvcConfig
. כאשר נכתוב את תצורת Spring Security, הוא יידע לאיזה עמוד אנו מתייחסים כאשר אנו משתמשים ב-"/login".
@Configuration
public class MvcConfig implements WebMvcConfigurer
{
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
דפים
אני משתמש במנוע תבנית Mustache כדי ליצור דפים . אתה יכול ליישם עוד אחד, זה לא משנה. נוצר קובץ meta.mustache עבור המטא מידע שנמצא בשימוש בכל הדפים. זה כולל גם Bootstrap כדי לגרום לדפי הפרויקט שלנו להיראות יפים יותר. דפים נוצרים בספריית "src/main/resources/templates". לקבצים יש את הסיומת שפם. הצבת קוד ה-html ישירות במאמר תהפוך אותו לגדול מדי, אז הנה קישור לתיקיית התבניות במאגר GitHub של הפרויקט .הגדרת Spring Security עבור התחברות קלאסית
Spring Security עוזרת לנו להגן על האפליקציה והמשאבים שלה מפני גישה לא מורשית. ניצור תצורת עבודה תמציתית בכיתהSecurityConfig
שעברה בירושה WebSecurityConfigurerAdapter
, אותה נמקם בחבילה config
. בואו נסמן אותו עם הערת @EnableWebSecurity, שתאפשר תמיכה ב-Spring Security, והערת @Configuration, שמציינת שהמחלקה הזו מכילה תצורה כלשהי. הערה: הגרסה של pom.xml שהוגדרה אוטומטית מכילה את רכיב האב של Spring Boot 2.1.4.RELEASE, שמנעה את הטמעת האבטחה בצורה שנקבעה. כדי למנוע התנגשויות בפרויקט, מומלץ לשנות את הגרסה ל-2.0.1.RELEASE.
תצורה בסיסית SecurityConfig
התצורה שלנו תוכל:-
הצפנת סיסמאות באמצעות
BCryptPasswordEncoder
:@Autowired private PasswordEncoder passwordEncoder; @Bean PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; }
-
היכנס באמצעות ספק אימות שנכתב במיוחד:
@Autowired private AuthProvider authProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authProvider); }
-
אפשר למשתמשים אנונימיים גישה לדף הבית, לדפי הרישום וההתחברות. כל שאר הבקשות חייבות להתבצע על ידי משתמשים מחוברים. הבה נקצה את ה-"/login" שתואר קודם כדף הכניסה. אם הכניסה מוצלחת, המשתמש יובא לדף עם רשימת הערות; אם יש שגיאה, המשתמש יישאר בדף הכניסה. לאחר יציאה מוצלחת, המשתמש יועבר לעמוד הראשי.
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/resources/**", "/", "/login**", "/registration").permitAll() .anyRequest().authenticated() .and().formLogin().loginPage("/login") .defaultSuccessUrl("/notes").failureUrl("/login?error").permitAll() .and().logout().logoutSuccessUrl("/").permitAll(); }
התחברות משתמש מותאמת אישית
אחד שנכתב בעצמוAuthProvider
יאפשר למשתמש להיכנס לא רק בדוא"ל, אלא גם לפי שם משתמש.
@Component
public class AuthProvider implements AuthenticationProvider
{
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
String username = authentication.getName();
String password = (String) authentication.getCredentials();
User user = (User) userService.loadUserByUsername(username);
if(user != null && (user.getUsername().equals(username) || user.getName().equals(username)))
{
if(!passwordEncoder.matches(password, user.getPassword()))
{
throw new BadCredentialsException("Wrong password");
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, password, authorities);
}
else
throw new BadCredentialsException("Username not found");
}
public boolean supports(Class<?> arg)
{
return true;
}
}
כפי שאולי שמתם לב, הכיתה UserService
שנמצאת בחבילה אחראית לטעינת המשתמש services
. במקרה שלנו, הוא מחפש משתמש לא רק לפי השדה username
, כמו היישום המובנה, אלא גם לפי שם משתמש, שם חשבון Google ודוא"ל חשבון Google. שתי השיטות האחרונות יהיו שימושיות עבורנו בעת יישום התחברות באמצעות OAuth2. כאן השיעור ניתן בגרסה מקוצרת.
@Service
public class UserService implements UserDetailsService
{
@Autowired
private UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User userFindByUsername = userRepo.findByUsername(username);
//Остальные поиски
if(userFindByUsername != null)
{
return userFindByUsername;
}
//Остальные проверки
return null;
}
}
הערה: אל תשכח לכתוב את השיטות הדרושות ב- UserRepo
!
בואו נשפר את הבקר
הגדרנו את Spring Security. עכשיו זה הזמן לנצל את זה בבקר ההערות שלך. כעת כל מיפוי יקבל פרמטר Principal נוסף, שבאמצעותו הוא ינסה למצוא את המשתמש. למה אני לא יכול להזריק ישירות לכיתהUser
? אז תהיה התנגשות עקב אי התאמה בין סוגי המשתמשים כאשר אנו כותבים כניסה דרך רשתות חברתיות. אנו מספקים את הגמישות הנדרשת מראש. קוד בקר ההערות שלנו נראה כעת כך:
@GetMapping("/notes")
public String notes(Principal principal, Model model)
{
User user = (User) userService.loadUserByUsername(principal.getName());
List<Note> notes = noteRepo.findByUserId(user.getId());
model.addAttribute("notes", notes);
model.addAttribute("user", user);
return "notes";
}
@PostMapping("/addnote")
public String addNote(Principal principal, String title, String note)
{
User user = (User) userService.loadUserByUsername(principal.getName());
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
newNote.setUserId(user.getId());
noteRepo.save(newNote);
return "redirect:/notes";
}
הערה: לפרויקט הגנת CSRF מופעלת כברירת מחדל , אז או שתבטל אותו בעצמך (http.csrf().disable()), או אל תשכח, כמחבר המאמר, להוסיף שדה נסתר עם אסימון csrf לכל הבקשות לפרסום.
לְהַשִׁיק
אנחנו מנסים להשיק את הפרויקט.הגדרת OAuth2 באמצעות Google כדוגמה ב-Spring Security
בעת הטמעת OAuth2, הסתמכתי על הדרכה רשמית זו מ-Spring . כדי לתמוך ב-OAuth2, הוסף את הספרייה הבאה אל pom.xml:<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
בואו נשנה את תצורת ה-Spring Security שלנו ב- SecurityConfig
. ראשית, בואו נוסיף את ההערה @EnableOAuth2Client. זה יביא אוטומטית את מה שאתה צריך כדי להיכנס דרך רשתות חברתיות.
תצורת סינון ויישום.מאפיינים
בואו נזריק את OAuth2ClientContext לשימוש בתצורת האבטחה שלנו.@Autowired
private OAuth2ClientContext oAuth2ClientContext;
OAuth2ClientContext משמש בעת יצירת מסנן המאמת את בקשת ההתחברות החברתית של המשתמש. המסנן זמין הודות לביאור @EnableOAuth2Client. כל מה שאנחנו צריכים לעשות הוא לקרוא לזה בסדר הנכון, לפני מסנן ה-Spring Security הראשי. רק אז נוכל לתפוס הפניות מחדש במהלך תהליך הכניסה עם OAuth2. לשם כך, אנו משתמשים ב- FilterRegistrationBean
, שבו אנו מגדירים את העדיפות של המסנן שלנו ל-100.
@Bean
public FilterRegistrationBean oAuth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter)
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(oAuth2ClientContextFilter);
registration.setOrder(-100);
return registration;
}
private Filter ssoFilter()
{
OAuth2ClientAuthenticationProcessingFilter googleFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/google");
OAuth2RestTemplate googleTemplate = new OAuth2RestTemplate(google(), oAuth2ClientContext);
googleFilter.setRestTemplate(googleTemplate);
CustomUserInfoTokenServices tokenServices = new CustomUserInfoTokenServices(googleResource().getUserInfoUri(), google().getClientId());
tokenServices.setRestTemplate(googleTemplate);
googleFilter.setTokenServices(tokenServices);
tokenServices.setUserRepo(userRepo);
tokenServices.setPasswordEncoder(passwordEncoder);
return googleFilter;
}
אתה גם צריך להוסיף מסנן חדש לפונקציה configure(HttpSecurity http):
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
המסנן גם צריך לדעת שהלקוח נרשם דרך גוגל. ההערה @ConfigurationProperties מציינת אילו מאפייני תצורה לחפש ב- application.properties.
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
return new AuthorizationCodeResourceDetails();
}
כדי להשלים את האימות, עליך לציין את נקודת הקצה של פרטי המשתמש של Google:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
return new ResourceServerProperties();
}
לאחר שרשמנו את האפליקציה שלנו ב- Google Cloud Platform , נוסיף נכסים עם הקידומות המתאימות ל application.properties:
google.client.clientId=yourClientId
google.client.clientSecret=yourClientSecret
google.client.accessTokenUri=https://www.googleapis.com/oauth2/v4/token
google.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/v2/auth
google.client.clientAuthenticationScheme=form
google.client.scope=openid,email,profile
google.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo
google.resource.preferTokenInfo=true
דגשים ברישום אפליקציה ב- Google Cloud Platform
נתיב: ממשקי API ושירותים -> אישורים חלון בקשת גישה ל-OAuth:- שם האפליקציה: טופס התחברות אביבית ומדריך OAuth2
- כתובת דוא"ל לתמיכה: האימייל שלך
- היקף עבור Google API: אימייל, פרופיל, openid
- דומיינים מורשים: me.org
- קישור לעמוד הראשי של האפליקציה: http://me.org:8080
- קישור למדיניות הפרטיות של האפליקציה: http://me.org:8080
- קישור לתנאי השימוש של היישום: http://me.org:8080
- סוג: יישום אינטרנט
- כותרת: טופס התחברות אביבית ומדריך OAuth2
- מקורות JavaScript מותרים: http://me.org, http://me.org:8080
- כתובת URI מותרת להפניה מחדש: http://me.org:8080/login, http://me.org:8080/login/google
CustomUserInfoTokenServices
שמתם לב למילה Custom בתיאור פונקציית הסינון? מעמדCustomUserInfoTokenServices
. כן, ניצור מחלקה משלנו עם בלאק ג'ק ויכולת להציל את המשתמש במסד הנתונים! באמצעות קיצור המקשים Ctrl-N ב- IntelliJ IDEA, תוכל למצוא ולראות כיצד UserInfoTokenServices
מיושמת ברירת המחדל. הבה נעתיק את הקוד שלו למחלקה החדשה שנוצרה CustomUserInfoTokenServices
. ניתן להשאיר את רובו ללא שינוי. לפני שנשנה את ההיגיון של הפונקציות, בואו נוסיף UserRepo
ושדות פרטיים של המחלקה PasswordEncoder
. בואו ניצור עבורם מגדירים. בואו נוסיף את @Autowired UserRepo userRepo למחלקה SecurityConfig. אנו מסתכלים כיצד המצביע לשגיאה בשיטת יצירת המסנן נעלם, ואנחנו שמחים. מדוע לא ניתן היה להחיל את @Autowired ישירות על CustomUserInfoTokenServices? מכיוון שהמחלקה הזו לא תקלוט את התלות, מכיוון שהיא עצמה אינה מסומנת בשום הערת Spring, והקונסטרוקטור שלה נוצר במפורש כשהמסנן מוצהר. בהתאם לכך, מנגנון ה-DI של Spring אינו יודע על כך. אם נציין את @Autowired על משהו במחלקה הזו, נקבל NullPointerException בעת השימוש. אבל דרך מגדירים מפורשים הכל עובד טוב מאוד. לאחר הטמעת הרכיבים הדרושים, אובייקט העניין העיקרי הופך לפונקציית loadAuthentication, שבה מאחזר את המפה<String, Object> עם מידע על המשתמש. בפרויקט זה יישמתי את שמירתו של משתמש מחובר דרך רשת חברתית במסד הנתונים. מכיוון שאנו משתמשים בחשבון Google כספק OAuth2, אנו בודקים אם המפה מכילה את השדה "משנה" האופייני לגוגל. אם הוא קיים, זה אומר שהמידע על המשתמש התקבל בצורה נכונה. אנו יוצרים משתמש חדש ושומרים אותו במסד הנתונים.
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException
{
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if(map.containsKey("sub"))
{
String googleName = (String) map.get("name");
String googleUsername = (String) map.get("email");
User user = userRepo.findByGoogleUsername(googleUsername);
if(user == null)
{
user = new User();
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
}
user.setName(googleName);
user.setUsername(googleUsername);
user.setGoogleName(googleName);
user.setGoogleUsername(googleUsername);
user.setPassword(passwordEncoder.encode("oauth2user"));
userRepo.save(user);
}
if (map.containsKey("error"))
{
this.logger.debug("userinfo returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
בעת שימוש במספר ספקים, ניתן לציין אפשרויות שונות ב- CustomUserInfoTokenServices אחד, ולרשום מחלקות שונות של שירותים דומים בשיטת הצהרת המסנן.
@GetMapping("/")
public String index(Principal principal)
{
if(principal != null)
{
return "redirect:/notes";
}
return "index";
}
GO TO FULL VERSION