728x90

Entity 클래스에 @Id, @Size, @NotBlank 등을 붙여주고 Controller 클래스에서 @Valid 어노테이션을 추가적으로 사용함으로써 기본적인 검증은 가능하다.

 

1. 기본 form 입력값 검증하기

@Data
@Entity
@DynamicUpdate
@DynamicInsert
@Table(name = "tb_userinfo")
@IdClass(MemberID.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Id
    @NotNull
    @Pattern(regexp = "^[a-z0-9]*$", message = "username은 영문(소문자)과 숫자만 가능합니다.")
    @Size(min = 5, max = 20, message = "username은 5자 이상 20자 이하입니다.")
    private String username;

    @NotNull
    @Size(min = 6, max = 100, message = "암호는 6자 이상 100자 이하 입니다.")
    private String password;

    @NotNull
    @Size(min = 6, max = 45, message = "올바른 이메일을 입력하세요.")
    @Email(message = "이메일 형식을 맞춰주세요.")
    private String email;

    @Column
    private String role;
}
  • 각 필드별로 제약조건 어노테이션을 추가해준다.
@GetMapping("/joinForm")
public String joinForm(Model model) {
    model.addAttribute("member", new Member());
    return "/account/joinForm";
}

@PostMapping("/join")
public String join(@Valid Member member, BindingResult bindingResult){ // view의 form->input 의 name과 매핑됨.
    if(bindingResult.hasErrors()) {
        return "/account/joinForm";
    }

    String encPwd = memberService.pwdEncoding(member.getPassword());
    member.setPassword(encPwd);

    memberService.join(member);

    return "redirect:/loginForm";
}
  • GetMapping으로 처음 회원가입 화면으로 진입할 때 Member 객체를 만들어서 member라는 키값으로 전달해준다.
  • PostMapping에서는 View에서 데이터를 담아 전달해준 member 객체에 @Valid 어노테이션을 달아준다.
  • BindingResult.hasErrors()를 통해 기본적인 에러 유무를 알아 낼 수 있고, 다시 회원가입 화면으로 돌려보낸다.
<form th:action="@{/join}" method="post" th:object="${member}">          
          <div class="form-floating mt-4">
            <input type="text" class="form-control" th:field="*{username}" th:classappend="${#fields.hasErrors('username')} ? 'is-invalid'" id="floatingInput" placeholder="Username">
            <label for="floatingInput">Username</label>
            <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}" id="validationServer03Feedback" class="invalid-feedback text-start">
              Username Error
            </div>
          </div>
          <div class="form-floating">
            <input type="password" class="form-control mt-2" th:field="*{password}" th:classappend="${#fields.hasErrors('password')} ? 'is-invalid'" id="floatingPassword" placeholder="Password">
            <label for="floatingPassword">Password</label>
            <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-bottom: 8px;">
              Password Error
            </div>
          </div>
          <div class="form-floating">
            <input type="email" class="form-control" th:field="*{email}" th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'" id="floatingPassword" placeholder="Email">
            <label for="floatingPassword">example@board.com</label>
            <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-top: 8px;">
              Email Error
            </div>
          </div>
      
          <div class="checkbox mb-3">
          </div>
          <button class="w-100 btn btn-lg btn-primary mt-2" type="submit">Get started!</button>
          <a type="button" class="w-100 btn btn-lg btn-primary mt-2" th:href="@{/}">exit</a>
        </form>
  • 타임리프의 th:if가 달린 3개의 div 태그에서 에러를 출력해준다.
  • th:if=${#fields.hasErrors('필드명')} : 에러가 발생한 경우 true 값을 반환하여 div태그를 표시해준다.
  • form 태그의 th:object="{member}" : GetMapping 때 전달받은 Member의 키 값을 넣어 전달 받은 객체
  • "*{username}" :  위 설명을 참고해서 Member.getUsername()을 View에서 사용할 수 있도록 한 값이다. -> ${member.username} 과 같다고 생각!
  • th:errors="*{password}" : password에 대한 에러 내용을 표시해준다.
728x90

문제 : 게시글 수정 작업 중 org.springframework.beans.NotReadablePropertyException 오류가 발생했다.

 

해결 : Thymeleaf에서 컨트롤러로 보내주는 파라미터 설정을 잘못하여 이를 고쳐줬다.

@Entity
@Data
@Table(name = "tb_board")
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Size(min = 2, max = 30, message = "제목은 2자 이상 30자 이하입니다.")
    private String title;

    @NotNull
    @Size(min = 1, message = "내용을 입력하세요.")
    private String content;

    @ManyToOne(targetEntity = Member.class, fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "writer", referencedColumnName = "username"),
            @JoinColumn(name = "writer_id", referencedColumnName = "id")
    })
    private Member member;
    private String image;
}
<form action="#" th:action="@{/board/form}" th:object="${board}" method="post">
	<input type="hidden" th:field="*{id}">
	<input type="hidden" th:field="*{member.id}">
	<input type="hidden" th:field="*{member.username}">
                .
                .
                .
	<div class="nav justify-content-end">
		<button type="submit" class="me-2 btn btn-primary">write</button>
	</div>
</form>
  • 타임리프에서 th:field="writer_id" 라고 작성해서 파라미터를 보내줬었는데, 참조하는 Entity의 필드명인 id를 적어주고 해결하였다.

 

'Web > Tymeleaf' 카테고리의 다른 글

[타임리프] Thymeleaf 정리  (0) 2021.12.18
728x90

문제점 : 게시판 프로젝트를 진행 중에 로그인과 게시판 기능을 일부 완성하고 연결하려하니 JPA 관련 오류가 발생했다.

  • 이유는 Entity객체에 JPA 사용을 위한 복합키 처리와 조인 처리를 안했던 것이었다.

해결 : 데이터베이스 설계를 다음 링크와 같이 바꾸었다. https://black-mint.tistory.com/21

 

[DataBase] MySQL 복합키 설계

문제점 : USERINFO의 PK(기본키)를 참조해서 BOARD에 FK를 만들려고하는데 계속 오류가 났다. 오류의 이유는 기존 USERINFO테이블의 PK를 id컬럼에만 적용하고는 BOARD 테이블에서 USERINFO테이블의 username컬

black-mint.tistory.com

그 후 기존 Entity클래스(Member, Board)에 @IdClass 어노테이션을 적용하기 위해 [Member, MemberID, Board] 3개의 클래스로 만들었다.

@Data
@Entity
@DynamicUpdate
@DynamicInsert
@Table(name = "tb_userinfo")
@IdClass(MemberID.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Id
    @NotNull(message = "username을 입력하세요.")
    @Size(min = 1, max = 20, message = "username은 1자 이상 20자 이하입니다.")
    private String username;

    @NotNull(message = "암호를 입력하세요.")
    @Size(min = 6, max = 100, message = "암호는 6자 이상 100자 이하 입니다.")
    private String password;

    @NotNull
    @Size(min = 6, max = 45, message = "이메일은 6자 이상 45자 이하 입니다.")
    @Email
    private String email;

    @Column
    private String role;
}
  • @Data : lombok을 설치해야 사용 가능. Getter/Setter 등 자동 생성
  • @IdClass(식별자 클래스) : 식별자 클래스 지정 (복합키를 넣을 클래스)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberID implements Serializable {

    @Column(name = "id")
    private long id;

    @Column(name = "username")
    private String username;
}
  • 식별자 클래스는 public 이어야 하며, Serializable 인터페이스를 구현해야 한다. 또한, 기본 생성자 필수!
  • Entity객체와 필드명 같게 작성
  • @Data : 식별자 클래스는 equals(), hashCode()를 필수적으로 작성해줘야 한다. 이를 자동 생성
  • @AllArgsConstructor : 모든 필드 값을 파라미터로 받는 생성자를 생성
  • @NoArgsConstructor : 파라미터가 없는 생성자 생성
@Entity
@Data
@Table(name = "tb_board")
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Size(min = 2, max = 30, message = "제목은 2자 이상 30자 이하입니다.")
    private String title;

    @NotNull
    @Size(min = 1, message = "내용을 입력하세요.")
    private String content;

    @ManyToOne(targetEntity = Member.class, fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "writer", referencedColumnName = "username"),
            @JoinColumn(name = "writer_id", referencedColumnName = "id")
    })
    private Member member;
    private String image;
}
  • 게시판 - 사람 이기때문에 ManyToOne으로 구현. (여러 관계가 있으나, 주인 쪽이 FK를 관리하도록 설계하는 것이 좋다고 한다.)
  • @ManyToOne : 게시판 - 사람 관계이므로 @ManyToOne을 사용
  • @JoinColumns : 복합키 일 때 사용
  • @JoinColumn : 그림과 같이 참조하는 필드명과 참조받는 필드명을 작성
728x90

문제점 : USERINFO의 PK(기본키)를 참조해서 BOARD에 FK를 만들려고하는데 지속적으로 오류가 발생했다.

  • 오류의 이유는 기존 USERINFO테이블의 PK를 id컬럼에만 적용하고는 BOARD 테이블에서 USERINFO테이블의 username컬럼을 참조하려고해서 그랬다.

해결 : USERINFO테이블의 기본키를 id와 username 두 개를 설정하여 해결했다. (id를 해제하려했으나 Auto Increment는 PK가 적용된 컬럼만 된다고 한다.)

 

* 복합키 : 이렇게 2개의 컬럼을 PK로 지정하면 이를 복합키라고 한다.

 

수정 후 테이블 상태 (BOARD 테이블의 writer_id와 writer는 각각 USERINFO의 id와 username 컬럼을 참조한다.)

 

USERINFO 테이블

BOARD 테이블

728x90

문제점 : 게시판 제목을 클릭하면 해당 글로 이동하도록 했는데 하이퍼 링크가 너무 이쁘지 않아 밑줄을 제거하고 검은색으로 바꾸고자 한다.

 

 

<td><a th:text="${board.title}" th:href="@{/board/form(id=${board.id})}" style="text-decoration:none; color:black;">제목</a></td>

style부분만 참고!

밑줄 제거 : style="text-decoration:none;"

글자색 적용 : style="color:black;"

'Web > CSS' 카테고리의 다른 글

[CSS] div 태그 요소 가운데 정렬  (0) 2022.01.13
Bootstrap 시작하기  (0) 2021.08.07
728x90

스프링부트에서 타임리프(Thymeleaf)로 프로젝트를 진행 중 2번 이상 다시 찾아본 개념에 대해 정리해놓으려 합니다.

기본 타임리프 문서 : https://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf.html#standard-htmlxml-comments

 

Tutorial: Using Thymeleaf

1 Introducing Thymeleaf 1.1 What is Thymeleaf? Thymeleaf is a Java library. It is an XML/XHTML/HTML5 template engine able to apply a set of transformations to template files in order to display data and/or text produced by your applications. It is better s

www.thymeleaf.org

수시로 업데이트!


1. 해당 조건이 만족할 때 컨텐츠 나타내기 (조건문 th:if)

<button class="btn btn-outline-secondary" th:if="${board.member.username == #httpServletRequest.remoteUser}" type="submit">삭제</button>
  • th:if문이 true로 나오면 컨텐츠 표시, 아니면 숨김
  • #httpServletRequest.remoteUser -> 현재 로그인한 유저 아이디
                <div id="uploadForm" th:if="${param.boardId == null}">
                    <div id="uploadElement">
                        <input id="uploadInput" type="file" class="btn btn-outline-primary" name="files" accept="image/*"
                            onchange="setThumbnail(event);" multiple /> <span style="font-size: small;"> * jpeg / png 타입의
                            이미지를
                            7개까지
                            등록해주세요.</span>
                    </div>
                    <a id="reset" class="mt-3 btn btn-danger" onclick="resetImg()">Reset</a>
                </div>
  • th:if문과 param(boardId가 url에 없을 때 div 태그가 보임)을 이용해서 컨텐츠를 보이고 안보이게 한다.
<div th:if="${menu} == 'home' or ${menu} == 'addAdmin'"></div>
  • 자바에서와 같이 || 나 && 사용 불가능
  • 대신 or 또는 and 키워드를 사용

2. 컨트롤러로 값 전달

<form th:if="${board.member.username == #httpServletRequest.remoteUser}" th:action="@{/board/delete}"
                    method="post">
    <input type="hidden" th:name="boardId" th:value="${board.id}">
    <button class="btn btn-outline-secondary" type="submit">delete</button>
</form>

 

    @PostMapping("/delete")
    public String boardDelete(Long boardId) {
        boardService.delete(boardId);

        return "redirect:/board/list";
    }
  • form 태그 안에 input 태그를 생성
  • input태그의 th:name으로 컨트롤러에 매핑할 변수값을 지정
  • 버튼 클릭 -> input태그의 th:value를 이용해서 값 제출

2-1. 체크박스 값 서버로 전달

            <form class="row g-3 justify-content-end" method="post" th:action="@{/board/myPost/delete}">
                <table class="table caption-top table-bordered table-hover">
                    <tbody>
                        <tr th:each="board : ${boardList}">
                            <td class="mt-5 text-center" scope="row">
                                <div class="checkbox">
                                    <input type="checkbox" name="boardIdList" th:value="${board.id}">
                                </div>
                            </td>
                            <td class="mt-5 text-center" scope="row" th:text="${board.id}">1</td>
                            <td><a th:text="${board.title}" th:href="@{/board/post(id=${board.id})}"
                                    style="text-decoration:none; color:black;">제목</a></td>
                            <td class="text-center" th:text="${#dates.format(board.createDate, 'yyyy/MM/dd HH:mm')}">작성일
                            </td>
                            <td class="text-center" th:text="${board.writer}">작성자</td>
                        </tr>
                    </tbody>
                </table>
		<button type="submit" class="btn btn-danger">Delete</button>
	</form>

 

    @PostMapping("/myPost/delete")
    public String boardDelete(@RequestParam(required = false) List<String> boardIdList) {
        if(boardIdList == null) return "redirect:/board/myPost";
        if(boardIdList.size() > 0) {
            for(int i = 0; i < boardIdList.size(); i ++) {
                boardService.temporaryDelete(Long.parseLong(boardIdList.get(i)));
            }
        }

        return "redirect:/board/myPost";
    }
  • form태그 안에 다음과 같이 체크 박스 name과 value를 지정. (th:each로 인해 게시글 개수만큼 체크박스 생성)
  • 체크박스에 체크하고 버튼으로 form을 제출하면 컨트롤러에 List 형태로 전달됨.

3. th:fragment 2개 이상 인자 전달

프로젝트 구조

<!-- Page -->
<nav th:replace="fragments/pagingCommon :: pagination(${pagination}, 'trash')"></nav>
  • 해당 코드는 board/trash 안에 작성된 페이징 관려 nav태그
  • th:replace="경로 :: 프래그먼트 명(${컨트롤러로부터 받은 객체 또는 모델명})
  • th:replace="경로 :: 프래그먼트 명('넘길 문자')"
  • trash는 4번에서 설명

컨트롤러에서 넘긴 객체

<body>
	<nav aria-label="Page navigation example" th:fragment="pagination(pagination, menu)">
		<ul class="pagination justify-content-center">
			<th:block th:with="start = ${pagination.startPage}, end = ${pagination.endPage}">
				<li class="page-item" th:classappend="${pagination.prev == false} ? 'disabled'">
					<a class="page-link" th:href="@{'/board/' + ${menu}(page=${start - 1}, range=${pagination.range - 1}, 
					searchText=${param.searchText})}">Previous</a>
				</li>

				<li class="page-item" th:classappend="${i == pagination.page} ? 'disabled'" th:if="${i != 0}"
					th:each="i : ${#numbers.sequence(start, end)}"><a class="page-link" href="#" th:href="@{'/board/' + ${menu}(page=${i}, 
					range=${pagination.range}, searchText=${param.searchText})}" th:text="${i}">1</a></li>

				<li class="page-item" th:classappend="${pagination.next == false} ? 'disabled'">
					<a class="page-link" th:href="@{'/board/'+ ${menu}(page=${end + 1}, range=${pagination.range + 1}, 
					searchText=${param.searchText})}" href="#">Next</a>
				</li>
			</th:block>
		</ul>
	</nav>
</body>

<페이징 관련 fragment 소스>

  • th:replace로 호출한 프레그먼트가 있는 html 파일에서 th:fragment="프리그먼트명(받은 값들)" 로 보내준다.

4. th:href 안에 변수 넣어 사용하기 (링크 컨트롤)

  • 페이징 소스를 사용하는 화면 곳곳마다 이동하는 링크는 다를테니 'list', 'myPost', 'trash' 같은 값을 replace할 때 인자로 보내서 th:href를 처리할 것이다.
  • 해당 처리를 하지 않은 코드는 th:href="@{/board/trash/(page=${start ...} ... )}" 
  • 처리 후 코드 : th:href="@{'/board/' + ${menu}(page=${start ...} ... )}"

5. 시큐리티 관련 메소드

사용을 위해 의존성 추가 (gradle)

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

 

<a th:href="@{/loginForm}" class="btn btn-secondary fw-bolder me-4" sec:authorize="!isAuthenticated()">Login</a>
<form class="d-flex me-4" th:action="@{/logout}" sec:authorize="isAuthenticated()">
	<span class="text-white me-4 mt-2 fw-bolder" sec:authentication="name">사용자</span>
    <a th:href="@{/admin}" class="btn btn-danger" style="font-weight: bold" sec:authorize="hasRole('ROLE_ADMIN')">관리자 화면</a>
	<button th:href="@{/logout}" class="btn btn-secondary fw-bolder">Logout</button>
</form>
  • sec:authorize="isAuthenticated()  -> 로그인한 사용자에게 보여줌
  • sec:authentication="name"  -> 로그인한 사용자 이름을 표시
  • sec:authorize="hasRole('ROLE_ADMIN')" -> 권한이 ROLE_ADMIN인 사용자에게만 보여줌

참고, Authentication : 로그인, Authroization : 권한

6. Validator 관련 내용 (에러 내용 표시) + th:object, th:field, th:errors

<form th:action="@{/join}" method="post" th:object="${member}">          
          <div class="form-floating mt-4">
            <input type="text" class="form-control" th:field="*{username}" th:classappend="${#fields.hasErrors('username')} ? 'is-invalid'" id="floatingInput" placeholder="Username">
            <label for="floatingInput">Username</label>
            <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}" id="validationServer03Feedback" class="invalid-feedback text-start">
              Username Error
            </div>
          </div>
          <div class="form-floating">
            <input type="password" class="form-control mt-2" th:field="*{password}" th:classappend="${#fields.hasErrors('password')} ? 'is-invalid'" id="floatingPassword" placeholder="Password">
            <label for="floatingPassword">Password</label>
            <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-bottom: 8px;">
              Password Error
            </div>
          </div>
          <div class="form-floating">
            <input type="email" class="form-control" th:field="*{email}" th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'" id="floatingPassword" placeholder="Email">
            <label for="floatingPassword">example@board.com</label>
            <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-top: 8px;">
              Email Error
            </div>
          </div>
      
          <div class="checkbox mb-3">
          </div>
          <button class="w-100 btn btn-lg btn-primary mt-2" type="submit">Get started!</button>
          <a type="button" class="w-100 btn btn-lg btn-primary mt-2" th:href="@{/}">exit</a>
        </form>
  • form 태그 안에서 사용된 타임리프의 th:if가 달린 3개의 div 태그에서 에러를 출력해준다.
  • th:if=${#fields.hasErrors('필드명')} : 에러가 발생한 경우 true 값을 반환하여 div태그를 표시해준다.
  • form 태그의 th:object="{member}" : GetMapping 때 전달받은 Member의 키 값을 넣어 전달 받은 객체
  • th:field="*{username}" :  위 설명을 참고해서 Member.getUsername()을 View에서 사용할 수 있도록 한 값이다. -> ${member.username} 과 같다고 생각!
  • th:errors="*{password}" : password에 대한 에러 내용을 표시해준다.

7. th:block - th:with 로 변수 정의, th:each - #numbers.sequence(from, to), 링크 url

<body>
	<nav aria-label="Page navigation example" th:fragment="pagination(pagination)">
		<ul class="pagination justify-content-center">
			<th:block th:with="start = ${pagination.startPage}, end = ${pagination.endPage}">
				<li class="page-item" th:classappend="${pagination.prev == false} ? 'disabled'">
					<a class="page-link" th:href="@{/board/list(page=${start - 1}, range=${pagination.range - 1}, 
					searchText=${param.searchText})}">Previous</a>
				</li>

				<li class="page-item" th:classappend="${i == pagination.page} ? 'disabled'"
					th:each="i : ${#numbers.sequence(start, end)}"><a class="page-link" href="#" th:href="@{/board/list(page=${i}, 
					range=${pagination.range}, searchText=${param.searchText})}" th:text="${i}">1</a></li>

				<li class="page-item" th:classappend="${pagination.next == false} ? 'disabled'">
					<a class="page-link" th:href="@{/board/list(page=${end + 1}, range=${pagination.range + 1}, 
					searchText=${param.searchText})}" href="#">Next</a>
				</li>
			</th:block>
		</ul>
	</nav>
</body>
  • th:block 태그에 th:with로 사용할 변수에 값을 초기화해서 사용가능.
  • th:each에 #numbers.sequence(from, to)를 사용해서 from에서부터 to까지 i변수에 넣어 사용
  • th:href="@{/board/list(page=${end + 1}, range=${pagination.range + 1}" : board/list?page=...&range=...  

7.2 th:block th:with 조건문 사용

                        <tbody>
                            <th:block th:with="no = ${param.page != null} ? (${#numbers.formatInteger(param.page, 1)} - 1) * 10 : 0">
                                <tr th:each="member : ${memberList}">
                                    <td class="mt-5 text-center" scope="row">
                                        <div class="checkbox">
                                            <input type="checkbox" name="memberIdList" th:value="${member.id}">
                                        </div>
                                    </td>
                                                           .
                                                           .
                                                           .
                                </tr>
                            </th:block>
                        </tbody>
                    </table>
  • 삼항 연산자를 통해 특정 변수를 선언하고 값을 초기화한다.
  • #numbers.formatInteger(int형으로 변환할 변수, 자릿수) : no변수는 url에 page 파라미터가 없을 경우는 기본값 0
  • 이를 활용하면 파라미터의 값이 있을 때와 없을 때 변수값을 다르게 설정할 수 있다. 즉, 파라미터 값 유무에 따라 다르게 처리 할 수 있다.

8. 타임리프에서 날짜 형식 바꾸기

<td class="text-center" th:text="${#dates.format(board.createDate, 'yyyy/MM/dd HH:mm')}">작성일</td>

9. th:each 활용

index  현재 반복 인덱스 (0부터 시작)
count 현재 반복 인덱스 (1부터 시작)
size 총 개수
current 현재 요소
even 현재 요소가 짝수인지 여부 (index 기준)
odd 현재 요소가 홀수인지 여부 (index 기준)
first 현재 요소가 첫번째인지 여부
last 현재 요소가 마지막인지 여부
  • 사용방법
                            <tr th:each="member : ${memberList}">
                                <td class="text-center" th:text="${memberStat.count}">count</td>
                                <td class="text-center" th:text="${memberStat.index}">index</td>
                            </tr>

10. 타임리프 문자열 비교

<p th:text="${#strings.equals(member.role, 'ROLE_ADMIN') ? 'ADMIN' : 'USER'}">권한</p>
  • #strings.equals를 이용해 첫 인자와 두번째 인자가 같은지 체크할 수 있다.

 

728x90

로그인 후 그 객체의 사용법을 정리해보자.

1. Bean을 통해 가져오기

SecurityContextHolder를 통해 가져온다.

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 
UserDetails userDetails = (UserDetails)principal; 
String username = principal.getUsername(); 
String password = principal.getPassword();

 

2. Controller에서 사용자 정보 얻기

Principal 객체에 접근해서 정보를 가져온다.

    @GetMapping("/list")
    public String test(Model model, Principal principal){
        String loginUsername = principal.getName();        
        model.addAttribute("loginUsername", loginUsername);
        
        return "board/list";
    }

 

-

 

출처 : https://djunnni.gitbook.io/springboot/2019-11-30

 

추가 : 로그인 초기화 (회원탈퇴 등에 사용)

SecurityContextHolder.clearContext();
728x90

1. 시큐리티Config 파일에서 .access("hasRole('ADMIN')") 을 이용한다.

.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")

 

 * 시큐리티Config 전체 코드 참고 : https://black-mint.tistory.com/11

 

[Spring Boot] 1. Security 사용 설정, 비밀번호 인코딩

1. 의존성 추가  - build.gradle 에 다음 의존성을 추가해준다. implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.security:spring-security-test'..

black-mint.tistory.com

 

2. @EnableGlobalMethodSecurity를 사용한다.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http
                .authorizeRequests()
                    .antMatchers("/css/**").permitAll()
                    .antMatchers("/board/**").authenticated()
                    .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") // 권한 설정
                    .anyRequest().permitAll() 
                    .and()
                .formLogin()
                    .loginPage("/loginForm") // 로그인 페이지
                    .loginProcessingUrl("/login")
                    .defaultSuccessUrl("/")
                    .and()
                .logout()
                    .permitAll();
    }
}
  • 시큐리티 Config파일 상단에 @Secured 어노테이션과 @preAuthorize를 활성화하는 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)을 추가한다. 
  • 컨트롤러에 @Secured를 추가해서 페이지 권할을 설정한다.
@Secured("ADMIN")
@GetMapping("/admin")
public String admin() {
    return "/admin";
}
  • 2개 이상 권한을 주고 싶을 때는 @PreAuthorize를 추가하면 된다.
@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
@GetMapping("/admin")
public String admin() {
    return "/admin";
}

 

-

 

* 해당 글은 인프런에 게시된 '최주호'님의 강의를 듣고 개인적으로 정리한 글입니다.

강의 출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0

728x90

작업의 루틴은 다음과 같다.

  1. 시큐리티가 로그인을 진행시키면 UserDetails타입의 객체를 실행한다.
  2. 로그인 진행이 완료되면 시큐리티 session을 생성하고 Security ContextHolder라는 키 값으로 세션 정보 저장한다.
  3. 키에 대한 값으로 들어가는 오브젝트 타입은 Authentication이다.
  4. Authentication 안에는 User(UserDetails타입) 정보가 있어야 됨.

1. 시큐리티가 로그인을 진행시키면 실행 될 UserDetails타입의 클래스를 생성하기 위해 UserDetails를 implements한다.

public class PrincipalDetails implements UserDetails {

    private Member member;

    public PrincipalDetails(Member member) {
        this.member = member;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return member.getRole();
            }
        });
        return collection;
    }

    @Override
    public String getPassword() {
        return member.getPassword();
    }

    @Override
    public String getUsername() {
        return member.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;
    }
}
  • getAuthorities() : 해당 유저의 권한을 리턴한다. 리턴 타입이 GrantedAuthority이므로 Collection을 이용해서 반환 타입을 설정
  • isAccountNonExpired() : 계정이 만료됐는지 체크
  • isCredentialsNonExpired() : 비밀번호를 오래 사용했는지 체크
  • isEnabled() : 계정의 활성화 여부를 체크

2. 로그인 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername 함수가 실행되므로 UserDetailsService를 implements하는 서비스 클래스 생성

@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private JpaMemberRepository jpaMemberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member memberEntity = jpaMemberRepository.findByUsername(username);

        if(memberEntity != null) {
            return new PrincipalDetails(memberEntity);
        }
        return null;
    }
}
  • 파라미터로 받는 username은 form에서 받는 input태그와 name이 같아야 한다.
  • 다를 경우 시큐리티 config 파일에서 수정할 수 있다. (userNameParameter()이용)

결국 Security Session 안 -> Authentication객체 안 -> UserDetails(PrincipalDetails) 형식이 완성되고 시큐리티에 의해 로그인이 완료된다.

 

-

 

* 해당 글은 인프런에 게시된 '최주호'님의 강의를 듣고 개인적으로 정리한 글입니다.

강의 출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0

728x90

JPA를 사용하면서 DTO랑 Entity를 따로 둘 필요가 있나? 라는 의문점이 생겨 간단하게 정리한다.

1. Entity : 실제 DB테이블과 1:1로 매핑 되는 클래스

  • DB테이블에 존재하는 컬럼만을 속성(필드)로 가져야 한다.
  • 최대한 해당 클래스 안에서 필요한 로직을 구현 (외부에서 getter 사용을 최소화)
  • setter 금지 (데이터 값이 무분별하게 바뀌면 위험)

2. DTO : 데이터 전송 객체(Data Transfer Object)이라는 의미라는 가졌으며, 주로 비동기 처리에 사용한다.

  • DB데이터를 서비스나 컨트롤러에 데이터를 전송할 때 사용된다.
  • getter/setter 메소드만 가진다. (로직X)

3. VO : DTO와 같은 개념이지만 read only 속성을 가진다.

  • 특정한 비즈니스 값을 담는 객체

+ Recent posts