29. Spring Security : 인증 주소 설계하기

Feb 13, 2024
29. Spring Security : 인증 주소 설계하기

1. SecurityConfig 만들기

  • 코드의 변경이 없으면 부모 이름(추상적)으로 리턴할 필요 없음
package shop.mtcoding.blog._core.config; 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.web.SecurityFilterChain; @Configuration// 메모리에 띄우기 위한 문법 public class SecurityConfig { @Bean // IoC에 뜸 SecurityFilterChain configure(HttpSecurity http) throws Exception{ return http.build(); // 코드의 변경이 없으면 부모 이름(추상적)으로 리턴할 필요 없음 } }
 

2. 현재 상황

  • 모든 것을 무효 상태로 만듦
  • 하나씩 닫아줘야 함
  • 원래 있던 FilterChain에서 아무것도 없는 FilterChain을 만들어 그냥 다 통과된 것
notion image
 

3. 예전 코드

package shop.mtcoding.blog._core.config; 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.web.SecurityFilterChain; @Configuration// 메모리에 띄우기 위한 문법 public class SecurityConfig { @Bean SecurityFilterChain configure(HttpSecurity http) throws Exception{ http.authorizeRequests().anyRequest(); // 예전 코드 return http.build(); // 코드의 변경이 없으면 부모 이름(추상적)으로 리턴할 필요 없음 } }
  • @Deprecated
runtime 때 알려줌
해당 요소(클래스, 메서드, 필드 등)가 더 이상 권장되지 않음
여전히 동작은 하지만, 이후의 버전에서는 사용을 지양하거나 사용을 중단할 수 있음
notion image
 

4. 기본 주소를 우리가 만든 페이지로 변경하기

  • 인터페이스를 볼 때 중요한 것 : 이 메서드가 리턴 타입 or method가 있는지 없는지 확인!
  • 주소를 잘 만들어야 함
*: 하나의 주소만 가능
** : 모든 주소가 가능
  • authenticated() : 인증된 사용자만 접근 가능
  • anyRequest() : 모든 요청에 대한 설정
  • permitAll() : 모든 사용자가 접근 가능
package shop.mtcoding.blog._core.config; 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.web.SecurityFilterChain; @Configuration// 메모리에 띄우기 위한 문법 public class SecurityConfig { @Bean SecurityFilterChain configure(HttpSecurity http) throws Exception { // 주소로 필터링 : 인증이 필요한 페이지를 주소로 구분 http.authorizeHttpRequests(a -> { // 리턴 타입이 있으면 {} 필요 없음 a.requestMatchers("/user/updateForm, /board/**").authenticated() // 인증이 필요한 페이지 .anyRequest().permitAll(); // 인증이 필요없는 페이지 }); // 기본 주소를 우리가 만든 페이지로 변경함 http.formLogin(f -> { f.loginPage("/loginForm"); }); return http.build(); // 코드의 변경이 없으면 부모 이름(추상적)으로 리턴할 필요 없음 } }
notion image
notion image
 

5. 컨트롤러에서 인증 로직 삭제하기

  • UserController 수정하기
package shop.mtcoding.blog.user; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import shop.mtcoding.blog.board.Board; import shop.mtcoding.blog.board.BoardRequest; @AllArgsConstructor @Controller public class UserController { // fianl 변수는 반드시 초기화 되어야 함 private final UserRepository userRepository; // null private final HttpSession session; // @AllArgsConstructor를 사용하면서 필요 없어짐 // public UserController(UserRepository userRepository, HttpSession session) { // this.userRepository = userRepository; // this.session = session; // } @GetMapping("/loginForm") // view만 원함 public String loginForm() { return "user/loginForm"; } // 원래는 get요청이나 예외 post요청하면 됨 // 민감한 정보는 쿼리 스트링에 담아보낼 수 없음 //원래는 get요청이나 예외 post요청하면 됨 //민감한 정보는 쿼리 스트링에 담아보낼 수 없음 // @PostMapping("/login") // public String login(UserRequest.LoginDTO requestDTO) { // // // 1. 유효성 검사 // if (requestDTO.getUsername().length() < 3) { // return "error/400"; // } // // // 2. 모델 필요 select * from user_tb where username=? and password=? // User user = userRepository.findByUsernameAndPassword(requestDTO); // DB에 조회할때 필요하니까 데이터를 받음 // if (user == null) { // return "error/401"; // } else { // session.setAttribute("sessionUser", user); // return "redirect:/"; // } // } @GetMapping("/joinForm") // view만 원함 public String joinForm() { return "user/joinForm"; } @PostMapping("/join") public String join(UserRequest.JoinDTO requestDTO) { System.out.println(requestDTO); // 1. 유효성 검사 if (requestDTO.getUsername().length() < 3) { return "error/400"; } userRepository.save(requestDTO); // 모델에 위임하기 return "redirect:/loginForm"; //리다이렉션불러놓은게 있어서 다시부른거 } @GetMapping("/user/updateForm") public String updateForm() { // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); return "/user/updateForm"; } @PostMapping("/user/update") public String updateUser(UserRequest.UpdateDTO requestDTO, HttpServletRequest request) { // 세션에서 사용자 정보 가져오기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; // 로그인 페이지로 리다이렉트 } // 비밀번호 업데이트 userRepository.userUpdate(requestDTO, sessionUser.getId()); session.setAttribute("sessionUser", sessionUser); return "redirect:/"; // 홈 페이지로 리다이렉트 } @GetMapping("/logout") public String logout() { // 1번 서랍에 있는 uset를 삭제해야 로그아웃이 됨 session.invalidate(); // 서랍의 내용 삭제 return "redirect:/"; }
  • BoardController 수정하기
package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; // DI private final BoardRepository boardRepository; // DI // ?title=제목1&content=내용1 // title=제목1&content=내용1 // 쿼리 스트링과 -x-www-form-urlencoded와 파싱 방법이 동일함 // http://localhost:8080?page=0 @GetMapping({"/"}) public String index(HttpServletRequest request) { List<Board> boardList = boardRepository.findAll(); request.setAttribute("boardList", boardList); return "index"; } // 상세보기시 호출 @GetMapping("/board/{id}") // 1이 프라이머리키 -> 뭐든 넣어도 실행시키려면 변수화시켜서 {} public String detail(@PathVariable int id, HttpServletRequest request) { System.out.println("id : " + id); // 1. 바로 모델 진입 -> 상세보기 데이터 가져오기 // body 데이터가 없으면 유효성 검사할 필요 없음 BoardResponse.DetailDTO reponseDTO = boardRepository.findByIdWithUser(id); //메서드 이름 변경 // user 객체를 가져와서 session 값 받기 : object라 다운 캐스팅 해야함 User sessionUser = (User) session.getAttribute("sessionUser"); //System.out.println("sessionUser: " + sessionUser); // 2. 페이지 주인 여부 체크(board의 userId와 sessionId의 값 비교) boolean pageOwner = false; if (reponseDTO.getUserId() == sessionUser.getId()) { //System.out.println("getUserId:" + reponseDTO.getUserId()); pageOwner = true; } request.setAttribute("board", reponseDTO); request.setAttribute("pageOwner", pageOwner); // 이 값을 mustache에게 줘야함! return "board/detail"; } @GetMapping("/board/saveForm") // /board/saveForm Get요청이 옴 public String saveForm() { // session 영역에 접근하기 위한 // 1. session 영역에 sessionUser 키 값에 user 객체가 있는지 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // null이 아니면 /board/saveForm으로 이동 return "board/saveForm"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request) { // 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); System.out.println("sessionUser:" + sessionUser); // 바디 데이터 확인 및 유효성 검사 System.out.println(requestDTO); if (requestDTO.getTitle().length() > 30) { request.setAttribute("status", 400); request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요"); return "error/40x"; // BadRequest } // 3. 모델 위임 // insert into board_tb(title, content, user_id, created_at) values(?,?,?, now()); boardRepository.save(requestDTO, sessionUser.getId()); return "redirect:/"; } @GetMapping("/board/{id}/updateForm") // 보드에 해당 페이지 public String updateFormn(@PathVariable int id, HttpServletRequest request) { // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { request.setAttribute("status", 403); request.setAttribute("msg", "게시글을 수정할 권한이 없습니다"); return "error/40x"; // 리다이렉트 하면 데이터 사라지니까 하면 안됨 } // 가방에 담기 request.setAttribute("board", board); return "board/updateForm"; } @PostMapping("/board/{id}/update") public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO) { // System.out.println(requestDTO); 정보 받기 확인 // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } // update board_tb set title =?, content =?, where id =? boardRepository.update(requestDTO, id); return "redirect:/board/" + id; // 수정한 게시글로 돌아가기 } @PostMapping("/board/{id}/delete") // body데이터가 없어서 유효성 검사 안해도 됨 public String delete(@PathVariable int id) { // 인증 검사하기 User sessionUser = (User) session.getAttribute("sessionUser"); // 권한 검사하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } boardRepository.deleteById(id); return "redirect:/"; } }
 

5. 테스트해보기

notion image
  • “/user/updateForm”으로 요청 시 “loginForm”으로 리다이렉션 됨
  • “/board/1”로 요청 시 “loginForm”으로 리다이렉션 됨
notion image
  • 핵심 : 앞에서 막는 것
뒤에서 많으면 코드를 중복해서 적어야 함
  • 현재 상황
안 막은 main페이지를 제외한 /board관련 아무것도 안들어가짐
 
  • 주소 잘 짓는 방법
인증이 필요한 페이지 앞에 특정 키워드를 적음
@Getmappin("/api/user/updateForm") -> /api/**로 끝남
 

6. SecurityConfig에 ignore() 만들기

  • 인증과 상관없이 열어야 하는 주소
  • 정적 파일만 security filter에서 제외시키기
  • 보통은 잠글 주소를 적어주는 것이 맞음
  • 처음부터 주소 설계를 잘해야 함
package shop.mtcoding.blog._core.config; 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.WebSecurityCustomizer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.RegexRequestMatcher; @Configuration// 메모리에 띄우기 위한 문법 public class SecurityConfig { // 인증과 상관없이 열어야 하는 주소 // 주소 설계를 잘해야 함 @Bean // IoC에 뜸 public WebSecurityCustomizer ignore(){ // 정적 파일만 security filter에서 제외시키기 return w -> w.ignoring().requestMatchers("/board/**", /static/**", "/h2-console/**"); } @Bean // IoC에 뜸 SecurityFilterChain configure(HttpSecurity http) throws Exception { http.csrf(c->c.disable()); // 주소로 필터링 : 인증이 필요한 페이지를 주소로 구분 http.authorizeHttpRequests(a -> { a.requestMatchers(RegexRequestMatcher.regexMatcher("/board/\\d+")).permitAll() .requestMatchers("/user/**", "/board/**").authenticated() // 인증이 필요한 페이지 .anyRequest().permitAll(); // 인증이 필요없는 페이지 }); // 기본 주소를 우리가 만든 페이지로 변경함 http.formLogin(f -> { // 시큐리티가 들고있는 페이지를 사용할 것 f.loginPage("/loginForm").loginProcessingUrl("/login").defaultSuccessUrl("/") .failureUrl("/loginForm"); // 실패하면 이동 }); return http.build(); // 코드의 변경이 없으면 부모 이름(추상적)으로 리턴할 필요 없음 } }
Share article

vosw1