-
Notifications
You must be signed in to change notification settings - Fork 0
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
#1 회원가입 기능 구현 시큐리티설정 작업 #8
The head ref may contain hidden characters: "#1_\uB85C\uADF8\uC778,\uD68C\uC6D0\uAC00\uC785"
Changes from 4 commits
a5fcdd3
2fd005c
35e2707
0f32fd7
26ae524
e67b593
674aac6
a6adfc5
8326c73
fdea642
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package com.main.board.config; | ||
|
||
import com.main.board.member.Member; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
|
||
public class CustomUserDetails implements UserDetails { | ||
|
||
private final Member member; | ||
|
||
public CustomUserDetails(Member member) { | ||
this.member = member; | ||
} | ||
|
||
@Override | ||
public Collection<? extends GrantedAuthority> getAuthorities() { | ||
ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>(); //ArrayList객체생성 | ||
auth.add(new SimpleGrantedAuthority("ROLE_USER"));//ROLE_USER권한을 부여 | ||
return auth; //권한리스트반환 | ||
} | ||
|
||
@Override | ||
public String getPassword() { | ||
return member.getPassword(); | ||
} | ||
|
||
|
||
@Override | ||
public String getUsername() { | ||
return member.getName(); | ||
} | ||
|
||
@Override | ||
public boolean isAccountNonExpired() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean isAccountNonLocked() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean isCredentialsNonExpired() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean isEnabled() { | ||
return true; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.main.board.config; | ||
|
||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.io.IOException; | ||
|
||
/* | ||
기본적으로 시큐리티에서 로그인 실패시에는 SimpleUrlAuthenticationFailureHandler를 사용한다. | ||
사용자가 인증에 실패하면 AuthenticationException이 발생 | ||
SimpleUrlAuthenticationFailureHandler는 기본적으로 실패 후 로그인 페이지로 리다이렉트하며, URL에 ?error를 추가 | ||
*/ | ||
|
||
public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler { | ||
|
||
@Override | ||
public void onAuthenticationFailure(HttpServletRequest request, | ||
HttpServletResponse response, | ||
AuthenticationException exception) throws IOException, ServletException { | ||
//로그인 실패로그 | ||
System.out.println("로그인 실패: " + exception.getMessage()); | ||
// 실패 응답 커스터마이즈 | ||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 401 Unauthorized | ||
response.setContentType("application/json"); | ||
response.setCharacterEncoding("UTF-8"); | ||
response.getWriter().write("{\"error\": \"Login failed\", \"message\": \"" + exception.getMessage() + "\"}"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.main.board.config; | ||
|
||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.io.IOException; | ||
|
||
|
||
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { | ||
|
||
@Override | ||
public void onAuthenticationSuccess(HttpServletRequest request, | ||
HttpServletResponse response, | ||
Authentication authentication) throws IOException, ServletException { | ||
// 성공 로그 출력 | ||
System.out.println("로그인 성공: " + authentication.getName()); | ||
|
||
// 성공 응답 커스터마이징 (예: JSON 응답 반환) | ||
response.setStatus(HttpServletResponse.SC_OK); | ||
response.setContentType("application/json"); | ||
response.setCharacterEncoding("UTF-8"); | ||
response.getWriter().write("{\"message\": \"Login successful\", \"user\": \"" + authentication.getName() + "\"}"); | ||
} | ||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package com.main.board.config; | ||
|
||
import com.main.board.util.BcryptEncoding; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.authentication.AuthenticationManager; | ||
import org.springframework.security.authentication.AuthenticationProvider; | ||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; | ||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; | ||
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.SessionManagementConfigurer; | ||
import org.springframework.security.config.http.SessionCreationPolicy; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; | ||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; | ||
|
||
@Configuration // 스프링부트에게 이 클래스가 설정파일임을 알려줌 (빈등록) | ||
@EnableWebSecurity // Spring Security 활성화 기본보안 필터체인이 적용된다 | ||
public class SecurityConfig { | ||
|
||
private UserDetailService userDetailsService; | ||
|
||
public SecurityConfig(UserDetailService userDetailsService) { | ||
this.userDetailsService = userDetailsService; | ||
} | ||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //final HttpSecurity http 이점이있는가? | ||
http | ||
.csrf((auth) -> auth.disable()) // csrf 비활성화 (REST API등 비상태 통신에서는 CSRF토큰이 필요하지 않을수있다) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CSRF 토큰은 어떤 역할을 할까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CSRF는 공격자가 원하지않는 요청을 실행하는 취약점입니다 진행하고 있는 프로젝트가 만약 실제 클라이언트를 위한 서비스로 진행한다면 |
||
.httpBasic((auth) -> auth.disable()) // httpBasic 비활성화 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 옵션을 끈 이유가 있을까요? 아래 문서를 참고해서 답변해보세요~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. httpBasic옵션은 |
||
.formLogin((auth) -> auth.disable()) // formLogin 비활성화 | ||
.authorizeHttpRequests((auth) -> auth | ||
.requestMatchers("/", "/member/signup", "/auth/login").permitAll() // "/" 경로는 모든 사용자에게 허용 | ||
.anyRequest().authenticated()) | ||
.sessionManagement(session -> session | ||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) //세션정책설정 (인증이 필요할때만 생성) | ||
/* | ||
**세션 고정 공격(Session Fixation Attack)**을 방지하기 위한 설정입니다. | ||
세션 고정 공격은 공격자가 사용자의 세션 ID를 미리 설정하거나 가로채어 악용하는 공격입니다. | ||
|
||
Spring Security에서는 세션 고정 공격을 방지하기 위해 새로운 세션을 생성하는 방식을 제공합니다. | ||
newSession 설정은 인증 후 항상 새로운 세션을 생성합니다. | ||
사용자가 로그인하거나 인증할 때 기존 세션을 폐기하고 새로운 세션을 생성합니다. | ||
이로 인해 기존 세션 ID가 무효화되며, 세션 고정 공격을 방지합니다. | ||
*/ | ||
.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::newSession)//세션고정공격방지 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 세션 고정공격에 대한 방지는 기본값인 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 우선 세션고정 공격이라는것은 1.migrateSession은 인증후에는 새로운 세션 ID를 발급하지만 기존 세션에 담긴 데이터는 유지되는것이 특징입니다 (ex:장바구니, 폼입력데이터)
현재 프로젝트는 기본적인 게시판 즉 폼데이터를 사용할것이라 예측되기때문에 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 내용 [newSesion보다는 migrateSession이 조금더 적합] 이 아직 적용안된것으로 보입니다. 적용해주세요~ |
||
.maximumSessions(1) // 동시세션수 제한 (하나의 사용자계정이 유지할수있는 세션의 수를 제한) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동시세션수를 제한할 필요가 없다면, 해당 설정은 뺴는게 좋습니다. |
||
); | ||
|
||
return http.build(); | ||
} | ||
|
||
// 로그인시에 Spring Security의 인증 진입점이다 클라이언트가 제공한 인증정보를 받아 AuthenticationManager를 통해 인증을 위임한다, Bean설정 필수 | ||
// LoginController에서 사용 | ||
@Bean | ||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { | ||
return authenticationConfiguration.getAuthenticationManager(); | ||
} | ||
|
||
/* | ||
실제인증을 처리하는 부분인 Provider이다 | ||
DaoAuthenticationProvider는 UserDetailsService를 통해 사용자 정보를 가져오고 비밀번호를 확인한다 | ||
|
||
*/ | ||
@Bean | ||
public AuthenticationProvider authenticationProvider() { | ||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); | ||
|
||
provider.setUserDetailsService(userDetailsService); // 사용자 정보를 로드할 서비스 | ||
provider.setPasswordEncoder(passwordEncoder()); // 비밀번호 암호화 확인 | ||
|
||
return provider; | ||
} | ||
|
||
|
||
//해당 방식으로 스프링 시큐리티가 CustomUserDetails 객체에서 반환된 비밀번호와 로그인 요청에서 받은 비밀번호를 비교. | ||
@Bean | ||
public PasswordEncoder passwordEncoder() { | ||
return new BcryptEncoding(); | ||
} | ||
|
||
//인스턴스 생성 | ||
@Bean | ||
public AuthenticationFailureHandler loginFailHandler() { | ||
return new LoginFailHandler(); | ||
} | ||
|
||
//인스턴스 생성 | ||
@Bean | ||
public AuthenticationSuccessHandler loginSuccessHandler() { | ||
return new LoginSuccessHandler(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.main.board.config; | ||
|
||
import com.main.board.member.Member; | ||
import com.main.board.member.repository.MemberRepository; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.core.userdetails.UserDetailsService; | ||
import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||
import org.springframework.stereotype.Service; | ||
|
||
// Spring Security는 해당 객체를 기반으로 인증을 처리한다 | ||
@Service | ||
@RequiredArgsConstructor | ||
public class UserDetailService implements UserDetailsService { | ||
|
||
private final MemberRepository memberRepository; | ||
|
||
//로그인시 유저를 찾고 유저가 있으면 CustomUserDetails를 반환 | ||
@Override | ||
public CustomUserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { | ||
Member member = memberRepository.findMemberById(userId) | ||
.orElseThrow(() -> new UsernameNotFoundException("해당하는 사용자가 없습니다.")); | ||
return new CustomUserDetails(member); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 깔끔하게 잘 구현하셨네요 👍 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package com.main.board.exception; | ||
|
||
public class CustomException { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package com.main.board.exception; | ||
|
||
public class GlobalException { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.main.board.login.DTO; | ||
|
||
import lombok.Getter; | ||
import lombok.Setter; | ||
|
||
@Getter | ||
@Setter | ||
public class LoginRequest { | ||
private String username; | ||
private String password; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. password는 평문 / 암호화된 암호문 두가지 상태가 있습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rawPassword 변수명 작성 하도록하겠습니다! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package com.main.board.login.controller; | ||
|
||
import com.main.board.login.DTO.LoginRequest; | ||
import jakarta.servlet.http.HttpSession; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.authentication.AuthenticationManager; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.Authentication; | ||
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 //JSON을 받기위한 어노테이션 | ||
@RequestMapping("/auth") | ||
public class LoginController { | ||
|
||
private final AuthenticationManager authenticationManager; | ||
|
||
public LoginController(AuthenticationManager authenticationManager) { | ||
this.authenticationManager = authenticationManager; | ||
} | ||
|
||
@PostMapping("/login") | ||
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpSession session) { | ||
try { | ||
/* | ||
인증 요청 | ||
토큰이라고 해서 토큰방식을 사용하는것이 아니고 | ||
Spring Security에서 토큰 기반의 인증을 수행하는 객체이다 | ||
Spring Secutiry 내부 에서 사용자 인증 정보를 담기위한 객체이다 | ||
*/ | ||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); //토큰생성 | ||
/* | ||
1. authenticationManager가 Provider에게 인증을 위임 | ||
2. config에 설정한 내용대로 UserDetailsService를 통해 유저정보를 가져온다 | ||
3. 입력된비밀번호와 가져온정보의 비밀번호를 비교하여 인증에 성공하면 Authentication 객체를 생성하여 리턴 | ||
*/ | ||
Authentication authentication = authenticationManager.authenticate(token); // AuthenticationManager를 통해 인증을 시도한다 | ||
|
||
// 세션에 인증 정보 저장 | ||
/* | ||
SecurityContextHolder는 SpringSecurity의 인증 정보를 저장하고 조회하는 컨텍스트 | ||
1. 인증이 성공하면 Authentication 객체를 SecurityContextHolder에 저장 | ||
2. 이후 클라인언트의 요청은 사용자 인증상태를 유지하게끔 한다 | ||
3. SPRING_SECURITY_CONTEXT는 SpringSecurity에서 사용하는 세션의 기본키이다 | ||
*/ | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인증이 성고한뒤 해당 정보를 SecurityContextHolder에 저장하고난뒤 Spring Security가 기본적으로 컨텍스트에 저장한뒤 세션에 저장해주는 일이 존재하는걸 해당 코멘트를 보고나서 찾아보니 알게되었습니다.... 중복작업이기때문에 해당 구문은 삭제하도록하겠습니다! |
||
|
||
return ResponseEntity.ok("Login successful"); | ||
} catch (Exception e) { | ||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Username / Password가 일치하지 않을 때 발생하는 예외는 정해져있습니다. |
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이런 비즈니스로직은 Controller layer에 존재하면 안됩니다. |
||
} | ||
|
||
@PostMapping("/logout") | ||
public ResponseEntity<?> logout(HttpSession session) { | ||
session.invalidate(); //로그아웃시에 세션을 무효화하여 저장된 모든 데이터를 삭제한다 "SPRING_SECURITY_CONTEXT"해당 키 삭제 | ||
SecurityContextHolder.clearContext(); //SecurityContextHolder의 인증정보를 삭제한다 | ||
return ResponseEntity.ok("Logout successful"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 200 OK 상태코드정도만 보내도 충분할것으로 보입니다~ |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.main.board.member.DTO; | ||
|
||
import com.main.board.member.Member; | ||
|
||
import java.time.LocalDate; | ||
|
||
public class SignUpResponse { | ||
private String memberId; | ||
private String password; | ||
private String name; | ||
private LocalDate createDate; | ||
|
||
public SignUpResponse(Member member) { | ||
this.memberId = member.getMemberId(); | ||
this.password = member.getPassword(); | ||
this.name = member.getName(); | ||
this.createDate = member.getCreateDate(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
불변선언 좋습니다 👍