💡 회원 기능
요구사항
Open-Session-in-view 옵션: false (트랜잭션 내부의 프록시객체 초기화 필요)
@ManyToOne 단방향 매핑으로 진행
FetchType : Lazy
1. 회원 등록 (카카오 로그인 & 사이트 자체 회원가입)
2. 로그인 (사이트 자체 로그인)
3. 로그인한 회원 목록 조회
4. 로그인한 회원 Role 조회
5. 회원 삭제
6. 비밀번호 암호화
7. 비밀번호 복호화
💡 소스코드
Entity
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(unique = true, length = 50, nullable = false)
@Email
private String email;
@Column(length = 200)
// @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@!%*#?&])[A-Za-z\\d@!%*#?&]{8,}$")
private String password;
// 소셜 로그인 인증 같은 경우 비밀번호를 직접 다루지 않기 때문에 nullable
@Column(nullable = false, length = 20)
private String memberName;
//실명
@Column(length = 10)
private String nickName;
//닉네임 추가
@Column(length = 10)
private String birth;
// 생년월일 저장
@Column(length = 200)
private String profile;
// 해당 회원의 프로필 사진 주소 저장
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private Role role;
// 따로 권한테이블을 두어서 여러 역할을 가질 수 있도록 설정할 수도 있음
// 현재는 한개만 갖도록 구성(일반 유저와 Admin 유저로 구분)
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private MemberType memberType;
// 카카오 회원인지, 네이버 회원인지 Enum으로 관리
@Enumerated(EnumType.STRING)
@Column
private Status status;
@Column(length = 250)
private String refreshToken;
// 리프레쉬 토큰
private LocalDateTime tokenExpirationTime;
// 토큰 만료 시간
@Builder
public Member(Long memberId, String email, String password, String memberName, String nickName, String birth, String profile, Role role, MemberType memberType, String refreshToken, LocalDateTime tokenExpirationTime) {
this.memberId = memberId;
this.email = email;
this.password = password;
this.memberName = memberName;
this.nickName = nickName;
this.birth = birth;
this.profile = profile;
this.role = role;
this.memberType = memberType;
this.refreshToken = refreshToken;
this.tokenExpirationTime = tokenExpirationTime;
}
public void updateRefreshToken(JwtTokenDto jwtTokenDto) {
this.refreshToken = jwtTokenDto.getRefreshToken();
this.tokenExpirationTime = DateTimeUtils.convertToLocalDateTime(jwtTokenDto.getRefreshTokenExpireTime());
}
public void expireRefreshToken(LocalDateTime now) {
this.tokenExpirationTime = now;
}
}
DTO
public class MemberDto {
@Getter
public static class Post{
@Email
private String email;
private String memberName;
// @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@!%*#?&])[A-Za-z\\d@!%*#?&]{8,}$")
// @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$")
private String password;
private String confirmPassword;
private String nickName;
private String birth;
}
@Getter
public static class Login{
// @Pattern(regexp = "\t^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$")
private String email;
// @Pattern(regexp = "\t^.*(?=^.{8,15}$)(?=.*\\d)(?=.*[a-zA-Z])(?=.*[!@#$%^&+=]).*$")
private String password;
private String memberType;
}
@Getter
@NoArgsConstructor
public static class Patch{
private Long memberId;
private String nickName;
private String password;
private String newPassword;
private String confirmNewPassword;
private String birth;
public void updateMemberId(Long memberId){
this.memberId = memberId;
}
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public static class Response{
private Long memberId;
private String email;
private String password;
private String memberName;
private String nickName;
private String birth;
private Role role;
private MemberType memberType;
private String refreshToken;
private LocalDateTime tokenExpirationTime;
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public static class MyPageResponse{
private Long memberId;
private Role role;
private String email;
private String memberName;
private String nickName;
private MemberType memberType;
private String birth;
private String password;
}
@Getter @Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class LoginResponse {
private String grantType;
private String accessToken;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private Date accessTokenExpireTime;
private String refreshToken;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private Date refreshTokenExpireTime;
private Role role;
public static LoginResponse of(JwtTokenDto jwtTokenDto, Role role){
return LoginResponse.builder()
.role(role)
.grantType(jwtTokenDto.getGrantType())
.accessToken(jwtTokenDto.getAccessToken())
.accessTokenExpireTime(jwtTokenDto.getAccessTokenExpireTime())
.refreshToken(jwtTokenDto.getRefreshToken())
.refreshTokenExpireTime(jwtTokenDto.getRefreshTokenExpireTime())
.build();
}
}
}
Mapper
@Mapper(componentModel = "spring")
public interface MemberMapper {
MemberDto.MyPageResponse memberToMyPageResoponse(Member member);
MemberDto.Response memberToMemberResponse(Member member);
Member memberPostDtoToMember(MemberDto.Post memberPostDto);
Member memberLoginDtoToMember(MemberDto.Login memberLoginDto);
@Mapping(source = "newPassword", target = "password")
Member memberPatchDtoToMember(MemberDto.Patch memberPatchDto);
List<MemberDto.Response> membersToMemberMyPageResponses(List<Member> members);
}
Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
Optional<Member> findByRefreshToken(String refreshToken);
}
Service
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final TokenManager tokenManager;
private final JasyptConfig jasyptConfig; //암호화, 복호화를 위한 di
/**
* 회원가입(카카오 로그인 시 사용)
*/
public Member registerMember(Member member) {
validateDuplicateMember(member); //동일 이메일있는지 확인
return memberRepository.save(member);
}
/**
* 회원가입(사이트 자체 회원가입)
*/
public Member createMember(Member member){
validateDuplicateMember(member); //동일 이메일있는지 확인
member.setMemberType(MemberType.DEFAULT); //자체 회원가입
member.setRole(Role.USER);
member.setPassword(encryptPassword(member.getPassword())); //비밀번호 암호화
return memberRepository.save(member);
}
/**
* 회원 로그인(사이트 자체 로그인)
*/
public MemberDto.LoginResponse login(Member member) {
JwtTokenDto jwtTokenDto;
Optional<Member> optioanlMember = findMemberByEmail(member.getEmail()); //이메일로 회원 검색
if(optioanlMember.isEmpty()) {
throw new EntityNotFoundException(ErrorCode.MEMBER_NOT_EXISTS); //존재 안하면 예외 처리
}
Member findMember = optioanlMember.get();
if(findMember.getStatus() == Status.DELETE){
throw new EntityNotFoundException(ErrorCode.MEMBER_WITHDRAWN);
}
if(!decryptPassword(findMember.getPassword()).equals(member.getPassword())){
throw new AuthenticationException(ErrorCode.WRONG_PASSWROD); //비밀번호 일치 하지 않으면 예외처리
}
jwtTokenDto = tokenManager.createJwtTokenDto(findMember.getMemberId(), findMember.getRole()); //토큰 생성
findMember.updateRefreshToken(jwtTokenDto); //토큰값 설정
memberRepository.save(findMember); //db에 리프레쉬 토큰 업데이트
return MemberDto.LoginResponse.of(jwtTokenDto, findMember.getRole());
}
/**
* 회원 목록 조회
*/
@Transactional(readOnly = true)
public Page<Member> findMembers(int page, int size) {
Page<Member> findAllMember = memberRepository.findAll(
PageRequest.of(page,size, Sort.by("memberId").descending())
);
return findAllMember;
}
/**
* 회원 수정 (카카오 회원은 비밀번호 수정 불가능)
*/
public Member updateMember(Member member) {
Member preMember = findVerifiedMemberByMemberId(member.getMemberId()); //멤버 조회
if(member.getPassword() == null){ //닉네임만 수정
Optional.ofNullable(member.getNickName())
.ifPresent(nickName -> preMember.setNickName(nickName));
}else{ //비밀번호도 같이 수정
Optional.ofNullable(member.getNickName())
.ifPresent(nickName -> preMember.setNickName(nickName));
String password = encryptPassword(member.getPassword()); //새 비밀번호 암호화
preMember.setPassword(password); //암호화된 비밀번호 설정
}
Optional.ofNullable(member.getBirth())
.ifPresent(birth -> preMember.setBirth(birth));
return memberRepository.save(preMember);
}
/**
* 회원 삭제 (마이페이지를 통해서 삭제하기 때문에 별도 인증 과정 X)
*/
public Member deleteMember(Long memberId) {
Member member = findVerifiedMemberByMemberId(memberId);
member.setStatus(Status.DELETE);
return memberRepository.save(member);
}
/**
* 로그인 회원 정보 조회
*/
@Transactional(readOnly = true)
public Member getLoginMember(HttpServletRequest httpServletRequest){
String authorizationHeader = httpServletRequest.getHeader("Authorization");
String accessToken = authorizationHeader.split(" ")[1]; // Bearer askdhqwdkjwqbdkjwqbdkjqwbdkjwqb
Claims tokenClaims = tokenManager.getTokenClaims(accessToken);
Long memberId = Long.valueOf( (Integer) tokenClaims.get("memberId"));
return findVerifiedMemberByMemberId(memberId);
}
/**
* 로그인한 회원 Role 조회(USER OR counselor)
* */
@Transactional(readOnly = true)
public Role getLoginRole(HttpServletRequest httpServletRequest) {
String authorizationHeader = httpServletRequest.getHeader("Authorization");
String accessToken = authorizationHeader.split(" ")[1];
Claims tokenClaims = tokenManager.getTokenClaims(accessToken);
String role = (String) tokenClaims.get("role");
if(role.equals("USER")) {
return (Role) Enum.valueOf(Role.class, role);
}else if (role.equals("COUNSELOR")){
return (Role) Enum.valueOf(Role.class, role);
}else{
return (Role) Enum.valueOf(Role.class, "ADMIN");
}
}
/**
* 회원 중복 확인(있으면 예외)
*/
private void validateDuplicateMember(Member member) {
Optional<Member> optionalMember = memberRepository.findByEmail(member.getEmail());
if(optionalMember.isPresent()) {
throw new BusinessException(ErrorCode.ALREADY_REGISTERED_MEMBER);
//동일 이메일 있는 경우 에러 처리
}
}
/**
* 회원 존재 확인(없으면 예외)
*/
@Transactional(readOnly = true)
public Member findVerifiedMemberByMemberId(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.MEMBER_NOT_EXISTS));
}
@Transactional(readOnly = true)
public Optional<Member> findMemberByEmail(String email) {
return memberRepository.findByEmail(email);
}
@Transactional(readOnly = true)
public Member findMemberByRefreshToken(String refreshToken) {
Member member = memberRepository.findByRefreshToken(refreshToken)
.orElseThrow(() -> new AuthenticationException(ErrorCode.REFRESH_TOKEN_NOT_FOUND));
LocalDateTime tokenExpirationTime = member.getTokenExpirationTime();
if(tokenExpirationTime.isBefore(LocalDateTime.now())) {
throw new AuthenticationException(ErrorCode.REFRESH_TOKEN_EXPIRED); //refresh 토큰이 만료됐을 경우
}
return member;
}
/**
* 비밀번호 암호화
*/
public String encryptPassword(String password){
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
encryptor.setPoolSize(4);
encryptor.setPassword(jasyptConfig.getPassword());
encryptor.setAlgorithm("PBEWithMD5AndTripleDES");
return encryptor.encrypt(password);
}
/**
* 비밀번호 복호화
*/
public String decryptPassword(String password){
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
encryptor.setPoolSize(4);
encryptor.setPassword(jasyptConfig.getPassword());
encryptor.setAlgorithm("PBEWithMD5AndTripleDES");
return encryptor.decrypt(password);
}
}
Controller
@RestController
@RequestMapping("/api/members")
@Validated
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final MemberMapper memberMapper;
private final OauthValidator oauthValidator;
//회원가입
@PostMapping("/new")
public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post memberPostDto){
if(!memberPostDto.getPassword().equals(memberPostDto.getConfirmPassword())){ //비밀번호와 비밀번호 확인이 같지 않으면
throw new AuthenticationException(ErrorCode.PASSWORD_MISMATCH); //에러 발생
}
Member member = memberService.createMember(memberMapper.memberPostDtoToMember(memberPostDto));
MemberDto.Response response = memberMapper.memberToMemberResponse(member);
return new ResponseEntity<>(
new SingleResponseDto<>(response), HttpStatus.OK
);
}
//로그인
@PostMapping("/login")
public ResponseEntity loginMember(@Valid @RequestBody MemberDto.Login memberLoginDto){
oauthValidator.validateMemberType(memberLoginDto.getMemberType());
MemberDto.LoginResponse jwtTokenResponseDto = memberService.login(memberMapper.memberLoginDtoToMember(memberLoginDto));
return new ResponseEntity<>(
new SingleResponseDto<>(jwtTokenResponseDto), HttpStatus.OK
);
}
//회원 조회(회원정보 수정 페이지)
// @GetMapping("/look-up/{memberId}")
// public ResponseEntity getMember(@PathVariable("memberId") @Positive Long memberId){
// Member member = memberService.findVerifiedMemberByMemberId(memberId);
// MemberDto.MyPageResponse response = memberMapper.memberToMyPageResoponse(member);
//
// return new ResponseEntity<>(
// new SingleResponseDto<>(response), HttpStatus.OK
// );
// }
//회원 조회(회원정보 수정 페이지) 수정본
@GetMapping("/look-up")
public ResponseEntity getMember(HttpServletRequest httpServletRequest){
Member member = memberService.getLoginMember(httpServletRequest);
MemberDto.MyPageResponse response = memberMapper.memberToMyPageResoponse(member);
return new ResponseEntity<>(
new SingleResponseDto<>(response), HttpStatus.OK
);
}
//// getLoginMemberId 정상 확인 테스트
// @GetMapping("/info")
// public void getMember(HttpServletRequest httpServletRequest){
//// Long Id = memberService.getLoginMemberId(httpServletRequest);
// System.out.printf("======================================"+ Id + "===========================================");
// }
/**
* 회원 전체 조회
*/
@GetMapping("/total-look-up")
public ResponseEntity getMembers(@Positive @RequestParam("page") int page,
@Positive @RequestParam("size") int size){
Page<Member> pageMembers = memberService.findMembers(page-1, size);
List<Member> members = pageMembers.getContent();
return new ResponseEntity<>(new MultiResponseDto<>(
memberMapper.membersToMemberMyPageResponses(members), pageMembers)
,HttpStatus.OK);
}
//회원 수정
@PatchMapping("/edit/{memberId}")
public ResponseEntity updateMember(@PathVariable("memberId") @Positive Long memberId,
@Valid @RequestBody MemberDto.Patch memberPatchDto){
memberPatchDto.updateMemberId(memberId);
Member preMember = memberService.findVerifiedMemberByMemberId(memberId); //멤버 조회
if(memberPatchDto.getPassword() != null) {
if (!memberPatchDto.getNewPassword().equals(memberPatchDto.getConfirmNewPassword())) {
throw new EntityNotFoundException(ErrorCode.PASSWORD_MISMATCH); //새 비밀번호와 비밀번호 확인이 같지 않을 경우 예외 처리
}
String password = memberService.decryptPassword(preMember.getPassword()); //기존 비밀번호 복호화
if (!password.equals(memberPatchDto.getPassword())) {
throw new AuthenticationException(ErrorCode.WRONG_PASSWROD); //기존 비밀번호와 현재 비밀번호가 일치 하지 않으면 예외처리
}
}
Member member = memberService.updateMember(memberMapper.memberPatchDtoToMember(memberPatchDto));
MemberDto.MyPageResponse response = memberMapper.memberToMyPageResoponse(member);
return new ResponseEntity<>(
new SingleResponseDto<>(response), HttpStatus.OK
);
}
//회원 삭제
@PatchMapping("/delete/{memberId}")
public ResponseEntity deleteMember(@PathVariable("memberId") @Positive Long memberId){
memberService.deleteMember(memberId);
return new ResponseEntity<>(HttpStatus.OK);
}
}
'Project > Main Project' 카테고리의 다른 글
💻 기능 개발 - 공지사항 (0) | 2023.01.26 |
---|---|
💻 기능 개발 - 게시물 (0) | 2023.01.26 |
📄 API 명세서 - 상담프로그램 & 결제 & 예약 (0) | 2023.01.26 |
📄 API 명세서 - 상담사 & 게시물 & 공지사항 (0) | 2023.01.26 |
📄 API 명세서 - 사용자 & 로그인 & 로그아웃 (0) | 2023.01.26 |