Java/Spring
Spring - Spring Security 6 + JWT 로그인, 로그아웃(최종편 - user 파일)
- -
개요
Spring Security 6 + JWT 로그인, 로그아웃 과정의 마지막 글인 user 패키지에 대한 글이다.
파일 구조
/src/main/java/project
├── global
│ ├── config
│ │ └── security
│ │ └── SecurityConfig.java
│ └── entity
│ └── AuditingFields.java
├── jwt
│ ├── entity
│ │ └── Token.java
│ ├── filter
│ │ └── TokenAuthenticationFilter.java
│ ├── infrastructure
│ │ ├── CustomUserDetails.java
│ │ └── JwtAuthenticationToken.java
│ ├── repository
│ │ └── TokenRepository.java
│ ├── service
│ │ ├── CustomUserDetailsService.java
│ │ └── TokenService.java
│ └── util
│ └── JwtTokenizer.java
└── user
├── controller
│ └── UserController.java
├── dto
│ ├── UserJoinDto.java
│ ├── UserLoginDto.java
│ └── UserLoginResponseDto.java
├── entity
│ ├── Authority.java
│ └── User.java
├── repository
│ └── UserRepository.java
└── service
└── UserService.java
UserController.java
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/user")
@Slf4j
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
private final JwtTokenizer jwtTokenizer;
private final TokenService tokenService;
@PostMapping("/login")
public ResponseEntity login(@RequestBody @Valid UserLoginDto userLoginDto, BindingResult bindingResult, HttpServletResponse httpServletResponse) {
// 필드 에러 확인
if (bindingResult.hasErrors()) {
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
Map<String, String> errors = fieldErrors.stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
return new ResponseEntity(Map.of("errors", errors), HttpStatus.BAD_REQUEST);
}
User user = userService.findByEmail(userLoginDto.getEmail());
// 비밀번호 일치여부 체크
if (user == null || !passwordEncoder.matches(userLoginDto.getPassword(), user.getPassword())) {
return new ResponseEntity(
Map.of("errors", Map.of("password", "이메일과 비밀번호가 일치하지 않습니다.")),
HttpStatus.UNAUTHORIZED
);
}
// 토큰 발급
String accessToken = jwtTokenizer.createAccessToken(user.getId(), user.getEmail(), user.getNickname(), user.getAuthority());
String refreshToken = jwtTokenizer.createRefreshToken(user.getId(), user.getEmail(), user.getNickname(), user.getAuthority());
// 리프레시 토큰 디비 저장
Token token = Token.builder()
.user(user)
.accessToken(accessToken)
.refreshToken(refreshToken)
.grantType("Bearer")
.build();
tokenService.saveOrUpdate(token);
// 토큰 쿠키 저장
Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setPath("/");
accessTokenCookie.setMaxAge(Math.toIntExact(JwtTokenizer.accessTokenExpire / 1000));
httpServletResponse.addCookie(accessTokenCookie);
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(Math.toIntExact(JwtTokenizer.refreshTokenExpire / 1000));
httpServletResponse.addCookie(refreshTokenCookie);
// 응답 값
UserLoginResponseDto loginResponseDto = UserLoginResponseDto.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.userId(user.getId())
.nickname(user.getNickname())
.build();
return new ResponseEntity(loginResponseDto, HttpStatus.OK);
}
@DeleteMapping("/logout")
public ResponseEntity logout(HttpServletRequest request, HttpServletResponse response) {
String accessToken = null;
// access / refresh token cookie 삭제
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
switch (cookie.getName()) {
case "accessToken":
accessToken = cookie.getValue();
case "refreshToken":
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
break;
}
}
}
// tokens 데이터 삭제
tokenService.deleteByAccessToken(accessToken);
return new ResponseEntity("로그아웃 되었습니다.", HttpStatus.OK);
}
@PatchMapping("/refreshToken")
public ResponseEntity refreshToken(HttpServletRequest request, HttpServletResponse response) {
// refresh token cookie 가져오기
String refreshToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("refreshToken".equals(cookie.getName())) {
refreshToken = cookie.getValue();
break;
}
}
}
// refresh token이 없을 경우
if (refreshToken == null) {
return new ResponseEntity("토큰이 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
}
// refresh token 파싱
Claims claims = jwtTokenizer.parseRefreshToken(refreshToken);
// 유저 정보 가져오기
Long userId = Long.valueOf((Integer) claims.get("userId"));
User user = userService.findById(userId).orElseThrow(() -> new IllegalArgumentException("사용자를 찾지 못했습니다."));
Authority authority = Authority.valueOf(claims.get("authority", String.class));
// access token 생성
String accessToken = jwtTokenizer.createAccessToken(userId, user.getEmail(), user.getNickname(), authority);
// accessToken 쿠키 생성
Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setPath("/");
accessTokenCookie.setMaxAge(Math.toIntExact(JwtTokenizer.accessTokenExpire / 1000));
response.addCookie(accessTokenCookie);
// Token 데이터 access token 값 업데이트
tokenService.updateByRefreshToken(refreshToken, accessToken);
// 응답 값
UserLoginResponseDto responseDto = UserLoginResponseDto.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.nickname(user.getNickname())
.userId(user.getId())
.build();
return new ResponseEntity(responseDto, HttpStatus.OK);
}
}
UserLoginDto.java
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.*;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginDto {
@NotEmpty(message = "이메일을 입력해주세요.")
@Pattern(regexp = "^[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*\\.[a-zA-Z]{2,3}$",
message = "올바르지 않은 이메일 형식입니다.")
private String email;
// 영문, 특수문자, 숫자 포함 8 ~ 16자
@NotEmpty(message = "비밀번호를 입력해주세요.")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*?_]).{8,16}$",
message = "올바르지 않은 비밀번호 형식입니다.")
private String password;
}
UserLoginResponseDto.java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginResponseDto {
private String accessToken;
private String refreshToken;
private Long userId;
private String nickname;
}
User.java
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "users")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false, unique = true)
private String nickname;
@Setter
@Column(nullable = false)
private String password;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Authority authority;
}
Authority.java
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
@Getter
@AllArgsConstructor
public enum Authority implements GrantedAuthority {
ADMIN,
USER;
/**
* 유저의 권한을 String으로 반환하는 메소드
* @return authority name
*/
@Override
public String getAuthority() {
return name();
}
}
UserRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email);
}
UserService.java
import lombok.AllArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public User findByEmail(String email) {
return userRepository.findByEmail(email);
}
@Transactional(readOnly = true)
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
}
이전 글
반응형
Contents
소중한 공감 감사합니다