본문 바로가기

Spring공부

블로그의 인증, 인가 쿠키&세션

스프링시큐리티

스프링시큐리티는 스프링기반의 애플리케이션 보안을 담당하는 스프링 하위 프레임워크이다. 보안 관련 옵션을 많이 제공한다. 그리고 애너테이션으로 설정도 매우 쉽다. CSRF 공격, 세션 고정 공격을 방어해주고, 요청 헤더도 보안 처리를 해주므로 개발자가 보안 관련 개발을 해야 하는 부담을 크게 줄여준다.

필터기반으로 동작하는 스프링 시큐리티

 

인증과 인가

인증은 사용자의 신원을 입증하는 과정이다.인가는 인증과 다르다. 인가는 사이트의 특정 부분에 접근할 수 있는지 권한을 확인하는 작업이다. 스프링 시큐리티를 사용하여 구현하면 쉽게 처리 할수 있다.

  • 인증 절차: 사용자가 로그인 요청을 하면 UsernamePasswordAuthenticationFilter 또는 BasicAuthenticationFilter와 같은 필터가 사용자의 자격 증명을 받아 인증 요청을 수행합니다. 인증이 성공하면 AuthenticationSuccessHandler가 호출되고, 실패하면 AuthenticationFailureHandler가 호출됩니다.
  • 인가 절차: 인증이 성공한 후, 사용자의 권한을 확인하여 요청한 자원에 접근할 수 있는지 결정합니다. FilterSecurityInterceptor가 이를 처리하며, 접근이 허용되면 요청이 진행되고, 거부되면 예외가 발생합니다.
필터명 설명
SecurityContextPersistenceF
ilter
Security ContextRepository에서 SecurityContext(접근 주체와 인증에 대한 정보를 있는 객체) 가져오거나 저장하는 역할을 합니다.
LogoutFilter 설정된 로그아웃 URL 오는 요청을 확인해 해당 사용자를 로그아웃 처리합니다.
UsernamePassword
AuthenticationFilter
인증 관리자입니다. 기반 로그인을 사용되는 필터로 아이디, 패스워드 데이터를 파싱 인증 요청을 위임합니다. 인증이 성공하면 AuthenticationSuccessHlandler, 인증에 실패하면 AuthenticationFailureHandler 실행합니다.
DefaultLoginPageGenerating
Filter
사용자가 로그인 페이지를 따로 지정하지 않았을 기본으로 설정하는 로그인 페이지 관련 필터입니다.
BasicAuthenticationFilter 요청 헤더에 있는 아이디와 패스워드를 파싱해서 인증 요청을 위임합니다. 인증이 성공하면
AuthenticationSuccessHandler A 0 AuthenticationFailure Handler
실행합니다.
RequestCacheAwareFilter 로그인 성공 , 관련 있는 캐시 요청이 있는지 확인하고 캐시 요청을 처리해줍니다. 예를 들어 로그인하지 않은 상태로 방문했던 페이지를 기억해두었다가 로그인 이후에 페이지로 이동 시켜줍니다.
SecurityContextHolderAware
RequestFilter
HttpServletRequest 정보를 감쌉니다. 필터 체인 상의 다음 필터들에게 부가 정보를 제공 되기 위해 사용합니다.
AnonymousAuthentication
Filter
필터가 호출되는 시점까지 인증되지 않았다면 익명 사용자 전용 객체인 Anonymous Authentication 만들어 SecurityContext 넣어줍니다.
SessionManagementFilter 인증된 사용자와 관련된 세션 관련 작업을 진행합니다. 세션 변조 방지 전략을 설정하고, 유효 하지 않은 세션에 대한 처리를 하고, 세션 생성 전략을 세우는 등의 작업을 처리합니다.
ExceptionTranslationFilter 요청을 처리하는 중에 발생할 있는 예외를 위임하거나 전달합니다.
FilterSecurityInterceptor 접근 결정 관리자입니다. AccessDecsionManager 권한 부여 처리를 위임함으로써 접근 제어 결정을 쉽게 해줍니다. 과정에서는 이미 사용자가 인증되어 있으므로 유효한 사용자인
지도 있습니다. , 인가 관련 설정할 있습니다.

 

User Entity 작성

package me.kimgunwoo.springbootstudy.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
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;

@Getter
@Table(name="users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class User implements UserDetails {//UserDetails를 상속받아 인증 객체로 사용

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="name",nullable = false)
    private Long id;

    @Column(name = "email",nullable = false,unique = true)
    private String email;

    @Column(name = "password",nullable = false)
    private String password;

    @Builder
    public User(String email, String password, String auth){
        this.email=email;
        this.password=password;
    }

    @Override//권한 반환
    public Collection<? extends GrantedAuthority> getAuthorities(){
        return List.of(new SimpleGrantedAuthority("user"));
    }

    //사용자의 id를 반환(고유한 값)
    @Override
    public String getUsername(){
        return email;
    }

    //사용자의 패스워드 반환
    @Override
    public  String getPassword(){
        return password;
    }

    //계정 만료 여부 반환
    @Override
    public boolean isAccountNonExpired(){
        //만료되었는지 확인하는 로직
        return true; // true -> 만료되자 않았음
    };

    //계정 잠금 여부 반환
    @Override
    public boolean isAccountNonLocked(){
        //계정 잠금되었는지 확인하는 로직
        return true;// true -> 잠금되지 않았음
    }

    //패스워드 만료 여부 반환
    @Override
    public boolean isCredentialsNonExpired(){
        //패스워드가 만료되었는지 확인하는 로직
        return true;
    }

    //계정 사용가능 여부 반환
    @Override
    public boolean isEnabled(){
        //계정이 사용 가능한지 확인하는 로직
        return true; //true -> 사용가능
    }
}

 

메서드 변환 타입 설명
getAuthorities() Collection<? extends GrantedAuthority> 사용자가 가지고 있는 권한의 목록을 반환합니다. 현재 예제 코드에서는 user 권한만 담아 반환합니다.
getUsername() String 사용자를 식별할 수 있는 사용자 이름을 반환합니다. 이때 사용되는 사용자 이름은 반드시 고유해야 합니다. 현재 예제 코드에서는 유니크 속성이 적용된 이메일을 반환합니다.
getPassword() String 사용자의 비밀번호를 반환합니다. 이때 저장되어 있는 비밀번호는 암호화되어 저장되어야 합니다.
isAccountNonExpired() boolean 계정이 만료되었는지 확인하는 메서드입니다. 만약 만료되지 않은 때는 true를 반환합니다.
isAccountNonLocked() boolean 계정이 잠금되었는지 확인하는 메서드입니다. 만약 잠금되지 않은 때는 true를 반환합니다.
isCredentialsNonExpired() boolean 비밀번호가 만료되었는지 확인하는 메서드입니다. 만약 만료되지 않은 때는 true를 반환합니다.
isEnabled() boolean 계정이 사용 가능한지 확인하는 메서드입니다. 만약 사용 가능하다면 true를 반환합니다.

 

UserRepository 작성

@Repository
public interface UserRepository extends JpaRepository<Long, User> {
    Optional<User> findByEmail(String email); //email로 사용자 정보를 가져옴
}

findByEmail() 메서드가 요청하는 쿼리 예

FROM users

WHERE email = {email}

코드 설명 쿼리
findByName() "name" 컬럼의 값 중 파라미터로 들어오는 값과 같은 데이터를 반환 ... WHERE name = ?1
findByNameAndAge() 첫 번째 파라미터는 "name" 컬럼에서, 두 번째 파라미터는 "age" 컬럼에서 조회한 데이터를 반환 ... WHERE name = ?1 AND age = ?2
findByNameOrAge() 첫 번째 파라미터는 "name" 컬럼에서, 두 번째 파라미터는 "age" 컬럼에서 조회한 데이터를 반환 ... WHERE name = ?1 OR age = ?2
findByAgeLessThan() "age" 컬럼의 값 중 파라미터로 들어온 값보다 작은 데이터를 반환 ... WHERE age < ?1
findByAgeGreaterThan() "age" 컬럼의 값 중 파라미터로 들어온 값보다 큰 데이터를 반환 ... WHERE age > ?1
findByName(Is)Null() "name"컬럼의 값 중 null인 데이터 반환 ...WHERE name IS NULL

 

 

유저디테일 서비스 작성

@RequiredArgsConstructor
@Service
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public User loadUserByUsername(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException((email)));
    }
}

스프링 시큐리티에서 사용자의 정보를 가져오는 UserDetailsService 인터페이스를 구현한다.필수로 구현해야하는 loadUserByUsername() 메서드를 오버라이딩해서 사용자 정보를 가져오는 로직을 작성한다.

 

 

웹 시큐리티 작성

@RequiredArgsConstructor
@Configuration
public class WebSecurityConfig {

    private final UserDetailService userService;

    @Bean
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers("/static/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests()
                .requestMatchers("/login", "/signup", "/user").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/articles")
                .and()
                .logout()
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true)
                .and()
                .build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userService)
                .passwordEncoder(bCryptPasswordEncoder)
                .and()
                .build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

접근 권한 설정

  1. requestMatchers(): 특정 요청 URL에 대한 접근 권한을 설정합니다.
  2. permitAll(): 누구나 접근할 수 있도록 설정합니다. 예: /login, /signup, /user 경로는 인증/인가 없이 접근이 가능합니다.
  3. anyRequest(): 위에서 설정한 URL 이외의 모든 요청에 대해 설정합니다.
  4. authenticated(): 별도의 인가는 필요 없지만, 인증이 성공된 상태여야 접근할 수 있도록 설정합니다.

로그인 설정

  1. loginPage(): 로그인 페이지의 경로를 설정합니다.
  2. defaultSuccessUrl(): 로그인이 완료되었을 때 이동할 경로를 설정합니다.

로그아웃 설정

  1. logoutSuccessUrl(): 로그아웃이 완료되었을 때 이동할 경로를 설정합니다.
  2. invalidateHttpSession(): 로그아웃 이후 세션을 전체 삭제할지 여부를 설정합니다.

CSRF 설정

  • CSRF 비활성화: CSRF 공격 방지를 위해 기본적으로 활성화되어야 하지만, 실습을 위해 비활성화할 수 있습니다.

인증 관리자 설정

  • 사용자 정보를 가져올 서비스를 재정의하거나, LDAP, JDBC 기반 인증 등을 설정할 때 사용합니다.

사용자 서비스 설정

  1. userDetailsService(): 사용자 정보를 가져올 서비스를 설정합니다. 이 서비스 클래스는 반드시 UserDetailsService를 상속받은 클래스여야 합니다.
  2. passwordEncoder(): 비밀번호를 암호화하기 위한 인코더를 설정합니다.

패스워드 인코더 빈 등록

  • 패스워드 인코더를 빈으로 등록: 비밀번호 인코더를 스프링 빈으로 등록하여 사용할 수 있습니다.

 

 

회원가입 dto코드 작성

@Getter
@Setter
public class AddUserRequest {
    private String email;
    private String password;
    private String auth;
}

 

 

서비스 코드 작성

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public Long save(AddUserRequest dto) {
        return userRepository.save(User.builder()
                .email(dto.getEmail())
                .auth(dto.getAuth())
                .password(bCryptPasswordEncoder.encode(dto.getPassword()))
                .build()).getId();
    }
}

패스워드를 저장할 때 시큐리티를 설정하며 패스워드 인코딩용으로 등록한 빈을 사용해서 암호화한 후에 저장한다.

 

 

UserApiController 작성

@RequiredArgsConstructor
@Controller
public class UserApiController {

    private final UserService userService;

    @PostMapping("/user")
    public String signup(AddUserRequest request) {
        userService.save(request);
        return "redirect:/login";
    }

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
        return "redirect:/login";
    }
}

 

회원 가입 처리가 된 다음 로그인 페이지로 이동하기 위해 redirect: 접두사를 붙였다. 이렇게 하면 회원 가입 처리가 끝나면 강제로 /login URL에 해당하는 화면으로 이동한다.

 

/logout Get 요청을 하면 로그아웃을 담당하는 핸들러인 SecurityContextLogoutHandler의   logout()메서드를 호출해서 로그아웃 한다.

'Spring공부' 카테고리의 다른 글

240524_TIL  (0) 2024.05.24
로그인/로그아웃 구현하기 JWT  (0) 2024.05.24
240520_TIL  (0) 2024.05.20
스프링 부트3로 블로그 만들기  (0) 2024.05.20
데이터베이스  (0) 2024.05.17