Welcome, ${currentUser}!
#if> <#-- Show only to administrators --> <#if isAdmin> Admin Dashboard #if> <#-- Show only to anonymous users --> <#if !currentUser??> Login Register #if> ``` `??` ოპერატორი FreeMarker-ში ამოწმებს, არსებობს თუ არა ცვლადი და არ არის თუ არა იგი null. ეს მიდგომა მარტივია, ტესტირებადია და თავიდან გვარიდებს JSP taglibs-ის FreeMarker-თან ინტეგრაციის სირთულეს. --- ## 8.11 Login და Logout გვერდების მორგება ### Custom Login გვერდი დააკონფიგურირეთ login ფორმა security filter chain-ში: ```java .formLogin(form -> form .loginPage("/login") .loginProcessingUrl("/perform-login") .defaultSuccessUrl("/dashboard", true) .failureUrl("/login?error=true") .permitAll() ) ``` `loginPage("/login")` განსაზღვრავს URL-ს, რომელიც აჩვენებს login ფორმას — თქვენ უნდა შექმნათ Controller-ის mapping-ი და შაბლონი ამისთვის. `loginProcessingUrl("/perform-login")` არის URL, სადაც ფორმა აგზავნის კრედენციალებს — Spring Security ამას ავტომატურად მართავს, ამიტომ Controller-ის დაწერა არ გჭირდებათ. `defaultSuccessUrl("/dashboard", true)` ამისამართებს `/dashboard`-ზე წარმატებული login-ის შემდეგ. `failureUrl("/login?error=true")` ამისამართებს უკან login გვერდზე error პარამეტრით წარუმატებლობის შემთხვევაში. შექმენით Controller-ი, რომელიც ემსახურება login გვერდს: ```java @Controller public class AuthController { @GetMapping("/login") public String loginPage() { return "login"; } } ``` და FreeMarker შაბლონი: ```html <#import "/spring.ftl" as spring> <#import "layout.ftlh" as layout> <@layout.page title="Login">Invalid username or password.
#if> <#if RequestParameters.logout??>You have been logged out.
#if> @layout.page> ``` CSRF ტოკენის დამალული (hidden) ველი კრიტიკულია — Spring Security უარყოფს ნებისმიერ POST მოთხოვნას, რომელიც არ შეიცავს ვალიდურ CSRF ტოკენს. `_csrf` ობიექტი ავტომატურად ხელმისაწვდომია მოდელში, როდესაც CSRF დაცვა ჩართულია. ### Custom Logout ```java .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ) ``` Logout ფორმა უნდა იყოს POST მოთხოვნა (CSRF დაცვისთვის): ```html ``` --- ## 8.12 მომხმარებლის რეგისტრაცია რეგისტრაციის პროცესი მოიცავს ფორმას, ვალიდაციას, პაროლის ენკოდინგს და მომხმარებლის შენახვას ბაზაში: ```java @Controller public class RegistrationController { private final UserService userService; public RegistrationController(UserService userService) { this.userService = userService; } @GetMapping("/register") public String showRegistrationForm(Model model) { model.addAttribute("registrationForm", new RegistrationForm()); return "register"; } @PostMapping("/register") public String registerUser(@Valid @ModelAttribute RegistrationForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) { if (bindingResult.hasErrors()) { return "register"; } if (userService.existsByUsername(form.getUsername())) { bindingResult.rejectValue("username", "error.username", "Username already exists"); return "register"; } userService.registerUser(form.getUsername(), form.getPassword()); redirectAttributes.addFlashAttribute("success", "Registration successful! Please log in."); return "redirect:/login"; } } ``` ეს მიჰყვება იმავე პატერნებს მე-3 თავიდან — `@Valid` იწყებს ვალიდაციას, `BindingResult` ინახავს შეცდომებს, ხოლო `RedirectAttributes` ატარებს flash შეტყობინებას გადამისამართებისას. დუბლიკატი username-ის დამატებითი შემოწმება ხდება მას შემდეგ, რაც ვალიდაცია წარმატებით გაივლის, რისთვისაც გამოიყენება `bindingResult.rejectValue` ველის დონის შეცდომის დასამატებლად, რომელსაც შაბლონი აჩვენებს ისევე, როგორც სხვა ვალიდაციის შეცდომებს. --- ## 8.13 REST API-ების დაცვა REST API-ები მოითხოვს უსაფრთხოების განსხვავებულ მიდგომას, ვიდრე სერვერზე დარენდერებული გვერდები. API-ები როგორც წესი stateless არის — თითოეული მოთხოვნა თავად შეიცავს კრედენციალებს და არ ეყრდნობა სერვერის სესიას. არ არსებობს login გვერდი — კრედენციალები მოდის HTTP header-ებში. თქვენ შეგიძლიათ განსაზღვროთ ცალკე `SecurityFilterChain` API endpoint-ებისთვის: ```java @Bean public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN") .anyRequest().authenticated() ) .httpBasic(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()); return http.build(); } ``` რამდენიმე რამ განსხვავდება ვებ გვერდის კონფიგურაციისგან. `securityMatcher("/api/**")` ზღუდავს ამ filter chain-ს მხოლოდ API endpoint-ებისთვის. თქვენ შეგიძლიათ გქონდეთ რამდენიმე filter chain — ერთი ვებ გვერდებისთვის (form login და CSRF-ით) და მეორე API-ებისთვის (HTTP Basic და CSRF-ის გარეშე). Spring Security იყენებს იმ chain-ს, რომლის `securityMatcher` ემთხვევა შემოსულ მოთხოვნას. `httpBasic(Customizer.withDefaults())` რთავს HTTP Basic აუტენტიფიკაციას, სადაც კლიენტი აგზავნის კრედენციალებს `Authorization` header-ით ყოველ მოთხოვნაზე. ეს მარტივია და შესაფერისია სერვერებს შორის კომუნიკაციისთვის ან დეველოპმენტის ტესტირებისთვის. `csrf(csrf -> csrf.disable())` თიშავს CSRF დაცვას API-სთვის. CSRF დაცვა გვიცავს ბრაუზერზე დაფუძნებული შეტევებისგან, სადაც ბრაუზერი ავტომატურად აგზავნის cookies-ს. Stateless API-ები, რომლებიც არ იყენებენ cookies-ს, არ არიან მოწყვლადი CSRF-ის მიმართ, ამიტომ დაცვა არასაჭიროა. **შენიშვნა:** Spring Security 7-ში CSRF ჩართულია ყველა endpoint-ისთვის სტანდარტულად, API-ების ჩათვლით. თუ თქვენი API მართლაც stateless არის და არ იყენებს cookie-ზე დაფუძნებულ აუტენტიფიკაციას, CSRF-ის გამორთვა API მარშრუტებისთვის (routes) გამართლებულია. --- ## 8.14 CORS-ის კონფიგურაცია **CORS (Cross-Origin Resource Sharing)** აკონტროლებს, რომელ დომენებს შეუძლიათ გამოიძახონ თქვენი API ბრაუზერიდან. თუ თქვენი React ფრონტენდი ეშვება `http://localhost:3000`-ზე და თქვენი Spring Boot API `http://localhost:8080`-ზე, ბრაუზერი სტანდარტულად დაბლოკავს API გამოძახებებს — რადგან ისინი სხვადასხვა წარმოშობისაა (origin). CORS კონფიგურაცია ეუბნება ბრაუზერს, რომელი cross-origin მოთხოვნებია დაშვებული. ### გლობალური კონფიგურაცია ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsConfigurationRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:3000", "https://myfrontend.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } } ``` ეს უშვებს მოთხოვნებს მითითებული origin-ებიდან ყველა `/api/**` endpoint-ზე ჩამოთვლილი HTTP მეთოდების გამოყენებით. `allowCredentials(true)` აძლევს ბრაუზერს უფლებას გააგზავნოს cookies cross-origin მოთხოვნებთან ერთად. `maxAge(3600)` ეუბნება ბრაუზერს დააქეშოს (cache) CORS preflight პასუხი ერთი საათის განმავლობაში, რითაც ამცირებს preflight OPTIONS მოთხოვნების რაოდენობას. ### CORS Security Filter Chain-ში როდესაც Spring Security აქტიურია, CORS ასევე უნდა დაკონფიგურირდეს security filter chain-ში — წინააღმდეგ შემთხვევაში, security filter უარყოფს cross-origin preflight მოთხოვნებს მანამ, სანამ ისინი მიაღწევენ MVC CORS კონფიგურაციას: ```java http .cors(cors -> cors.configurationSource(corsConfigurationSource())) // ... other configuration @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:3000")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return source; } ``` ### კონტროლერის დონის CORS უფრო მარტივი შემთხვევებისთვის, შეგიძლიათ გამოიყენოთ `@CrossOrigin` პირდაპირ კონტროლერზე: ```java @RestController @RequestMapping("/api/products") @CrossOrigin(origins = "http://localhost:3000") public class ProductRestController { // ... } ``` ეს მოსახერხებელია სწრაფი დეველოპმენტისთვის, მაგრამ არ არის კარგი მასშტაბირებისთვის — გლობალური კონფიგურაცია უმჯობესია, როდესაც რამდენიმე კონტროლერს ერთი და იგივე CORS პოლიტიკა სჭირდება. --- ## 8.15 სესიაზე დაფუძნებული vs. ტოკენზე დაფუძნებული აუტენტიფიკაცია ყველაფერი, რაც აქამდე განვიხილეთ, იყენებს **სესიაზე დაფუძნებულ აუტენტიფიკაციას (session-based authentication)** — სერვერი ქმნის სესიას ავტორიზაციის შემდეგ, ინახავს მას მეხსიერებაში და უგზავნის session ID cookie-ს ბრაუზერს. ბრაუზერი აგზავნის ამ cookie-ს ყოველ მომდევნო მოთხოვნასთან ერთად, ხოლო სერვერი ეძებს სესიას მომხმარებლის იდენტიფიცირებისთვის. **ტოკენზე დაფუძნებული აუტენტიფიკაცია (token-based authentication)** განსხვავებულად მუშაობს. ავტორიზაციის შემდეგ, სერვერი გასცემს **JWT-ს (JSON Web Token)** — თვითკმარ ტოკენს, რომელიც შეიცავს მომხმარებლის იდენტობას და როლებს, და ხელმოწერილია სერვერის მიერ. კლიენტი ინახავს ტოკენს და აგზავნის მას `Authorization` header-ში ყოველ მოთხოვნასთან ერთად. სერვერი ამოწმებს ტოკენის ხელმოწერას და მისგან იღებს ინფორმაციას მომხმარებლის შესახებ — სერვერის მხარეს სესიის შენახვა საჭირო არ არის. JWT შედგება სამი Base64-ით დაენკოდებული ნაწილისგან, რომლებიც გამოყოფილია წერტილებით: header (ალგორითმი და ტოკენის ტიპი), payload (claims — მომხმარებლის ინფორმაცია, როლები, ვადის გასვლის დრო) და signature (ადასტურებს, რომ ტოკენი არ არის შეცვლილი). ### როდის რომელი გამოვიყენოთ | სესიაზე დაფუძნებული (Session-Based) | ტოკენზე დაფუძნებული (Token-Based / JWT) | |---------------|-------------------| | სერვერი ინახავს სესიის მდგომარეობას (state). | სერვერი არის stateless. | | კარგია ტრადიციული სერვერზე დარენდერებული ვებ აპლიკაციებისთვის. | კარგია SPA-ებისთვის, მობაილ აპლიკაციებისთვის და მიკროსერვისებისთვის. | | იყენებს cookies. | იყენებს `Authorization` header-ს. | | მარტივია დასანერგად. | უკეთ მასშტაბირებადია (არ საჭიროებს სესიის შენახვას სერვერზე). | | Logout მარტივია (სესიის გაუქმება). | Logout მოითხოვს ტოკენის შავ სიაში (blacklist) შეყვანას ან ხანმოკლე ვადის დაწესებას. | ### JWT პროცესი ტიპური JWT აუტენტიფიკაციის პროცესი ასე მუშაობს: კლიენტი აგზავნის კრედენციალებს login endpoint-ზე (მაგ. `POST /api/auth/login`), სერვერი ამოწმებს კრედენციალებს და აგენერირებს ხელმოწერილ JWT-ს, კლიენტი ინახავს JWT-ს და რთავს მას `Authorization` header-ში მომდევნო მოთხოვნებისთვის (`Authorization: Bearer eyJhbGciOiJIUzI1...`), ხოლო სერვერი ამოწმებს JWT-ს ხელმოწერას და იღებს მომხმარებლის ინფორმაციას ყოველ მოთხოვნაზე. სრული JWT იმპლემენტაცია მოითხოვს დამატებით ბიბლიოთეკებს (როგორიცაა `jjwt`) და custom security filter-ს. ეს არის მოწინავე (advanced) თემა, რომელიც სცდება ამ თავის ფარგლებს, მაგრამ კონცეფციის გაგება მნიშვნელოვანია — JWT არის დომინანტური აუტენტიფიკაციის მექანიზმი თანამედროვე API-ებისთვის. --- ## 8.16 ყველაფრის გაერთიანება: სრული უსაფრთხოების კონფიგურაცია აქ არის სრული კონფიგურაცია, რომელიც აერთიანებს მომხმარებლის custom აუტენტიფიკაციას, URL-ზე დაფუძნებულ ავტორიზაციას, მეთოდის დონის უსაფრთხოებას, form login-ს და logout-ს: ```java @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/home", "/register").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() ) .logout(logout -> logout .logoutSuccessUrl("/login?logout") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` 8.7 სექციაში შექმნილ `CustomUserDetailsService`-ს ავტომატურად პოულობს Spring Boot, რადგან მას აქვს `@Service` ანოტაცია და აიმპლემენტირებს `UserDetailsService`-ს. უსაფრთხოების კონფიგურაციაში მისი პირდაპირ გაწერა (wiring) საჭირო არ არის. `@EnableMethodSecurity` ანოტაცია ააქტიურებს `@PreAuthorize` მხარდაჭერას, რათა სერვისის მეთოდებმა შეძლონ თავიანთი ავტორიზაციის წესების აღსრულება URL პატერნების მიღმა. სტატიკური რესურსები (`/css/**`, `/js/**`, `/images/**`) აშკარად უნდა იქნას დაშვებული (permitted) — წინააღმდეგ შემთხვევაში Spring Security მოითხოვს აუტენტიფიკაციას სტილებისთვის და JavaScript ფაილებისთვის, რაც დაარღვევს საჯარო გვერდების გარეგნობასა და ფუნქციონალს, მათ შორის login გვერდისას. --- ## შეჯამება ამ თავში განვიხილეთ Spring Boot აპლიკაციის დაცვა Spring Security 7-ის გამოყენებით. **გავრცელებული მოწყვლადობები** — CSRF, XSS, SQL ინექცია, დარღვეული აუტენტიფიკაცია — ხაზს უსვამს, რატომ არ შეიძლება უსაფრთხოებაზე მოგვიანებით ფიქრი. Spring Security უზრუნველყოფს დაცვას ბევრი მათგანისგან ყოველგვარი დამატებითი კონფიგურაციის გარეშე. **Security starter-ის დამატება** მყისიერად იცავს ყველა endpoint-ს, ქმნის დეფოლტ მომხმარებელს, რთავს login ფორმას, ააქტიურებს CSRF დაცვას და აყენებს უსაფრთხოების header-ებს. **`SecurityFilterChain`** არგებს წვდომის წესებს `authorizeHttpRequests`-ის გამოყენებით, სადაც ხელმისაწვდომია მატჩერები (matchers) როგორიცაა `permitAll()`, `hasRole()`, `hasAnyRole()`, და `authenticated()`. წესები ფასდება თანმიმდევრულად — კონკრეტული წესები თავიდან, ყოვლისმომცველი ბოლოს. **აუტენტიფიკაცია** შეიძლება იყენებდეს in-memory მომხმარებლებს (დეველოპმენტისთვის), JDBC-ზე დაფუძნებულ მომხმარებლებს (სტანდარტული სქემებისთვის), ან custom `UserDetailsService`-ს (მომხმარებლების ჩატვირთვაზე სრული კონტროლისთვის თქვენი საკუთარი entity მოდელიდან). **პაროლის ენკოდინგი** `BCryptPasswordEncoder`-ით უზრუნველყოფს, რომ პაროლები არასოდეს ინახება plain text-ით. BCrypt უზრუნველყოფს ავტომატურ salting-ს და კონფიგურირებად გამოთვლის სირთულეს. **როლებზე დაფუძნებული წვდომის კონტროლი** აღასრულებს ავტორიზაციას URL დონეზე (filter chain-ში) და მეთოდის დონეზე (`@PreAuthorize` ანოტაციით და `@EnableMethodSecurity`-ით). **FreeMarker-ის ინტეგრაცია** Spring Security-სთან მიიღწევა `@ControllerAdvice`-ის საშუალებით, რომელიც აწვდის აუტენტიფიკაციის ინფორმაციას როგორც მოდელის ატრიბუტებს, რაც შესაძლებელს ხდის კონტენტის პირობით ჩვენებას მომხმარებლის იდენტობისა და როლების მიხედვით. **მორგებული login და logout გვერდები** წარმოადგენს FreeMarker შაბლონებს, რომლებიც აგზავნიან ინფორმაციას Spring Security-ს processing URL-ებზე. CSRF ტოკენები უნდა იქნას დართული როგორც დამალული (hidden) ველები ყველა POST მოთხოვნის ფორმაში. **REST API უსაფრთხოება** იყენებს ცალკე `SecurityFilterChain`-ს, შეზღუდულს `securityMatcher`-ით, როგორც წესი HTTP Basic აუტენტიფიკაციით და გამორთული CSRF დაცვით stateless endpoint-ებისთვის. **CORS კონფიგურაცია** საშუალებას აძლევს cross-origin მოთხოვნებს კონკრეტული frontend დომენებიდან, კონფიგურირებულს როგორც MVC შრეში, ისე security filter chain-ში. **ტოკენზე დაფუძნებული აუტენტიფიკაცია** JWT-ის გამოყენებით არის სესიაზე დაფუძნებული აუტენტიფიკაციის ალტერნატივა, რომელიც მორგებულია SPA-ებისთვის, მობაილ აპლიკაციებისთვის და მიკროსერვისებისთვის. ის აქრობს სერვერის მხარეს სესიის შენახვის საჭიროებას, მაგრამ მოითხოვს ტოკენების ფრთხილ მენეჯმენტს. --- ## რესურსები - [Spring Security Reference Documentation](https://docs.spring.io/spring-security/reference/index.html) - [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture/) - [What's New in Spring Security 7](https://docs.spring.io/spring-security/reference/whats-new.html) - [OWASP Top Ten](https://owasp.org/www-project-top-ten/) - [Baeldung: Spring Security](https://www.baeldung.com/security-spring) - [BCrypt Password Hashing](https://en.wikipedia.org/wiki/Bcrypt) - [JWT.io](https://jwt.io/) --- ## ლაბორატორიული დავალება: დაიცავით თქვენი ვებ აპლიკაცია დაამატეთ აუტენტიფიკაცია და ავტორიზაცია თქვენს არსებულ ვებ აპლიკაციას. **მოთხოვნები:** 1. **დაამატეთ Spring Security** და დააკონფიგურირეთ `SecurityFilterChain` URL-ზე დაფუძნებული წვდომის წესებით: საჯარო გვერდები (home, about, registration) ხელმისაწვდომი ყველასთვის, დაცული გვერდები, რომლებიც მოითხოვს აუტენტიფიკაციას, და ადმინისტრატორის გვერდები, რომლებიც ხელმისაწვდომია მხოლოდ `ADMIN` როლის მქონე მომხმარებლებისთვის. 2. **დააიმპლემენტირეთ მომხმარებლის აუტენტიფიკაცია** მორგებული (custom) `UserDetailsService`-ის გამოყენებით, რომელიც ეყრდნობა JPA entity-ს და repository-ს. შეინახეთ პაროლები დაენკოდებული `BCryptPasswordEncoder`-ის გამოყენებით. 3. **შექმენით მომხმარებლის რეგისტრაციის პროცესი** რეგისტრაციის ფორმით, Bean Validation-ით, დუბლიკატი username-ის შემოწმებით და პაროლის ენკოდინგით შენახვამდე. 4. **მოარგეთ login და logout გვერდები** FreeMarker შაბლონების გამოყენებით. დაურთეთ CSRF ტოკენები ყველა POST ფორმაში. აჩვენეთ შეცდომისა და წარმატების შეტყობინებები (არასწორი კრედენციალები, logout დადასტურება, რეგისტრაციის წარმატება). 5. **დაამატეთ პირობითი კონტენტი** თქვენს შაბლონებში აუტენტიფიკაციის სტატუსის მიხედვით — აჩვენეთ მისალმების შეტყობინება და logout ღილაკი ავტორიზებული მომხმარებლებისთვის, აჩვენეთ login და register ლინკები არაავტორიზებული მომხმარებლებისთვის, და აჩვენეთ ადმინისტრატორის ნავიგაცია მხოლოდ ადმინისტრატორებისთვის. 6. **დაამატეთ მეთოდის დონის უსაფრთხოება** `@PreAuthorize`-ით მინიმუმ ერთ სერვისის მეთოდზე — მაგალითად, წაშლის (delete) ოპერაციის შეზღუდვა მხოლოდ ადმინისტრატორებისთვის.