하루에 4개씩 기초지식!
더보기
1. Spring Filter와 Intercepter의 사용예시
- 필터는 말 그대로 요청과 응답을 거른 뒤 정제하는 역할을 한다!
- 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너에 의해 관리가 되는 것이고, 스프링 범위 밖에서 처리된다!
- Dispatcher Servlet에 요청이 전달되기 전 / 후에 url 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 제공한다!
- 사용 사례
- 보안 및 인증 / 인가 관련 작업
- 모든 요쳉에 대한 로깅 또는 검사
- 이미지 / 데이터 압축 및 문자열 인코딩
- Spring과 분리되어야 하는 기능
- 보안 및 인증 / 인가 관련 작업
- 인터셉터는 요청에 대한 작업 전/ 후로 가로채 요청과 응답을 참조하거나 가공하는 역할을 합니다.
- 웹 컨테이너에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작합니다.
- Dispatcher Servlet이 Controller를 호출하기 전 / 후에 인터셉터가 끼어들어 요청과 등답을 참조하거나 가공할 수 있는 기능을 제공한다!
- 사용 사례
- 세부적인 보안 및 인증 / 인가 공통 작업
- API 호출에 대한 로깅 또는 검사
- Controller로 넘겨주는 정보(데이터)의 가공
- 세부적인 보안 및 인증 / 인가 공통 작업
2. 관점지향 프로그래밍이란?
- AOP(Aspect Oriented Programming)는 핵심 비즈니스 로직에 있는 공통 관심사항을 분리하여 각각을 모듈화 하는 것을 의미하며 공통 모듈인 인증, 로깅, 트랜잭션 처리에 용이합니다
- 핵심 비즈니스 로직에 부가기능을 하는 모듈이 중복되어 분포되어 있을 경우 사용할 수 있습니다.
- AOP의 가장 큰 특징이자 장점은 중복 코드 제거, 재활용성의 극대화, 변화 수용의 용이성이 좋다는 점이다!
3. Lombok 넌 뭐냐..
- Lombok은 메소드를 컴파일하는 과정에 개입해서 추가적으로 코드를 만들어 냅니다. 이것을 어노테이션 프로세싱이라고 하는데, 어노테이션 프로세싱은 자바 컴파일러가 컴파일 단계에서 어노테이션을 분석하고 처리하는 기법을 말합니다.
4. Servlet 이란?
- 클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술이다!
- Spring MVC에서 Controller로 이용되며ㅡ 사용자의 요청을 받아 처리한 후에 결과를 반환합니다.
- 간단히 - 자바를 사용해 웹을 만들기 위해 필요한 기술.
JWT Token을 사용해서 회원가입 / 로그인/ 회원 탈퇴를 만들어보자!
1. Entity
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class UserEntity extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "userId")
private Long userId;
@Column(nullable = false, name = "username")
private String username;
@Column(nullable = false, unique = true, name = "email" )
private String email;
@Column(nullable = false, name = "password")
private String password;
@Column(nullable = false, name = "gender")
@Enumerated(EnumType.STRING)
private Gender gender;
@Column(nullable = true, name = "introduce")
private String introduce;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Status status = Status.ACTIVE;
// 친구관련 추가(진호)
@OneToMany (mappedBy = "requestedUserId", cascade = CascadeType.ALL, orphanRemoval = true)
private List<FriendRequest> friendRequests = new ArrayList<>();
@OneToMany (mappedBy = "id", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Friend> friends = new ArrayList<>();
public UserEntity(String username, String email, String password, Gender gender) {
this.username = username;
this.email = email;
this.password = password;
this.gender = gender;
this.status = Status.ACTIVE;
}
public UserEntity(String email) {
this.email = email;
}
public enum Gender {
MALE, FEMALE
}
public enum Status{
ACTIVE, WITHDRAWN
}
}
2. Repository
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByEmail(String email);
boolean existsByEmail(String email);
Optional<UserEntity> findByUserId(Long otherUserId);
}
3. Service
@Service
@RequiredArgsConstructor
@Transactional
public class AuthService {
private final UserRepository userRepository;
private final JwtUtil jwtTokenUtil;
private final PasswordEncoder passwordEncoder;
private final UserValidationUtil userValidationUtil;
public String userRegiser(UserRegisterRequestDto userRegisterRequestDto) {
//이메일 중복 여부 확인
userValidationUtil.checkEmailDuplication(userRegisterRequestDto.getEmail(), userRepository);
//이메일 형식 검증
EmailValidator emailValidator = new EmailValidator();
emailValidator.validateEmail(userRegisterRequestDto.getEmail());
//비밀번호 형식 검증
PasswordValidator passwordValidator = new PasswordValidator();
passwordValidator.validatePassword(userRegisterRequestDto.getPassword());
//비밀번호 암호화 저장
String encodePassword = passwordEncoder.encode(userRegisterRequestDto.getPassword());
UserEntity user = new UserEntity(
userRegisterRequestDto.getUsername(),
userRegisterRequestDto.getEmail(),
encodePassword,
userRegisterRequestDto.getGender()
);
UserEntity saveUser = userRepository.save(user);
return jwtTokenUtil.generateToken(saveUser.getUserId(), saveUser.getEmail());
}
public String loginUser(LoginRequestDto loginRequestDto) {
UserEntity user = userRepository.findByEmail(loginRequestDto.getEmail())
.orElseThrow(() -> new IllegalArgumentException("해당 계정의 유저는 없습니다."));
return jwtTokenUtil.generateToken(user.getUserId(), user.getEmail());
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserValidationUtil userValidationUtil;
private final PasswordEncoder passwordEncoder;
//자신의 프로필 조회
public UserEntity getMe(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
}
// 다른 유저 프로필 조회
public UserEntity getUserByEmail(String email, String loginEmail) {
// 사용자가 자신의 프로필을 조회하려 할 때 예외 처리
if (loginEmail.equals(email)) {
throw new IllegalArgumentException("본인 프로필 정보는 '자신의 프로필 조회' 탭에서 조회 가능합니다.");
}
// 다른 사용자의 이메일을 통해 조회
return userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
}
public void updateUser(Long userId, Map<String, Object> updateFields) {
UserEntity user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
//UpdateUtil에서 수정을 원하는 필드만 수정하게 만들어줌
UpdateUtil.updateUserFields(user, updateFields, passwordEncoder);
// 변경된 사용자 정보 저장
userRepository.save(user);
}
public void withdrawUser(String email, String password) {
//이메일 조회
UserEntity user = userRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다."));
//탈퇴 여부 확인
if (user.getStatus() == UserEntity.Status.WITHDRAWN) {
throw new IllegalArgumentException("이미 탈퇴한 사용자입니다.");
}
//비밀번호 일치 확인
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
//탈퇴 상태로 변경
user.setStatus(UserEntity.Status.WITHDRAWN);
userRepository.save(user);
}
}
4. Controller
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class AuthController {
public final AuthService authService;
// 회원가입
@PostMapping("/register")
public ResponseEntity<Void> userRegister(@RequestBody UserRegisterRequestDto userRegisterRequestDto) {
String bearerToken = authService.userRegiser(userRegisterRequestDto);
return ResponseEntity
.ok()
.header(HttpHeaders.AUTHORIZATION, bearerToken)
.build();
}
// 로그인
@PostMapping("/login")
public ResponseEntity<Void> loginUser(@RequestBody LoginRequestDto loginRequestDto) {
String bearerToken = authService.loginUser(loginRequestDto);
return ResponseEntity
.ok()
.header(HttpHeaders.AUTHORIZATION, bearerToken)
.build();
}
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
//자신의 프로필 조회
@GetMapping("/me")
public ResponseEntity<UserEntity> getMe (@Auth AuthUser authUser){
Long userId = authUser.getUserId();
UserEntity user = userService.getMe(userId);
return ResponseEntity.ok(user);
}
//다른 사람의 프로필 조회
//모든 사람이 확인하면 안되므로 토큰으로 인증된(로그인된) 사용자만 확인할 수 있도록함
@GetMapping("/otherUser")
public ResponseEntity<UserEntity> getOtherUser (@Auth AuthUser authUser, @RequestParam String email){
// JWT 토큰에서 로그인한 사용자의 이메일 추출
String loginEmail = authUser.getEmail();
// 다른 사용자의 정보를 이메일로 조회
UserEntity otherUser = userService.getUserByEmail(email, loginEmail);
// 로그인한 사용자와 다른 사용자일 때 민감한 정보 제거
otherUser.setPassword(null); // 비밀번호는 노출되지 않게 null 처리
return ResponseEntity.ok(otherUser);
}
@PutMapping("/update")
public ResponseEntity<String> updateUser(
@Auth AuthUser authUser, // AuthUser에서 userId 추출
@RequestBody Map<String, Object> updateFields) {
Long userId = authUser.getUserId(); // userId 추출
userService.updateUser(userId, updateFields); // userId와 updateFields 전달
return ResponseEntity.ok("회원 정보가 수정되었습니다.");
}
@DeleteMapping("/withdraw")
public ResponseEntity<String> withdrawUser (@RequestBody WithdrawRequestDto withdrawRequestDto){
try {
userService.withdrawUser(withdrawRequestDto.getEmail(), withdrawRequestDto.getPassword());
return ResponseEntity.ok("회원 탈퇴가 완료되었습니다");
}
//회원 탈퇴 시
catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
5. RequestDto
@Getter
public class LoginRequestDto {
private String email;
private String password;
}
@Getter
public class UserRegisterRequestDto {
private String username;
private String email;
private String password;
private UserEntity.Gender gender;
public UserRegisterRequestDto( // 어노테이션 추가
@JsonProperty("username") String username,
@JsonProperty("email") String email,
@JsonProperty("password") String password,
@JsonProperty("gender") UserEntity.Gender gender
) {
this.username = username;
this.email = email;
this.password = password;
this.gender = gender;
}
}
@Getter
public class WithdrawRequestDto {
private String email;
private String password;
public WithdrawRequestDto(String email, String password) {
this.email = email;
this.password = password;
}
}
@Getter
@Setter
public class UserRequestDto {
private String userName;
private String email;
private String currentPassword;
private String newPassword;
public UserRequestDto() {
}
public UserRequestDto(String userName, String email, String currentPassword, String newPassword) {
this.userName = userName;
this.email = email;
this.currentPassword = currentPassword;
this.newPassword = newPassword;
}
}
6. ResponseDto
@Getter
public class LoginResponseDto {
private String messege;
private String token;
}
@Getter
public class UserRegisterResponseDto {
private final Long id;
private final String username;
private final String email;
private final UserEntity.Gender gender;
private final LocalDateTime createdAt;
public UserRegisterResponseDto(Long id, String username, String email, UserEntity.Gender gender, LocalDateTime createdAt) {
this.id = id;
this.username = username;
this.email = email;
this.gender = gender;
this.createdAt = createdAt;
}
}
@Getter
public class UserResponseDto {
private Long userId;
private String userName;
private String email;
private String gender;
private String introduce;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public UserResponseDto() {
}
public UserResponseDto(Long userId, String userName, String email, String gender, String introduce, LocalDateTime createdAt, LocalDateTime modifiedAt) {
this.userId = userId;
this.userName = userName;
this.email = email;
this.gender = gender;
this.introduce = introduce;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
}
}
7.Util
@Slf4j(topic = "JwtTokenUtil")
@Component
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L;
private String secretKey = "===========";
private Key key;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String generateToken(Long userId, String email) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
.signWith(SignatureAlgorithm.HS256, key) // 암호화 알고리즘
.compact();
}
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
log.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
public Claims extractClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
@Component
public class UpdateUtil {
public static void updateUserFields(UserEntity user, Map<String, Object> updateFields, PasswordEncoder passwordEncoder) {
if (updateFields.containsKey("email")) {
String newEmail = (String) updateFields.get("email");
user.setEmail(newEmail);
}
// 비밀번호 수정 시 조건 추가
if (updateFields.containsKey("currentPassword") && updateFields.containsKey("newPassword")) {
String currentPassword = (String) updateFields.get("currentPassword");
String newPassword = (String) updateFields.get("newPassword");
if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
throw new IllegalArgumentException("현재 비밀번호가 일치하지 않습니다.");
}
if (passwordEncoder.matches(newPassword, user.getPassword())) {
throw new IllegalArgumentException("새 비밀번호는 현재 비밀번호와 동일할 수 없습니다.");
}
user.setPassword(passwordEncoder.encode(newPassword));
updateFields.remove("currentPassword");
updateFields.remove("newPassword");
}
if (updateFields.containsKey("username")) {
String newUsername = (String) updateFields.get("username");
user.setUsername(newUsername);
}
if (updateFields.containsKey("gender")) {
UserEntity.Gender newGender = UserEntity.Gender.valueOf((String) updateFields.get("gender"));
user.setGender(newGender);
}
if (updateFields.containsKey("introduce")) {
String newIntroduce = (String) updateFields.get("introduce");
user.setIntroduce(newIntroduce);
}
}
}
@Component
public class UserValidationUtil {
//이메일 중복 확인 메서드
public void checkEmailDuplication(String email, UserRepository userRepository) {
if(userRepository.existsByEmail(email)){
throw new IllegalArgumentException("해당 이메일은 이미 사용중입니다.");
}
}
//로그인 시 아이디ㅣ 비밀번호 일치 여부 확인
public UserEntity findByEmailValidateLogin(String email, String password, UserRepository userRepository) {
Optional<UserEntity> userOptional = userRepository.findByEmail(email);
if (userOptional.isEmpty()) {
throw new IllegalArgumentException("Invalid email or password");
}
return userOptional.get();
}
}
8. Filter
@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {
private final JwtUtil jwtUtil;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String url = httpRequest.getRequestURI();
if (url.startsWith("/api/register") || url.startsWith("/api/login")) {
chain.doFilter(request, response);
return;
}
String bearerJwt = httpRequest.getHeader("Authorization");
if (bearerJwt == null || !bearerJwt.startsWith("Bearer ")) {
// 토큰이 없는 경우 400을 반환합니다.
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
return;
}
String jwt = jwtUtil.substringToken(bearerJwt);
try {
// JWT 유효성 검사와 claims 추출
Claims claims = jwtUtil.extractClaims(jwt);
// 사용자 정보를 ArgumentResolver 로 넘기기 위해 HttpServletRequest 에 세팅
httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
httpRequest.setAttribute("email", claims.get("email", String.class));
chain.doFilter(request, response);
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
} catch (Exception e) {
log.error("JWT 토큰 검증 중 오류가 발생했습니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰 검증 중 오류가 발생했습니다.");
}
}
}
9. config
public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null;
boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class);
// @Auth 어노테이션과 AuthUser 타입이 함께 사용되지 않은 경우 예외 발생
if (hasAuthAnnotation != isAuthUserType) {
throw new IllegalArgumentException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다.");
}
return hasAuthAnnotation;
}
@Override
public Object resolveArgument(
@Nullable MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory
) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
// JwtFilter 에서 set 한 userId, email 값을 가져옴
Long userId = (Long) request.getAttribute("userId");
String email = (String) request.getAttribute("email");
return new AuthUser(userId, email);
}
}
@Configuration
@RequiredArgsConstructor
public class FilterConfig {
private final JwtUtil jwtTokenUtil;
@Bean
// jwtfilter를 등록하기 위해서 bean 생성
public FilterRegistrationBean<JwtFilter> jwtFilter() {
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
//등록할 필터로 jwtfilter 설정
registrationBean.setFilter(new JwtFilter(jwtTokenUtil));
//필터가 적용될 URL 설정
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
@Component
public class PasswordEncoder {
//비밀번호를 해싱하여 암호화
public String encode(String rawPassword) {
return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
}
//입력한 비밀번호가 암호화된 비밀번호랑 일치하는지 확인
public boolean matches(String rawPassword, String encodedPassword) {
BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
return result.verified;
}
}
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
// ArgumentResolver 등록
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthUserArgumentResolver());
}
}
'스프링' 카테고리의 다른 글
최적화? Indexing? (0) | 2024.10.18 |
---|---|
스프링 인증/인가..! JWt..? (0) | 2024.09.10 |
[TroubleShooting] 의존성 주입,,,실패하다,, (1) | 2024.08.29 |
Spring JPA 뭐하는 키워드지..? (1) | 2024.08.24 |
스프링 의존성 주입? 강한 결합? 느슨한 결합? (0) | 2024.08.21 |