Java/JPA

JPA 기초(security) - AWS 풀스택 과정 85일차

awspspgh 2024. 11. 26. 09:36
목차
1. security

 

1. security

build.gradle (boot_JPA)

plugins {
	id 'java'
	id 'war'
	id 'org.springframework.boot' version '3.2.11'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.ezen'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
	implementation 'org.springframework.boot:spring-boot-starter-security'
	// https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity6
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	// https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	// https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
	// https://mvnrepository.com/artifact/org.apache.tika/tika-core
	implementation 'org.apache.tika:tika-core:2.4.1'
	// https://mvnrepository.com/artifact/org.apache.tika/tika-parsers
	implementation 'org.apache.tika:tika-parsers:2.4.1'
	// https://mvnrepository.com/artifact/net.coobird/thumbnailator
	implementation 'net.coobird:thumbnailator:0.4.17'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	/*providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'*/
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

 SecurityConfig.java

package com.ezen.boot_JPA.config;

import com.ezen.boot_JPA.security.CustomUserService;
import com.ezen.boot_JPA.security.LoginFailureHandler;
import com.ezen.boot_JPA.security.LoginSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    PasswordEncoder passwordEncoder () {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests((authorize)->authorize
                        .requestMatchers("/js/**", "/dist/**", "/upload/**", "/",
                                "/index", "/user/join", "/user/login", "/board/list",
                                "/board/detail/**", "/comment/list/**").permitAll()
                        .requestMatchers("/user/list").hasAnyRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .formLogin(login -> login
                        .usernameParameter("email")
                        .passwordParameter("pwd")
                        .loginPage("/user/login")
                        .successHandler(authenticationSuccessHandler())
                        .failureHandler(authenticationFailuresHandler())
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/user/logout")
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID")
                        .logoutSuccessUrl("/")
                )
                .build();
    }

    @Bean
    UserDetailsService customUserService(){
        return new CustomUserService();
    }

    @Bean
    AuthenticationManager authenticationManger(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    AuthenticationSuccessHandler authenticationSuccessHandler(){
        return new LoginSuccessHandler();
    }

    @Bean
    AuthenticationFailureHandler authenticationFailuresHandler(){
        return new LoginFailureHandler();
    }
}

 

 CustomUserService.java

package com.ezen.boot_JPA.security;

import com.ezen.boot_JPA.dto.UserDTO;
import com.ezen.boot_JPA.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Slf4j
public class CustomUserService implements UserDetailsService {

    @Autowired
    public UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // username이 DB에 있는지 확인
        // user 테이블이 email 객체를 가져오기
        UserDTO userDTO = userService.selectEmail(username);
        log.info(">>> login User >>> {}", userDTO);
        if(userDTO == null){
            throw new UsernameNotFoundException(username);
        }

        return new AuthMember(userDTO);
    }
}

 

 LoginSuccessHandler.java

package com.ezen.boot_JPA.security;

import com.ezen.boot_JPA.service.UserService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Getter
    @Setter
    private String authUrl;

    @Getter
    @Setter
    private String authEmail;

    // 성공 후 가야하는 경로 생성 (객체)
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    // request 객체의 저장공간 : 직전 갔던 경로 저장
    private RequestCache requestCache = new HttpSessionRequestCache();

    @Autowired
    @Getter
    public UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        setAuthEmail(authentication.getName());
        setAuthUrl("/board/list");

        boolean isOk = userService.updateLastLogin(getAuthEmail());

        HttpSession ses = request.getSession();
        if(!isOk || ses == null){
            return;
        }else{
            ses.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
        }
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        log.info(">>> saveRequest >>> {}", savedRequest);
        redirectStrategy.sendRedirect(request, response,
                (savedRequest != null ? savedRequest.getRedirectUrl() : getAuthUrl()));
    }
}

 

 LoginFailureHandler.java

package com.ezen.boot_JPA.security;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Slf4j
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String errorMessage;
        if(exception instanceof BadCredentialsException){
            errorMessage = "아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해주세요.";
        }else if(exception instanceof InternalAuthenticationServiceException){
            errorMessage = "내부 시스템 문제로 로그인 처리를 할 수 없습니다. 관리자에게 문의해주세요.";
        }else if(exception instanceof UsernameNotFoundException) {
            errorMessage = "계정 확인 후 로그인 해주세요.";
        }else if(exception instanceof AuthenticationCredentialsNotFoundException){
            errorMessage = "인증 요청이 거부되었습니다. 관리자에게 문의해주세요.";
        }else{
            errorMessage = "관리자에게 문의해주세요.";
        }

        errorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8);
        setDefaultFailureUrl("/user/login?error=true&exception="+errorMessage);
        super.onAuthenticationFailure(request, response, exception);
    }
}

 

 User.java

package com.ezen.boot_JPA.entity;

import com.ezen.boot_JPA.dto.AuthUserDTO;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User extends TimeBase{
    @Id
    @Column(length = 200, nullable = false)
    private String email;

    @Column(length = 250, nullable = false)
    private String pwd;

    @Column(name = "nick_name", length = 100)
    private String nickName;

    @Column(name = "last_login")
    private LocalDateTime lastLogin;
}

 

 UserDTO.java

package com.ezen.boot_JPA.dto;

import lombok.*;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDTO {
    private String email;
    private String pwd;
    private String nickName;
    private LocalDateTime lastLogin;
    private LocalDateTime regAt, modAt;
    private List<AuthUserDTO> authList;
}

 

 AuthUser.java

package com.ezen.boot_JPA.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "auth_user")
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AuthUser {
    @Id
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
    private long id;

    @Column(length = 200, nullable = false)
    private String email;

    @Column(length = 50, nullable = false)
    private String auth;
}

 

 AuthUserDTO.java

package com.ezen.boot_JPA.dto;

import lombok.*;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AuthUserDTO {
    private long id;
    private String email;
    private String auth;
}

 

 UserController.java

package com.ezen.boot_JPA.controller;

import com.ezen.boot_JPA.dto.UserDTO;
import com.ezen.boot_JPA.service.CommentService;
import com.ezen.boot_JPA.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.security.Principal;

@Slf4j
@RequestMapping("/user/*")
@RequiredArgsConstructor
@Controller
public class UserController {
    private final UserService userService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/join")
    public void join(){}

    @PostMapping("/join")
    public String join(UserDTO userDTO){
        log.info(">>> userDTO >>> {}", userDTO);
        // password 암호화
        userDTO.setPwd(passwordEncoder.encode(userDTO.getPwd()));
        String email = userService.register(userDTO);
        log.info(">>> email >>> {}", email);
        return (email == null ? "/user/join" : "/index");
    }

    @GetMapping("/login")
    public void login(@RequestParam(value = "error", required = false) String error,
                      @RequestParam(value = "exception", required = false) String exception,
                      Model model){
        /* 에러의 예외값을 담아 화면으로 전달 */
        model.addAttribute("error", error);
        model.addAttribute("exception", exception);
    }

    @GetMapping("/list")
    public void list(Model model){
        model.addAttribute("userList", userService.getList());
    }

    @GetMapping("/modify")
    public void modfiy(Principal principal, Model model){
        model.addAttribute("userDTO", userService.selectEmail(principal.getName()));
    }

    @PostMapping("/modify")
    public String modify(UserDTO userDTO){
        if(userDTO.getPwd().length() > 0){
            userDTO.setPwd(passwordEncoder.encode(userDTO.getPwd()));
        }
        String email = userService.modify(userDTO);
        return "redirect:/user/logout";
    }

    @GetMapping("/remove")
    public String remove(@RequestParam("email") String email){
        userService.remove(email);
        return "redirect:/user/logout";
    }
}

 

 UserService.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.AuthUserDTO;
import com.ezen.boot_JPA.dto.UserDTO;
import com.ezen.boot_JPA.entity.AuthUser;
import com.ezen.boot_JPA.entity.User;

import java.util.List;

public interface UserService {
    /* convert 작업
    * UserDTO(화면) => User(저장) 변환
    * */

    default User convertDtoToEntity(UserDTO userDTO){
        return User.builder()
                .email(userDTO.getEmail())
                .pwd(userDTO.getPwd())
                .nickName(userDTO.getNickName())
                .lastLogin(userDTO.getLastLogin())
                .build();
    }

    default AuthUser convertDtoToAuthEntity(UserDTO userDTO){
        return AuthUser.builder()
                .email(userDTO.getEmail())
                .auth("ROLE_USER")
                .build();
    }

    /* user(entity) => userDTO */
    default AuthUserDTO convertEntityToAuthDto(AuthUser authUser){
        return AuthUserDTO.builder()
                .id(authUser.getId())
                .email(authUser.getEmail())
                .auth(authUser.getAuth())
                .build();
    }

    /* user(entity) => userDTO */
    default UserDTO convertEntityToDto(User user, List<AuthUserDTO> authUserDTOList) {
        return UserDTO.builder()
                .email(user.getEmail())
                .pwd(user.getPwd())
                .nickName(user.getNickName())
                .lastLogin(user.getLastLogin())
                .regAt(user.getRegAt())
                .modAt(user.getModAt())
                .authList(authUserDTOList)
                .build();
    }

    String register(UserDTO userDTO);

    UserDTO selectEmail(String username);

    boolean updateLastLogin(String authEmail);

    List<UserDTO> getList();

    String modify(UserDTO userDTO);

    void remove(String email);
}

 

 UserServiceImpl.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.UserDTO;
import com.ezen.boot_JPA.entity.AuthUser;
import com.ezen.boot_JPA.entity.User;
import com.ezen.boot_JPA.repository.AuthUserRepository;
import com.ezen.boot_JPA.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
    private final UserRepository userRepository;
    private final AuthUserRepository authUserRepository;

    @Override
    public String register(UserDTO userDTO) {
        String email = userRepository.save(convertDtoToEntity(userDTO)).getEmail();
        if(email != null){
            authUserRepository.save(convertDtoToAuthEntity(userDTO));
        }
        return email;
    }

    @Transactional
    @Override
    public UserDTO selectEmail(String username) {
        // findById : ID(PK)를 조건으로 해당 객체를 검색 후 리턴(Optional)
        // 내가 검색하고자 하는 조건이 ID가 아닐 경우. Repository 등록 후 사용
        // optional.isPresent() : 값이 있는지 확인 true / false
        // optional.isEmpty() : 값이 비었는지 확인 true / false
        Optional<User> optional = userRepository.findById(username);
        List<AuthUser> authUserList = authUserRepository.findByEmail(username);
        if(optional.isPresent()){
            UserDTO userDTO = convertEntityToDto(optional.get(),
                    authUserList
                            .stream()
                            .map(u -> convertEntityToAuthDto(u))
                            .toList()
            );
            log.info(">>> userDTO >>> {}", userDTO);
            return userDTO;
        }
        return null;
    }

    @Override
    public boolean updateLastLogin(String authEmail) {
        Optional<User> optional = userRepository.findById(authEmail);
        if(optional.isPresent()){
            User user = optional.get();
            // LocalDateTime.now() => 현재 날짜 시간
            user.setLastLogin(LocalDateTime.now());
            String email = userRepository.save(user).getEmail();
            return email == null ? false : true;
        }
        return false;
    }

    @Override
    public List<UserDTO> getList() {
    // findAll : select * from user;
        List<User> userList = userRepository.findAll();
        List<UserDTO> userDTOList = new ArrayList<>();
        for(User user : userList){
            List<AuthUser> authUserList = authUserRepository.findByEmail(user.getEmail());
            UserDTO userDTO = convertEntityToDto(user,
                    authUserList.stream()
                            .map(u -> convertEntityToAuthDto(u))
                            .toList());
            userDTOList.add(userDTO);
        }
        return userDTOList;
    }

    @Override
    public String modify(UserDTO userDTO) {
        Optional<User> optional = userRepository.findById(userDTO.getEmail());
        if(optional.isPresent()){
            User user = optional.get();
            if(userDTO.getPwd().length() > 0){
                user.setPwd(userDTO.getPwd());
            }
            user.setNickName(userDTO.getNickName());
            return userRepository.save(user).getEmail();
        }
        return null;
    }

    @Override
    public void remove(String email) {
        Optional <User> optional = userRepository.findById(email);
        if(optional.isPresent()){
            User user = optional.get();
            List<AuthUser> authUserList = authUserRepository.findByEmail(user.getEmail());
            for(AuthUser authUser : authUserList){
                authUserRepository.deleteById(authUser.getId());
            }
        }
        userRepository.deleteById(email);
    }

}

 

 AuthUserRepository.java

package com.ezen.boot_JPA.repository;

import com.ezen.boot_JPA.entity.AuthUser;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface AuthUserRepository extends JpaRepository<AuthUser, Long> {
    List<AuthUser> findByEmail(String email);

}

 

 UserRepositroy.java

package com.ezen.boot_JPA.repository;

import com.ezen.boot_JPA.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String> {
}

 

 AuthMember.java

package com.ezen.boot_JPA.security;

import com.ezen.boot_JPA.dto.UserDTO;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;
import java.util.stream.Collectors;

@Slf4j
@Getter
public class AuthMember extends User {

    private UserDTO userDTO;

    public AuthMember(String username, String password, Collection<? extends GrantedAuthority> authorities, UserDTO userDTO) {
        super(username, password, authorities);
        this.userDTO = userDTO;
    }

    public AuthMember(UserDTO userDTO) {
        super(userDTO.getEmail(), userDTO.getPwd(),
                userDTO.getAuthList()
                        .stream()
                        .map(auth -> new SimpleGrantedAuthority(auth.getAuth()))
                        .collect(Collectors.toList()));
        this.userDTO = userDTO;
    }
}

 

 header.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extra/spring-security">
<!--/* th:fragment="이름" : 레이아웃에서 사용할 조각 */-->
<div th:fragment="header">
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">Boot</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="/">Home</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/board/register">BoardRegister</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/board/list">BoardList</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAnonymous()">
                        <a class="nav-link" href="/user/join">Join</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAnonymous()">
                        <a class="nav-link" href="/user/login">Login</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/user/logout">Logout</a>
                    </li>
                    <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                        <a class="nav-link" href="/user/list">UserList</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/user/modify">[[${#authentication.name}]]회원정보수정</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
</div>

 

 join.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <div class="container-md">
        <h1>User join Page</h1>
        <hr>
        <form action="/user/join" method="post">
            <div class="mb-3">
                <label for="e" class="form-label">Email</label>
                <input type="text" class="form-control" name="email" id="e" placeholder="email...">
            </div>
            <div class="mb-3">
                <label for="p" class="form-label">Password</label>
                <input type="text" class="form-control" name="pwd" id="p" placeholder="password...">
            </div>
            <div class="mb-3">
                <label for="n" class="form-label">Nickname</label>
                <input type="text" class="form-control" name="nickName" id="n" placeholder="nickname...">
            </div>
            <button type="submit" class="btn btn-primary">submit</button>
        </form>
    </div>
</div>

 

 login.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <div class="container-md">
        <h1>User login Page</h1>
        <hr>
        <form action="/user/login" method="post">
            <div class="mb-3">
                <label for="e" class="form-label">Email</label>
                <input type="text" class="form-control" name="email" id="e" placeholder="email...">
            </div>
            <div class="mb-3">
                <label for="p" class="form-label">Password</label>
                <input type="text" class="form-control" name="pwd" id="p" placeholder="password...">
            </div>
            <div class="mb-3" th:if="${error}">
                <p class="text-danger">[[${exception}]]</p>
            </div>
            <button type="submit" class="btn btn-primary">submit</button>
        </form>
    </div>
</div>

 

 list.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <h1>User List Page</h1>
    <hr>
    <div class="col" th:each="uvo:${userList}">
        <div class="card border-dark mb-3" style="max-width: 540px;">
            <div class="row g-0">
                <div class="col-md-4">
                    <img src="/image/boo.png" class="img-fluid rounded-start" alt="...">
                </div>
                <div class="col-md-8">
                    <div class="card-body">
                        <h5 class="card-title">[[${uvo.nickName}]]</h5>
                        <p class="card-text">[[${uvo.email}]] ([[${uvo.regAt}]]) </p>
                        <p class="card-text">Last Login : [[${uvo.lastLogin}]]</p>

                        <span class="badge rounded-pill text-bg-success" th:each="authList:${uvo.authList}">[[${authList.auth}]]</span>

                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

 

 modify.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content">
  <h1>User List Page</h1>
  <hr>
  <div class="mt-4 p-5 rounded" th:with="auth=${#authentication.getPrincipal()}">
    <div class="row">
      <div class="col">
        <div class="card border-dark mb-3" style="max-width: 540px;" >
          <div class="row g-0">
            <div class="col-md-4">
              <img src="/image/boo.png" class="img-fluid rounded-start" alt="...">
            </div>
            <div class="col-md-8">
              <form action="/user/modify" method="post">
                <h5 class="card-body">
                  <input type="email" name="email" class="form-control" th:value="${userDTO.email}" readonly>
                  <input type="text" name="nickName" class="form-control"  th:value="${userDTO.nickName}">
                  <input type="text" name="pwd" class="form-control" placeholder="Enter Password">
                </h5>
                <span class="badge rounded-pill text-bg-success" th:each="authList:${userDTO.authList}">[[${authList.auth}]]</span>
                <button type="submit" class="btn btn-primary btn-sm">modify</button>
                <a th:href="@{/user/remove(email=${userDTO.email})}"><button type="button" class="btn btn-danger btn-sm">delete</button></a>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

 

출력