Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bp 12 implement user login #5

Merged
merged 14 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security:2.3.3.RELEASE'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -55,6 +56,8 @@ dependencies {
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'com.epages:restdocs-api-spec:0.19.2'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.19.2'
testImplementation 'org.springframework.security:spring-security-test:6.3.1'

swaggerUI 'org.webjars:swagger-ui:5.0.0'
}

Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gdsc.konkuk.platformcore.application.auth;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import gdsc.konkuk.platformcore.global.exceptions.ErrorCode;
import gdsc.konkuk.platformcore.global.responses.ErrorResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

private final ObjectMapper objectMapper;

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws
IOException, ServletException {

ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INVALID_USER_INFO);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
objectMapper.writeValue(response.getWriter(), errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gdsc.konkuk.platformcore.application.auth;

import java.io.IOException;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private RequestCache requestCache = new HttpSessionRequestCache();

private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

setDefaultTargetUrl("/");
SavedRequest savedRequest = requestCache.getRequest(request, response);

if(savedRequest != null){
String targetUrl = savedRequest.getRedirectUrl();
redirectStrategy.sendRedirect(request, response, targetUrl);
}else {
redirectStrategy.sendRedirect(request, response, getDefaultTargetUrl());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gdsc.konkuk.platformcore.application.auth;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import gdsc.konkuk.platformcore.domain.member.entity.Member;

public class CustomUserDetails implements UserDetails {

private final Member member;

public CustomUserDetails(Member member) {
this.member = member;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(member.getRole().toString()));
Copy link
Collaborator Author

@ekgns33 ekgns33 Jul 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"ROLE_" + "실제Role" 로 권한을 부여하는데 저는 그냥 Enum 클래스의 toString 메소드를 오버라이딩하여 Enum의 String 필드를 리턴 해주고 있습니다. 혹시 어떻게 생각하시나요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 느끼기엔 크게 문제가 될 여지는 없어 보입니다. 일반적으로는 어떻게 구현하는지 궁금하네요.

return authorities;
}

@Override
public String getPassword() {
return member.getPassword();
}

@Override
public String getUsername() {
return member.getMemberId();
}

@Override
public boolean isAccountNonExpired() {
return member.isActivated();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gdsc.konkuk.platformcore.application.auth;

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;

import gdsc.konkuk.platformcore.application.auth.exceptions.InvalidUserInfoException;
import gdsc.konkuk.platformcore.domain.member.entity.Member;
import gdsc.konkuk.platformcore.domain.member.repository.MemberRepository;
import gdsc.konkuk.platformcore.global.exceptions.ErrorCode;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final MemberRepository memberRepository;

@Override
public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException {

Member member = memberRepository.findByMemberId(memberId)
.orElseThrow(()-> InvalidUserInfoException.of(ErrorCode.USER_NOT_FOUND));

return new CustomUserDetails(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package gdsc.konkuk.platformcore.application.auth.exceptions;

import gdsc.konkuk.platformcore.global.exceptions.BusinessException;
import gdsc.konkuk.platformcore.global.exceptions.ErrorCode;

public class InvalidUserInfoException extends BusinessException {

private InvalidUserInfoException(String message, String logMessage) {
super(message, logMessage);
}

public static InvalidUserInfoException of(ErrorCode errorCode) {
return new InvalidUserInfoException(errorCode.getMessage(), errorCode.getLogMessage());
}

}

This file was deleted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

유저 엔티티에 저희 ERD 합친거랑 이야기한거로 반영했는데 어떤 것은 Enum으로 관리했어요. DeleteStatus의 경우 0, 1, 2, 3등의 정수값으로 정의할까 하다가. Enum을 통해 관리하는 것이 코드적으로나, 데이터베이스에서도 (명확하게 표현 가능) 좋을 것 같다고 생각해서 Enum을 활용해요. Ordinal이 아닌 String으로 저장하는 것에 의견있으신가요?

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package gdsc.konkuk.platformcore.domain.member.entity;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

@Id @GeneratedValue
private Long id;

@Column(name = "member_id",unique = true)
private String memberId;

@Column(name = "password")
private String password;

@Column(name = "member_name")
private String name;

@Column(name = "member_email")
private String email;

@Column(name = "profile_image_url")
private String profileImageUrl;

@Column(name = "is_activated")
private boolean isActivated;

@Column(name = "is_deleted")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 주시면 반영하겠습니다

Suggested change
@Column(name = "is_deleted")
@Enumerated(EnumType.STRING)
@Column(name = "is_deleted")
Suggested change
@Column(name = "is_deleted")
@Enumerated(EnumType.ORDINAL)
@Column(name = "is_deleted")

Copy link
Contributor

@goldentrash goldentrash Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 Enum이나 참조 테이블 둘 중 하나면 괜찮다고 생각해요. Delete Policy라 Enum의 field가 추가될 일이 있을 것 같지도 않고요. 이부분은 선호하시는 방향대로 진행해주시면 감사합니다 :)

EnumTypeORDINAL 보다는 STRING이 훨씬 직관적이라고 생각합니다. 약간의 byte 공간 아끼는게 크게 의미가 있지도 않을것 같고요 :)

그런데 한가지 궁금한건, is_deleted는 soft delete가 수행된 후, hard-deleted 상태를 의미하는것으로 보이는데, 이 경우 marking이 아닌 row(tuple) 삭제로 처리하지 않은 이유가 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제로 데이터를 테이블에서 지우는지 좀 찾아보다가 넣어놨는데 빼도 상관없을것같아요

private boolean isDeleted;

@Column(name = "soft_deleted_at")
private LocalDateTime softDeletedAt;

@Enumerated(EnumType.STRING)
@Column(name = "member_role")
private MemberRole role;

@Column(name = "batch")
private int batch;

@Builder
public Member(Long id, String memberId, String password, String name, String email, String profileImageUrl,
boolean isActivated, boolean isDeleted, LocalDateTime deletedAt, MemberRole role, int batch) {
this.id = id;
this.memberId = memberId;
this.password = password;
this.name = name;
this.email = email;
this.profileImageUrl = profileImageUrl;
this.isActivated = isActivated;
this.isDeleted = isDeleted;
this.softDeletedAt = deletedAt;
this.role = role;
this.batch = batch;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gdsc.konkuk.platformcore.domain.member.entity;

public enum MemberRole {
LEAD("ROLE_LEAD"),
ADMIN("ROLE_ADMIN"),
MEMBER("ROLE_MEMBER");

private final String authority;

MemberRole(String authority) {
this.authority = authority;
}

@Override
public String toString() {
return this.authority;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gdsc.konkuk.platformcore.domain.member.repository;

import java.util.Optional;

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

import gdsc.konkuk.platformcore.domain.member.entity.Member;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByMemberId(String memberId);

Member save(Member member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package gdsc.konkuk.platformcore.global.configs;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;

import gdsc.konkuk.platformcore.application.auth.CustomAuthenticationFailureHandler;
import gdsc.konkuk.platformcore.application.auth.CustomAuthenticationSuccessHandler;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.addFilterBefore(new SecurityContextPersistenceFilter(), BasicAuthenticationFilter.class)

.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/docs/**").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/member").hasRole("MEMBER")
.anyRequest().authenticated())

.formLogin(login -> login
.defaultSuccessUrl("/")
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.permitAll()
);
return httpSecurity.build();
}

@Bean
public SecurityFilterChain swaggerFilterchain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.securityMatcher("/docs")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/**").authenticated());
return httpSecurity.build();
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import gdsc.konkuk.platformcore.global.exceptions.BusinessException;
import gdsc.konkuk.platformcore.global.exceptions.ErrorCode;
import gdsc.konkuk.platformcore.global.responses.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -13,6 +14,14 @@
@RestControllerAdvice
public class GlobalExceptionHandler {


@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(final BusinessException e) {
log.error("BusinessException Caught! [{}]", e.getLogMessage());
final ErrorResponse response = ErrorResponse.of(e.getMessage(), e.getLogMessage());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Exception Uncaught! [{}]", e.getCause().toString());
Expand Down
Loading