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.

Authentication auth = new UsernamePasswordAuthenticationToken(
"admin", // username (principal)
"12345", // password (credentials)
null // authorities (hələ yoxdur)
);


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));
}

}










































Комментарии

Популярные сообщения из этого блога

Interview questions

Lesson1: JDK, JVM, JRE

Lesson_2: Operations in Java