스프링수업 4일차

한 일

  • 직원현황에서 각 직원이 진행중인 프로젝트 갯수 표기하기
  • 스프링 의존성 주입 DI
  • MySQL로 DB 변경하기
  • 서비스 클래스로 기능 분산하기
  • 로그 만들기
  • 시큐리티 (인증과 허가)
  • csrf란?

gitignore 파일 자동으로 생성하기


직원현황에서 각 직원이 진행중인 프로젝트 갯수 표기하기

h2.console DB에서

select LAST_NAME, FIRST_NAME, count(PROJECT_ID) as count
from EMPLOYEE e left join PROJECT_EMPLOYEE pe
on e.EMPLOYEE_ID = pe.EMPLOYEE_ID
group by LAST_NAME, FIRST_NAME, e.EMPLOYEE_ID
order by count DESC;

프로젝트에 하나도 참가하지 않은 직원도 보여주기 위해 leftjoin 사용.

- EmployeeRepository.interface -

public interface EmployeeRepository extends CrudRepository<Employee, Long>{
	// 자동으로 CRUD 객체 생성
	@Override
	List<Employee> findAll();
	
	// 쿼리문을 실행하여 결과를 리스트로 리턴함 (dto의 객체 EmployeeProject로 리턴)
	@Query(nativeQuery = true, value = 
			  "SELECT LAST_NAME AS lastName ,FIRST_NAME AS firstName , COUNT(PROJECT_ID) AS count "
			+ "FROM EMPLOYEE e "
			+ "LEFT JOIN PROJECT_EMPLOYEE pe "
			+ "ON e.EMPLOYEE_ID = pe.EMPLOYEE_ID "
			+ "GROUP BY LAST_NAME ,FIRST_NAME, e.EMPLOYEE_ID "
			+ "ORDER BY count DESC")
	public List<EmployeeProject> employeeProjects();
}


sql문의 결과를 받아 올 새 인터페이스 생성

- EmployeeProject.interface -

public interface EmployeeProject {
	// 스프링이 자동으로 생성할 수 있게 get메서드만 작성함
	// set메서드는 필요없음. DB에서 쿼리 결과를 가져오기만 하면 됨.
	public String getLastName();		// lastName
	public String getFirstName();		// firstName
	public String getCount();			// count
}

- HomeController -
수정

@Controller
public class HomeController {
	
	@Autowired		// db에서 project테이블을 가져오기 위함
	ProjectRepository projectRepository;
	
	@Autowired
	EmployeeRepository employeeRepository;
	
	@GetMapping("/")
	public String displayHome(Model model) {
		List<Project> projectList = projectRepository.findAll();
		List<EmployeeProject> empProList = employeeRepository.employeeProjects();
		model.addAttribute("projectList", projectList);
		model.addAttribute("empProList", empProList);
		
		return "main/home";
	}
	@GetMapping("/list")
	public String displayemployee(Employee employee) {
		return "employees/employeeList";
	}	
}

결과를 empProList형태로 home.html로 전달.

- home.html -
직원에 대한 정보를 받는 부분만 수정.
결과를 줄때 empProList로 줬으므로 받을때도 empProList로 받음.

<div class="container">
  <h3>직원 현황</h3>
  <table class="table table-hover">
    <thead class="table-dark">
      <tr>
        <th></th>
        <th>이름</th>
        <th>참여중인 프로젝트 수</th>
      </tr>
    </thead>
    <tbody>
      <tr th:each="emp : ${empProList}">
        <td th:text="${emp.lastName}"></td>
        <td th:text="${emp.firstName}"></td>
        <td th:text="${emp.count}"></td>
      </tr>
    </tbody>
  </table>
</div>

emp : ${empProList}
=> empProList로 받은 것을 차례로 emp에 넣어 반복함.

- style.css -

table tr td,
table tr th {
	text-align: center;
}
table {
	margin-top: 10px;
}


변경이 완료된 결과.


스프링 의존성 주입 DI

참고

객체를 직접 생성하여 사용하지 않고, 의존을 주입받아 사용하는 방법.

의존성 주입 과정
1. 어노테이션을 통해 스프링 객체를 선언
스프링이 관리하는 객체 어노테이션
@Bean @Component @Service @Repository
2. 의존성 객체 주입: autoWired 어노테이션을 통해 스프링이 알아서 객체를 관리하도록 함
3. 의존성 객체 메서드를 호출하여 사용

의존성을 주입함으로써 Bean들은 싱글톤 패턴의 특징을 가진다.

싱글톤 패턴이란?
객체를 한 번만 생성한 후 메서드를 호출하여 사용함으로써, 메모리의 불필요한 낭비를 막는 디자인패턴.
싱글톤 패턴에 대한 참고


MySQL로 DB 변경하기

application복사 붙여넣기로 application-test.properties 생성.

application은 MySQL로 변경, application-test은 테스트용 h2 DB로 사용예정.



pom.xml에서 MySQL 추가

- application.properties -

# MySQL DB 설정
spring.datasource.url=jdbc:mysql://localhost:3306/pma?useSSL=false&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=1234

# jpa에서 sql을 사용할때마다 log로 콘솔에 출력
spring.jpa.show-sql=true

# 초기 테스트용 (자동으로 table생성 및 수정/삭제)
spring.jpa.hibernate.ddl-auto=update

# data.sql 파일이 있을 시 실행
spring.jpa.defer-datasource-initialization=true

MySQL workbench에서 pma스키마 생성.

use pma;
DROP TABLE IF EXISTS employee;

CREATE TABLE IF NOT EXISTS employee (
	employee_id BIGINT AUTO_INCREMENT PRIMARY KEY ,
	email VARCHAR(100) NOT NULL,
	first_name VARCHAR(100) NOT NULL,
	last_name VARCHAR(100) NOT NULL
);

DROP TABLE IF EXISTS project;
CREATE TABLE IF NOT EXISTS project (
	project_id BIGINT AUTO_INCREMENT PRIMARY KEY,
	name VARCHAR(100) NOT NULL,
	stage VARCHAR(100) NOT NULL,
	description VARCHAR(500) NOT NULL
);

DROP TABLE IF EXISTS project_employee;
CREATE TABLE IF NOT EXISTS project_employee (
	project_id BIGINT REFERENCES project(project_id), 
	employee_id BIGINT REFERENCES employee(employee_id)
);

- application.properties -

# MySQL DB 설정
spring.datasource.url=jdbc:mysql://localhost:3306/pma?useSSL=false&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=1234

# jpa에서 sql을 사용할때마다 log로 콘솔에 출력
spring.jpa.show-sql=true

# 초기 테스트용 (자동으로 table생성 및 수정/삭제)	=> 실제 사용 시 none
spring.sql.init.mode=never
spring.jpa.hibernate.ddl-auto=none

쿼리문 수정.
제대로 반영이 안될 시 pom.xml > maven > Update Project > Force Update of Snapshots/Releases 체크 후 OK 하여 업데이트 후 다시 확인.




데이터 입력 후 DB에 입력되었는지 확인.
MySQL DB에 연동을 마치면 서버를 재시작해도 데이터가 그대로 유지됨.


서비스 클래스로 기능 분산하기

새 클래스 생성

생성 후 복붙하여 같은 위치에 ProjectService도 생성.

- EmployeeController -

@Controller
@RequestMapping("/employees")
public class EmployeeController {
	
    @Autowired
	private EmployeeService employeeService;
	
	@GetMapping("/")
	public String displayEmployeeForm(Model model) {
		List<Employee> employeeList = employeeService.findAll();
		model.addAttribute("employeeList", employeeList);
		return "employees/employeeList";
	}
	
	@GetMapping("/new")
	public String newEmployeeForm(Model model) {
		Employee e = new Employee(); 
		model.addAttribute("employee", e);
		return "employees/new-employee";
	}
	
	@PostMapping("/save")
	public String createEmployee(Employee employee) {
		employeeService.save(employee);
		return "redirect:/employees/";
	}
}

=> EmployeeService에서 findAll, save메서드를 처리하도록 변경.

- EmployeeService -

@Service
public class EmployeeService {

	@Autowired
	private EmployeeRepository employeeRepository;
	
	public List<Employee> findAll() {
		return employeeRepository.findAll();
	}
	
	public void save(Employee employee) {
		employeeRepository.save(employee);
	}
}

- ProjectController -

@Controller
@RequestMapping("/projects")
public class ProjectController {
	
	// 스피링에서 repository 객체를 처음에 자동생성하여 가지고 있다가 Autowired를 만나면 관련 객체가 필요할때 자동으로 연결해줌  
	@Autowired
	private ProjectService projectService;
	
	@Autowired
	private EmployeeService employeeService;
	
	@GetMapping("/")
	public String displayProjectForm(Model model) {
		List<Project> projectList = projectService.findAll();
		model.addAttribute("projectList", projectList);
		return "projects/projectList";
	}
	
	@GetMapping("/new")
	public String newProjectForm(Model model) {
		Project p = new Project();
		model.addAttribute("project", p);
		List<Employee> empList = employeeService.findAll();
		model.addAttribute("empList", empList);
		return "projects/new-project";
	}
	
	@PostMapping("/save")
	public String createProject(Project project) {
		projectService.save(project);	// project객체를 DB의 테이블에 저장
		return "redirect:/projects/";	// post-redirect-get 패턴(/new > /save > /new)
	}
}

- ProjectService -

@Service
public class ProjectService {

	@Autowired
	private ProjectRepository projectRepository;
	
	public List<Project> findAll() {
		return projectRepository.findAll();
	}
	
	public void save(Project project) {
		projectRepository.save(project);
	}
}

employeeController처럼 projectController도 projectService가 기능을 가지도록 변경.

- HomeConroller -
마찬가지로 service가 기능을 가지도록 변경함.

@Controller
public class HomeController {
	
	@Autowired		// db에서 project테이블을 가져오기 위함
	ProjectService projectService;
	
	@Autowired
	EmployeeService employeeService;
	
	@GetMapping("/")
	public String displayHome(Model model) {
		List<Project> projectList = projectService.findAll();
		List<EmployeeProject> empProList = employeeService.employeeProjects();
		
		model.addAttribute("projectList", projectList);
		model.addAttribute("empProList", empProList);
		return "main/home";
	}		
}

- EmployeeService -
HomeConroller에서 사용할 메서드 추가

public List<EmployeeProject> employeeProjects() {
	return employeeRepository.employeeProjects();
}

로그 만들기

- application.properties -

# MySQL DB 설정
spring.datasource.url=jdbc:mysql://localhost:3306/pma?useSSL=false&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=1234

# jpa에서 sql을 사용할때마다 log로 콘솔에 출력
spring.jpa.show-sql=true

# 초기 테스트용 (자동으로 table생성 및 수정/삭제)	=> 실제 사용 시 none
spring.sql.init.mode=never
spring.jpa.hibernate.ddl-auto=none

# 로그 레벨 설정
logging.level.root=warn
logging.level.com.myapp.pma=debug
logging.level.org.springframework=info

# 로그 파일 설정 app.log 파일에 로그내용 저장
logging.file.name=app.log

root에 대한 로그 레벨을 warnning으로 설정하여 치명적인 오류 외에는 안보이도록 처리.
com.myapp.pma 패키지 이상은 debug레벨로 설정하여 로그를 자세하게 볼 수 있도록 설정.
springframework 에 대한 로그는 info레벨로 설정하여 초기값과 동일하게 유지.

로그 내용을 계속 모니터링할 수 없으므로 app.log파일에 로그내용을 저장하도록 설정함.


로그 저장위치는 위의 사진을 참고.


시큐리티

초보가 이해하는 스프링 시큐리티

인증 기능

로그인 기능 더하기

pom.xml에 spring

이제까지와 마찬가지로 pom.xml을 선택 후 finish를 눌러 다운로드가 완료되길 기다림.


실행해보면 위 사진처럼 로그인 화면이뜨고 id는 user, pw는 로그 중간에 뜬 것을 복사하여 입력하면 앞서 만들었던 메인화면이 나타남.


새 클래스 생성

- securityConfiguration -

// 시큐리티 설정을 위해서 WebSecurityConfigurerAdapter를 상속받음
@EnableWebSecurity
public class securityConfiguration extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 메모리에 유저, 비밀번호, roles(허용범위 = 유저, 관리자)을 저장함
		auth.inMemoryAuthentication()
			.withUser("0829kuj").password("1234").roles("user");
	}
}

유저 인증 메서드를 오버라이드하여 설정해줌.
허가는 roles로 설정함.

spring의 시큐리티에는 허가(Roles)와 인증(Authentication)이 있으며,
허가는 유저와 관리자의 기능 범위를 설정하는 역할, 인증은 허용된 id와 password로만 로그인 할 수 있도록 하는 것.

여기까지 하고 다시 확인.
로그아웃을 원할경우 http://localhost:8080/login 주소로 이동하여 다시 로그인부터 하면 됨.
혹은 http://localhost:8080/logout 로 접근하여 로그아웃을 진행.

지정한 id와 password가 적용이 안된다면 securityConfiguration에 임시테스트코드를 추가하여 확인한다.

- securityConfiguration -

@EnableWebSecurity
@Configuration
public class securityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 메모리에 유저, 비밀번호, roles(허용범위 = 유저, 관리자)을 저장함
		auth.inMemoryAuthentication()
			.withUser("0829kuj").password(getPasswordEncoder().encode("1234")).roles("user");
	}
	@Bean		// 패스워드 암호화용 객체
	public PasswordEncoder getPasswordEncoder() {
		return NoOpPasswordEncoder.getInstance();	// 테스트용 비밀번호 암호화x
	}
}

테스트용으로 강제로 암호화를 한 것이므로 정식적인 코드는 아님.
이제 다시 실행해보면 설정한 id와 password를 사용하여 메모리 인증이 가능하다.

허가 기능

인증기능을 넣었으므로 다음은 관리자용 사용자를 추가 후 허가기능 넣기.

- securityConfiguration -

// 시큐리티 설정을 위해서는 1) WebSecurityConfigurerAdapter를 상속받아야하며 2) 어노테이션 EnableWebSecurity가 필요  
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	// 3) 인증	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 메모리에 유저, 비밀번호, roles(허용범위 = 유저, 관리자)을 저장함
		auth.inMemoryAuthentication()
			.withUser("0829kuj").password( getPasswordEncoder().encode("1234") ).roles("USER")
			.and()
			.withUser("admin").password( getPasswordEncoder().encode("pass") ).roles("ADMIN");
	}
	
	@Bean   //패스워드 암호화 객체
	public PasswordEncoder getPasswordEncoder() {
		return NoOpPasswordEncoder.getInstance(); //테스트용이라 암호화가 안되어 그대로 보임
	}		
	// 4) 허가
	@Override
	protected void configure(HttpSecurity http) throws Exception {	
		http.authorizeHttpRequests()
			.antMatchers("/projects/new").hasRole("ADMIN")		// 관리자의 허용범위 지정(새 프로젝트, 새 직원 추가 가능)
			.antMatchers("/projects/save").hasRole("ADMIN")
			.antMatchers("/employees/new").hasRole("ADMIN") 
			.antMatchers("/employees/save").hasRole("ADMIN")
			.antMatchers("/employees").authenticated()			// 인증된 유저(로그인 한 모든 유저)에게만 허용
			.antMatchers("/projects").authenticated()
			.antMatchers("/").permitAll()						// 그 외 페이지는 인증과 관계없이 모두에게 열람허용
			.and().formLogin()
			.and().exceptionHandling().accessDeniedPage("/"); //예외 발생시 기본페이지로
		// 시큐리티에서는 기본적으로 csrf방지가 적용됨
//		http.csrf().disable();	
		// csrf룰(사용자가 의도치않은 요청)에 걸리는 부분에 대해 막아줌(/save실행시 redirect로 요청을 보내게 되어있기때문) 												
	}
}

.authenticated(): 인증을 마친 사용자에게 허용
.permitAll(): 인증과 관계없이 모든 사용자에게 허용

시큐리티에서는 기본적으로 csrf방지가 적용된다.

new-employee.html, new-project.html 수정.
crsf로 인한 오류를 해결하기 위해 추가.

<!-- 타임리프의 th:action 주소는 csrf 토큰을 내장함 -->
<form action="@{/employees/save}" method="post" th:object="${employee}">

<form action="@{/projects/save}" method="post" th:object="${project}">


일반계정(위의 0829kuj)으로 로그인 시 허가되지 않은 기능인 '직원추가', '프로젝트 추가'에 접근 시 위의 사진대로 화면이 나온다.

관리자 권한의 계정인 admin으로 접속 시 모든 동작에 대한 권한이 있으므로 정상작동함.


csrf란?

사용자가 예상하지않은 경로로 중복해서 요청을 넣는 경우를 보안상 차단해주는 기능.
spring에서는 csrf token을 이용해 이를 방지한다.
spring에서 thymeleaf문법을 이용해 뷰를 작성하는것을 권장하는이유중 하나로 생각됨.

csrf에 대한 조취없이 코드를 작성 시 에러가 발생하는데, 이를 방지하려면
1) 뷰에서 thymeleaf문법을 이용해 form태그의 action속성을 작성할때 th:action="@{/예시/내용}" 처럼 th:를 사용하면 hidden 타입으로 csrf 토큰을 만들어준 것과 동일하게 동작한다.
=> aciton속성 대신 th:action을 사용 시 hidden타입으로 토큰을 만들어 준 코드와 동일하다는 의미임.

csrf에 대한 참고
csrf token에 대한 참고

2) 해당 문제가 발생하는 지점에서 http.csrf().disable() 코드로 중복된 요청을 방지하는 방법.
보통 1번 방법을 사용함.

좋은 웹페이지 즐겨찾기