generated from GDG-on-Campus-SKHU/24-25-Assignment-template
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Spring Security 를 적용하여 '세션 로그인 방식'의 회원가입 및 로그인 구현
- Loading branch information
Showing
12 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
송하은/sugang-system/src/main/java/com/example/sugangsystem/controller/MemberController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package com.example.sugangsystem.controller; | ||
|
||
import com.example.sugangsystem.dto.request.auth.LoginRequestDto; | ||
import com.example.sugangsystem.dto.response.auth.MemberLoginResponseDto; | ||
import com.example.sugangsystem.dto.response.auth.UserInfo; | ||
import com.example.sugangsystem.service.MemberService; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpSession; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.context.SecurityContext; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/members") | ||
public class MemberController { | ||
|
||
private final MemberService memberService; | ||
|
||
/** | ||
* 통합된 회원가입 및 사용자 정보 저장 API | ||
* @param userInfo 사용자 정보 DTO | ||
* @return 성공 시 회원 로그인 응답 DTO와 200 OK 반환 | ||
*/ | ||
@PostMapping("/join") | ||
public ResponseEntity<MemberLoginResponseDto> registerOrUpdateUserInfo(@RequestBody UserInfo userInfo){ | ||
MemberLoginResponseDto response = memberService.saveUserInfo(userInfo); | ||
// return new ResponseEntity<>(response,HttpStatus.OK); 와 같은 방식이다. | ||
// 하지만 아래 방식으로 하는 것이 가독성과 유연성을 높인다. (필요한 경우 옵션도 여러개 추가 가능) | ||
return ResponseEntity.status(HttpStatus.OK).body(response); | ||
} | ||
|
||
/** | ||
* 로그인 API | ||
* @param loginRequestDto 로그인 요청 DTO | ||
* @return 성공 시 회원 로그인 응답 DTO와 200 OK 반환 | ||
*/ | ||
@PostMapping("/login") | ||
public ResponseEntity<MemberLoginResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletRequest request) { | ||
MemberLoginResponseDto response = memberService.login(loginRequestDto); | ||
|
||
// 세션을 가져오거나 없으면 생성한다. | ||
HttpSession session = request.getSession(true); | ||
|
||
// Authentication 객체를 생성하여, 사용자가 인증된 상태임을 나타내는 정보를 담는다. | ||
Authentication authentication = new UsernamePasswordAuthenticationToken(loginRequestDto.loginId(),null,null); | ||
|
||
// SecurityContext 를 생성하고, 여기에 Authentication 객체를 설정한다. | ||
// 이로써 사용자가 인증되었다는 정보를 Spring Security 가 알 수 있게 된다. | ||
SecurityContext context = SecurityContextHolder.createEmptyContext(); | ||
context.setAuthentication(authentication); | ||
|
||
// 세션에 SecurityContext 저장 | ||
// 이 설정으로 인해 이후 요청에서도 세션을 통해 인증 상태가 유지된다. | ||
SecurityContextHolder.setContext(context); | ||
// "SPRING_SECURITY_CONTEXT"는 Spring Security에서 인증 정보를 세션에 저장할 때 사용하는 기본 키 | ||
session.setAttribute("SPRING_SECURITY_CONTEXT", context); // 현재 세션에 SecurityContext를 저장하여 사용자가 인증된 상태를 유지할 수 있도록 하는 코드 | ||
|
||
return ResponseEntity.ok(response); | ||
} | ||
|
||
} |
35 changes: 35 additions & 0 deletions
35
송하은/sugang-system/src/main/java/com/example/sugangsystem/domain/auth/Member.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package com.example.sugangsystem.domain.auth; | ||
|
||
import jakarta.persistence.*; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
|
||
@Entity | ||
@Getter | ||
@NoArgsConstructor | ||
|
||
public class Member { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Enumerated(EnumType.STRING) | ||
private Role role; | ||
|
||
private String loginId; | ||
|
||
private String pwd; | ||
|
||
private String name; | ||
|
||
@Builder | ||
private Member(Role role, String loginId, String pwd, String name) { | ||
this.role = role; | ||
this.loginId = loginId; | ||
this.pwd = pwd; | ||
this.name = name; | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
송하은/sugang-system/src/main/java/com/example/sugangsystem/domain/auth/Role.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.example.sugangsystem.domain.auth; | ||
|
||
public enum Role { | ||
ROLE_ADMIN, ROLE_USER | ||
} |
8 changes: 8 additions & 0 deletions
8
...sugang-system/src/main/java/com/example/sugangsystem/dto/request/auth/JoinRequestDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.example.sugangsystem.dto.request.auth; | ||
|
||
public record JoinRequestDto( | ||
String loginId, | ||
String pwd, | ||
String name | ||
){ | ||
} |
7 changes: 7 additions & 0 deletions
7
...ugang-system/src/main/java/com/example/sugangsystem/dto/request/auth/LoginRequestDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.example.sugangsystem.dto.request.auth; | ||
|
||
public record LoginRequestDto( | ||
String loginId, | ||
String pwd | ||
) { | ||
} |
15 changes: 15 additions & 0 deletions
15
...stem/src/main/java/com/example/sugangsystem/dto/response/auth/MemberLoginResponseDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.example.sugangsystem.dto.response.auth; | ||
|
||
import com.example.sugangsystem.domain.auth.Member; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
public record MemberLoginResponseDto( | ||
Member findMember | ||
) { | ||
public static MemberLoginResponseDto from(Member member) { | ||
return MemberLoginResponseDto.builder() | ||
.findMember(member) | ||
.build(); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
송하은/sugang-system/src/main/java/com/example/sugangsystem/dto/response/auth/UserInfo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.example.sugangsystem.dto.response.auth; | ||
|
||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
// 모든 필드를 final 로 선언해서 불변 객체롤 만든다. => record 사용 | ||
public record UserInfo( | ||
String loginId, | ||
String pwd, | ||
String name | ||
) { | ||
} |
19 changes: 19 additions & 0 deletions
19
송하은/sugang-system/src/main/java/com/example/sugangsystem/global/config/AppConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.example.sugangsystem.global.config; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
|
||
|
||
/* | ||
AppConfig 라는 설정 클래스에 PasswordEncoder() 빈을 등록한다. | ||
*/ | ||
@Configuration // 이 클래스가 스프링 설정 클래스임을 나타낸다. 스프링 부트가 애플리케이션을 시작할 때 이 클래스를 읽어, 정의된 빈들을 스프링 컨테이너에 등록한다. | ||
public class AppConfig { | ||
|
||
@Bean // 메서드의 반환값을 스프링 빈으로 등록하겠다. 싱글톤으로 관리되어 필요한 곳에서 자동으로 주입하여 사용할 수 있다. | ||
public PasswordEncoder passwordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
송하은/sugang-system/src/main/java/com/example/sugangsystem/global/config/SecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package com.example.sugangsystem.global.config; | ||
|
||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
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.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; | ||
|
||
@Configuration // 스프링의 설정 클래스임을 나타낸다. | ||
@EnableWebSecurity // 스프링 시큐리티 활성화, 보안 필터가 모든 HTTP 요청을 보호하도록 설정 | ||
public class SecurityConfig { | ||
|
||
/* | ||
SecurityFilterChain 을 빈으로 등록하고, | ||
스프링 시큐리티의 보안 규칙을 정의한다. | ||
*/ | ||
@Bean | ||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
http | ||
.httpBasic(AbstractHttpConfigurer::disable) // HTTP Basic 인증 비활성화. 대신 다른 인증방식(여기서는 세션 인증방식)을 사용할 수 있다. | ||
.csrf(AbstractHttpConfigurer::disable) // CSRF 보호 비활성화 | ||
.sessionManagement(sessionManagement -> // 세션을 필요할 때만 생성하도록 설정 | ||
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) | ||
) | ||
.formLogin(AbstractHttpConfigurer::disable) // 기본 폼 로그인 비활성화 | ||
.logout(logout -> logout // 로그아웃 관련 설정 | ||
.logoutUrl("/api/members/logout") | ||
.logoutSuccessHandler(logoutSuccessHandler()) | ||
.invalidateHttpSession(true) // 로그아웃 시 현재 세션 무효화 | ||
.deleteCookies("JSESSIONID") // 쿠키 삭제 | ||
.permitAll() // 모든 사용자가 로그아웃 기능 사용할 수 있도록 설정 | ||
) | ||
.authorizeHttpRequests(authorizeRequests -> | ||
authorizeRequests | ||
.requestMatchers("/api/members/join","/api/members/login").permitAll() // 회원가입, 로그인 요청 모든 사용자에게 허용 | ||
.anyRequest().authenticated() // 그외 모든 요청은 인증 필요 | ||
); | ||
return http.build(); | ||
} | ||
|
||
// 로그아웃 성공시 실행되는 동작 | ||
@Bean | ||
public LogoutSuccessHandler logoutSuccessHandler() { | ||
return (request,response,authentication) -> { | ||
response.setStatus(HttpServletResponse.SC_OK); // 로그아웃 성공시 HTTP 상태코드 200 OK 로 설정 | ||
response.setContentType("text/plain; charset=UTF-8"); | ||
response.getWriter().write("로그아웃 완료"); // 응답 본문에 "로그아웃 완료" 메시지 작성 | ||
response.getWriter().flush(); // 응답을 즉시 클라이언트로 전송한다. | ||
}; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
송하은/sugang-system/src/main/java/com/example/sugangsystem/repository/MemberRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.example.sugangsystem.repository; | ||
|
||
import com.example.sugangsystem.domain.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); | ||
} |
65 changes: 65 additions & 0 deletions
65
송하은/sugang-system/src/main/java/com/example/sugangsystem/service/MemberService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package com.example.sugangsystem.service; | ||
|
||
import com.example.sugangsystem.domain.auth.Member; | ||
import com.example.sugangsystem.domain.auth.Role; | ||
import com.example.sugangsystem.dto.request.auth.LoginRequestDto; | ||
import com.example.sugangsystem.dto.response.auth.MemberLoginResponseDto; | ||
import com.example.sugangsystem.dto.response.auth.UserInfo; | ||
import com.example.sugangsystem.repository.MemberRepository; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Transactional(readOnly = true) | ||
public class MemberService { | ||
|
||
private final MemberRepository memberRepository; | ||
private final PasswordEncoder passwordEncoder; | ||
|
||
@Transactional | ||
public MemberLoginResponseDto saveUserInfo(UserInfo userInfo) { | ||
validateNotFoundLoginId(userInfo.loginId()); | ||
|
||
Member member = memberRepository.findByLoginId(userInfo.loginId()) | ||
.orElseGet(() -> createMember(userInfo)); | ||
|
||
return MemberLoginResponseDto.from(member); | ||
} | ||
|
||
private void validateNotFoundLoginId(String loginId) { | ||
if (loginId == null || loginId.trim().isEmpty()) { | ||
throw new IllegalStateException("로그인 ID가 필요합니다."); | ||
} | ||
} | ||
|
||
private Member createMember(UserInfo userInfo) { | ||
return memberRepository.save( | ||
Member.builder() | ||
.loginId(userInfo.loginId()) | ||
.pwd(passwordEncoder.encode(userInfo.pwd())) | ||
.name(userInfo.name()) | ||
.role(Role.ROLE_USER) | ||
.build() | ||
); | ||
} | ||
|
||
// 로그인 요청 시 사용자가 입력한 loginId와 비밀번호가 서버에 저장된 정보와 일치하는지 확인하는 역할 | ||
public MemberLoginResponseDto login(LoginRequestDto loginRequestDto) { | ||
Member member = memberRepository.findByLoginId(loginRequestDto.loginId()) | ||
.orElseThrow(() -> new IllegalArgumentException("회원 정보를 찾을 수 없습니다.")); | ||
|
||
// 유저가 입력한 pwd 와 db에 암호화되어 저장된 pwd 를 비교한다. | ||
validationPassword(loginRequestDto.pwd(), member.getPwd()); | ||
|
||
return MemberLoginResponseDto.from(member); | ||
} | ||
|
||
private void validationPassword(String rawPassword, String encodedPassword) { | ||
if(!passwordEncoder.matches(rawPassword, encodedPassword)) { | ||
throw new IllegalStateException("비밀번호가 일치하지 않습니다."); | ||
} | ||
} | ||
} |