inblog logo
|
{CODE-RYU};
    SPIRNG

    스프링 시큐리티 로그인 구현하기

    Feb 15, 2024
    스프링 시큐리티 로그인 구현하기
    Contents
    1. 스프링 시큐리티 구조2. SecurityConfig2. username 조회 메서드 만들기3. MyLoginServise4. MyLoginUser 객체 만들기5. 로그인 테스트 해보기6. View에 인증 데이터 전달7. 불필요한 인증 코드 삭제
     
     

    1. 스프링 시큐리티 구조

     
    notion image
     
    • 사용자가 로그인 요청을 한다.
    • 스프링 시큐리티는 UserDetails 인터페이스를 통해 비밀번호, 권한 등을 인증한다.
    • ScurityContextHolder 에 Authentication 객체로 저장함.
     
     
    notion image
     

    2. 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.RegexRequestMatcher; @Configuration //컨퍼넌트 스캔. public class SecurityConfig { @Bean // 시큐리티 컨텍스트 홀더에 값이 생김. public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); } //보안을 위한 필터 @Bean // 현재는 모든 걸 무효화. SecurityFilterChain configure(HttpSecurity http) throws Exception { http.csrf(c -> c.disable()); //CSRF 보호 기능을 비활성화하는 코드 // 람다식으로 만들어짐 http.authorizeHttpRequests(a -> { // "/board/**" /board/ 뒤에 모든 주소는 인증이 필요함 //anyRequest().permitAll() 앞의 주소가 아닌 주소는 허용해줌 a.requestMatchers(RegexRequestMatcher.regexMatcher("/board/\\d+" + "")).permitAll()// /board/숫자는 제외하기 위해 .requestMatchers("/user/**","/board/**").authenticated().anyRequest().permitAll(); // 인증이 되지 않으면 못들어가는 페이지 }); http.formLogin(f -> { // 로그인 페이지로 리다이렉션 f.loginPage("/loginForm").loginProcessingUrl("/login").defaultSuccessUrl("/").failureUrl("/loginForm"); // } ); return http.build(); // 시큐리티 필터 체인을 반환 } }
     
    💡
    authorizeHttpRequests : HTTP 요청에 대한 인가(Authorization) 설정을 구성
    requestMatchers(RegexRequestMatcher.regexMatcher("/board/\\d+")).permitAll() : 정규식으로 “/board/”숫자를 허용
    requestMatchers("/user/**","/board/**") : 주소에 대한 매칭
    authenticated() : 인증된 사용자에게만 접근 허용
    anyRequest() : 모든 요청에 대해서 설정을 적용
    loginProcessingUrl("/login") : 로그인을 처리하는 url
    defaultSuccessUrl("/") : 로그인 성공하면 전환될 페이지
    failureUrl("/loginForm") : 로그인 실패 시 전환될 페이지
     
     

    2. username 조회 메서드 만들기

     
    user/UserRepository
    public User findByUsername(String username) { Query query = em.createNativeQuery("select * from user_tb where username=?",User.class); query.setParameter(1,username); try{ User user = (User) query.getSingleResult(); return user; }catch (Exception e){ return null ; } }
     
    username 으로 데이터를 조회한다. 기존 로그인은 username 과 password 를 받아야 하지만, 스프링 시큐리티를 사용하면 username 만 받아서 조회할 수 있다.

    3. MyLoginServise

     
     
    package shop.mtcoding.blog._core.config.security; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; 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 shop.mtcoding.blog.user.User; import shop.mtcoding.blog.user.UserRepository; // 때려지는 조건 post , /login, x-www-form-urlencoded 로 와야야됨, 키 값이 반드시 username,password 여야 함 //login @RequiredArgsConstructor @Service // 컨포넌트 스캔. UserDetailsService 를 @Service가 무력화. 내가 구혀한 loadUserByUsername 가 실행됨 public class MyLoginServise implements UserDetailsService { private final UserRepository userRepository ; private final HttpSession session; // UserDetails 를 만들어서 리턴해주면 됨. @Override //username 만 넘어감. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("loadUserByUsername:"+username); User user = userRepository.findByUsername(username); if(user==null){ System.out.println("user는 null"); return null ; //fail }else { //머스태치 접근을 위해 세션을 따로 만듬 return new MyLoginUser(user) ; // 리턴을 하는 이유는 세션에 저장하려고. } //세션에 넣기 직전에 getPassword 메서드를 호출함. 패스워드랑 유저 객체랑 비교해서 알아서 패스워드를 체크해줌 //세션에 저장되는게 아니라 SecurityContextHolder 에 저장 } //ioc 컨테이너에 UserDetailsService가 뜸/ }
     
    MyLoginServise 클래스를 생성한다. UserDetailsService 클래스를 구현한다.
    UserDetailsService 는 loadUserByUsername 를 구현해야 하는데 이 클래스로 username 을 전달한다.
    loadUserByUsername 는 UserDetails 를 리턴한다. 따라서 UserDetails 을 구현해야 한다.

    4. MyLoginUser 객체 만들기

     
    @RequiredArgsConstructor public class MyLoginUser implements UserDetails { private final User user ; //DB에서 패스워드 조회하기 위해 의존성주입하려고 @Override public String getPassword() { return user.getPassword(); //DB의 비밀번호 } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } }
     
    MyLoginUser 는 UserDetails 를 구현한다. 그러면 필수적으로 아래의 메서드를 구현해야 하며, 각 메서드로 권한 인증을 한다.
     

    5. 로그인 테스트 해보기

    @GetMapping({"/"}) public String index(HttpServletRequest request,@AuthenticationPrincipal MyLoginUser myLoginUser) { System.out.println("로그인 되었나? : "+myLoginUser.getUsername()); // 이걸 작성하면 메인페이지가 접속되지 않음. myLoginUser 데이터 넘어오는지 확인하고 삭제 List<Board> boardList = boardRepository.findAll(); request.setAttribute("boardList", boardList); return "index"; }
     
    notion image
     
    @AuthenticationPrincipal 를 통해 인증된 유저의 데이터를 가져온다.
     

    6. View에 인증 데이터 전달

     
    스프링 시큐리티를 사용하게 되면 세션이 아니라 Authentication 에 저장된다.
    따라서 머스태치에 데이터를 전달하려면 다음과 같이 전달되어야 한다.
     
    {{#SecurityContextHolder.SecurityContext.Authentication}} {{/SecurityContextHolder.SecurityContext.Authentication}}
     
    위의 코드처럼 Authentication를 꺼내기 위해 불필요한 코드를 적게 된다.
     
    그래서 편리하게 사용하기 위해 머스태치에 사용할 값을 세션에 따로 저장해둔다.
     
    @Override //username 만 넘어감. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ System.out.println("user는 null"); return null ; //fail }else { //머스태치 접근을 위해 세션을 따로 만듬 System.out.println("user를 찾음"); session.setAttribute("sessionUser",user); // 머스태치용 return new MyLoginUser(user) ; // 리턴을 하는 이유는 세션에 저장하려고. } //세션에 넣기 직전에 getPassword 메서드를 호출함. 패스워드랑 유저 객체랑 비교해서 알아서 패스워드를 체크해줌 //세션에 저장되는게 아니라 SecurityContextHolder 에 저장 }
    notion image
     
    머스태치 용으로 세션 값을 저장한다.
     
    layout/header.mustache
    <div class="collapse navbar-collapse" id="collapsibleNavbar"> <ul class="navbar-nav"> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="/board/saveForm">글쓰기</a> </li> <li class="nav-item"> <a class="nav-link" href="/user/updateForm">회원정보보기</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/joinForm">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/loginForm">로그인</a> </li> {{/sessionUser}} </ul> </div>
     

    7. 불필요한 인증 코드 삭제

     
    package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.config.security.MyLoginUser; import shop.mtcoding.blog.user.User; import java.util.HashMap; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; private final BoardRepository boardRepository; @PostMapping("/board/{id}/update") public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO,@AuthenticationPrincipal MyLoginUser myLoginUser){ // 권한 체크 Board board = boardRepository.findById(id); if(board.getUserId() != myLoginUser.getUser().getId()){ return "error/403"; } // 핵심 로직 // update board_tb set title = ?, content = ? where id = ?; boardRepository.update(requestDTO, id); return "redirect:/board/"+id; } @GetMapping("/board/{id}/updateForm") public String updateForm(@PathVariable int id, HttpServletRequest request,@AuthenticationPrincipal MyLoginUser myLoginUser){ // 모델 위임 (id로 board를 조회) Board board = boardRepository.findById(id); if(board.getUserId() != myLoginUser.getUser().getId()){ return "error/403"; } // 가방에 담기 request.setAttribute("board", board); return "board/updateForm"; } @PostMapping("/board/{id}/delete") public String delete(@PathVariable int id, HttpServletRequest request,@AuthenticationPrincipal MyLoginUser myLoginUser) { // 권한 인증 Board board = boardRepository.findById(id); if (board.getUserId() != myLoginUser.getUser().getId()) { request.setAttribute("status", 403); request.setAttribute("msg", "게시글을 삭제할 권한이 없습니다"); return "error/40x"; } boardRepository.deleteById(id); return "redirect:/"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request,@AuthenticationPrincipal MyLoginUser myLoginUser) { // 바디 데이터 확인 및 유효성 검사 System.out.println(requestDTO); if (requestDTO.getTitle().length() > 30) { request.setAttribute("status", 400); request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요"); return "error/40x"; // BadRequest } // 모델 위임 // insert into board_tb(title, content, user_id, created_at) values(?,?,?, now()); boardRepository.save(requestDTO, myLoginUser.getUser().getId()); return "redirect:/"; } @GetMapping({"/"}) public String index(HttpServletRequest request,@AuthenticationPrincipal MyLoginUser myLoginUser) { System.out.println("로그인 되었나? : "+myLoginUser.getUsername()); List<Board> boardList = boardRepository.findAll(); request.setAttribute("boardList", boardList); return "index"; } @GetMapping("/board/saveForm") public String saveForm() { return "board/saveForm"; } @GetMapping("/board/{id}") public String detail(@PathVariable int id, HttpServletRequest request,@AuthenticationPrincipal MyLoginUser myLoginUser){ // 1. 모델 진입 - 상세보기 데이터 가져오기 BoardResponse.DetailDTO responseDTO = boardRepository.findByIdWithUser(id); boolean pageOwner; if (myLoginUser == null) { pageOwner = false; } else { int 게시글작성자번호 = responseDTO.getUserId(); int 로그인한사람의번호 = myLoginUser.getUser().getId(); pageOwner = 게시글작성자번호 == 로그인한사람의번호; } request.setAttribute("board", responseDTO); request.setAttribute("pageOwner", pageOwner); return "board/detail"; } }
     
     
    User sessionUser = (User) session.getAttribute("sessionUser"); if(sessionUser==null){ return "redirect:/loginForm";
     
    기존 인증이 필요한 모든 컨트롤러에 적용되었던 코드를 삭제했다.
    스프링 시큐리티를 사용하면 인증 처리를 한 번에 할 수 있다.
    Share article

    {CODE-RYU};

    RSS·Powered by Inblog