2024. 5. 9. 15:25ㆍ프로젝트 일지
프로젝트 배경
오늘은 5월9일 데드라인이 6일 남았다,
3일 - 회원기능 구현 (JWT 로그인 방식, 이메일 인증,
3일 - 나머지 api 구현 ? ...
시간이 촉박하다.. JWT 로그인을 오늘내에 끝내야한다
씨발
JWT 로그인 구현 방법 생각
어제 스프링시큐리티의 인증, 인가 작동방식과
JWT로그인 방식, JWT, JWT보관방식, JWT만료기간확인 방식
에 대해 알아냈으니 이제 두 지식을 섞어서 JWT 로그인 구현 방법을 생각해내야한다
절차에 따라 생각해볼거다
사용자가 username과 password를 JSON body에 담아 /login경로로 ajax 요청을 보낸다
usernamepassword어센시케이션filter가 /login 경로로 온 요청을 잡아다가 username과 password를 추출해서
usernamepassword어센시케이션token에 담아줘야하는데, 폼태그를 통한 로그인이 아니라서
폼태그에서 username, password 를 추출하는게 아니라 JSON body에서 추출할 수 있도록
usernamepassword어센시케이션filter를 커스터마이징해줘야한다 그리고
커스터마이징 된 customusernamepassword어센시케이션filter 를 시큐리티필터체인 객체에 등록해주어야한다
등록된 customusernamepassword어센시케이션filter가 JSON body에서 추출한 username, password를
usernamepassword어센시케이션token에 담아 어센시케이션메니저에게 전달한다
어센시케이션메니저는 유저디테일서비스의 loadByUsername() 을 호출해 유저디테일 객체를 가져오고
usernamepassword어센시케이션token에 담긴 username, password 와
유저디테일 객체에 담긴 username, password를 비교한다, 일치할 경우 유저디테일 객체를 어센시케이션 객체 안에
넣어준다, 그리고 어센시케이션 객체를 시큐리티콘텍스트 객체에 담아주고 시큐리티콘텍스트 객체를 시큐리티콘택스트 홀더에 담아주고 인증의 과정이 끝나게된다
하지만 나는 인증이 끝나면 브라우저로 에세스JWT 리프레시JWT를 JSON 헤더에 담아서 보내주어야하고, 서버 메모리에
유저의ID를 Key값으로 가지고 에세스JWT와 리프레시JWT를 Value를 가지는 Map에 저장해주어야한다
이걸 위해서 UserDetailService에서 loadByUsername()을 호출할때 loadByUsername()내부에서
에세스JWT, 리프레시JWT를 발급해주고 UserDetail객체를 커스텀해주어서,
UserDetail객체에 username, password, 에세스JWT, 리프레시JWT를 담아주도록해서 최종적으로 어센시케이션 객체에
에세스JWT, 리프레시JWT가 담길 수 있도록 해준다 이를 통해 인증이 끝났을때 어센시케이션 객체에 담긴
에세스JWT, 리프레시JWT를 JSON 헤더에 담아서 브라우저로 보내주도록 한다, 그리고 loadByUsername()내부에서
에세스JWT, 리프레시JWT를 발급해주는 시점에 서버 메모리에 {사용자ID : {에세스JWT : asdasdas}, {리프레시JWT : as}}
형태로 Map 안에 담아주면댄다
이렇게 하면 인증과정이 끝나게 된다
이후 사용자가 어떠한 요청을 보내면 필터시큐리티인터셉터가 요청을 가로채고, 필터시큐리티인터셉터는
이 요청에대한 접근권한이 요청을보낸 사용자에게 있는지에대한 검사 결과를 기다리게된다
스프링시큐리티에서 에세스디시즌메니저의 구현체를 생성하고 시큐리티콘택스트에서 어센시케이션 객체를 가져와
에세스디시즌메니저의 구현체에게 전달한다, 이후
에세스디시즌메니저는 에세스디시즌보트구현체에게 어센시케이션 객체를 전달하면서 어센시케이션 객체안에담긴
사용자의 정보를 기반으로 해당 유저가 요청한 URL의 접근 권한이 있는지 없는지에 대한 검사를 진행한 후 접근권한이 있다면 ACCESS_GRANTED 를 vote()를 호출하면 반환하게한다, 이후
에세스디시즌보트구현체들의 투표결과를 에세스디시즌메니저의 구현체가 종합하고, 종합 결과에 따라
해당 사용자가 요청한 URL의 접근 권한이 있는지 없는지에 대한 정보를 필터시큐리티인터셉터에게 보내게된다
필터시큐리티인터셉터가 사용자가 접근권한이 있다는 정보를 받았다면
필터시큐리티인터셉터 가로챗던 요청을 돌려주고, 해당 요청에 대한 api가 실행되게된다
나는 에세스디시즌보트 구현체에서 사용자가보낸 에세스JWT의 유효성을 검증하고싶었지만, 에세스디시즌보트 구현체는
어센시케이션 객체를 기반으로만 인가를 위한 검사를 진행하기떄문에, 사용자가 보낸 에세스JWT를 기반으로 에세스디시즌보트 구현체에서 유효성을 검사할 수가 없다, 그래서 위 인가에 대한 과정은 건들이지않고
컨트롤러단에서 JWT에 대한 유효성검사를진행한 검사 결과에따라 api를 실행시켜주도록 해야한다
컨트롤러단에서 서버메모리에있는 에세스JWT or 리프레시JWT를 가져와서 사용자가 보낸
에세스JWT or 리프레시JWT와 비교해 검사를 해주고, 리프레시토큰을 보낸다면 재발급도 해주고 북치고장구치고 해야한다
구현을 위해서 건들여야하는걸 정리하자면 이렇다
1. usernamepassword어센시케이션filter 커스터마이징한 뒤 시큐리티필터체인 객체에 필터 추가해주기
2. 유저디테일객체에 들어갈 사용자 데이터커스터마이징해서 리프레시토큰 엑세스토큰 넣어주고, 서버 메모리에도 넣어주기, 그리고 로그인 응답으로 리프레시 토큰 엑세스토큰 JSON 헤더에 담아서 응답해주기
3. 사용자가 JSON 헤더에 담아서 보낸 에세스토큰, 리프레시토큰 유효성 검사하고, 리프레시토큰 오면 JSON 헤더에 토큰 재발급해서 보내주는 로직 컨트롤러단에 만들어주기
이게 전부다
인가 과정 좀 파고나니 인가과정에서 가져다 응용할게 하나도 없다.. 시발.. 그래도 의미가 없진 않았다
투표를 진행한다는 구현방식이 나름 영감을 주었다,.... ^^^^^^^^^^^^^^^ㅣ발
JWT 로그인 방법 구현
우선 스프링시큐리티를 이용한 전형적인 세션 로그인 방법으로 구현을 해두어 테스트페이지를 만들어놓았다
세션 로그인 방식을 수정해서 JWT를 구현할거기때문에, 세션 로그인 방식을 우선 구현해두었다
일반적인 폼 로그인 방식을 할땐,
폼로그인을 요청하는 페이지와 로그인 성공시 입장하는 페이지의 리소스경로가 일치하면 안 되기때문에
로그인페이지를 따로 만들어서 그곳에서 로그인을하고 성공하면 테스트페이지로 돌아오도록 구현해두었다
하지만, 나의 경우
프론트쪽에서 ajax 요청으로 바디 안에다 사용자의 username, password 정보를 넣어 로그인요청이 들어올것이기때문에
프론트코드를 위에 맞게 수정해주고
usernamepassword어센시케이션filter를 커스터마이징해주어서 username, password 정보를 사용자 요청의 body에서 추출해 usernamepassword어센시케이션token에 담아줄 수 있도록 해주어야한다
우선 프론트를 수정하자
사용자가 username, password를 입력하고 버튼을 누르면 ajax 요청을 보내도록 프론트 코드를 작성해두었다
이제
usernamepassword어센시케이션filter를 커스터마이징해주어서
username, password 정보를 사용자 요청의 body에서 추출해 usernamepassword어센시케이션token에 담아주는 코드
작성 이후,
시큐리티필터체인에 커스터마이징한 필터를 등록해주면 된다
우선 usernamepassword어센시케이션filter를 상속받는 customusernampassword어센시케이션filter을 생성해주고
usernamepassword어센시케이션filter에서 username, password의 추출을 담당하는 어템프어센시케이션() 메소드를
오버라이딩해서 JSON 요청 body에서 username과 password를 추출하도록 해주자
package com.example.testLogin;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.Map;
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
try {
Map<String, String> credentials = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = credentials.get("username");
String password = credentials.get("password");
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return super.attemptAuthentication(request, response);
}
}
이제 스프링시큐리티콘피그에 커스텀필터를 추가해주어야한다
시큐리티필터체인에 CustomUsernamePasswordAuthenticationFilter 를 추가해주어야한다
그러기 위해서 아래 코드를 사용해
http.addFilterBefore()를 사용하여 기존의 UsernamePasswordAuthenticationFilter보다
CustomUsernamePasswordAuthenticationFilter 가 앞에 실행되도록 해주면 된다
http.addFilterBefore(new CustomUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
그리고 원래 기존의 usernamepassword어센시케이션filter를 이용하면 스프링시큐리티가 어센시케이션 메니저에 대해서 자동으로 만들어주지만, 필터를 커스터마이징해주었을 경우에는 어센시케이션 메니저와 유저디테일서비스를 직접 구현해줘야한다
이를 위해서 GlobalAuthenticationConfigurerAdapter를 상속해주는 AuthenticationConfig 클래스를 만들어서
내부에
어센시케이션메니저를 구현해서 사용자 인증을 처리하고
유저디테일서비스를 구현해서 사용자 정보를 로드해야한다
package com.example.testLogin;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
public class AuthenticationConfig extends GlobalAuthenticationConfigurerAdapter {
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Bean
public UserDetailsService userDetailsService() {
// 사용자 정보 로드 구현
}
}
//사용자 정보 로드 구현
부분에 유저디테일 객체를 전달해주면 된다
근데 유저디테일 객체는 username, password, 권한 이렇게 3개의 정보만 담을 수 있다
나는 유저디테일 객체에 에세스토큰과 리프레시토큰도 넣어줘야하기때문에
유저디테일 클래스를 커스터마이징 해주어야하고, 유저디테일서비스도 커스터마이징 해주어야한다
우선 커스텀유저디테일 클래스를 구현해주자
package com.example.testLogin;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class CustomUserDetails implements UserDetails {
private final Long id;
private final String username;
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
private final String accessToken;
private final String refreshToken;
public CustomUserDetails(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities, String accessToken, String refreshToken) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
public String getAccessToken() {
return accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
@Override
public String getPassword() {
return "";
}
@Override
public String getUsername() {
return "";
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
// Getters and other methods...
}
이제 커스텀 유저디테일서비스도 구현해주자
package com.example.testLogin;
import com.example.testLogin.Patient.PaitentRepository;
import com.example.testLogin.Patient.Patient;
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 CustomUserDetailsService implements UserDetailsService {
private final PaitentRepository patientRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Patient> patient = patientRepository.findByUsername(username);
if (patient == null) {
throw new UsernameNotFoundException("User not found");
}
// 사용자 정보와 액세스 토큰, 리프레시 토큰을 포함하는 CustomUserDetails 객체 생성
return new CustomUserDetails(
patient.get().getId(),
patient.get().getUsername(),
patient.get().getPassword(),
//여기에 에세스토큰 발급해서 넣으면댐,
//여기에 리프레시토큰 발급해서 넣으면댐
);
}
}
에세스토큰과 리프레시토큰을 발급해주는코드를 작성하고 주석자리에 끼워넣으면 된다
일단 그 전에
AuthenticationConfig 클래스에서 customUserDetailsService() 메서드를 구현하고
CustomUserDetailsService 빈을 등록하는걸 먼저 해줘서 어센시케이션콘피그 코드작성을 완료해주자
package com.example.testLogin;
import com.example.testLogin.Patient.PaitentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@Configuration
@RequiredArgsConstructor
public class AuthenticationConfig extends GlobalAuthenticationConfigurerAdapter {
private final PaitentRepository patientRepository;
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService());
}
@Bean
public CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService(patientRepository);
}
}
이제 에세스토큰과 리프레시토큰을 발급해서 로드바이유저네임으로 리턴해줄 커스텀유저디테일 객체에 에세스토큰과 리프레시토큰값을 넘겨주면 된다
JWT 토큰을 만드는건 아래 블로그를 참조해서
내일 만들자 ^^ 내일은 ㄹㅇ 로그인구현 끝낼거다 시발
'프로젝트 일지' 카테고리의 다른 글
(김유선) 개인프로젝트 SSR로 처음부터 끝까지 진행하기 - 페이지 제작(1) (0) | 2024.05.10 |
---|---|
(김유선) 개인프로젝트 SSR로 처음부터 끝까지 진행하기 - 회원가입,로그인,로그아웃 기능 (0) | 2024.05.09 |
(장준영) 팀 프로젝트 (1일차) - 알약 상세정보 검색 서비스 [백엔드] (0) | 2024.05.09 |
(김유선) 개인프로젝트 SSR로 처음부터 끝까지 진행하기 - 회원가입 기능 (0) | 2024.05.08 |
(김유선) 개인프로젝트 SSR로 처음부터 끝까지 진행하기 - 데이터 가공 (0) | 2024.05.08 |