# თავი 8: ვებ აპლიკაციების უსაფრთხოება (Securing Web Applications) --- ## 8.1 რატომ არის უსაფრთხოება მნიშვნელოვანი თითოეული აპლიკაცია, რომელიც აქამდე ავაშენეთ, სრულიად ღია იყო — ნებისმიერს შეეძლო ნებისმიერ გვერდზე წვდომა, ნებისმიერი ფორმის გაგზავნა, ნებისმიერი API endpoint-ის გამოძახება და ნებისმიერი მონაცემის მოდიფიცირება. დეველოპმენტის გარემოში ეს მოსახერხებელია, თუმცა პროდაქშენში (production) სრულიად მიუღებელია. ვებ აპლიკაციები ღიაა ინტერნეტისთვის და მუდმივად წარმოადგენენ თავდამსხმელთა სამიზნეს. ეს თავი გაგაცნობთ Spring Security-ს — ფრეიმვორქს, რომელიც მართავს აუტენტიფიკაციას (ვინ ხართ თქვენ?) და ავტორიზაციას (რისი გაკეთების უფლება გაქვთ?) Spring Boot აპლიკაციებისთვის. ჩვენ ვისწავლით, თუ როგორ დავამატოთ უსაფრთხოება პროექტს, დავაკონფიგურიროთ წვდომის წესები, დავნერგოთ მომხმარებლის კრედენციალების შენახვის სხვადასხვა სტრატეგიები, უსაფრთხოდ დავაენკოდიროთ (encode) პაროლები, აღვასრულოთ როლებზე დაფუძნებული წვდომის კონტროლი, მოვარგოთ login და logout გვერდები, დავიცვათ REST API-ები, დავაკონფიგურიროთ CORS და გავიგოთ სხვაობა სესიაზე (session) და ტოკენზე (token) დაფუძნებულ აუტენტიფიკაციებს შორის. Spring Boot 4-ს მოყვება **Spring Security 7**, რომელშიც ამოღებულია წინა ვერსიების რამდენიმე მოძველებული (deprecated) API და შეცვლილია ზოგიერთი საწყისი პარამეტრი. ამ თავში მოცემული ყველა კოდი იყენებს მიმდინარე API-ს. --- ## 8.2 გავრცელებული ვებ მოწყვლადობები სანამ Spring Security-ის ფუნქციონალს განვიხილავთ, სასარგებლოა იმ საფრთხეების გაგება, რომელთაგანაც ის გვიცავს: **CSRF (Cross-Site Request Forgery)** — თავდამსხმელი ატყუებს ავტორიზებულ მომხმარებელს, რათა მან მავნე მოთხოვნა გაგზავნოს. მაგალითად, თავდამსხმელის ვებსაიტზე არსებულმა დამალულმა ფორმამ შეიძლება თანხა გადარიცხოს მსხვერპლის საბანკო ანგარიშიდან, თუ ბანკის აპლიკაცია არ ამოწმებს მოთხოვნის წარმომავლობას. Spring Security სტანდარტულად მოიცავს CSRF დაცვას. **XSS (Cross-Site Scripting)** — თავდამსხმელი უშვებს მავნე JavaScript-ს იმ გვერდზე, რომელსაც სხვა მომხმარებლები ათვალიერებენ. თუ მომხმარებლის მიერ შეყვანილი ინფორმაცია დარენდერდება (rendered) ე.წ. escaping-ის გარეშე, შეყვანილ სკრიპტს შეუძლია მოიპაროს cookies, გადაამისამართოს მომხმარებლები ან შეცვალოს გვერდის კონტენტი. FreeMarker-ის სტანდარტული output escaping (რომელიც მე-3 თავში დავაკონფიგურირეთ) გვეხმარება ამის თავიდან აცილებაში. **SQL ინექცია (SQL Injection)** — თავდამსხმელი უშვებს მავნე SQL კოდს მომხმარებლის შეყვანილი მონაცემების საშუალებით, რათა წაიკითხოს, შეცვალოს ან წაშალოს მონაცემები ბაზაში. JPA-ს გამოყენება პარამეტრიზებული ქუერებით (parameterized queries, როგორც ეს მე-4 თავში გავაკეთეთ) ხელს უშლის ამას SQL სტრუქტურისა და მონაცემების ერთმანეთისგან განცალკევებით. **დარღვეული აუტენტიფიკაცია (Broken Authentication)** — სუსტი პაროლები, logout ფუნქციის არარსებობა, session fixation ან პროგნოზირებადი session ID-ები თავდამსხმელებს ლეგიტიმური მომხმარებლების განსახიერების საშუალებას აძლევს. **ობიექტებზე პირდაპირი არასაიმედო მითითებები (Insecure Direct Object References - IDOR)** — მომხმარებლებს შეუძლიათ რესურსებზე წვდომა URL-ებში ID-ების მანიპულირებით — მაგალითად, `/orders/123`-ის შეცვლით `/orders/124`-ზე, რათა ნახონ სხვა მომხმარებლის შეკვეთა. Spring Security უზრუნველყოფს დაცვას ამ საფრთხეების უმეტესობისგან ყოველგვარი დამატებითი ძალისხმევის გარეშე (out of the box). მოწყვლადობების გაგება დაგეხმარებათ დააფასოთ, რატომ არსებობს უსაფრთხოების გარკვეული ფუნქციები და რატომ არ უნდა გამორთოთ ისინი (განსაკუთრებით CSRF დაცვა) საფუძვლიანი დაფიქრების გარეშე. --- ## 8.3 Spring Security-ს დამატება პროექტში დაამატეთ Spring Security starter დეპენდენსი (dependency) თქვენს `pom.xml` ფაილში: ```xml org.springframework.boot spring-boot-starter-security ``` როგორც კი ამ დეპენდენსის დაამატებთ და აპლიკაციას გადატვირთავთ, ყველაფერი შეიცვლება. Spring Security ავტომატურად იცავს **ყველა endpoint-ს** — თითოეული გვერდი და API გამოძახება მოითხოვს აუტენტიფიკაციას. ის ქმნის დეფოლტ მომხმარებელს სახელით `user` და შემთხვევითი პაროლით, რომელიც ჩართვისას კონსოლში იბეჭდება. ის რთავს სტანდარტულ login ფორმას `/login` მისამართზე. რთავს CSRF დაცვას. ის ამატებს უსაფრთხოების response header-ებს (`X-Content-Type-Options`, `X-Frame-Options`, `Content-Security-Policy`). ეს "secure by default" ფილოსოფია გამიზნულია. ბევრად უფრო უსაფრთხოა დაიწყოთ ყველაფრის დაბლოკვით და შერჩევითად გახსნათ წვდომა, ვიდრე დაიწყოთ ღია სისტემით და მოგვიანებით სცადოთ არ დაგავიწყდეთ ყველაფრის დაბლოკვა. --- ## 8.4 Security Filter Chain-ის კონფიგურაცია სტანდარტული კონფიგურაცია — დაბლოკო ყველაფერი და უზრუნველყო შემთხვევითი პაროლი — არ არის გამოსადეგი რეალური აპლიკაციისთვის. უსაფრთხოების ქცევის მოსარგებად, თქვენ უნდა შექმნათ კონფიგურაციის კლასი `SecurityFilterChain` bean-ით: ```java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/home", "/register", "/css/**", "/js/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll() ); return http.build(); } } ``` ეს კონფიგურაცია განსაზღვრავს წესების სამ შრეს. **ავტორიზაციის წესები** (`authorizeHttpRequests`) განსაზღვრავს, ვის რაზე აქვს წვდომა. წესები ფასდება თანმიმდევრობით — პირველი დამთხვევა იმარჯვებს. საჯარო გვერდები (`/`, `/home`, `/register`) და სტატიკური რესურსები (`/css/**`, `/js/**`) ხელმისაწვდომია ყველასთვის. ადმინისტრატორის გვერდები მოითხოვს `ADMIN` როლს. მომხმარებლის გვერდები მოითხოვს `USER` ან `ADMIN` როლს. ყველაფერი დანარჩენი მოითხოვს აუტენტიფიკაციას, მაგრამ არა კონკრეტულ როლს. **Form login** კონფიგურაცია ეუბნება Spring Security-ს, გამოიყენოს ჩვენ მიერ შექმნილი login გვერდი `/login` მისამართზე (რომელსაც შევქმნით როგორც FreeMarker შაბლონს) და გადაამისამართოს მომხმარებლები `/dashboard`-ზე წარმატებული ავტორიზაციის შემდეგ. `permitAll()` გამოძახება უზრუნველყოფს, რომ თავად login გვერდი ხელმისაწვდომი იყოს აუტენტიფიკაციის გარეშე — ამის გარეშე, არაავტორიზებული მომხმარებლები გადამისამართდებოდნენ login გვერდზე, რომელიც თავის მხრივ ისევ გადაამისამართებდა მათ login გვერდზე და შეიქმნებოდა უსასრულო ციკლი (infinite loop). **Logout** კონფიგურაცია განსაზღვრავს logout-ის URL-ს და სად უნდა მოხდეს გადამისამართება logout-ის შემდეგ. Spring Security თავად მართავს რეალურ logout პროცესს — აუქმებს სესიას (invalidating the session), ასუფთავებს უსაფრთხოების კონტექსტს და შლის cookies-ს. ### ძირითადი ავტორიზაციის მეთოდები | მეთოდი | წვდომის წესი | |--------|-------------| | `permitAll()` | წვდომის ნებართვა აუტენტიფიკაციის გარეშე. | | `authenticated()` | მოითხოვს აუტენტიფიკაციას (ნებისმიერი როლი). | | `hasRole("ADMIN")` | მოითხოვს `ADMIN` როლს. | | `hasAnyRole("USER", "ADMIN")` | მოითხოვს ჩამოთვლილთაგან ნებისმიერ როლს. | | `hasAuthority("SCOPE_read")` | მოითხოვს კონკრეტულ უფლებამოსილებას (უფრო დეტალური ვიდრე როლები). | განსხვავება როლებსა და authority-ებს შორის მცირეა, მაგრამ მნიშვნელოვანი. როლი არის მაღალი დონის კონცეფცია (`ADMIN`, `USER`). Authority არის დეტალური ნებართვა (`SCOPE_read`, `product:write`). შიდა დონეზე, Spring Security როლებს ინახავს როგორც authority-ებს `ROLE_` პრეფიქსით — `hasRole("ADMIN")` ამოწმებს უზრუნველყოფილია თუ არა `ROLE_ADMIN` authority. აპლიკაციების უმეტესობისთვის როლები სავსებით საკმარისია. --- ## 8.5 აუტენტიფიკაცია: In-Memory მომხმარებლები დეველოპმენტისთვის, ტესტირებისთვის ან ძალიან მარტივი აპლიკაციებისთვის შეგიძლიათ მომხმარებლები პირდაპირ თქვენს კონფიგურაციაში განსაზღვროთ: ```java @Bean public UserDetailsService userDetailsService() { UserDetails user = User.builder() .username("user") .password(passwordEncoder().encode("password")) .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password(passwordEncoder().encode("admin123")) .roles("USER", "ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } ``` `InMemoryUserDetailsManager` ინახავს მომხმარებლებს მეხსიერებაში — ისინი არსებობენ მხოლოდ აპლიკაციის მუშაობის პერიოდში. ეს გამოსადეგია სწრაფი ტესტირებისთვის, მაგრამ არ არის შესაფერისი პროდაქშენისთვის, სადაც გჭირდებათ მონაცემთა ბაზაში შენახული მომხმარებლები რეგისტრაციის, პაროლის შეცვლისა და ანგარიშების მართვის შესაძლებლობით. ყურადღება მიაქციეთ, რომ პაროლები დაენკოდებულია `passwordEncoder().encode(...)` გამოყენებით. Spring Security უარს ამბობს plain-text პაროლებთან მუშაობაზე. პაროლის ენკოდინგს დეტალურად მალევე განვიხილავთ. --- ## 8.6 აუტენტიფიკაცია: JDBC-ზე დაფუძნებული მომხმარებლები Spring Security-ს შეუძლია მომხმარებლების აუტენტიფიკაცია პირდაპირ მონაცემთა ბაზის ცხრილებიდან `JdbcUserDetailsManager`-ის გამოყენებით: ```java @Bean public UserDetailsService userDetailsService(DataSource dataSource) { return new JdbcUserDetailsManager(dataSource); } ``` ეს მოითხოვს კონკრეტულ ცხრილების სტრუქტურას: ```sql CREATE TABLE users ( username VARCHAR(50) NOT NULL PRIMARY KEY, password VARCHAR(100) NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) ); ``` `JdbcUserDetailsManager` მართავს აუტენტიფიკაციას, მომხმარებლის შექმნას, პაროლის შეცვლასა და authority-ების მართვას. ის კარგად მუშაობს, როდესაც თქვენთვის მისაღებია მისი წინასწარ განსაზღვრული ცხრილების სქემა. თუ გჭირდებათ განსხვავებული სქემა — მაგალითად, `id` სვეტის გამოყენება `username`-ის ნაცვლად როგორც primary key, ან როლების განსხვავებულად შენახვა — დაგჭირდებათ მორგებული `UserDetailsService`. --- ## 8.7 აუტენტიფიკაცია: Custom UserDetailsService მომხმარებლების ჩატვირთვის პროცესზე სრული კონტროლისთვის, დააიმპლემენტირეთ `UserDetailsService` ინტერფეისი: ```java @Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { AppUser appUser = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException( "User not found: " + username)); return User.builder() .username(appUser.getUsername()) .password(appUser.getPassword()) .roles(appUser.getRoles().toArray(new String[0])) .build(); } } ``` ეს ერთი მეთოდი — `loadUserByUsername` — ერთადერთია, რაც Spring Security-ს მომხმარებლების აუტენტიფიკაციისთვის სჭირდება. ის ეძებს მომხმარებელს თქვენს ბაზაში და პოვნის შემთხვევაში, აბრუნებს `UserDetails` ობიექტს, რომელიც შეიცავს username-ს, დაენკოდებულ პაროლს და როლებს. ამის შემდეგ Spring Security ადარებს login ფორმიდან შემოსულ პაროლს (ენკოდინგის შემდეგ) შენახულ პაროლთან. ### User Entity ```java @Entity @Table(name = "app_users") public class AppUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "role") private Set roles = new HashSet<>(); // constructors, getters, setters } ``` `@ElementCollection` ანოტაცია აკავშირებს როლებს როგორც მარტივი მნიშვნელობების კოლექციას ცალკეულ ცხრილში (`user_roles`). ამ ცხრილის თითოეული ჩანაწერი შეიცავს `user_id`-ს და `role` სტრინგს. `FetchType.EAGER` უზრუნველყოფს, რომ როლები მომხმარებელთან ერთად დაუყოვნებლივ ჩაიტვირთოს — ეს აუცილებელია, რადგან Spring Security-ს როლები აუტენტიფიკაციის დროს სჭირდება, რაც სტანდარტული JPA სესიის მიღმა ხდება. ცხრილის სახელი `app_users` თავიდან აგვარიდებს კონფლიქტებს `users`-თან, რომელიც ზოგიერთ ბაზაში (მაგალითად, PostgreSQL) რეზერვირებული სიტყვაა. როდესაც Spring Boot აპლიკაციის კონტექსტში აღმოაჩენს `UserDetailsService` bean-ს, ის ავტომატურად იყენებს მას აუტენტიფიკაციისთვის — არ არის საჭირო მისი აშკარად გაწერა უსაფრთხოების კონფიგურაციაში. --- ## 8.8 პაროლის ენკოდინგი (Password Encoding) პაროლები არასოდეს უნდა შეინახოს plain text-ით. თუ თავდამსხმელი მოიპოვებს წვდომას თქვენს მონაცემთა ბაზაზე, plain-text პაროლები ყველა მომხმარებლის ანგარიშს საფრთხეში აგდებს. ჰეშირებული (hashed) პაროლებიც კი მოწყვლადია, თუ ჰეშირების ალგორითმი სუსტია (MD5, SHA-1) ან არ არის გამოყენებული salt. Spring Security მოითხოვს `PasswordEncoder` bean-ს. სტანდარტული არჩევანია `BCryptPasswordEncoder`: ```java @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } ``` BCrypt არის ცალმხრივი ჰეშირების ალგორითმი, რომელიც შექმნილია სპეციალურად პაროლებისთვის. მას აქვს ორი თვისება, რაც მას ამ მიზნისთვის შესაფერისს ხდის. ის ავტომატურად აგენერირებს შემთხვევით **salt**-ს თითოეული ჰეშისთვის, ასე რომ ერთი და იგივე პაროლი ყოველთვის განსხვავებულ ჰეშს აწარმოებს — ეს ხელს უშლის ე.წ. rainbow table შეტევებს. გარდა ამისა, ის განზრახ **ნელია** — გამოთვლების სირთულე კონფიგურირებადია და შეიძლება გაიზარდოს ტექნიკის გაუმჯობესებასთან ერთად, რაც brute-force შეტევებს არაპრაქტიკულს ხდის. ახალი მომხმარებლის შექმნისას, დააენკოდირეთ პაროლი მის შენახვამდე: ```java @Service public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public void registerUser(String username, String rawPassword) { AppUser user = new AppUser(); user.setUsername(username); user.setPassword(passwordEncoder.encode(rawPassword)); user.setRoles(Set.of("USER")); userRepository.save(user); } } ``` `encode` მეთოდი იღებს საწყის (raw) პაროლს და ქმნის BCrypt ჰეშს, მაგალითად `$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy`. აუტენტიფიკაციის დროს Spring Security იძახებს `passwordEncoder.matches(rawPassword, storedHash)` მეთოდს, რათა შეადაროს შესვლის მცდელობა შენახულ ჰეშთან — ის არასოდეს ახდენს ჰეშის დეკოდირებას საწყის პაროლამდე. --- ## 8.9 როლებზე დაფუძნებული წვდომის კონტროლი ავტორიზაცია განსაზღვრავს, რისი გაკეთება შეუძლიათ ავტორიზებულ მომხმარებლებს. Spring Security მხარს უჭერს ავტორიზაციას ორ დონეზე: URL-ზე დაფუძნებული წესები უსაფრთხოების კონფიგურაციაში და მეთოდის დონის ანოტაციები. ### URL-ზე დაფუძნებული ავტორიზაცია ჩვენ ეს უკვე ვნახეთ `SecurityFilterChain`-ში: ```java http .authorizeHttpRequests(auth -> auth .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") .requestMatchers("/api/**").hasRole("API_USER") .anyRequest().authenticated() ); ``` წესები ფასდება ზემოდან ქვემოთ. განათავსეთ კონკრეტული წესები თავდაპირველად და ყოვლისმომცველი `anyRequest()` ბოლოს. ### მეთოდის დონის ავტორიზაცია @PreAuthorize-ით უფრო დეტალური კონტროლისთვის, ჩართეთ მეთოდის დონის უსაფრთხოება და გამოიყენეთ `@PreAuthorize` ინდივიდუალურ სერვისის ან კონტროლერის მეთოდებზე: ```java @Configuration @EnableMethodSecurity public class SecurityConfig { // ... } ``` ```java @Service public class AdminService { @PreAuthorize("hasRole('ADMIN')") public void deleteUser(Long userId) { userRepository.deleteById(userId); } @PreAuthorize("hasRole('ADMIN') or #username == authentication.name") public UserProfile getProfile(String username) { return userRepository.findByUsername(username); } } ``` `@PreAuthorize` აფასებს SpEL (Spring Expression Language) გამოსახულებას მეთოდის გაშვებამდე. თუ გამოსახულება დააბრუნებს `false`-ს, Spring Security ისვრის `AccessDeniedException`-ს და მეთოდი არასოდეს გაეშვება. მეორე მაგალითი აჩვენებს მძლავრ პატერნს: `#username == authentication.name` საშუალებას აძლევს მომხმარებლებს წვდომა ჰქონდეთ საკუთარ პროფილზე, ხოლო სხვა მომხმარებლების პროფილებზე წვდომას მხოლოდ ადმინისტრატორებს უშვებს. `#username` მიუთითებს მეთოდის პარამეტრზე, ხოლო `authentication.name` ეხება მიმდინარე ავტორიზებული მომხმარებლის username-ს. `@EnableMethodSecurity` ააქტიურებს `@PreAuthorize` მხარდაჭერას. კონფიგურაციის კლასზე ამ ანოტაციის გარეშე, მეთოდებზე არსებული `@PreAuthorize` ანოტაციები უბრალოდ იგნორირდება — რაც დაბნეულობის ხშირი მიზეზია. --- ## 8.10 უსაფრთხოების ინფორმაციის ჩვენება FreeMarker შაბლონებში Thymeleaf-ისგან განსხვავებით, FreeMarker-ს არ გააჩნია ოფიციალური Spring Security დიალექტი. ყველაზე პრაქტიკული მიდგომა არის აუტენტიფიკაციის ინფორმაციის მიწოდება `@ControllerAdvice`-ის საშუალებით, რომელიც ამატებს მიმდინარე მომხმარებელს და მათ როლებს მოდელში: ```java @ControllerAdvice public class SecurityModelAdvice { @ModelAttribute("currentUser") public String currentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) { return auth.getName(); } return null; } @ModelAttribute("isAdmin") public boolean isAdmin() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null) return false; return auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); } } ``` ახლა ყველა FreeMarker შაბლონს შეუძლია პირობითად აჩვენოს კონტენტი აუტენტიფიკაციის მიხედვით: ```html <#-- Show only to authenticated users --> <#if currentUser??>

Welcome, ${currentUser}!

<#-- Show only to administrators --> <#if isAdmin> Admin Dashboard <#-- Show only to anonymous users --> <#if !currentUser??> Login Register ``` `??` ოპერატორი 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">

Sign In

<#if RequestParameters.error??>

Invalid username or password.

<#if RequestParameters.logout??>

You have been logged out.

``` 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) ოპერაციის შეზღუდვა მხოლოდ ადმინისტრატორებისთვის.