1. 스프링 시큐리티 구조

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

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")
: 로그인을 처리하는 urldefaultSuccessUrl("/")
: 로그인 성공하면 전환될 페이지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";
}

@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 에 저장
}

머스태치 용으로 세션 값을 저장한다.
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