MS Lesson 21: Spring Security
1. Authentication vs Authorization
* Authentication - "Who are you?"
Istifadecinin kimliyinin yoxlanilmasi. Esasen username ve passwordun yoxlanilmasi.
Dogru olmadigi halda 401 Unauthorized xetasi verir.
* Authorization - "What can you do?"
Authentifikasiya olunmush istifadecinin hansi emeliyyatlara icazesi oldugunun yoxlanilmasi.
Dogru olmadigi halda 403 (Forbidden) xetasi verir.
2. Spring Security internal flow
Authentication - login melumatlarini dashiyan Java obyektidir.
AuthenticationManager - Authentication prosesini idare eden koordinator.
Default implementasiya ProviderManager. Filter-den Authentication obyektini alir, hansi AuthenticationProvider-in ishleyeceyini secir, Provider-e oturur.
AuthenticationProvider - esl Authentication mentiqinin oldugu yer. Default implementasiya DaoAuthenticationProvider. UserDetailsService-den user-i tapir, PasswordEncoder ile parolu yoxlayir, ugurlu olsa tam Authentication obytekti qaytarir.
UserDetailsService/Manager - User melumatlarini DB-den oxuyan servis. Default implementasiyasi InMemoryUserDetailsManager. Esas metodu: UserDetails loadUserByUsername(String username);
PasswordEncoder - parollari hash eden ve muqayise eden komponent. Default (production) BCryptPasswordEncoder.
SecurityContext - Authentikasiya olunmush user-in melumatinin saxlandigi yer. SecurityContextHolder (ThreadLocal) . Tomcat her request ucun ayri thread verir, ona gore paralel requestler bir birine qarishmir.
2. Indi ise sade Spring Security app yazaq.
Step1: dependency elave etmek:
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
Step2: Controller yaradaq
package az.etibarli.lessonspecification.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/test")
public class TestController {
@GetMapping("/public")
public String publicEndpoint() {
return "public endpoint";
}
@GetMapping("/user")
public String userEndpoint() {
return "user endpoint";
}
@GetMapping("/admin")
public String adminEndpoint() {
return "admin endpoint";
}
}
Step3: Security konfiqurasiya yaradaq
package az.etibarli.lessonspecification.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/test/public").permitAll()
.anyRequest().authenticated()
);
http.httpBasic(Customizer.withDefaults());
return http.build();
}
}
Step4: Mock user-ler yaradaq
package az.etibarli.lessonspecification.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class UserConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("12345"))
.roles("ADMIN")
.build();
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("12345"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}
Step5: indi ise endpointlere access-leri deyishek
package az.etibarli.lessonspecification.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/test/user").hasRole("USER")
.requestMatchers("/api/v1/test/admin").hasRole("ADMIN")
.requestMatchers("/api/v1/test/public").permitAll()
.anyRequest().authenticated()
);
http.httpBasic(Customizer.withDefaults());
return http.build();
}
}
Step6: indi ise logout sehifesi elave edek
package az.etibarli.lessonspecification.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/test/user").hasRole("USER")
.requestMatchers("/api/v1/test/admin").hasRole("ADMIN")
.requestMatchers("/api/v1/test/public").permitAll()
.anyRequest().authenticated()
);
http.formLogin(Customizer.withDefaults());
http.logout(logout -> logout
.logoutRequestMatcher(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, "/logout"))
.logoutUrl("/logout")
.logoutSuccessUrl("/api/v1/test/public")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.clearAuthentication(true)
.permitAll()
);
http.csrf(csrf -> csrf.disable());
return http.build();
}
}
Step7: Indi ise User entity yaradaq
package az.etibarli.lessonspecification.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.proxy.HibernateProxy;
import java.util.Objects;
@Entity
@Getter
@Setter
@ToString
@Table(name = "users")
@RequiredArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
User user = (User) o;
return getId() != null && Objects.equals(getId(), user.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
}
}
package az.etibarli.lessonspecification.repository;
import az.etibarli.lessonspecification.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
Step8: Registrasiya ucun controller yazaq
package az.etibarli.lessonspecification.controller;
import az.etibarli.lessonspecification.dto.request.SignUpRequestDto;
import az.etibarli.lessonspecification.dto.response.SignUpResponseDto;
import az.etibarli.lessonspecification.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/signup")
public ResponseEntity<SignUpResponseDto> signUp(@RequestBody SignUpRequestDto request) {
return ResponseEntity.ok(authService.signUp(request));
}
}
package az.etibarli.lessonspecification.service.impl;
import az.etibarli.lessonspecification.dto.request.SignUpRequestDto;
import az.etibarli.lessonspecification.dto.response.SignUpResponseDto;
import az.etibarli.lessonspecification.entity.User;
import az.etibarli.lessonspecification.repository.UserRepository;
import az.etibarli.lessonspecification.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public SignUpResponseDto signUp(SignUpRequestDto request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
throw new IllegalArgumentException("Username already exists: " + request.getUsername());
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setRole("USER");
User saved = userRepository.save(user);
return new SignUpResponseDto(
saved.getId(),
saved.getUsername(),
"User registered successfully"
);
}
}
package az.etibarli.lessonspecification.security;
import az.etibarli.lessonspecification.entity.User;
import az.etibarli.lessonspecification.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
Optional<User> userOptional = userRepository.findByUsername(username);
if (userOptional.isEmpty()) {
throw new UsernameNotFoundException("User not found: " + username);
}
User userFromDb = userOptional.get();
UserDetails userDetails = org.springframework.security.core.userdetails.User.builder()
.username(userFromDb.getUsername())
.password(userFromDb.getPassword())
.roles(userFromDb.getRole())
.build();
return userDetails;
}
}
Step9: Login endpointi yazaq
package az.etibarli.lessonspecification.service.impl;
import az.etibarli.lessonspecification.dto.request.LoginRequestDto;
import az.etibarli.lessonspecification.dto.request.SignUpRequestDto;
import az.etibarli.lessonspecification.dto.response.LoginResponseDto;
import az.etibarli.lessonspecification.dto.response.SignUpResponseDto;
import az.etibarli.lessonspecification.entity.User;
import az.etibarli.lessonspecification.repository.UserRepository;
import az.etibarli.lessonspecification.service.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
@Override
public SignUpResponseDto signUp(SignUpRequestDto request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
throw new IllegalArgumentException("Username already exists: " + request.getUsername());
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setRole("USER");
User saved = userRepository.save(user);
return new SignUpResponseDto(
saved.getId(),
saved.getUsername(),
"User registered successfully"
);
}
@Override
public LoginResponseDto login(LoginRequestDto request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
Authentication authentication;
try {
authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
} catch (AuthenticationException e) {
throw new ResponseStatusException(UNAUTHORIZED, "Invalid username or password");
}
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
securityContextRepository.saveContext(context, httpRequest, httpResponse);
User user = userRepository.findByUsername(request.getUsername())
.orElseThrow(() -> new ResponseStatusException(UNAUTHORIZED, "Invalid username or password"));
return new LoginResponseDto(
user.getId(),
user.getUsername(),
user.getRole(),
"Login successful"
);
}
}
Step10: UserDetailsServiceImpl sadeleshdirek
package az.etibarli.lessonspecification.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@Entity
@Getter
@Setter
@ToString
@Table(name = "users")
@RequiredArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@ToString.Exclude
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role));
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
User user = (User) o;
return getId() != null && Objects.equals(getId(), user.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
}
}
package az.etibarli.lessonspecification.security;
import az.etibarli.lessonspecification.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
}
Комментарии
Отправить комментарий