Skip to content

Commit

Permalink
Merge pull request #15 from BOOK-TALK/#10-auth-service
Browse files Browse the repository at this point in the history
#10 auth service
  • Loading branch information
chanwoo7 authored Jul 31, 2024
2 parents fabef52 + bee4686 commit bb3418b
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 10 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ dependencies {
// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

// modelmapper
implementation 'org.modelmapper:modelmapper:3.1.0'

developmentOnly 'org.springframework.boot:spring-boot-devtools'

compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.book.backend.domain.auth.controller;

import com.book.backend.domain.auth.dto.LoginDto;
import com.book.backend.domain.auth.dto.SignupDto;
import com.book.backend.domain.auth.service.AuthService;
import com.book.backend.domain.user.dto.UserDto;
import jakarta.servlet.http.HttpServletRequest;
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.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {

private final AuthService authService;

@PostMapping("/signup")
public ResponseEntity<UserDto> signup(@Valid @RequestBody SignupDto signupDto) {
log.info("signup 호출");
UserDto userDto = authService.signup(signupDto);
return ResponseEntity.ok(userDto);
}

@PostMapping("/login")
public ResponseEntity<UserDto> login(@Valid @RequestBody LoginDto loginDto) {
log.info("login 호출");
UserDto userDto = authService.login(loginDto);
return ResponseEntity.ok(userDto);
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request) {
log.info("logout 호출");
request.getSession().invalidate();
SecurityContextHolder.clearContext();
return new ResponseEntity<>(HttpStatus.OK);
}

@DeleteMapping("/delete")
public ResponseEntity<Void> deleteAccount(HttpServletRequest request) {
log.info("deleteAccount 호출");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String loginId = authentication.getName();

authService.deleteAccountByLoginId(loginId);
request.getSession().invalidate();
SecurityContextHolder.clearContext();

return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/book/backend/domain/auth/dto/LoginDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.book.backend.domain.auth.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class LoginDto {
@NotBlank(message = "아이디는 필수 입력값입니다.")
private String loginId;

@NotBlank(message = "비밀번호는 필수 입력값입니다.")
private String password;
}
40 changes: 40 additions & 0 deletions src/main/java/com/book/backend/domain/auth/dto/SignupDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.book.backend.domain.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;

@Getter
@Setter
@NoArgsConstructor
public class SignupDto {
@NotBlank(message = "아이디는 필수 입력값입니다.")
private String loginId;

@NotBlank(message = "닉네임은 필수 입력값입니다.")
private String nickname;

@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}",
message = "비밀번호는 숫자, 영문자, 특수문자를 포함한 8~16자리여야 합니다.")
private String password;

@NotBlank(message = "성별은 필수 입력값입니다.")
@Pattern(regexp = "NOT_SELECTED|MAN|WOMAN", message = "성별은 NOT_SELECTED, MAN, WOMAN 중 하나여야 합니다.")
private String gender;

@Past(message = "현재 날짜보다 이전 날짜여야 합니다.")
private LocalDate birthDate;

@Email(message = "유효한 이메일 주소여야 합니다.")
private String email;

@Pattern(regexp = "010-\\d{4}-\\d{4}", message = "전화번호는 010-XXXX-XXXX 형태여야 합니다.")
private String phone;

}
32 changes: 32 additions & 0 deletions src/main/java/com/book/backend/domain/auth/mapper/AuthMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.book.backend.domain.auth.mapper;

import com.book.backend.domain.auth.dto.SignupDto;
import com.book.backend.domain.user.entity.Gender;
import com.book.backend.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AuthMapper {

private final ModelMapper mapper;
private final PasswordEncoder passwordEncoder;

public User convertToUser(SignupDto signupDto) {
User user = mapper.map(signupDto, User.class);
user.setGender(convertStringToGender(signupDto.getGender()));
user.setPassword(passwordEncoder.encode(signupDto.getPassword()));
return user;
}

private Gender convertStringToGender(String gender) {
return switch (gender) {
case "MAN" -> Gender.G1;
case "WOMAN" -> Gender.G2;
default -> Gender.G0;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.book.backend.domain.auth.service;

import com.book.backend.domain.auth.dto.LoginDto;
import com.book.backend.domain.auth.dto.SignupDto;
import com.book.backend.domain.auth.mapper.AuthMapper;
import com.book.backend.domain.user.dto.UserDto;
import com.book.backend.domain.user.entity.User;
import com.book.backend.domain.user.mapper.UserMapper;
import com.book.backend.domain.user.repository.UserRepository;
import com.book.backend.exception.CustomException;
import com.book.backend.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {
private final UserRepository userRepository;
private final AuthMapper authMapper;
private final UserMapper userMapper;
private final AuthenticationManager authenticationManager;

@Transactional
public UserDto signup(SignupDto signupDto) {
Optional<User> userOptional = userRepository.findByLoginId(signupDto.getLoginId());

if (userOptional.isPresent()) {
throw new CustomException(ErrorCode.LOGIN_ID_DUPLICATED);
}

User user = authMapper.convertToUser(signupDto);
user.setRegDate(LocalDateTime.now());

User savedUser = userRepository.save(user);

return userMapper.convertToUserDto(savedUser);
}

public UserDto login(LoginDto loginDto) {
try {
// 사용자 인증 시도
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDto.getLoginId(), loginDto.getPassword()));

// 인증 성공 시 SecurityContextHolder에 인증 정보 저장
SecurityContextHolder.getContext().setAuthentication(authentication);

User user = userRepository.findByLoginId(loginDto.getLoginId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

return userMapper.convertToUserDto(user);
} catch (AuthenticationException e) {
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
}

@Transactional
public void deleteAccountByLoginId(String loginId) {
User user = userRepository.findByLoginId(loginId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
userRepository.delete(user);
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/book/backend/domain/user/dto/UserDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.book.backend.domain.user.dto;

import lombok.*;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private Long userId;

private LocalDateTime regDate;

private String loginId;

private String password;

private String gender;

private LocalDate birthDate;

private String email;

private String phone;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

@RequiredArgsConstructor
@Getter
public enum Sex {
S0("NOT_SELECTED"),
S1("MAN"),
S2("WOMAN");
public enum Gender {
G0("NOT_SELECTED"),
G1("MAN"),
G2("WOMAN");

public final String name;
}
4 changes: 3 additions & 1 deletion src/main/java/com/book/backend/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public class User {
@Column(unique = true)
private String loginId;

private String nickname;

private String password;

@Enumerated(EnumType.ORDINAL)
private Sex sex;
private Gender gender;

private LocalDate birthDate;

Expand Down
27 changes: 27 additions & 0 deletions src/main/java/com/book/backend/domain/user/mapper/UserMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.book.backend.domain.user.mapper;

import com.book.backend.domain.user.dto.UserDto;
import com.book.backend.domain.user.entity.User;
import com.book.backend.exception.CustomException;
import com.book.backend.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class UserMapper {

private final ModelMapper mapper;

public UserDto convertToUserDto(User user) {
if (user == null) {
throw new CustomException(ErrorCode.USER_NOT_FOUND);
}

UserDto userDto = mapper.map(user, UserDto.class);
userDto.setGender(user.getGender().name());

return userDto;
}
}
4 changes: 3 additions & 1 deletion src/main/java/com/book/backend/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "404", "해당하는 사용자를 찾을 수 없습니다.");
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "401", "인증 오류가 발생했습니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "404", "해당하는 사용자를 찾을 수 없습니다."),
LOGIN_ID_DUPLICATED(HttpStatus.CONFLICT,"409", "사용자의 아이디가 중복됩니다.");

private final HttpStatus status;
private final String code;
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/book/backend/global/ModelMapperConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.book.backend.global;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ModelMapperConfig {

@Bean
public ModelMapper modelMapper() {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
return mapper;
}
}
17 changes: 15 additions & 2 deletions src/main/java/com/book/backend/global/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand All @@ -21,14 +24,24 @@ public class SecurityConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/**").permitAll() // 임시로 모든 경로 권한 해제, 추후 수정 필요
);
.requestMatchers("/api/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable);

return http.build();
}

@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
Expand Down
Loading

0 comments on commit bb3418b

Please sign in to comment.