회원가입이 마무리 되지 않았을 때 처리
- EventUserService
- 기존 인증코드를 삭제하고 인증코드 재발송
public boolean checkEmailDuplicate(String email) {
boolean exists = eventUserRepository.existsByEmail(email);
log.info("Checking email {} is duplicate : {}", email, exists);
if (exists && notFinish(email)) {
return false;
}
if (!exists) processSignUp(email);
return exists;
}
private boolean notFinish(String email) {
EventUser eventUser = eventUserRepository.findByEmail(email).orElseThrow();
if (!eventUser.isEmailVerified() || eventUser.getPassword() == null) {
EmailVerification ev = emailVerificationRepository
.findByEventUser(eventUser)
.orElse(null);
if (ev != null) emailVerificationRepository.delete(ev);
generateAndSendCode(email, eventUser);
return true;
}
return false;
}
로그인 검증
@PostMapping("/sign-in")
public ResponseEntity<?> singIn(@RequestBody LoginRequestDto dto) {
try {
eventUserService.authenticate(dto);
return ResponseEntity.ok().body("login success");
} catch (LoginFailException e) {
String errorMessage = e.getMessage();
return ResponseEntity.status(422).body(errorMessage);
}
}
public void authenticate(final LoginRequestDto dto) {
EventUser eventUser = eventUserRepository
.findByEmail(dto.getEmail())
.orElseThrow(
() -> new LoginFailException("가입된 회원이 아닙니다.")
);
if (!eventUser.isEmailVerified() || eventUser.getPassword() == null) {
throw new LoginFailException("회원가입이 중단된 회원입니다. 다시 가입해주세요.");
}
String inputPassword = dto.getPassword();
String encodedPassword = eventUser.getPassword();
if (!encoder.matches(inputPassword, encodedPassword)) {
throw new RuntimeException("비밀번호가 틀렸습니다.");
}
}
세션과 토큰 차이점
세션 (Stateful)
- 로그인한다 -> 로그인한 상태라는 것을 서버(또는 DB)에 저장
서버와 클라이언트가 연결된 상태로(Stateful), 다른 요청들을 처리
-> 서버가 클라이언트와 연결된 상태를 유지해야함 -> 사용자가 많아지면 버거움
토큰 (Stateless)
- 로그인을 할 때 서버가 클라이언트에 토큰을 줌
-> 클라이언트는 매 요청시마다 토큰을 같이 보내줌
-> 서버는 연결상태(로그인여부?)를 확인할 필요없이 토큰만 있으면 작업을 처리 (Stateless)
JWT 토큰
- JWT(JSON web token)는 웹에서 정보를 안전하게 전송하는 방법 중 하나
- JWT는 header, payload, signature로 이뤄져있음
토큰 생성
package com.study.event.api.auth;
@Component
@Slf4j
public class TokenProvider {
@Value("${jwt.secret}")
private String SECRET_KEY;
public String createToken(EventUser eventUser) {
Map<String, Object> claims = new HashMap<>();
claims.put("email", eventUser.getEmail());
claims.put("role", eventUser.getRole().toString());
return Jwts.builder()
.signWith(
Keys.hmacShaKeyFor(SECRET_KEY.getBytes())
, SignatureAlgorithm.HS512
)
.setClaims(claims)
.setIssuer("메롱메롱")
.setIssuedAt(new Date())
.setExpiration(Date.from(
Instant.now().plus(1, ChronoUnit.DAYS)
))
.setSubject(eventUser.getId())
.compact();
}
}
회원 인증 처리
- EventUserService
- 로그인 성공한 인증 정보 관리 어떻게 할거야? (세션 or 토큰 or 쿠키)
-> 토큰, 인증정보를 클라이언트에게 전송
public LoginResponseDto authenticate(final LoginRequestDto dto) {
EventUser eventUser = eventUserRepository.findByEmail(dto.getEmail())
.orElseThrow(
() -> new LoginFailException("가입된 회원이 아닙니다.")
);
if (!eventUser.isEmailVerified() || eventUser.getPassword() == null) {
throw new LoginFailException("회원가입이 중단된 회원입니다. 다시 가입해주세요.");
}
String inputPassword = dto.getPassword();
String encodedPassword = eventUser.getPassword();
if (!encoder.matches(inputPassword, encodedPassword)) {
throw new LoginFailException("비밀번호가 틀렸습니다.");
}
String token = tokenProvider.createToken(eventUser);
return LoginResponseDto.builder()
.email(dto.getEmail())
.role(eventUser.getRole().toString())
.token(token)
.build();
}
- LoginRequestDto
- 회원가입시 입력받았던 정보들을 클라이언트 -> 서버로
package com.study.event.api.event.dto.request;
@Getter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginRequestDto {
private String email;
private String password;
}
- LoginResponseDto
- 로그인 정보가 맞다면(로그인 성공) 토큰 생성해서 LoginResponseDto에 성공 정보를 담아 클라이언트에 반환
package com.study.event.api.event.dto.response;
import lombok.*;
@Getter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginResponseDto {
private String email;
private String role;
private String token;
}
@PostMapping("/sign-in")
public ResponseEntity<?> signIn(@RequestBody LoginRequestDto dto) {
try {
eventUserService.authenticate(dto);
return ResponseEntity.ok().body("login success");
} catch (LoginFailException e) {
String errorMessage = e.getMessage();
return ResponseEntity.status(422).body(errorMessage);
}
}
- SecurityConfig
- 세션 인증 사용안하는 설정 해야함
- 로그인 없이 이용가능한 부분, 로그인이 있어야 이용가능한 부분 인가 설정
package com.study.event.api.config;
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/", "/auth/**").permitAll()
.anyRequest().authenticated() // 인가 설정 on
;
return http.build();
}
}
- 이제 모든 요청에서 토큰검사가 선행되어야 함!
-> 컨트롤러의 모든 요청에서 검사하는 로직을 작성해도 되지만, JwtAuthfilter를 라는 객체를 생성하여 controller 보다 먼저 요청을 받는 객체를 만들어 토큰 검사를 진행 (interceptor보다 더 광범위한 개념)
-> filter를 작성해서 securityConfig에서 기존의 filterChain에 등록(연결) 시켜야한다.
- JwtAuthFilter
package com.study.event.api.auth.filter;
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = parseBearerToken(request);
log.info("토큰 위조 검사 필터 작동");
if (token != null) {
tokenProvider.validateAndGetTokenInfo(token);
}
} catch (Exception e) {
log.warn("토큰이 위조되었습니다.");
e.printStackTrace();
}
filterChain.doFilter(request, response);
}
private String parseBearerToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
return bearerToken.substring(7);
}
return null;
}
}
토큰에 저장한 데이터 컨트롤러에서 사용
@GetMapping("/page/{pageNo}")
public ResponseEntity<?> getList(
@AuthenticationPrincipal String userId,
@RequestParam(required = false) String sort,
@PathVariable int pageNo) throws InterruptedException {
log.info("token user id : {}", userId);
- JwtAuthFilter
- 여기 있는 userId에는 회원의 pk가 들어있음
AbstractAuthenticationToken auth
= new UsernamePasswordAuthenticationToken(
userId,
null,
new ArrayList<>()
);
- 토큰에 저장한 userId 사용하기 위해 userId 추가