안녕하세요
오랜만에 또 글을 쓰네요ㅋㅋ
지난번에 Board Entity를 통하여 게시글 작성하는 것까지 작성했었는데요... 그 이후로 로그인 기능까지 담당하게 되어서 그 동안 Spring만 붙잡고 살았습니다..
학교 수업이랑 팀 프로젝트 2개를 병행하려니 죽을 맛이네요 ㅋㅋ
거기다가 로그인 기능 구현은 아직 연습중이여서 그런지 시간이 좀 더 많이 걸리는 작업이었습니다.
뭐 결론적으로는 방금 로그인 기능 구현까지 마친 뒤에 커밋하고 왔습니다.
작업하는 동안은 죽을 맛이였는데, 또 막상 구현한거 보면은 뿌듯하더라구요 ㅋㅋ
package com.example.community_board.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username; // 사용자 이름
@Column(nullable = false, unique = true)
private String userId; // 사용자 ID
@Column(nullable = false)
private String password; // 사용자 비밀 번호
@Column(nullable = false, unique = true)
private String email; // 사용자 Email
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Authority authority; // 사용자 권한
}
package com.example.community_board.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class RefreshToken {
@Id
@Column(name = "keyValue")
private String key;
@Column(nullable = false)
private String value;
}
package com.example.community_board.service;
import com.example.community_board.dto.user.*;
import com.example.community_board.entity.Authority;
import com.example.community_board.entity.RefreshToken;
import com.example.community_board.entity.User;
import com.example.community_board.exception.user.NotValidTokenException;
import com.example.community_board.exception.user.UserIdCollisionException;
import com.example.community_board.exception.user.UserNotFoundException;
import com.example.community_board.exception.user.UserWrongPasswordException;
import com.example.community_board.jwt.TokenProvider;
import com.example.community_board.repository.RefreshTokenRepository;
import com.example.community_board.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalTime;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final TokenProvider tokenProvider;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
@Transactional // 회원 가입 로직
public void signUp(UserSignUpRequestDto requestDto) {
Optional<User> findUser = userRepository.findUserByUserId(requestDto.getUserId()); // 사용자 id를 통하여 검색
if(findUser.isPresent()) {
throw new UserIdCollisionException(); // 해당 유저가 존재한다는 뜻이므로, 오류 반환
}
User user; // 저장을 위한 User 객체 하나 생성
if(requestDto.getUserId().equals("admin")) {
user = User.builder() // 작성자의 ID값이 admin인 경우, 관리자 권한 획득
.username(requestDto.getUsername())
.userId(requestDto.getUserId())
.password(bCryptPasswordEncoder.encode(requestDto.getPassword().toLowerCase()))
.email(requestDto.getEmail())
.authority(Authority.ROLE_ADMIN)
.build();
userRepository.save(user); // 관리자 권한을 가진 User Entity 저장
}
else {
user = User.builder() // 그 외의 경우에는 일반 권한을 가진 유저로 회원가입을 진행하게됨
.username(requestDto.getUsername())
.userId(requestDto.getUserId())
.password(bCryptPasswordEncoder.encode(requestDto.getPassword().toLowerCase()))
.email(requestDto.getEmail())
.authority(Authority.ROLE_USER)
.build();
userRepository.save(user); // 일반 권한을 가진 유저 객체 저장
}
}
@Transactional // 로그인 로직
public UserLoginResponseDto login(UserLoginRequestDto requestDto) {
User findUser = userRepository.findUserByUserId(requestDto.getUserId())
.orElseThrow(UserNotFoundException::new); // UserId를 통하여 User 검색
if(bCryptPasswordEncoder.matches(requestDto.getPassword(), findUser.getPassword())) {
UsernamePasswordAuthenticationToken authenticationToken =
requestDto.getAuthenticationToken();
Authentication authentication = authenticationManagerBuilder
.getObject().authenticate(authenticationToken);
TokenDto jwt = tokenProvider.createToken(authentication);
RefreshToken refreshToken = RefreshToken.builder()
.key(authentication.getName())
.value(jwt.getRefreshToken())
.build();
refreshTokenRepository.save(refreshToken);
return new UserLoginResponseDto(jwt.getOriginToken(), jwt.getRefreshToken());
}
throw new UserWrongPasswordException();
}
@Transactional// 토큰 재발급 로직
public TokenReissueDto reIssue(TokenReissueDto requestDto) {
if(!tokenProvider.validateToken(requestDto.getRefreshToken())) {
throw new NotValidTokenException();
}
Authentication authentication = tokenProvider.getAuthentication(requestDto.getOriginToken());
RefreshToken refreshToken = refreshTokenRepository
.findById(authentication.getName())
.orElseThrow(() -> new RuntimeException("현재 로그인 하지 않은 회원입니다."));
if(!refreshToken.getValue().equals(requestDto.getRefreshToken())) {
throw new RuntimeException("토큰 정보가 일치하지 않습니다.");
}
TokenDto tokenDto = tokenProvider.createToken(authentication);
RefreshToken refreshTokenValue = RefreshToken.builder()
.key(tokenDto.getOriginToken())
.value(tokenDto.getRefreshToken())
.build();
refreshTokenRepository.save(refreshTokenValue);
return new TokenReissueDto(tokenDto.getOriginToken(), tokenDto.getRefreshToken());
}
@Transactional //email의 unique라는 속성을 활용하여 User Entity를 검색함
public String findUserId(UserEmailRequestDto requestDto) {
User findUser = userRepository.findUserByEmail(requestDto.getEmail()).orElseThrow(UserNotFoundException::new);
return findUser.getUserId(); // 검색한 User Entity의 Userid만을 리턴해줌
}
@Transactional // 아이디 검색 로직과 마찬가지로, email의 unique라는 속성을 활용하여 User Entity를 검색함
public String findUserPassword(UserEmailRequestDto requestDto) {
User findUser = userRepository.findUserByEmail(requestDto.getEmail()).orElseThrow(UserNotFoundException::new);
LocalTime tmp = LocalTime.now();
String reGeneratedPassword = tmp.toString().replaceAll("[^0-9]", "");
findUser.setPassword(bCryptPasswordEncoder.encode(reGeneratedPassword));
return reGeneratedPassword;
}
@Transactional // UserPasswordEditRequestDto를 통하여 사용자의 현재 비밀번호와 바꿀 비밀번호를 입력받음.
public void editPassword(UserPasswordEditRequestDto requestDto) {
String nowLoginUserId = SecurityContextHolder.getContext()
.getAuthentication().getName(); // 현재 사용자의 ID값을 받음
User nowLoginUser = userRepository.findUserByUserId(nowLoginUserId)
.orElseThrow(UserNotFoundException::new); // 사용자의 ID값은 unique 속성이 부여되어 있으므로, UserID를 통하여 사용자 검색
log.info(nowLoginUser.getAuthority().toString());
if(bCryptPasswordEncoder.matches(requestDto.getOriginPassword(), nowLoginUser.getPassword())) {
nowLoginUser
.setPassword(bCryptPasswordEncoder.encode(requestDto.getRefreshPassword().toLowerCase())); // 검색한 사용자의 비밀번호와 입력받은 비밀번호를 비교
}
else throw new UserWrongPasswordException();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
for(GrantedAuthority s : authentication.getAuthorities()) {
log.info(s.getAuthority());
}
}
@Transactional
public void delegateUser(UserDelegateRequestDto requestDto) {
User findUser = userRepository.findUserByUserId(requestDto.getUserId())
.orElseThrow(UserNotFoundException::new);
findUser.setAuthority(Authority.ROLE_MANAGER);
}
}
package com.example.community_board.contoller;
import com.example.community_board.dto.user.TokenReissueDto;
import com.example.community_board.dto.user.UserDelegateRequestDto;
import com.example.community_board.dto.user.UserEmailRequestDto;
import com.example.community_board.dto.user.UserLoginRequestDto;
import com.example.community_board.dto.user.UserPasswordEditRequestDto;
import com.example.community_board.dto.user.UserSignUpRequestDto;
import com.example.community_board.response.Response;
import com.example.community_board.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
@PostMapping("/signup")
@ResponseStatus(HttpStatus.CREATED) // 회원 가입
public void signup(@RequestBody @Valid UserSignUpRequestDto requestDto) {
userService.signUp(requestDto);
}
@PostMapping("/signin")
@ResponseStatus(HttpStatus.CREATED) // 로그인
public Response login(@RequestBody @Valid UserLoginRequestDto requestDto) {
return Response.success(userService.login(requestDto));
}
@PostMapping("/auth/reissue")
@ResponseStatus(HttpStatus.OK) // 토큰 재발급
public Response reissue(@RequestBody @Valid TokenReissueDto requestDto) {
return Response.success(userService.reIssue(requestDto));
}
@GetMapping("/userid")
@ResponseStatus(HttpStatus.OK) // 사용자 아이디 찾기
public Response findUserId(@RequestBody @Valid UserEmailRequestDto requestDto) {
return Response.success(userService.findUserId(requestDto));
}
@GetMapping("/password")
@ResponseStatus(HttpStatus.OK) // 임시 비밀번호 발급
public Response findUserPassword(@RequestBody @Valid UserEmailRequestDto requestDto) {
return Response.success(userService.findUserPassword(requestDto));
}
@PutMapping("/auth/password")
@ResponseStatus(HttpStatus.OK) // 비밀번호 변경
public void editUserPassword(@RequestBody @Valid UserPasswordEditRequestDto requestDto) {
userService.editPassword(requestDto);
}
@PutMapping("/admin")
@ResponseStatus(HttpStatus.OK)
public void delegate(@RequestBody @Valid UserDelegateRequestDto requestDto) {
userService.delegateUser(requestDto);
}
}
User Entity와 RefreshToken Entity를 사용하여 로그인 기능과 JWT 토큰에서의 재발급을 어떤식으로 활용할지 정하였습니다.
일단은 로그인까지 구현해보았고, 이제 JWT 토큰을 사용하여 사용자 인증을 수행할 수 있기 때문에 이를 활용하여 게시글 좋아요 기능까지 만들어보려고합니다.
화이팅!