1 비즈니스 레이어 개발
프레젠테이션 레이어(MVC) : 애플리케이션 사용자와의 커뮤니케이션 담당.
비즈니스 레이어(AOP, IoC) : 비즈니스 로직 처리.
[비즈니스 컴포넌트 구조 이해하기]
프로젝트 생성 및 설정
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.studyboot</groupId>
<artifactId>Chapter08</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Chapter08</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
############################################
### Business Layer ###
############################################
# Webapplication Type Setting
spring.main.web-application-type=none
# Banner Setting
spring.main.banner-mode=off
# DatsSource Setting
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
# JPA Setting
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=false
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# JPA 구현체로 사용할 하이버네이트가 생성한 sql을 콘솔에 출력할지 여부
spring.jpa.properties.hibernate.format_sql=true
# Logging Setting
logging.level.org.hibernate=info
비즈니스 컴포넌트 구조
비즈니스 로직을 처리할 비즈니스 컴포넌트는 엔티티, 리포지터리, 서비스 인터페이스, 서비스 구현 클래스로 구성.
① 엔티티 : 테이블과 매핑되는 엔티티 클래스
② 리포지터리 : 엔티티를 이용하여 실질적인 CRUD 처리하는 인터페이스
③ 서비스 인터페이스, ④ 서비스 구현 클래스 : 리포지터리를 통해 데이터베이스 연동을 포함한 비즈니스 로직을 처리.
비즈니스 컴포넌트에서 인터페이스를 작성하지 않고 ServiceImpl 클래스만 사용하는 경우
-
유지보수 과정에서 서비스 구현 클래스를 다른 클래스로 변경하지 않겠다는 것을 의미.
-
인터페이스 미작성 시, 서비스 구현 클래스를 다른 클래스로 변경할 때 마다때마다 비즈니스 컴포넌트를 사용하는 모든 컨트롤러를 수정해야 함
-
컴포넌트를 유연하게 변경하기 위해 반드시 인터페이스를 작성하자.
[비즈니스 컴포넌트 개발하기]
엔티티 클래스 만들기
① Board.class
package com.studyboot.domain;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString(exclude = "member")//순환 참조를 막기 위함
@Entity
public class Board {//게시판 테이블과 매핑할 변수들을 멤버로 선언
@Id
@GeneratedValue
private Long seq;
private String title;
private String content;
//기본값 가지도록 설정.
//JPA가 수정 처리할 때 JPA의 구현체인 하이버네이트가 수정 SQL에 해당칼럼을 포함하지 않도록 설정
@Temporal(TemporalType.TIMESTAMP)
@Column(updatable = false)
private Date createDate = new Date();
@Column(updatable = false)
private Long cnt = 0L;
//다대일 연관관계 매핑
@ManyToOne //양방향 연관관계에서 다(N)에 해당 & 외래키(FK)를 소유한 Board 엔티티가 연관관계의 주인.
@JoinColumn(name = "MEMBER_ID" //MEMBER_ID 칼럼을 통해서 외래키를 관리
,nullable = false //외부조인이 아닌 내부조인으로 처리하기 위해 nullable 속성 사용
,updatable = false)
private Member member;
public void setMember(Member member) {
this.member = member;
member.getBoardList().add(this);
}
}
② Member.class
package com.studyboot.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString(exclude = "boardList")
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String password;
private String name;
@Enumerated(EnumType.STRING) //권한 정보를 문자열로 저장
private Role role;
private boolean enabled; // 사용자 사용여부 제어하는 변수
//일대다 관계 매핑. Member가 일에 해당하므로 컬렉션 타입으로 해당하는 객체를 가지고 있어야 함.
@OneToMany(mappedBy = "member", // member가 연관관계의 주인이 아님을 표시
fetch = FetchType.EAGER) // 즉시 로딩
private List<Board> boardList = new ArrayList<Board>();
}
③ Role
package com.studyboot.domain;
public enum Role {
//회원의 권한 enum 클래스
ROLE_MEMBER, ROLE_ADMIN
}
리포지터리 인터페이스 작성하기
① MemberRepository
package com.studyboot.persistence;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Member;
public interface MemberRepository extends CrudRepository<Member, String>{
}
② BoardRepository
package com.studyboot.persistence;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Board;
public interface BoardRepository extends CrudRepository<Board, Long>{
//글 목록 검색
@Query("SELECT b FROM Board b")
public Page<Board> getBoardList(Pageable pageable); // 페이징 처리를 위해 Pageable 매개변수 사용
}
JPA 테스트하기
① BoardRepositoryTest
@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepo;
@Autowired
private MemberRepository memberRepo;
@Test //입력테스트
public void testInsert() {
Member mem1 = new Member();
mem1.setId("mem1");
mem1.setName("hello");
mem1.setPassword("123");
mem1.setRole(Role.ROLE_MEMBER);
memberRepo.save(mem1); //회원정보를 먼저 영속성 컨텍스트에 저장
Member mem2 = new Member();
mem2.setId("mem2");
mem2.setName("bye");
mem2.setPassword("456");
mem2.setRole(Role.ROLE_ADMIN);
memberRepo.save(mem2);
for (int i=1; i<=13; i++) {
Board b = new Board();
b.setMember(mem1);
b.setTitle("hello가 작성한 글" + i);
b.setContent("내용"+i);
boardRepo.save(b); //게시글 엔티티 저장
}
for (int i=1; i<=3; i++) {
Board b = new Board();
b.setMember(mem2);
b.setTitle("bye가 작성한 글" + i);
b.setContent("내용"+i);
boardRepo.save(b);
}
}
@Test //상세 조회 테스트
public void testGetBoard() {
Board board = boardRepo.findById(1L).get();
System.out.println("[ "+board.getSeq() + "번 게시글 ]");
System.out.println("제목\t :"+ board.getTitle());
System.out.println("작성자\t :"+ board.getMember().getName());
System.out.println("내용\t :" + board.getContent());
System.out.println("등록일\t :"+ board.getCreateDate());
System.out.println("조회수\t :"+ board.getCnt());
}
@Test //목록 조회 테스트
public void testGetBoardList() {
Member member = memberRepo.findById("mem1").get();
System.out.println("[ "+member.getName() + "의 게시글 ]");
for(Board b : member.getBoardList()){
System.out.println(">>>>>" + b.toString());
}
}
}
서비스 인터페이스와 클래스 만들기
① BoardService
클라이언트에 제공할 서비스 인터페이스 작성.
컨트롤러가 비즈니스 컴포넌트를 사용할 때는 반드시 컴포넌트가 제공하는 인터페이스를 사용해야만 한다.
package com.studyboot.service;
import org.springframework.data.domain.Page;
import com.studyboot.domain.Board;
public interface BoardService {
public void insertBoard(Board board);
public void updateBoard(Board board);
public void deleteBoard(Board board);
public Board getBoard(Board board);
public Page<Board> getBoardList(Board board);
}
② BoardServiceImpl 클래스는 BoardRepository를 이용하여 실질적인 데이터베이스 연동을 처리한다.
package com.studyboot.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import com.studyboot.domain.Board;
import com.studyboot.persistence.BoardRepository;
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardRepository boardRepo;
public void insertBoard(Board board) {
boardRepo.save(board);
}
public void updateBoard(Board board) {
Board findBoard = boardRepo.findById(board.getSeq()).get();
findBoard.setTitle(board.getTitle());
findBoard.setContent(board.getContent());
boardRepo.save(findBoard);
}
public void deleteBoard(Board board) {
boardRepo.deleteById(board.getSeq());
}
public Board getBoard(Board board) {
return boardRepo.findById(board.getSeq()).get();
}
public Page<Board> getBoardList(Board board) {
Pageable pageable = PageRequest.of(0, 10, Sort.Direction.DESC, "seq");
return boardRepo.getBoardList(pageable);
}
}
2 프레젠테이션 레이어 개발
[프레젠테이션 개발 준비하기]
프레젠테이션 설정 및 기본 화면 만들기
application.properties
############################################
### Presentation Layer ###
############################################
# Thymeleaf Cache Setting
spring.thymeleaf.cache=false
# Security log level Setting
logging.level.org.springframework.web=debug
logging.level.org.springframwork.security=debug
pom.xml
<dependencies>
<!-- 중간 생략 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-apt -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>src/main/querydsl</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
기본 화면으로 사용할 index 페이지를 생성한다. 시스템에 접근한 사용자에게 가장 먼저 제공하는 메인 페이지 개념.
index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>메인페이지</title>
</head>
<body>
<br>
<h3>게시판 프로그램</h3>
<hr>
<p align="center"><a href="board/getBoardList">글 목록 바로가기</a></p>
<p align="center"><a href="system/login">로그인</a></p>
<br>
<hr>
</body>
</html>
[게시판 기능 구현하기]
컨트롤러 작성
@Controller
@RequestMapping("/board/")
public class BoardController {
@Autowired
private BoardService boardService;
@RequestMapping("/getBoardList")
public String getBoardList(Model model, Search search ) {
if (search.getSearchCondition() == null) {
search.setSearchCondition("TITLE");
}
if (search.getSearchKeyword() == null) {
search.setSearchKeyword("");
}
Page<Board> boardList = boardService.getBoardList(search);
//Page<Board> boardList = boardService.getBoardList(board);
model.addAttribute("boardList", boardList);
return "board/getBoardList";
}
@RequestMapping("/getBoard")
public String getBoard(Model model, Board board) {
model.addAttribute("board", boardService.getBoard(board));
return "board/getBoard";
}
@GetMapping("/insertBoard")
public void insertBoardView(Board board
, @AuthenticationPrincipal SecurityUser principal) {
board.setMember(principal.getMember());
}
@PostMapping("/insertBoard")
public String insertBoard(Board board
, @AuthenticationPrincipal SecurityUser principal) {
board.setMember(principal.getMember());
boardService.insertBoard(board);
return "redirect:getBoardList";
}
@RequestMapping("/updateBoard")
public String updateBoard(Board board) {
boardService.updateBoard(board);
return "forward:getBoardList";
}
@RequestMapping("/deleteBoard")
public String deleteBoard(Board board) {
boardService.deleteBoard(board);
return "forward:getBoardList";
}
}
글 목록 기능
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" 를 루트 엘리먼트에 네임스페이스를 추가하면 HTML에서 스프링 시큐리티를 이용할 수 있다.
getBoardList.html
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<title>리스트 페이지</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body th:align="center">
<h3>게시글 목록</h3>
<!-- 타임리프에서 로그인정보 이용 isAuthenticated() : 인증 성공 여부 체크-->
<span sec:authorize="isAuthenticated()">
<b>
<font color="red">
<span sec:authentication="principal.member.name"></span>
</font>
</b>님 환영합니다...............................................
<a th:href="@{/system/logout}">LOGOUT</a>
<a th:href="@{/admin/adminPage}">게시판 관리</a>
</span>
<!-- 검색기능 -->
<form th:action="@{/board/getBoardList}" method="post" >
<table th:width="700" th:align="center">
<tr>
<td align="right">
<select name="searchCondition">
<option value="TITLE">제목</option>
<option value="CONTENT">내용</option>
</select>
<input name="searchKeyword" type="text"/>
<input type="submit" value="검색">
</td>
</tr>
</table>
</form>
<!-- 리스트 -->
<table th:align="center" border="1" th:cellpadding="0" th:cellspacing="0" th:width="700">
<tr>
<th bgcolor="#e3e3e3" width="100">번호</th>
<th bgcolor="#e3e3e3" width="200">제목</th>
<th bgcolor="#e3e3e3" width="150">작성자</th>
<th bgcolor="#e3e3e3" width="150">등록일</th>
<th bgcolor="#e3e3e3" width="100">조회수</th>
</tr>
<tr th:each="board, state : ${boardList}"><!-- 현재 컬렉션의 상태정보를 저장하는 상태변수 state 선언 후 사용 -->
<td th:text="${state.count}"></td><!-- 1부터 자동으로 1씩 증가하는 값을 출력하는 count -->
<td ><a th:href="@{/board/getBoard(seq=${board.seq})}" th:text="${board.title}"></a></td>
<td th:text="${board.member.name}"></td>
<td th:text="${#dates.format(board.createDate, 'yyyy-MM-dd')}"></td><!-- 날짜 형식 지정 -->
<td th:text="${board.cnt}"></td>
</tr>
</table>
<a th:href="@{/board/insertBoard}">글 등록</a>
</body>
</html>
글 상세/수정/삭제 기능
getBoard.html
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 상세</title>
</head>
<body th:align="center">
<h3>게시글 상세</h3>
<hr>
<form th:action="@{/board/updateBoard}" method="post">
<input name="seq" type="hidden" th:value="${board.seq}">
<table border="1" th:cellpadding="0" th:cellspacing="0" th:align="center">
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="제목"></td>
<td><input type="text" name="title" th:value="${board.title}"/></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="작성자"></td>
<td th:text="${board.member.name}"></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="내용"></td>
<td><textarea name="content" cols="40" rows="10" th:text="${board.content }"></textarea></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">등록일</td>
<td th:text="${board.createDate }"></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">조회수</td>
<td th:text="${board.cnt }"></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="수정하기"/>
</td>
</tr>
</table>
</form>
<hr>
<a th:href="@{/board/insertBoard}">글 등록</a>
<!-- 타임리프 로그인정보 이용, 어드인 권한 사용자만 삭제 링크 사용 -->
<a sec:authorize="hasRole('ROLE_ADMIN')"
th:href="@{/board/deleteBoard(seq=${board.seq })}">글 삭제</a>
<a th:href="@{/board/getBoardList}">리스트</a>
</body>
</html>
글 등록 기능
insertBoard.html
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 작성</title>
</head>
<body th:align="center">
<h3>게시글 등록</h3>
<hr>
<form th:action="@{/board/insertBoard}" method="post">
<table border="1" th:cellpadding="0" th:cellspacing="0" th:align="center">
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="제목"></td>
<td align="left"><input type="text" name="title"/></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="작성자"></td>
<td align="left">
<!-- 로그인한 사용자의 이름 출력 -->
<span sec:authentication="principal.member.name"></span>
</td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="내용"></td>
<td align="left"><textarea name="content" cols="40" rows="10"></textarea></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="등록하기"/>
</td>
</tr>
</table>
</form>
<hr>
<a th:href="@{/board/getBoardList}">리스트</a>
</body>
</html>
[시큐리티 적용하기]
시큐리티 커스터마이징하기
① UserDetail 클래스 생성
package com.studyboot.domain;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
public class SecurityUser extends User{
private static final long serialVersionUID = 1L;
private Member member;//회원 객체를 멤버 변수인 member에 할당. 인증된 회원정보를 html에서 사용하기 위함
public SecurityUser(Member member) {
//JPA에서 검색한 회원정보로 부모 클래스의 변수들을 초기화
super(member.getId(), member.getPassword(), //암호화 진행안하려면 {"noop"}+member.getPassword(),
AuthorityUtils.createAuthorityList(member.getRole().toString()));
this.member = member;
}
public Member getMember() {
return member;
}
}
② UserDetailsService 클래스 - 검색한 회원 정보를 설정
@Service
public class SecurityUserDetailService implements UserDetailsService{
@Autowired
private MemberRepository memberRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optional = memberRepo.findById(username); //사용자 정보 조회
if(!optional.isPresent()) {
throw new UsernameNotFoundException(username+" 사용자 없음");
} else {
Member member = optional.get();
return new SecurityUser(member); //정보 있을 경우 SecurityUser객체로 리턴
}
}
}
③ 시큐리티 설정 클래스 생성
@EnableWebSecurity //이 클래스로 생성된 객체는 시큐리티 설정 파일을 의미. 동시에 시큐리티에 필요한 객체들을 생성
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//WebSecurityConfigurerAdapter 클래스를 빈으로 설정하기만 해도 애플리케이션은 로그인을 강제하지 않음
@Autowired
private SecurityUserDetailService userDetailService;
@Bean //비밀번호 암호화
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override //시큐리티와 관련된 설정시 configure 메소드 사용
protected void configure(HttpSecurity security) throws Exception {
// 애플리케이션 자원에 대한 인증과 인가 제어 가능.
// authorizeRequests 사용자 인증과 권한 설정
// antMatchers AuthorizedUrl 반환
security.authorizeRequests() // 빌더 패턴을 사용
.antMatchers("/","/system/**").permitAll()
.antMatchers("/board/**").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN");
security.csrf().disable();
security.formLogin(); // 사용자에게 form 태그 기반의 로그인 화면 표시
security.formLogin()
.loginPage("/system/login") // 로그인시 사용할 화면 별도 지정.
.defaultSuccessUrl("/board/getBoardList", true); //로그인 성공시 이동할 url 지정
security.exceptionHandling() //ExceptionHandlingConfigurer 객체 리턴
.accessDeniedPage("/system/accessDenied"); // 인증되지 않은 사용자에게 제공할 url 지정
security.logout().logoutUrl("/system/logout")
.invalidateHttpSession(true) // 현재 브라우저와 연관된 세션을 강제종료
.deleteCookies() // 쿠키 삭제
.logoutSuccessUrl("/"); // 로그아웃 후 이동할 화면 리다이렉트
security.userDetailsService(userDetailService);
}
}
시큐리티 화면 개발
① 컨트롤러 작성
@Controller
public class SecurityController {
@GetMapping("/system/login")
public void login() {
}
@GetMapping("/system/accessDenied")
public void accessDenied() {
}
@GetMapping("/system/logout")
public void logout() {
}
@GetMapping("/admin/adminPage")
public void adminPage() {
}
}
② 로그인 페이지
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>시큐리티 테스트</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body th:align="center">
<h3 align="center">로그인</h3>
<form method="post">
<table th:align="center" border="1" th:cellpadding="0" th:cellspacing="0">
<tr>
<td bgcolor="#e4e4e4">아이디</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td bgcolor="#e4e4e4">비밀번호</td>
<td><input type="password" name="password"/></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="로그인"/></td>
</tr>
</table>
</form>
</body>
</html>
③ 접근 권한 없음 페이지
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>접근권한 에러</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h3 align="center">페이지에 대한 접근권한이 없습니다...</h3>
<h3 align="center">관리자로 다시 로그인하세요</h3>
<a th:href="@{/system/login}">다시 로그인 시도하기</a>
</body>
</html>
④ 관리자 페이지
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>admin page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h3 align="center">관리자 페이지 입니다.</h3>
<a th:href="@{/board/getBoardList}">게시판 목록으로 이동</a>
</body>
</html>
로그인 사용자 - 글 등록 기능 구현
@AuthenticationPrincipal가 붙어야 인증정보를 가지고 있는 SecurityUser 객체가 할당된다.
BoardController
@PostMapping("/insertBoard")
public String insertBoard(Board board, @AuthenticationPrincipal SecurityUser principal) {
board.setMember(principal.getMember());
boardService.insertBoard(board);
return "redirect:getBoardList";
}
[기타 기능 추가하기]
검색 기능 추가하기
동적 쿼리를 사용해야 하므로 QueryDSL를 활용한다.
BoardController
@RequestMapping("/getBoardList")
public String getBoardList(Model model, Search search ) {
if (search.getSearchCondition() == null) {
search.setSearchCondition("TITLE");
}
if (search.getSearchKeyword() == null) {
search.setSearchKeyword("");
}
Page<Board> boardList = boardService.getBoardList(search);
model.addAttribute("boardList", boardList);
return "board/getBoardList";
}
BoardRepository
public interface BoardRepository extends CrudRepository<Board, Long>
,QuerydslPredicateExecutor<Board>{
//글 목록 검색
@Query("SELECT b FROM Board b")
public Page<Board> getBoardList(Pageable pageable); // 페이징 처리를 위해 Pageable 매개변수 사용
}
BoardService
public interface BoardService {
public void insertBoard(Board board);
public void updateBoard(Board board);
public void deleteBoard(Board board);
public Board getBoard(Board board);
//public Page<Board> getBoardList(Board board);
public Page<Board> getBoardList(Search search);
}
BoardServiceImpl
public Page<Board> getBoardList(Search search) {
BooleanBuilder builder = new BooleanBuilder();
QBoard qBoard = QBoard.board;
if(search.getSearchCondition().equalsIgnoreCase("TITLE")) {
builder.and(qBoard.title.like("%"+search.getSearchKeyword()+"%"));
} else if(search.getSearchCondition().equalsIgnoreCase("CONTENT")) {
builder.and(qBoard.content.like("%"+search.getSearchKeyword()+"%"));
}
Pageable pageable = PageRequest.of(0, 10, Sort.Direction.DESC, "seq");
return boardRepo.findAll(builder, pageable);
}
1장부터 8장까지 정독한 후 실제 예제 소스를 작성하여 진행해보았다. 스프링 시큐리티나 JPA는 공부해보고 싶었기에 맛보기로 하기에 좋은 책이었다. 스프링 부트에 대한 기본기를 어렵지 않게 잡게 해 준 책에 감사하다! 짬짬이 회사가 끝난 뒤 공부했는데 사이드 프로젝트를 해볼까.. 정보보안기사를 시작할까.. 공부할 게 많구먼
참고서적 : 누구나 끝까지 따라 할 수 있는 스프링 부트 퀵스타트
'Dev > SpringBoot' 카테고리의 다른 글
[SpringBoot] 스프링 부트 시큐리티 (0) | 2020.01.02 |
---|---|
[Springboot] 스프링부트 화면 개발 (0) | 2020.01.01 |
[SpringBoot] 스프링 데이터 JPA (0) | 2019.12.28 |
[SpringBoot] JPA 퀵 스타트 (0) | 2019.12.23 |
[SpringBoot] 테스트와 로깅 (0) | 2019.12.18 |