스프링 게시판 만들기 - 5 (2) ( 로그인 처리 feat.스프링 시큐리티)
이전 스프링 게시판 만들기 - 5 (1) 에서 스프링 시큐리티란 무엇인가와 설정 파일에 대해
대략적으로 개념을 알아보았다.
그렇다면 이번에는 스프링 시큐리티를 이용하여 로그인을 간단히 구현해보자.
코드 구현
** 폴더 구조 변경
/config/SecurityConfig
package toyproject.board.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import toyproject.board.service.MemberService;
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final MemberService memberService;
// 비밀번호 암호화
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 인증 무시 -> static 디렉토리의 파일들은 항상 인증 무시 (통과)
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
}
//Request 가 들어오는 경우 권한 설정 & 로그인 & 로그아웃 처리
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/member/myinfo").hasRole("USER")
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/member/memberLoginForm")
.defaultSuccessUrl("/member/memberLoginResult")
.permitAll()
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
.logoutSuccessUrl("/member/logoutResult")
.invalidateHttpSession(true)
.and()
.exceptionHandling().accessDeniedPage("/member/denied");
}
// 모든 인증을 처리하기 위한 AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
}
}
이 전 챕터에서 이미 설명했지만 간략하게 다시 설명하자면
PasswordeEncoder 는 비밀번호 암호화를 위한 코드
configure(HttpSecurity http) 는 로그인 & 로그아웃 처리를 위한 코드이다.
-> http request 가 들어오면 , /admin 경로로 시작하는 페이지들은 전부 role 이 admin 이여야만 접근이 가능하고
/member/myinfo 는 role 이 user 인 경우만 접근이 가능하다.
그 외의 웹페이지 경로는 전부 허용해준다.
-> formLogin() , 폼을 통해 로그인을 진행하고 , loginPage 는 내가 만든 memberLoginForm.html 을 이용한다.
성공하면 memberLoginResult 로 보내주는데 이는 권한 상관없이 모두에게 허용시켜준다.
-> 로그아웃도 동일한 로직
-> exceptionHandling 은 만약 접근이 거부되는 경우 , member/denied 경로로 보내준다.
/domain/Member
package toyproject.board.domain;
import lombok.Builder;
import lombok.Getter;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "member")
@Getter
public class Member extends BaseTimeEntity implements Serializable {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
// @NotBlank(message = "아이디를 입력해주세요.")
// @Pattern(regexp = "^[a-zA-Z0-9]{3,12}$", message = "아이디를 3~12자로 입력해주세요. [특수문자 X]")
private String username;
// @NotBlank(message = "비밀번호를 입력해주세요.")
// @Pattern(regexp = "^[a-zA-Z0-9]{3,12}$", message = "비밀번호를 3~12자로 입력해주세요.")
private String password;
private String email;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Board> board = new ArrayList<>();
@Builder
public Member(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
protected Member() {}
}
/domain/Role (enum)
package toyproject.board.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum Role {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER");
private String value;
}
/domain/BaseTimeEntity
package toyproject.board.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
@Getter
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
생성일자 , 수정 일자 칼럼을 추가하기 위한 BaseTimeEntity 클래스이다. 해당 클래스를 작성 후 ,
@SpringBootApplication
@EnableJpaAuditing
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
다음과 같이 메인 어플리케이션에 @EnableAuditing을 적용한 뒤 , Member 클래스에서 클래스를 상속받으면
DB 에 CREATED_DATE, MODIFIED_DATE 가 추가됨을 확인할 수 있다.
/dto/MemberDto
package toyproject.board.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import toyproject.board.domain.Member;
@Getter
@Setter
@NoArgsConstructor
public class MemberDto {
private Long id;
private String username;
private String password;
private String email;
@Builder
public MemberDto(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
public Member toEntity(){
return Member.builder()
.username(username)
.password(password)
.email(email)
.build();
}
}
기존의 Member 클래스는 DB 생성 클래스이기 때문에 직접 접근하지 않고 , DTO 클래스로 새로 정의해서 DTO 를 이용해서 값을 저장한다.
/repository/MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByUsername(String username);
List<Member> findAll();
}
이전의 포스팅에서는 직접 JPA 로직을 만들었지만 , JpaRepository 를 상속받으면 편리하고 다양한 쿼리 사용 가능하다.
내가 작성한 Optional<Member> findByUsername(String username) 부분을 해석하면
select from Member where username=? 가 될 것이다. 이를 활용해서 본인 입맛대로 만들어 보면 되겠다.
/service/MemberService
package toyproject.board.service;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import toyproject.board.repository.MemberRepository;
import toyproject.board.domain.Member;
import toyproject.board.domain.Role;
import toyproject.board.dto.MemberDto;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
@AllArgsConstructor
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
/**
* 중복 아이디 검증
*/
// public void validateDuplicateId(Member member){
// List<Member> findMembers = memberRepository.findByUsername(member.getUsername());
// if(!findMembers.isEmpty()){
// throw new IllegalStateException("이미 존재하는 이름입니다. 다른 이름을 입력해주세요.");
// }
// }
public List<Member> findAll(){
return memberRepository.findAll();
}
@Transactional
public Long joinUser(MemberDto memberDto) {
// 비밀번호 암호화
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
memberDto.setPassword(passwordEncoder.encode(memberDto.getPassword()));
return memberRepository.save(memberDto.toEntity()).getId();
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> findName = memberRepository.findByUsername(username);
Member member = findName.get();
List<GrantedAuthority> authorities = new ArrayList<>();
if(("admin").equals(username)){
authorities.add(new SimpleGrantedAuthority(Role.ADMIN.getValue()));
}else {
authorities.add(new SimpleGrantedAuthority(Role.USER.getValue()));
}
return new User(member.getUsername(), member.getPassword() , authorities);
}
}
스프링 시큐리티를 이용하기 위해서는 UserDetailService 가 필요하다.
findAll() 메소드는 전체 회원 조회를 위한 메소드. 말 그대로 repository 에서 모든 member 를 꺼내준다.
@Transactional
public Long joinUser(MemberDto memberDto) {
// 비밀번호 암호화
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
memberDto.setPassword(passwordEncoder.encode(memberDto.getPassword()));
return memberRepository.save(memberDto.toEntity()).getId();
}
joinUser 메소드는 회원가입을 위한 코드이다. 위의 두 줄은 비밀번호 암호화를 위한 코드이다.
memberDto 에서 password 를 받아와서 암호화하여 저장한다.
문제는 아래에 있는 memberRepository.save() 이다.
우리는 save 메소드를 지정한 적 없으나 , memberRepository.save() 라는 메소드를 호출하였다. 과연 어디서 나온 걸까?
바로 SimpleRepository 에서 정의된 메소드이다.
원래는 저장소를 만들 때 , @Repository 어노테이션을 통해 사용해왔었지만 JpaRepsitory 를 상속함으로써 어노테이션은 사라지고 , 다양한 메소드들을 사용할 수 있게 됐다.
JpaRepository 중 SimpleJpaRepository 가 우리가 사용한 save 메소드 정보이다.
JpaRepositoryImplementation 을 상속받는 SimpleJpaRepsitory.
save() 내부 구현에서는 파라미터로 들어온 entity가 새로운 엔티티라면 persist를 호출하고 아니라면 merge를 합니다.
merge는 보통 detach 된 엔티티를 다시 영속 상태로 만들기 위해 사용하는데, 파라미터로 들어온 entity가
영속성 컨텍스트 1차 캐시에 있는지 확인 후 없다면 DB에 select 쿼리를 날려서 조회합니다.
DB에서 조회가 되어 1차 캐시에 엔티티가 저장이 되면 트랜잭션 커밋 시점에 파라미터 entity의 값과
1차 캐시에 저장되어 있는 entity의 값을 비교하여 다른 점이 있을 경우 update 쿼리가 발생합니다.
반대로, DB에 해당 값이 없을 때 insert 쿼리가 발생합니다.
즉, 나는 save() 메서드를 통해 단순히 저장하려고 해도 이미 동일한 데이터가 있는 경우 ,
update 쿼리가 발생할 수 있다는 것입니다.
그렇다면 entity가 새로운 엔티티인지는 어떻게 구별할까요?
바로 entity 식별자의 상태를 통해 구별합니다.
식별자가 객체(Long, String...)일 때는 null, 기본 타입(int, long..)일 때는 0 이면 새로운 엔티티입니다.
(이 차이가 생기는 이유는 Long , String 은 아무 값도 들어가지 않으면 기본 상태가 null 이고 ,
int , long 등의 기본 타입은 0 이기 때문입니다.) / Primitive Type , Reference Type
식별자에 @GeneratedValue 어노테이션을 설정하였다면, 데이터베이스에 식별자 생성을 위임하기 때문에
(AutoIncrement 가 되기 떄문에) save를 호출하는 시점에 새로운 엔티티로 인식하여 우리가 원하던대로 persist가 호출 되겠지만, @Id 만 사용해서 식별자를 직접 할당 하였다면, 이는 save 호출 시점에 새로운 엔티티가 아니므로 merge를 호출하게 됩니다.
만약에 DB에 해당 식별자로 하는 데이터가 이미 들어가 있었다면?
해당 데이터는 파라미터 entity의 값으로 모두 update 되어버립니다.
고로 save() 를 사용할 때는 , @GeneratedValue 어노테이션을 사용하여 혹여나 같은 값이 잘못 들어온 경우
update 되는 일을 방지하는 것이 좋을 것 같습니다.
ex)
memberA 가 이미 DB 에 존재 -> save() 메소드를 통해 다른 사용자가 memberA 로 회원 가입 진행
-> (AutoIncrement 상태가 아닌 경우 ) update 처리 -> 사용자의 정보가 덮어씌워지게 됨 -> 문제 발생
loadUserByUsername()
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> findName = memberRepository.findByUsername(username);
Member member = findName.get();
List<GrantedAuthority> authorities = new ArrayList<>();
if(("admin").equals(username)){
authorities.add(new SimpleGrantedAuthority(Role.ADMIN.getValue()));
}else {
authorities.add(new SimpleGrantedAuthority(Role.USER.getValue()));
}
return new User(member.getUsername(), member.getPassword() , authorities);
}
마지막으로 loadUserByUsername 메소드입니다.
loadUserByUsername()
상세 정보를 조회하는 메서드이며, 사용자의 계정정보와 권한을 갖는 UserDetails 인터페이스를 반환해야 합니다.
(반환 값 확인)
매개변수는 로그인 시 입력한 아이디인데, 엔티티의 PK를 뜻하는게 아니고 유저를 식별할 수 있는 어떤 값을 의미합니다. Spring Security에서는 username 이름으로 사용합니다.
authorities.add(new SimpleGrantedAuthority());
롤을 부여하는 코드입니다. 분명 더 좋은 코드가 존재하겠지만 복잡성을 줄이기 위해(역량 부족..),
'admin' 으로 회원가입 하는 경우에 ADMIN 권한을 부여하도록 했습니다.
new User()
return은 SpringSecurity에서 제공하는 UserDetails를 구현한 User를 반환합니다.
( org.springframework.security.core.userdetails.User )
생성자의 각 매개변수는 순서대로 아이디, 비밀번호, 권한리스트입니다.
/controller/MemberController
package toyproject.board.controller;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import toyproject.board.service.MemberService;
import toyproject.board.domain.Member;
import toyproject.board.dto.MemberDto;
import java.security.Principal;
import java.util.List;
@Controller
@AllArgsConstructor
@RequestMapping("/member")
public class MemberController {
private final MemberService memberService;
@GetMapping("/memberJoinForm")
public String addForm() {
return "member/memberJoinForm";
}
@PostMapping("/memberJoinForm")
public String createMember(@ModelAttribute MemberDto member) {
memberService.joinUser(member);
return "redirect:/";
}
@GetMapping("/memberLoginForm")
public String login() {
return "member/memberLoginForm";
}
@GetMapping("/memberLoginResult")
public String loginResult() {
return "member/memberLoginResult";
}
@GetMapping("/memberList")
public String findAllMember(Model model){
List<Member> members = memberService.findAll();
model.addAttribute("members", members);
return "member/memberList";
}
}
마지막으로 Controller 코드입니다. 저번 회원가입과 달라진 점은 파라미터가 Member -> MemberDto 로 변경됐습니다.
@GetMapping 을 이용하여 /member/memberLoginForm URL 에 들어오면 return 을 통해 로그인 폼을 호출하고
로그인한 결과를 memberLoginResult.html 에서 username 과 함께 보여줍니다.
(해당 부분은 처음에 작성한 SecurityConfig 에서 URL 매핑을 같이 확인하시면 됩니다.)
@GetMapping("/memberList")
findAllMember 는 전체 회원 가입한 목록을 출력하는 간단한 코드입니다. (memberRepository 참고)
model 에 전체 정보를 담아서 보내고 thymeleaf 를 이용해 전체 회원 정보 조회.
HTML
※ 참고 : 스프링 시큐리티를 이용하면 form 에 반드시 아래 코드를 넣어주어야 한다.
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
또는 , SecurityConfig 에서 configure 부분에 http.csrf.disable() 사용.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
CSRF란?
Cross site Request forgery로 사이트간 위조 요청으로, 즉 정상적인 사용자가 의도치 않은 위조요청을 보내는 것을 의미한다. rest api 를 이용한 서버라면 stateless 하기 때문에 disable 해도 되지만 세션 기반 인증에서는 포함하여야 한다.
그렇지만 해당 부분 개념도 충분치 않고 , 토이 프로젝트이기에 우선은 disable 로 처리합니다..
resources/templates/member/memberJoinForm.html (아래부터는 resources/templates 경로 생략)
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>Sing In</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/sign-in/">
<!-- Bootstrap core CSS -->
<link href="/static/css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"rel="stylesheet">
<link href="/static/css/signin.css"
th:href="@{/css/signin.css}"rel="stylesheet">
<link href="/static/css/headers.css"
th:href="@{/css/headers.css}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
</head>
<body class="text-center">
<main class="form-signin">
<form action="memberJoinForm.html" th:action method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- <img class="mb-4" src="../assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">-->
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
<div class="form-floating">
<input type="text" class="form-control" id="username" name="username" placeholder="Username">
<label for="username">username</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
<label for="password">Password</label>
</div>
<div class="form-floating">
<input type="text" class="form-control" id="email" name="email" placeholder="name@example.com">
<label for="email">Email</label>
</div>
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='/index.html'"
th:onclick="|location.href='@{/index}'|"
type="button">취소</button>
<p class="mt-5 mb-3 text-muted">© 2017–2021</p>
</form>
</main>
</body>
</html>
/member/memberLoginForm
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>Signin Template · Bootstrap v5.1</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/sign-in/">
<!-- Bootstrap core CSS -->
<link href="/static/css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"rel="stylesheet">
<link href="/static/css/signin.css"
th:href="@{/css/signin.css}"rel="stylesheet">
<link href="/static/css/headers.css"
th:href="@{/css/headers.css}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
</head>
<body class="text-center">
<main class="form-signin">
<form action="/member/memberLoginForm" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- <img class="mb-4" src="../assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">-->
<h1 class="h3 mb-3 fw-normal">Please Log in</h1>
<div class="form-floating">
<input type="text" class="form-control" id="username" name="username" placeholder="username">
<label for="username">Username</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
<label for="password">Password</label>
</div>
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2017–2021</p>
</form>
</main>
</body>
</html>
/member/memberLoginResult
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>Signin Template · Bootstrap v5.1</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/sign-in/">
<!-- Bootstrap core CSS -->
<link href="/static/css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"rel="stylesheet">
<link href="/static/css/signin.css"
th:href="@{/css/signin.css}"rel="stylesheet">
<link href="/static/css/headers.css"
th:href="@{/css/headers.css}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
</head>
<body>
<div class="container" sec:authorize="isAuthenticated()">
<div class="py-5 text-center">
<h2>로그인 결과</h2>
</div>
<h2 th:text="'로그인 완료!'" ></h2>
<div>
</div>
<div>
<td sec:authentication="name"></td> 님 환영합니다 !
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'" type="button">회원 정보 수정</button>
<!-- th:onclick="|location.href='@{/member/memberList/{memberId}/memberEditForm(memberId=${member.id})}'|"-->
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='index.html'"
th:onclick="|location.href='@{/index.html}'|"
type="button">메인으로</button>
</div>
</div>
</div> <!-- /container -->
</div>
</body>
</html>
※ 참고 2 : 스프링 시큐리티를 이용하면 이전에 회원가입에서 사용했던 th:text=${member.username} 과 같은 thymeleaf 문법 사용이 불가능합니다. 회원가입 때 사용했던 html을 그대로 돌리면 thymeleaf 오류가 발생!
해당 사항을 해결하기 위해서는 sec.authentication="name" 을 사용해주면 됩니다.
이는 스프링 게시판 - 5 (1) 에서 다루었던 스프링 시큐리티와 관련된 내용입니다.
sec.authentication="name" 은 Authentication 에서 인증된 정보를 가져오는 방식입니다.
하지만 이름을 제외하고는 role 정도만 가져올 수 있기 때문에 memberList 에서는 객체를
model 에 담아서 넘겨주고 , thymeleaf 문법으로 전체 조회했습니다.
(memberList 는 아직 URL 매핑을 하지 않아서 회원 리스트를 확인하고 싶은 경우 직접 Url 에
http://localhost:8080/member/memberList 같은 방식으로 접근해야 합니다. / 추후 추가 예정)
/member/memberList
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>Signin Template · Bootstrap v5.1</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/sign-in/">
<!-- Bootstrap core CSS -->
<link href="/static/css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"rel="stylesheet">
<link href="/static/css/signin.css"
th:href="@{/css/signin.css}"rel="stylesheet">
<link href="/static/css/headers.css"
th:href="@{/css/headers.css}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
</head>
<body>
<div class="container">
<div>
<table class="table table-striped">
<thead>
<tr>
<th>번호</th>
<th>아이디</th>
<th>비밀번호</th>
<th>이메일</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.username}"></td>
<td th:text="${member.password}"></td>
<td th:text="${member.email}"></td>
<td>
<!-- <a href="#" th:href="@{/items/{id}/edit (id=${item.id})}" class="btn btn-primary" role="button">수정</a>-->
</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
화면
DB
다음편!
https://dodokong.tistory.com/54?category=1249752