Technology-Share
thymeleaf+Spring Security를 이용한 로그인,회원가입 본문
thymeleaf
thymeleaf 란 ?!
- 자바 라이브러리로써 웹과 웹이 아닌 환경 양쪽에서 텍스트, HTML, XML, Javascript,CSS 그리고 텍스트를 생성할 수 있는 템플릿 엔진이다.
특징
스프링 MVC와 통합 모듈을 제공하며 애플리케이션에서 JSP로 만든 기능들을 완전히 대체 할 수 있다.
스프링부트 쪽에서는 JSP, 그루비 등 다른 템플릿도 사용가능 하지만 타임리프가 가장 많이 쓰인다고 한다
쉬운 사용성
스프링 시큐리티 지원
스프링 웹플로우 지원 ( AJAX 이벤트 포함)
반복문, 조건문, 변수 정의 가능
예시
<span th:text = "${data.text}"></span>
적용하기
의존성 추가
build.gradle
부분에compile('org.springframework.boot:spring-boot-starter-thymeleaf')
를 추가하고- 스프링 시큐리티를 위해서
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
도 추가한다.
common 파일 설정
항상 같은 뷰를 내보내는 상단 메뉴 부분을 묶어서
common.html
로 선언 후 필수 코드를 삽입해둔다.common.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <head th:fragment="head(title)"> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top" th:fragment="menu(menu)"> <a class="navbar-brand" href="#">Spring boot Service</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item" th:classappend="${menu} == 'home'? 'active'"> <a class="nav-link" href="#" th:href="@{/}">홈 <span class="sr-only" th:if="${menu} == 'home'">(current)</span></a> </li> <li class="nav-item" th:classappend="${menu} == 'board'? 'active'"> <a class="nav-link" href="#" th:href="@{/board/list}">게시판 <span class="sr-only" th:if="${menu} == 'board'">(current)</span></a> </li> </ul> <a class="btn btn-secondary mr-2 my-2 my-sm-0" th:href="@{/account/register}" sec:authorize="!isAuthenticated()">register</a> <a class="btn btn-secondary my-2 my-sm-0" th:href="@{/account/login}" sec:authorize="!isAuthenticated()">login</a> <!-- 로그인이 안된 경우 --> <form class="form-inline my-2 my-lg-0" th:action="@{/logout}" method="post" sec:authorize="isAuthenticated()"> <span class="text-white mr-2 mx-2" sec:authentication="name">사용자</span> <span class="text-white mr-2 mx-2" sec:authentication="principal.authorities">권한</span> <button class="btn btn-secondary my-2 my-sm-0" type="submit">logout</button> </form> </div> </nav> </body> </html>
xmlns:th="http://www.w3.org/1999/xhtml"
: 타임리프 사용xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
: 권한 인증을 위해 삽입한다.th:fragment
th:replace
와 세트로 사용하는데<head th:replace="fragments/common :: head('게시판')">
의 형태로 *다른 html 파일의 상위 부분에 삽입되도록 해준다. *- menu와 head 부분을 나누는데 사용하였다.
th:href="@{/}"
: 해당 링크로 연결해준다.th:classappend="${menu} == 'home'? 'active'">
- menu의 값이 home이라면 class active가 활성화되고
- 아니라면 class가 추가되지 않아 active가 표시되지 않는다.
- 현재 메뉴값을 확인하여 화면에 표시해준다.
sec:authentication="name"
- 시큐리티 로그인을 성공한 사용자의 name값을 가져온다.
sec:authentication="principal.authorities"
- 시큐리티 로그인한 사용자의 권한을 가져온다.
- 둘다 로그인하지 않았다면 사용자에게 보이지 않는다.
login.html
<body class="text-center"> <form class="form-signin" th:action="@{/account/login}" method="post"> <!-- 요청 보내기--> <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> <div th:if="${param.error}" class="alert alert-danger" role="alert"> Invalid username and password </div> <div th:if="${param.logout}" class="alert alert-primary" role="alert"> You have been logged out </div> <label for="username" class="sr-only">username</label> <input type="text" id="username" name="username" class="form-control" placeholder="username" required autofocus> <label for="password" class="sr-only">password</label> <input type="password" id="password" name="password" class="form-control" placeholder="password" required> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> <p class="mt-5 mb-3 text-muted">© 2017-2020</p> </form> </body>
th:if="${param.error}"
- param.error의 값이 존재한다면 해당 코드가 삽입되고 아니라면 실행하지 않는다.
- 여기선 로그인 실패의 메시지를 담아서 화면에 보여주는 형태이다.
th:action="@{/account/login}"
- 흔히 알고 있는 action과 동일한 일을 수행한다.
- button의 submit으로 보내진다.
register.html
<body class="text-center"> <form class="form-signin" th:action="@{/account/register}" method="post" th:object="${accountRequestDto}"> <!-- 요청 보내기--> <h1 class="h3 mb-3 font-weight-normal">회원가입</h1> <label for="username" class="sr-only">username</label> <input type="text" id="username" th:classappend="${#fields.hasErrors('username')} ? 'is-invalid'" name="username" class="form-control" required autofocus> <div class="invalid-feedback" th:if="${#fields.hasErrors('username')}" th:errors="*{username}"> 중복회원 </div> <label for="password" class="sr-only">password</label> <input type="password" id="password" th:classappend="${#fields.hasErrors('password')} ? 'is-invalid'" name="password" class="form-control" placeholder="password" required> <div class="invalid-feedback" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"> 비밀번호 불일치 </div> <label for="password" class="sr-only">password_confirm</label> <input type="password" id="password_confirm" name="password_confirm" class="form-control" placeholder="password_confirm" required> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> </form>
th:object="${accountRequestDto}"
- 서버에 보낼때 accountRequestDto 라는 객체로 보내도록 지정한다
th:if="${#fields.hasErrors('username')}" th:errors="*{username}"
- username의 이름의 에러가 있는지 검사한다.
- 만약 있다면 해당 내용을 표시해준다.
- 검사는AccountValidator이 수행하고 그 값을 리턴해주는데 이는 뒤에서 설명
서버
Model
Account
public class Account { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(unique =true) private String username; @NotNull private String password; @ManyToMany @JoinTable( name="account_role", joinColumns = @JoinColumn(name="account_id"), inverseJoinColumns = @JoinColumn(name="role_id")) private List<Role> roles = new ArrayList<>(); //null point 방지 public Account(AccountRequestDto accountRequestDto){ this.username=accountRequestDto.getUsername(); this.password=accountRequestDto.getPassword(); } @Builder public Account(String username, String password, List<Role> roles){ this.username=username; this.password=password; this.roles=roles; } public void encodePassword(PasswordEncoder passwordEncoder) { this.password = passwordEncoder.encode(this.password); } }
- 권한과는 ManyToMany로 연결했다
- 자신의 가지고 있는 password를 암호화 가능
Role
@Entity @Setter @Getter public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToMany(mappedBy = "roles") private List<Account> accounts; }
AccountRequestDto
package com.predict.stock.Dto.account; import com.predict.stock.account.Account; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor public class AccountRequestDto { private String username; private String password; private String password_confirm; @Builder public AccountRequestDto(String username, String password, String password_confirm){ this.username = username; this.password = password; this.password_confirm = password_confirm; } }
- 회원가입 요청이 오면 쓰이는 객체이다.
@Entity
로 테이블에 바로 매핑되는 객체가 아닌 Request 전용 모델을 선언 후 여러 작업을 거친 다음account
로 저장한다.- 이름과 비밀번호, 비밀번호 확인이 존재하며
- 해당 객체를 검사한 뒤 문제가 없으면
Account
객체로 전환하여 저장한다.
Validator
@Component public class AccountValidator implements Validator { @Autowired private AccountRepository accountRepository; @Override public boolean supports(Class<?> clazz) { return AccountRequestDto.class.equals(clazz); } @Override public void validate(Object obj, Errors errors) { AccountRequestDto accountRequestDto = (AccountRequestDto) obj; if(!((AccountRequestDto) obj).getPassword().equals(((AccountRequestDto) obj).getPassword_confirm())){ //비밀번호와 비밀번호 확인이 다르다면 errors.rejectValue("password", "key","비밀번호가 일치하지 않습니다."); } else if(accountRepository.findByUsername(((AccountRequestDto) obj).getUsername()) !=null){ // 이름이 존재하면 errors.rejectValue("username", "key","이미 사용자 이름이 존재합니다."); } }// 비밀번호 검사할때 쓰면 될듯 }
validate
를@Override
해서 넘어온 객체를 검사한다.- 조건은 동일한 이름이 존재하는지와 password, password_confirm의 동일 여부이다.
Controller
@Controller @RequiredArgsConstructor @RequestMapping("/account") public class AccountController { private final AccountService accountService; private final AccountValidator accountValidator; @GetMapping("/login") public String login(){ return "/account/login"; } @GetMapping("/register") public String register(Model model){ model.addAttribute("accountRequestDto",new AccountRequestDto()); return "/account/register"; } @PostMapping("/register") public String register(@Valid AccountRequestDto accountRequestDto, BindingResult bindingResult){ accountValidator.validate(accountRequestDto, bindingResult); System.out.println(bindingResult.hasErrors()); if(bindingResult.hasErrors()) { return "/account/register"; // 실패 } else { // 성공 accountService.save(accountRequestDto); return "redirect:/"; } } }
@GetMapping("/register")
- 빈 객체를 보내는 이유는 위쪽
register.html
에서 에러검사를 위해th:object="${accountRequestDto}"
로 객체를 받는 문장이 존재하기 때문에 안쓰면 오류를 발생시킨다. - 따라서 빈 객체 하나를 생성하여 넘겨준다.
- 빈 객체를 보내는 이유는 위쪽
@PostMapping("/register")
@Valid
로 requestBody로 들어온 객체의 검증BindResult
로 에러가 있다면 해당 에러 메시지를 (key, value) 형태로 전달 후 다시/account/register
를 호출한다.
Service
AccountService
@Service @RequiredArgsConstructor public class AccountService implements UserDetailsService {
private final AccountRepository accountRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
Account account = accountRepository.findByUsername(username);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority("role_user"));
System.out.println(new User(account.getUsername(),account.getPassword(),grantedAuthorities));
return new User(account.getUsername(),account.getPassword(),grantedAuthorities);
// UserDetails를 구현한 객체인 User를 리턴하면된다.
}
public Account save(AccountRequestDto accountRequestDto){
//입력한 비밀번호와 비밀번호 확인이 같다면
Role role = roleRepository.findByName("role_user");
// 일반 유저 권한을 찾아서 가져온다음.
Account account = new Account(accountRequestDto);
// 새로 account를 하나 만들어서
account.encodePassword(passwordEncoder);
account.getRoles().add(role);
return accountRepository.save(account);
}
// 요청이 들어오면 account가 들어오면 account의 비밀번호를 인코딩 한 뒤에 저장한다.
// DTO로 바꾸려면 들어오는 변수가 Dto로 바뀌고 Dto의 비밀번호를 인코딩 한 뒤 account에 전달하면 된다.
}
```
5. **Spring Security에서 로그인**
* **``UserDetailsService`` 인터페이스를 구현하면 ``loadUserByUsername`` 메소드가 오버라이딩 된다.**
* DB에서 사용자 정보를 저장하는 역할을 수행한다.
* **인증 절차는 미리 ``Validator``를 통해 수행하므로 그냥 바로 저장!**
*
'Project' 카테고리의 다른 글
[토이프로젝트-05] 프론트 라이브러리 셋팅 방법 (0) | 2020.10.06 |
---|---|
[EXERD] Eclipse Exerd Plugin 설치방법 (0) | 2020.07.11 |