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

Feb 15, 2024
스프링 시큐리티 로그인 구현하기
 
 

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 클래스를 구현한다.
UserDetailsServiceloadUserByUsername 를 구현해야 하는데 이 클래스로 username 을 전달한다.
loadUserByUsernameUserDetails 를 리턴한다. 따라서 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; } }
 
MyLoginUserUserDetails 를 구현한다. 그러면 필수적으로 아래의 메서드를 구현해야 하며, 각 메서드로 권한 인증을 한다.
 

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
RSSPowered by inblog