회원가입이 로그인보다 간단하므로 회원가입부터 구현한다.
이메일과 비밀번호를 입력받아 회원가입을 진행한다. 이때, 비밀번호를 db에 저장할 때 암호화해서 저장해야 한다. 그리고, 이미 존재하는 회원이면 에러코드를 출력한다.
1. 의존성 추가
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
2. SecurityConfig.java 작성
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder encodePassword() { /*비밀번호 암호화*/
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.headers().frameOptions().sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/swagger-ui/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v3/api-docs").permitAll()
.antMatchers("/user/**").permitAll()
/*위의 api는 무조건적인 접근 허용 */
.anyRequest().authenticated(); /*그 외는 인증된 사용자에 한해 접근 허용*/
}
}
3. User.java 엔티티 작성
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer userId;
private String email;
private String password;
@Enumerated(EnumType.STRING)
private UserConstants.Role role;
public void encryptPassword(PasswordEncoder passwordEncoder) {
password = passwordEncoder.encode(password);
}
}
4. 레포지토리 작성
UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {
}
UserRepositoryCustom.java
public interface UserRepositoryCustom {
Optional<User> findByEmail(String email);
}
UserRepositoryImpl.java
public class UserRepositoryImpl implements UserRepositoryCustom {
private final JPAQueryFactory queryFactory;
public UserRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public Optional<User> findByEmail(String email) {
return Optional.ofNullable(queryFactory.selectFrom(user)
.where(user.email.eq(email))
.fetchFirst());
}
}
5. 서비스 작성
UserService.java
public interface UserService {
SignupResponse singup(SignupRequest signupRequest);
}
UserServiceImpl.java
@RequiredArgsConstructor
@Service
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final JwtTokenProvider tokenProvider;
private final RedisTemplate redisTemplate;
@Override
public SignupResponse singup(UserDto.SignupRequest signupRequest) {
this.validateOverlap(signupRequest.getEmail()); //존재하는 회원인지 확인
User user = userMapper.toEntity(signupRequest);
user.encryptPassword(passwordEncoder); //비밀번호 암호화
user.setRole(ROLE_USER);
this.userRepository.save(user);
return new SignupResponse(signupRequest.getEmail());
}
private void validateOverlap(String email) {
userRepository.findByEmail(email)
.ifPresent((m -> {
throw new OverlapUserException();
}));
}
}
6. exception 작성
오류 코드 출력하는 것이 생각보다 까다롭다
UserException.java
public abstract class UserException extends ApplicationException {
protected UserException(String errorCode, HttpStatus httpStatus, String message) {
super(errorCode, httpStatus, message);
}
}
OverlapException.java
public class OverlapUserException extends UserException {
public OverlapUserException() {
super(UserConstants.UserExceptionList.OVERLAP_USER.getErrorCode(),
UserConstants.UserExceptionList.OVERLAP_USER.getHttpStatus(),
UserConstants.UserExceptionList.OVERLAP_USER.getMessage());
}
}
ApplicationException.java
public abstract class ApplicationException extends RuntimeException {
private final String errorCode;
private final HttpStatus httpStatus;
protected ApplicationException(String errorCode, HttpStatus httpStatus, String message) {
super(message);
this.errorCode = errorCode;
this.httpStatus = httpStatus;
}
public String getErrorCode() {
return errorCode;
}
public HttpStatus getHttpStatus() {
return httpStatus;
}
}
GlobalExceptionHandler.java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final String LOG_FORMAT = "Class : {}, Code : {}, Message : {}";
@ExceptionHandler(ApplicationException.class)
public ResponseEntity<ApiErrorResponse> applicationException(ApplicationException e) {
String errorCode = e.getErrorCode();
log.warn(
LOG_FORMAT,
e.getClass().getSimpleName(),
errorCode,
e.getMessage()
);
return ResponseEntity
.status(e.getHttpStatus())
.body(new ApiErrorResponse(errorCode, e.getMessage()));
}
}
상속 관계를 정리하면 아래 그림과 같다.
최종적으로 ApplicationException을 GlobalExceptionHandler에서 처리해 OverlapUserException인 경우 errorcode와 errormessage를 ResponseEntity로 감싸 객체로 반환해준다.
7. UserDto 작성
public abstract class UserDto {
@Getter
@AllArgsConstructor
@Builder
@ApiModel(description = "회원가입을 위한 요청객체")
public static class SignupRequest {
@NotBlank(message = "이메일을 입력해주세요")
@ApiModelProperty(notes = "이메일을 입력해주세요")
private String email;
@NotBlank(message = "비밀번호를 입력해주세요")
@ApiModelProperty(notes = "비밀번호를 입력해주세요")
private String password;
}
@Getter
@Builder
@ApiModel(description = "회원가입을 위한 응답객체")
public static class SignupResponse {
private String email;
@QueryProjection
public SignupResponse(String email) {
this.email = email;
}
}
}
8. UserController 작성
@RequiredArgsConstructor
@RestController
@RequestMapping("user")
@Api(tags = "User API")
public class UserController {
private final UserService userService;
@ApiOperation(value = "회원가입", notes = "회원가입을 합니다.")
@PostMapping("/signup")
public ResponseEntity<ResponseDto<SignupResponse>> singupUser(@Valid @ModelAttribute SignupRequest signupRequest) {
return ResponseEntity.ok(ResponseDto.create(UserConstants.EBoardResponseMessage.SIGNUP_SUCCESS.getMessage(), this.userService.singup(signupRequest)));
}
}
'Spring > Spring Security' 카테고리의 다른 글
[JWT] JWT 필터와 예외 처리 (0) | 2023.05.16 |
---|---|
[spring security] 시큐리티 & JWT 이용해서 로그인, 회원가입하기 - (2) 로그인 (1) | 2023.02.07 |