Spring Security의 개념
스프링 시큐리티는 스프링 기반의 어플리케이션의 보안(인증과 권한)을 담당하는 프레임워크를 말하는데, 보안과 관련해서 체계적으로 많은 옵션들을 지원해준다.
스프링 시큐리티는 필터(Filter) 기반으로 동작하여 스프링 MVC와 분리되어 관리 및 동작한다.
Filter와 Interceptor에 대해서 헷갈린다면,
Filter는 Dispatcher Serlvet으로 가기전에 적용되므로 가장 먼저 URL 요청을 받고,
Interceptor는 DispatcherServlet과 Controller사이에 위치한다. 따라서 이 둘은 적용시기에 차이가 있다라고 보면 된다.
Client -> Filter -> DispatcherServlet -> Interceptor -> Controller
Spring Security Architecture
스프링 시큐리티 구조의 처리 과정을 설명하자면 다음과 같다.
- 사용자가 로그인 정보와 함께 인증 요청을 한다.(Http Request)
- AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성한다
- AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달한다.
- ProviderManager는 등록된 AuthenticationProvider를 조회하여 인증을 요구한다.
- 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.
- 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만든다.
- AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교한다.
- 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.
- 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환된다.
- Authenticaton 객체를 SecurityContext에 저장한다.
최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다.사용자 정보를 저장한다는 것은 Spring Security가 전통적인 세션-쿠키 기반의 인증 방식을 사용한다는 것을 의미한다.
JWT의 개념
WT(Json Web Token)은 Json 객체에 인증에 필요한 정보들을 담은 후 비밀키로 서명한 토큰으로, 인터넷 표준 인증 방식이다. 공식적으로 인증(Authentication) & 권한허가(Authorization) 방식으로 사용된다.
여기서 일반 로그인 과정과 인증 방식을 헷갈릴 수 있다. 바로 jwt 프로세스를 살펴보자.
JWT 프로세스
- 사용자가 아이디와 비밀번호 혹은 소셜 로그인을 이용하여 서버에 로그인 요청을 보낸다
- 서버는 비밀키를 사용해 json 객체를 암호화한 JWT를 발급한다.
- JWT를 헤더에 담아 클라이언트에 보낸다.
여기까지가 JWT를 발급받기까지의 (로그인 전)과정이다. 로그인 이후에는 다음과 같은 과정이 이루어진다.
- 클라이언트는 JWT를 로컬에 저장해놓는다.
- API 호출을 할때마다 header에 JWT를 실어 보낸다.
- 서버는 헤더를 매번 확인하여 사용자의 JWT를 확인하고 인증된 JWT면 API에 대한 권한을 부여한다.
JWT의 구조
JWT는 Header, Payload, Signature 3개로 구성되어 있다
Header
- alg : Signature에서 사용하는 알고리즘
- typ : 토큰 타입
Payload
사용자 정보의 한 조각인 클레임(claim)이 들어있다.
- sub : 토큰 제목(subject)
- aud : 토큰 대상자(audience)
- iat : 토큰이 발급된 시각 (issued at)
- exp : 토큰의 만료 시각 (expired)
Signature
Signature는 헤더와 페이로드의 문자열을 합친 후에, 헤더에서 선언한 알고리즘과 key를 이용해 암호한 값이다.
Header와 Payload는 단순히 Base64url로 인코딩되어 있어 누구나 쉽게 복호화할 수 있지만, Signature는 key가 없으면 복호화할 수 없다. 이를 통해 보안상 안전하다는 특성을 가질 수 있게 되었다.
자, 이제 Spring Security와 JWT를 활용해 자체 로그인 기능을 구현해보자.
회원가입 구현
- 프로젝트 버
- Project: Gradle - Groovy Project
- Language: Java
- Spring Boot: 3.x.x
- 필수 의존성
- Lombok
- Spring Web
- Spring Security
- Spring Data JPA
- MySQL Driver
- JWT 필수 의존성
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
SecurityConfig
Spring Security를 시작하기 위해 SecurityConfig를 설정해주자
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf((auth) -> auth.disable());
http
.formLogin((auth) -> auth.disable());
http
.httpBasic((auth) -> auth.disable());
http
.authorizeHttpRequests(auth -> auth
//관리자 기능 api 권한
.requestMatchers("/admin/**").hasAnyAuthority("ADMIN")
//유저 기능 api 권한(수정, 등록, 삭제)
.requestMatchers("/user/**").hasAnyAuthority("USER")
//비로그인 회원은 조회만 가능하도록 설정
.anyRequest().permitAll()
);
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
BCryptPasswordEncoder란?
BCryptPasswordEncoder는 Spring Security 프레임워크에서 제공하는 클래스로 비밀번호를 암호화(해시)하는 데에 사용한다.
해시 함수에는 MD5나 SHA 등의 종류가 있지만 BCrypt는 단순히 입력을 1회 해시시키는 것이 아니라 솔트(salt)를 부여하여 여러번 해싱하므로 더 안전하게 암호를 관리할 수 있다.
BCrypt는 같은 비밀번호를 암호화하더라도 해시 값은 매번 다른 값이 도출된다.
따라서 BCryptPasswordEncoder에서는 사용자가 제출된 비밀번호와 암호화되어 저장된 비밀번호의 일치 여부를 확인하는 메소드가 제공된다.
reqeustMatchers("url")
이 메서드는 특정 요청 패턴에 대한 권한을 설정하는데, hasAnyAuthority() 메서드를 사용해 해당 요청경로에 대해 사용자가 가져야 하는 권한(authority)을 설정하고 있다. 여기서는 "/admin/" 패턴에 대한 요청은 "ADMIN" 권한을 가져야 하고, "/user/" 패턴에 대한 요청은 "USER" 권한을 가지도록 설정할 것이다.
이 설정을 통해 비로그인 사용자로 하여금 조회만 가능하게 할 것이고,
로그인한 유저는 게시물 등록이나 삭제와 같은 데이터 수정을 가능하게 하고,
admin 권한을 가지고 있을때는 관리자 기능을 수행하도록 할 것이다.
permitAll()을 통해 비로그인 사용자에 대한 기능을 허용해줄 것이다.
회원가입 로직
일단 간단한 회원가입부터 해보자
LoginController와 DTO를 이용해 UserEntity를 생성한후 DB에 저장하는 간단한 로직이다.
@RequiredArgsConstructor
@RestController
public class LoginController {
private final UserService userWriteService;
@PostMapping("/join")
public String signup(@RequestBody @Valid SignupRequestDto signupRequestDto){
userWriteService.signup(signupRequestDto);
return "OK";
}
}
@RequiredArgsConstructor
@Service
@Slf4j
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final AuthenticationRepository authenticationRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
//회원가입
@Transactional(rollbackFor = {Exception.class})
public User signup(SignupRequestDto signupRequestDto) {
log.info("signup password : " + signupRequestDto.getPassword());
Authentication authentication = Authentication.builder()
.userId(signupRequestDto.getUserId())
.email(signupRequestDto.getEmail())
.password(bCryptPasswordEncoder.encode(signupRequestDto.getPassword()))
.infoSet(InfoSet.DEFAULT)
.build();
authenticationRepository.save(authentication);
Address address = Address.builder()
.city(signupRequestDto.getCity())
.district(signupRequestDto.getDistrict())
.roadAddress(signupRequestDto.getRoadAddress())
.build();
User user = User.builder()
.username(signupRequestDto.getUsername())
.role(Role.USER)
.nickname(signupRequestDto.getNickname())
.sex(signupRequestDto.getSex())
.address(address)
.build();
user.setAuthentication(authentication);
userRepository.save(user);
return user;
}
}
유저의 기본 정보는 UserEntity로 관리하고 유저의 인증에 관한 정보들은 AuthenticationEntity를 만들어서 따로 관리하기로 했다.
전에 언급했던 BCryptPasswordEncoder의 의존성을 주입받아 패스워드를 암호화해서 UserEntity를 저장하는 모습이다.
2편에 이어서..
출처
https://dev-coco.tistory.com/174
https://velog.io/@chuu1019/%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-JWTJson-Web-Token
https://www.youtube.com/watch?v=NPRh2v7PTZg&t=357s
'Spring' 카테고리의 다른 글
Spring Security를 활용하여 JWT 발급, 자체 로그인, OAuth2 구현하기 - 3 (0) | 2024.03.07 |
---|---|
Spring Security를 활용하여 JWT 발급, 자체 로그인, OAuth2 구현하기 - 2 (0) | 2024.03.06 |
공통 관심사를 처리하기 위한 Filter와 Interceptor (0) | 2024.03.02 |
쿠키와 세션을 활용하여 로그인 처리하기 (1) | 2024.02.19 |
스프링 MVC의 구조 (1) | 2024.01.27 |