Skip to content

Commit

Permalink
Merge pull request #10 from gooormmoon/feature/login/GRTEAM-4
Browse files Browse the repository at this point in the history
Feat: 회원가입/로그인 추가, 인증 추가 GRTEAM-4
  • Loading branch information
Berygna authored Jun 13, 2024
2 parents 65298b3 + d917794 commit 32ae81a
Show file tree
Hide file tree
Showing 22 changed files with 845 additions and 1 deletion.
4 changes: 4 additions & 0 deletions algofi-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package gooroommoon.algofi_core.auth.filter;

import gooroommoon.algofi_core.auth.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if(header != null && header.startsWith("Bearer ")) {
String token = header.substring("Bearer ".length());
if(!jwtUtil.isExpired(token)) {
Authentication authentication = jwtUtil.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gooroommoon.algofi_core.auth.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gooroommoon.algofi_core.auth.handler;

import gooroommoon.algofi_core.dto.ExceptionResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class UnauthenticatedEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gooroommoon.algofi_core.auth.member;

public class DuplicateLoginIdException extends RuntimeException {
public DuplicateLoginIdException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package gooroommoon.algofi_core.auth.member;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
@Table(uniqueConstraints = { @UniqueConstraint(name = "UniqueLoginId", columnNames = "loginId") })
public class Member {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;

@NotNull
@Setter
private String name;

@NotNull
@Setter
private String nickname;

@NotNull
@Setter
private String loginId;

@NotNull
@Setter
private String password;

@Enumerated(EnumType.STRING)
private MemberRole role;

@Setter
private String profileImageUrl;

@Setter
private String description;

@Setter
private String alert;

@CreatedDate @NotNull
@Setter
private LocalDateTime createdDate;

@NotNull
@Setter
private LocalDateTime loginDate;

@LastModifiedDate @NotNull
private LocalDateTime updatedDate;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package gooroommoon.algofi_core.auth.member;

import gooroommoon.algofi_core.auth.member.dto.MemberRequest;
import gooroommoon.algofi_core.auth.member.dto.MemberResponse;
import gooroommoon.algofi_core.auth.member.dto.TokenResponse;
import gooroommoon.algofi_core.auth.util.JwtUtil;
import gooroommoon.algofi_core.dto.ExceptionResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/member")
public class MemberController {

private final MemberService memberService;

@PostMapping("/register")
public ResponseEntity<MemberResponse> register(@RequestBody @Valid MemberRequest.RegisterRequest registerRequest) {
return ResponseEntity.ok().body(memberService.register(registerRequest));
}

@ExceptionHandler(DuplicateLoginIdException.class)
public ResponseEntity<ExceptionResponse> handleDuplicateLoginIdException(DuplicateLoginIdException exception) {
return ResponseEntity.badRequest().body(new ExceptionResponse(exception.getMessage()));
}

@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@RequestBody @Valid MemberRequest.LoginRequest loginRequest) {
return ResponseEntity.ok().body(memberService.authenticate(loginRequest.getId(), loginRequest.getPassword()));
}

@GetMapping("")
public ResponseEntity<MemberResponse> myInfo(Authentication auth) {
MemberResponse memberResponse = memberService.getMemberResponseByLoginId(auth.getName());
return ResponseEntity.ok().body(memberResponse);
}

@PutMapping("/change-password")
public ResponseEntity<String> changePassword(Authentication auth, @RequestBody @Valid MemberRequest.ChangePasswordRequest changePasswordRequest) {
memberService.updateMemberPasswordByLoginId(auth.getName(), changePasswordRequest.getPassword());
return ResponseEntity.ok().build();
}

@PutMapping("")
public ResponseEntity<MemberResponse> updateInfo(Authentication auth, @RequestBody @Valid MemberRequest memberRequest) {
MemberResponse memberResponse = memberService.updateMemberInfoByLoginId(auth.getName(), memberRequest);
return ResponseEntity.ok().body(memberResponse);
}

@DeleteMapping("")
public ResponseEntity<String> deleteMember(Authentication auth, @RequestBody @Valid MemberRequest.LoginRequest deleteRequest) {
memberService.deleteMemberByLoginIdAndPassword(auth.getName(), deleteRequest.getPassword());
return ResponseEntity.ok().build();
}

@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<ExceptionResponse> handleUsernameNotFoundException(UsernameNotFoundException exception) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ExceptionResponse(exception.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gooroommoon.algofi_core.auth.member;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByLoginId(String loginId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package gooroommoon.algofi_core.auth.member;

public enum MemberRole {
USER, ADMIN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package gooroommoon.algofi_core.auth.member;

import gooroommoon.algofi_core.auth.member.dto.MemberRequest;
import gooroommoon.algofi_core.auth.member.dto.MemberResponse;
import gooroommoon.algofi_core.auth.member.dto.TokenResponse;
import gooroommoon.algofi_core.auth.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

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

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
private final MemberUserDetailService memberUserDetailService;
private final JwtUtil jwtUtil;

public MemberResponse register(MemberRequest.RegisterRequest registerRequest) {
Member member = Member.builder()
.loginId(registerRequest.getId())
.password(registerRequest.getPassword())
.name(registerRequest.getName())
.profileImageUrl(registerRequest.getProfileImageUrl())
.description(registerRequest.getDescription())
.loginDate(LocalDateTime.now())
.role(MemberRole.USER)
.build();

if(registerRequest.getNickname() == null)
member.setNickname("user%s".formatted(Math.random()*100));
else
member.setNickname(registerRequest.getNickname());

try {
memberRepository.save(member);
} catch (DataIntegrityViolationException exception) {
throw new DuplicateLoginIdException("이미 존재하는 아이디입니다.");
}
return fromMember(member);
}

public TokenResponse authenticate(String loginId, String password) {
MemberUserDetails memberDetails = (MemberUserDetails) memberUserDetailService.loadUserByUsername(loginId);

if(memberDetails.getPassword().equals(password)) {
memberDetails.getMember().setLoginDate(LocalDateTime.now());
memberRepository.save(memberDetails.getMember());

return new TokenResponse(jwtUtil.createToken(memberDetails.getUsername()));
}
else {
throw new UsernameNotFoundException("아이디나 비밀번호가 틀립니다.");
}
}

public MemberResponse getMemberResponseByLoginId(String loginId) {
Optional<Member> optionalMember = memberRepository.findByLoginId(loginId);
if(optionalMember.isPresent())
return fromMember(optionalMember.get());
else
throw new UsernameNotFoundException("존재하지 않는 아이디입니다.");
}

public void updateMemberPasswordByLoginId(String loginId, String password) {
Optional<Member> optionalMember = memberRepository.findByLoginId(loginId);
if(optionalMember.isPresent()) {
Member member = optionalMember.get();
member.setPassword(password);
memberRepository.save(member);
}
else
throw new UsernameNotFoundException("존재하지 않는 아이디입니다.");
}

public MemberResponse updateMemberInfoByLoginId(String loginId, MemberRequest memberRequest) {
Optional<Member> optionalMember = memberRepository.findByLoginId(loginId);


if(optionalMember.isPresent()) {
Member member = optionalMember.get();

Optional.ofNullable(memberRequest.getName()).ifPresent(member::setName);
Optional.ofNullable(memberRequest.getNickname()).ifPresent(member::setNickname);
Optional.ofNullable(memberRequest.getDescription()).ifPresent(member::setDescription);
Optional.ofNullable(memberRequest.getProfileImageUrl()).ifPresent(member::setProfileImageUrl);

memberRepository.save(member);
return fromMember(member);
}
else
throw new UsernameNotFoundException("존재하지 않는 아이디입니다.");
}

public void deleteMemberByLoginIdAndPassword(String loginId, String password) {
Optional<Member> optionalMember = memberRepository.findByLoginId(loginId);
if(optionalMember.isPresent() && optionalMember.get().getPassword().equals(password)) {
memberRepository.delete(optionalMember.get());
} else {
throw new UsernameNotFoundException("아이디나 비밀번호가 틀립니다.");
}
}

private MemberResponse fromMember(Member member) {
return MemberResponse.builder()
.id(member.getLoginId())
.name(member.getName())
.nickname(member.getNickname())
.profileImageUrl(member.getProfileImageUrl())
.description(member.getDescription())
.createdDate(member.getCreatedDate())
.loginDate(member.getLoginDate())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gooroommoon.algofi_core.auth.member;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberUserDetailService implements UserDetailsService {

private final MemberRepository memberRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByLoginId(username)
.orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 아이디입니다."));
return new MemberUserDetails(member);
}
}
Loading

0 comments on commit 32ae81a

Please sign in to comment.