
1. SecurityConfig에서 login 설정하기
- loginProcessingUrl : security가 가지고 있는 login 만들기
우리가 “/login”을 안 만들어도 됨
- defaultSuccessUrl : 로그인 성공시 어디로 리다이렉션 할 것인지 설정
- failureUrl : 로그인 실패시 어디로 리다이렉션 할 것인지 설정
- mustache화면에서 Form태그 → /login, post 요청 → username, password를 submit
→ submit하는 순간 Security가 /login요청을 filter에서 가로챔
→ UserDetailsService에 있는 특정 메서드 호출
- 기본적으로 Security 자체가 login을 구현하고 있는 화면(특정 메서드)을 커스터 마이징 할 예정

- 이미 IoC에 UserDetailsService 등록되어있음
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.web.SecurityFilterChain;
@Configuration// 메모리에 띄우기 위한 문법
public class SecurityConfig {
// 인증과 상관없이 열어야 하는 주소
// 주소 설계를 잘해야 함
@Bean
public WebSecurityCustomizer ignore(){
return w -> w.ignoring().requestMatchers("/board/*", "/static/**", "/h2-console/**");
}
@Bean
SecurityFilterChain configure(HttpSecurity http) throws Exception {
// 주소로 필터링 : 인증이 필요한 페이지를 주소로 구분
http.authorizeHttpRequests(a -> {
a.requestMatchers("/user/updateForm", "/board/**").authenticated() // 인증이 필요한 페이지
.anyRequest().permitAll(); // 인증이 필요없는 페이지
});
// 기본 주소를 우리가 만든 페이지로 변경함
http.formLogin(f -> {
// security가 들고있는 페이지를 사용할 것
f.loginPage("/loginForm").loginProcessingUrl("/login").defaultSuccessUrl("/")
.failureUrl("/loginForm"); // 실패하면 이동
});
return http.build(); // 코드의 변경이 없으면 부모 이름(추상적)으로 리턴할 필요 없음
}
}
2. config 패키지에 Security 패키지 만들고 MyLoginService class 만들기
- UserDetailsService를 implement 해야 함
spring security 라이브러리를 적용하면
최초에 스프링이 실행될 때 UserDetailsService가 new 되서 IoC 컨테이너에 등록이 되어있음
implement해서 메서드 구현하면 메모리에 안뜸 → @Service 붙이기
@Service : 내부에 component가 있어서 component scane이 됨
MyLoginService가 new가 되서 IoC 컨테이너에 들어감
기존에 UserDetailsService와 타입을 일치시켜 덮어 씌움
→ 무력화 시키고 MyLoginService가 떠있음
/login의 post(주소) 요청하는 순간 loadUserByUsername()가 실행됨
- loadUserByUsername()의 내부 : /login해서 구현했던 내용들
username 받아서 DB에 조회해서 password랑 동일한지 비교하고 session을 만들어주면 됨
- 로그인 실패 시 로그인 진행하던걸 취소하고 알아서 응답해줌
-> 반환할 페이지를 알려줘야 함
- 조건 : post 요청, "/login"요청, x-www-form-urlencoded, 키 값 username, password
- DB에서 조회한 객체를 넣어주지 않으면 오류가 남
- org.springframework 패키지 : Spring 프레임워크 자체의 핵심 라이브러리와 클래스를 포함
스프링의 핵심 기능 및 유틸리티 클래스 등이 포함
- shop.mtcoding 패키지 : 특정 프로젝트나 응용 프로그램의 사용자 정의 패키지
프로젝트에서 정의한 사용자 클래스 또는 기타 관련 클래스를 포함
package shop.mtcoding.blog._core.config.security;
import lombok.AllArgsConstructor;
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*/
@RequiredArgsConstructor
@Service
public class MyLoginService implements UserDetailsService {
private final UserRepository userRepository; // DI
@Override // security가 username만 줌
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("loadUserByUsername: " + username);
// username이 있는지 조회하기
User user = userRepository.findByUsername(username);
if (user == null) {
return null; // 로그인 진행하던걸 취소하고 알아서 응답해줌 -> 반환할 페이지를 알려줘야함
} else {
return new MyLoginUser(user); // DB에서 조회한 객체를 넣어주지 않으면 오류가 남
}
}
}
3. UserRepository에 findByUsername() 만들기
1) 세션에 저장하기 위해서 UserDetail로 리턴을 받는 이유는? session에 저장하기 위함
리턴 타입이 UserDetails라 User를 리턴할 수 없음 → User를 UserDetails로 커스터 마이징 해야 함
void라면 나보고 저장하라는 이야기임 / 의도를 파악해야 함
로그인 시스템에 대한 이해가 필요함
클라이언트한테 username과 password를 받음 → DB에 조회 → 정상 : session에 담음
2) object가 아니라 UserDetails로 고정한 이유는? UserDetails 인터페이스
Object타입이면 user를 리턴 받을 수 있으나 session에 object가 저장되어 사용할 수도 있음
내가 무슨 이름으로 객체를 만들지 라이브러리를 만드는 사람은 알 수가 없음
- getUsername() : 사용자의 식별자인 사용자명
- getPassword() : 사용자의 암호화된 비밀번호
- getAuthorities() : 사용자가 가지고 있는 권한 / retuen타입 : collection
여러가지 권한을 가질 수도 있음
- isEnabled() : 사용자 계정이 활성화되었는지 여부 / 활성화 = true
- isAccountNonExpired() : 사용자 계정의 만료 여부 / 만료 = false / 법적으로 중요함
- isAccountNonLocked() : 사용자 계정의 잠김 여부 / 시도가 여러 번 있을 경우 = false
- isCredentialsNonExpired() : 사용자 계정의 자격 증명(비밀번호)의 만료 여부 / 만료 = false
보안상의 이유로 정기적으로 비밀번호를 변경해야하는 것
3) password를 안 받는 이유는?
- 우린 조회만 해서 주면 session에 넣기 직전에 getPassword() 실행
return 되는 값이랑 User객체랑 맞는지 알아서 비교해서 인증해줌
비교해서 맞으면 session이 만들어짐
package shop.mtcoding.blog.user;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jdk.swing.interop.SwingInterOpUtils;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import shop.mtcoding.blog.board.Board;
import shop.mtcoding.blog.board.BoardRequest;
@Repository // IoC에 new하는 방법
public class UserRepository {
// DB에 접근할 수 있는 매니저 객체
// 스프링이 만들어서 IoC에 넣어둔다.
// DI에서 꺼내 쓰기만 하면된다.
private EntityManager em; // 컴포지션
// 생성자 주입 (DI 코드)
public UserRepository(EntityManager em) {
this.em = em;
}
@Transactional // db에 write 할때는 필수
public void save(UserRequest.JoinDTO requestDTO){
Query query = em.createNativeQuery("insert into user_tb(username, password, email, created_at) values(?,?,?, now())");
query.setParameter(1, requestDTO.getUsername());
query.setParameter(2, requestDTO.getPassword());
query.setParameter(3, requestDTO.getEmail());
query.executeUpdate();
}
public User findByUsernameAndPassword(UserRequest.LoginDTO requestDTO) {
Query query = em.createNativeQuery("select * from user_tb where username=? and password=?", User.class); // 알아서 매핑해줌
query.setParameter(1, requestDTO.getUsername());
query.setParameter(2, requestDTO.getPassword());
try { // 내부적으로 터지면 터지는 위치를 찾아서 내가 잡으면 됨
User user = (User) query.getSingleResult();
return user;
} catch (Exception e) {
return null;
}
}
public User findByIdAndEmail(int id) {
Query query = em.createNativeQuery("select username, email from user_tb where id=?");
query.setParameter(1, id);
try {
User user = (User) query.getSingleResult();
return user;
} catch (Exception e) {
return null;
}
}
@Transactional
public void userUpdate(UserRequest.UpdateDTO requestDTO, int id){
Query query = em.createNativeQuery("update user_tb set password=? where id = ?");
query.setParameter(1,requestDTO.getPassword() );
query.setParameter(2, id);
query.executeUpdate();
System.out.println("query:" + query);
}
//security가 username만 제공
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;
}
}
}
4. MyLoginUser 클래스 만들기
- User 자체를 implement해서 UserDetails로 맞춰서 같은 타입으로 만들어 담을 수 있음
→ 강제성 부여되어 메서드들을 구현해야함
테이블인데 복잡해짐
- 세션에 저장되는 오브젝트들을 관리하는 클래스
- 클라이언트한테 받은 password와 비교하기 위해 DB에서 조회된 값이 필요함
컴포지션 해야 함 / 생성자 주입
package shop.mtcoding.blog._core.config.config;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import shop.mtcoding.blog.user.User;
import java.util.Collection;
// 세션에 저장되는 오브젝트
@Getter
@RequiredArgsConstructor
public class MyLoginUser implements UserDetails {
private final User user; // 컴포지션 - 결합
@Override
public String getPassword() {
return user.getPassword(); // DB에서 조회된 값을 넣어야함 → 컴포지션 해야 함
}
@Override
public String getUsername() {
return user.getUsername(); // DB에서 조회된 값을 넣어야함
}
@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;
}
}
5. MyLoginService에서 머스태치에서만 sessionUser가져오기
package shop.mtcoding.blog._core.config.security;
import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor;
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*/
@AllArgsConstructor
@Service
public class MyLoginService implements UserDetailsService {
private final UserRepository userRepository;
private final HttpSession session;
@Override
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; // 로그인 진행하던걸 취소하고 알아서 응답해줌 -> 반환할 페이지를 알려줘야함
} else {
System.out.println("user를 찾았어요");
session.setAttribute ("sessionUser", user); // 머스태치에서만 가져오기
return new MyLoginUser(user); // securityContextHolder에 저장됨
}
}
}

- MyLoginUser가 null이라서 오류가 남

6. 머스태치에서 사용가능한 세션 정보
- session안에 Spring Security Context가 있고 안에 Security Context 안에 Authentication이 있고
MyLoginUser(UserDetails 타입) 안에 User가 있음
@AuthenticarionPrincipal로 꺼내 사용하면 됨
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
return (UserDetails) authentication.getPrincipal();
}
public String getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {
// 현재 사용자의 UserDetails를 사용하여 작업 수행
String username = userDetails.getUsername();
- 더 쉽게 하는 방법
내가 loadByUsername에서 만들어질 때
sessionUser를 만들어서 로그아웃 시 invalidate()할 때 둘 다 날리면 됨
- 다른 방법

6. Security의 장점
- 앞에서 주소로 필터링 해줌
- 로그인시 해쉬 비번 자동으로 해줌
- CSRF 인증 보안적인 것을 막아줌
7. 알아야 하는 개념
- 의존성 주입
- session
- login system
- security session의 저장 위치
8. 기존 controller에서 인증 부분 수정하기
- 주소 설계할 때 인증이 필요한 주소는 앞에 Entity이름을 안 붙임
- 실제는 AuthContoller 클래스를 따로 만들어서 인증이 필요한 모든 controller를 넣음
서버를 따로 만들어서 마이크로하게 째서 서버를 여러 개 띄움
- 인증이 필요하면 /api 붙이면 /api/**만 붙이면 됨
package shop.mtcoding.blog.user;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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._core.config.security.MyLoginUser;
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;
@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("/updateForm") // view만 원함
public String updateForm(HttpServletRequest request, @AuthenticationPrincipal MyLoginUser myLoginUser) {
User user = userRepository.findByUsername(myLoginUser.getUsername());
request.setAttribute("user", user);
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:/";
}
}
package shop.mtcoding.blog.board;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContext;
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;
// ?title=제목1&content=내용1
// title=제목1&content=내용1
@PostMapping("/board/{id}/update")
public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO, @AuthenticationPrincipal MyLoginUser myLoginUser) {
// 2. 권한 체크
Board board = boardRepository.findById(id);
if (board.getUserId() != myLoginUser.getUser().getId()) {
return "error/403";
}
// 3. 핵심 로직
// 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) {
// 2. 권한 없으면 나가
// 모델 위임 (id로 board를 조회)
Board board = boardRepository.findById(id);
if (board.getUserId() != myLoginUser.getUser().getId()) {
return "error/403";
}
// 3. 가방에 담기
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) {
// 2. 바디 데이터 확인 및 유효성 검사
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, myLoginUser.getUser().getId());
return "redirect:/";
}
@GetMapping("/")
public String index(HttpServletRequest request) {
List<Board> boardList = boardRepository.findAll();
request.setAttribute("boardList", boardList);
return "index";
}
// /board/saveForm 요청(Get)이 온다
@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";
}
}
Share article