Controller 부분
@PostMapping("/member/login")
public ResponseEntity<? super ResponseTokenDto> login(@RequestBody Map<String, String> paramMap) {
String email = paramMap.get("email");
String password = paramMap.get("password");
ResponseEntity<? super ResponseTokenDto> response = memberService.login(email,password, passwordEncoder);
return response;
}
현재는 Map으로 받아오지만 나중에 리팩토링으로 별도 DTO를 만들어서 가져올 예정
DTO 부분
@Getter
public class ResponseTokenDto extends ResponseDto{
private String token;
private ResponseTokenDto (String token) {
super();
this.token = token;
}
public static ResponseEntity<ResponseTokenDto> success (String token){
ResponseTokenDto responseBody = new ResponseTokenDto(token);
return ResponseEntity.status(HttpStatus.OK).body(responseBody);
}
public static ResponseEntity<ResponseDto> signInFail () {
ResponseDto responseBody = new ResponseDto(ResponseCode.SIGN_IN_FAIL , ResponseMessage.SIGN_IN_FAIL);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(responseBody) ;
}
}
- super()를 사용한 이유
- 부모 클래스의 초기화 : 부모 클래스의 정의된 변수나 메서드가 자식 클래스에서 사용될수 있기 때문에 부모클래스의 초기화가 필요
- 코드의 재사용: 부모 클래스에서 이미 정의된 초기화 로직을 재사용 가능
- 구조적인 일관성: 모든 클래스는 Object 클래스를 상속받으며, 이는 Java의 최상위 클래스 , super()를 통해 부모 클래스의 생성자를 호출하는 것은 클래스 상속 구조의 일관성을 유지
Service 부분
ResponseEntity<? super ResponseTokenDto> login(String email, String password , PasswordEncoder passwordEncoder);
비밀번호를 암호화해서 저장했기 떄문에 PasswordEncoder도 같이 전달
ServiceImpl 부분
@Override
public ResponseEntity<? super ResponseTokenDto> login(String email, String password , PasswordEncoder passwordEncoder) {
String token = null;
try{
if(email == null ||email.isEmpty()){
return ResponseDto.validationFail();
}
if(password == null ||password.isEmpty()){
return ResponseDto.validationFail();
}
Member findMember = memberRepository.findByEmail(email);
if(findMember == null){
return ResponseTokenDto.signInFail();
}
if (!passwordEncoder.matches(password, findMember.getPassword())) {
return ResponseTokenDto.signInFail();
}
token = jwtProvider.create(findMember.getEmail());
} catch (Exception exception){
exception.printStackTrace();
return ResponseDto.databaseError();
}
return ResponseTokenDto.success(token);
}
JwtProvider 부분
@Component
public class JwtProvider {
@Value("${secret-key}")
private String secretKey;
public String create(String email) {
Date expiredDate = Date.from(Instant.now().plus(1,ChronoUnit.HOURS));
Key key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
String jwt = Jwts.builder()
.signWith(key, SignatureAlgorithm.HS256)
.setSubject(email)
.setIssuedAt(new Date()).setExpiration(expiredDate)
.compact();
return jwt;
}
public String validate(String jwt) {
String subject = null;
Key key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
try{
subject = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(jwt)
.getBody()
.getSubject();
}catch (Exception exception){
exception.printStackTrace();
return null;
}
return subject;
}
}
@Value 는 어플 application.properties 또는 application.yml 파일에 있는 r값을 읽고 적용된 필드에 주입
JWT 토큰에는 민감한 정보를 저장하면 안되기에 지금은 간단히 이메일로만 체크하도록 설정
JwtAuthenticationFilter 부분
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final MemberRepository memberRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 토큰 추출
String token = parseBearerToken(request);
if( token == null){
filterChain.doFilter(request,response);
return;
}
// 토큰 검증
String email = jwtProvider.validate(token);
if(email == null){
filterChain.doFilter(request,response);
return;
}
//사용자 조회 및 권한 설정
Member member = memberRepository.findByEmail(email);
String role = member.getRole();
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(role));
//인증 객체 생성 및설정
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
AbstractAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(member.getEmail() , /*이부분에 패스워드 넣어도됨*/null ,authorities );
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
securityContext.setAuthentication(authenticationToken);
SecurityContextHolder.setContext(securityContext);
//예외처리
} catch (Exception exception){
exception.printStackTrace();
}
//다음 필터로 전달
filterChain.doFilter(request,response);
}
private String parseBearerToken(HttpServletRequest request){
String authorization = request.getHeader("Authorization");
boolean hasAuthorization = StringUtils.hasText(authorization);
if(!hasAuthorization){
return null;
}
boolean isBearer = authorization.startsWith("Bearer ");
if(!isBearer){
return null;
}
String token = authorization.substring(7);
return token;
}
}
- doFilterInternal가 작동하는 순서
- 토큰 추출 : parseBearerToken 메서드를 호출하여 요청 헤더에서 "Authorization" 헤더를 읽고, "Bearer "로 시작하는 토큰을 추출합니다. 토큰이 없거나 형식이 잘못된 경우, 필터 체인의 다음 필터로 요청을 전달합니다.
- 토큰 검증: jwtProvider.validate(token)를 호출하여 토큰을 검증하고, 유효한 경우 이메일 주소를 반환받습니다. 이메일이 없으면 다음 필터로 요청을 전달합니다
- 사용자 조회 및 권한 설정: memberRepository.findByEmail(email)을 호출하여 사용자 정보를 조회합니다. 사용자의 역할(role)을 가져와서 GrantedAuthority 리스트를 만듭니다.
- 인증 객체 생성 및 설정: UsernamePasswordAuthenticationToken을 사용하여 인증 객체를 생성하고, 요청의 세부 정보를 설정한 후, SecurityContextHolder에 인증 객체를 저장합니다.
- 예외처리: 예외가 발생하면 스택 트레이스를 출력하고, 필터 체인의 다음 필터로 요청을 전달합니다.
- 다음 필터로 전달: 모든 작업이 끝난 후 filterChain.doFilter(request, response)를 호출하여 요청을 체인의 다음 필터로 전달합니다. 이 호출이 필수적인 이유는 필터 체인이 중단되지 않고 끝까지 실행되도록 보장하기 위해서입니다
이메일과 비밀번호를 틀렸을때 무엇을 알려주면 좋을수 있으나 보안상의 이유로 로그인이 잘못된다는 정보만 출력
'프로젝트' 카테고리의 다른 글
스프링 부트 프로젝트(소셜로그인) (0) | 2024.06.16 |
---|---|
스프링 부트 프로젝트 (회원 가입) (0) | 2024.05.29 |