본문 바로가기

카테고리 없음

데이터베이스 mysql 회원가입, 로그인, 필터, 스웨거 안쓰고 웹이랑 연결해 로그인

회원가입을 구현하기 위해 먼저 빌드그래들에

// JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// MySQL
    runtimeOnly 'com.mysql:mysql-connector-j'

어플레케이션에

#회원가입용
spring.datasource.url=jdbc:mysql://localhost:3306/auth
spring.datasource.username=root
spring.datasource.password={비밀번호}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

비밀번호는 자기가 쓰는걸로 넣으면 된다. 만약 그렇지 않으면

java.lang.NullPointerException: Cannot invoke "org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)" because the return value of "org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()" is null  

이라고 나온다.

 

그리고 mysql commend line로 가서 CREATE DATABASE auth;로 데이터베이스를 생성한뒤

인텔리제이로 가서

이런식으로 설정해준다.

 그리고

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("username", "username");
        return "index";
    }
}
@Controller
@RequestMapping("/api")
public class UserController {

    @GetMapping("/user/login-page")
    public String loginPage() {
        return "login";
    }

    @GetMapping("/user/signup")
    public String signupPage() {
        return "signup";
    }
}

이렇게 컨트롤러를 두개를 만들어준다.

 

나누는 이유는 하나는 HomeController는 홈페이지를 제어하고, UserController는 사용자 관련 페이지(로그인, 회원가입 등)를 제어하도록 해서 기능별로 나눔으로서 가독성, 유지보수성을 높이기 위해서이다.

 

그리고 src > main > resources > templates 아래에

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="preconnect" href="https://fonts.gstatic.com">

    <link rel="stylesheet" href="/css/style.css">
    <script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
    <script src="/js/basic.js"></script>
    <title>나만의 셀렉샵</title>
</head>
<body>
<div class="header" style="position:relative;">
    <div id="login-true" style="display: none">
        <div id="header-title-login-user">
            <span th:text="${username}"></span> 님의
        </div>
        <div id="header-title-select-shop">
            Select Shop
        </div>
        <a id="login-text" href="javascript:logout()">
            로그아웃
        </a>
    </div>
    <div id="login-false" >
        <div id="header-title-select-shop">
            My Select Shop
        </div>
        <a id="sign-text" href="/api/user/signup">
            회원가입
        </a>
        <a id="login-text" href="/api/user/login-page">
            로그인
        </a>
    </div>
</div>
</body>
</html>

 

이렇게 index

 

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" type="text/css" href="/css/style.css">
    <meta charset="UTF-8">
    <title>로그인 페이지</title>
</head>
<body>
<div id="login-form">
    <div id="login-title">Log into Select Shop</div>
    <br>
    <br>
    <button id="login-id-btn" onclick="location.href='/api/user/signup'">
        회원 가입하기
    </button>
    <form action="/api/user/login" method="post">
        <div class="login-id-label">아이디</div>
        <input type="text" name="username" class="login-input-box">

        <div class="login-id-label">비밀번호</div>
        <input type="password" name="password" class="login-input-box">

        <button id="login-id-submit">로그인</button>
    </form>
    <div id="login-failed" style="display: none" class="alert alert-danger" role="alert">로그인에 실패하였습니다.</div>
</div>
</body>
<script>
    const href = location.href;
    const queryString = href.substring(href.indexOf("?")+1)
    if (queryString === 'error') {
        const errorDiv = document.getElementById('login-failed');
        errorDiv.style.display = 'block';
    }
</script>
</html>

 

 

login

 

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" type="text/css" href="/css/style.css">
    <meta charset="UTF-8">
    <title>회원가입 페이지</title>
    <script>
        function onclickAdmin() {
            // Get the checkbox
            var checkBox = document.getElementById("admin-check");
            // Get the output text
            var box = document.getElementById("admin-token");

            // If the checkbox is checked, display the output text
            if (checkBox.checked == true){
                box.style.display = "block";
            } else {
                box.style.display = "none";
            }
        }
    </script>
</head>
<body>
<div id="login-form">
    <div id="login-title">Sign up Select Shop</div>

    <form action="/api/user/signup" method="post">
        <div class="login-id-label">Username</div>
        <input type="text" name="username" placeholder="Username" class="login-input-box">

        <div class="login-id-label">Password</div>
        <input type="password" name="password" class="login-input-box">

        <div class="login-id-label">E-mail</div>
        <input type="text" name="email" placeholder="E-mail" class="login-input-box">

        <div>
            <input id="admin-check" type="checkbox" name="admin" onclick="onclickAdmin()" style="margin-top: 40px;">관리자
            <input id="admin-token" type="password" name="adminToken" placeholder="관리자 암호" class="login-input-box" style="display:none">
        </div>
        <button id="login-id-submit">회원 가입</button>
    </form>
</div>
</body>
</html>

 

signup를 만들어준다.

 

그리고

src > main > resources > static 에 css, js directory파일을, 즉 경로를 선택해서 만들고

css파일 안쪽에 스타일시트를 새로 추가해준다. 이렇게

* {
    font-family: 'Georgia', serif;
}

body {
    margin: 0px;
}

.header {
    height: 255px;
    box-sizing: border-box;
    background-color: #15aabf;
    color: white;
    text-align: center;
    padding-top: 80px;
    /*padding: 50px;*/
    font-size: 45px;
    font-weight: bold;
}

#header-title-login-user {
    font-size: 36px;
    letter-spacing: -1.08px;
}

#header-title-select-shop {
    margin-top: 20px;
    font-size: 45px;
    letter-spacing: 1.1px;
}

#login-form {
    width: 538px;
    height: 710px;
    margin: 70px auto 141px auto;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    /*gap: 96px;*/
    padding: 56px 0 0;
    border-radius: 10px;
    box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.15);
    background-color: #ffffff;
}

#login-title {
    width: 303px;
    height: 32px;
    /*margin: 56px auto auto auto;*/
    flex-grow: 0;
    font-family: SpoqaHanSansNeo;
    font-size: 32px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: -0.96px;
    text-align: left;
    color: #212529;
}

#login-kakao-btn {
    border-width: 0;
    margin: 96px 0 8px;
    width: 393px;
    height: 62px;
    flex-grow: 0;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 10px;
    /*margin: 0 0 8px;*/
    padding: 11px 12px;
    border-radius: 5px;
    background-color: #ffd43b;

    font-family: SpoqaHanSansNeo;
    font-size: 20px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    color: #414141;
}

#login-id-btn {
    width: 393px;
    height: 62px;
    flex-grow: 0;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 10px;
    /*margin: 8px 0 0;*/
    padding: 11px 12px;
    border-radius: 5px;
    border: solid 1px #212529;
    background-color: #ffffff;

    font-family: SpoqaHanSansNeo;
    font-size: 20px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    color: #414141;
}

.login-input-box {
    border-width: 0;

    width: 370px !important;
    height: 52px;
    margin: 14px 0 0;
    border-radius: 5px;
    background-color: #e9ecef;
}

.login-id-label {
    /*width: 44.1px;*/
    /*height: 16px;*/
    width: 382px;
    padding-left: 11px;
    margin-top: 40px;
    /*margin: 0 337.9px 14px 11px;*/
    font-family: NotoSansCJKKR;
    font-size: 16px;
    font-weight: normal;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: -0.8px;
    text-align: left;
    color: #212529;
}

#login-id-submit {
    border-width: 0;
    width: 393px;
    height: 62px;
    flex-grow: 0;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 10px;
    margin: 40px 0 0;
    padding: 11px 12px;
    border-radius: 5px;
    background-color: #15aabf;

    font-family: SpoqaHanSansNeo;
    font-size: 20px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: normal;
    text-align: center;
    color: #ffffff;
}

#sign-text {
    position:absolute;
    top:48px;
    right:110px;
    font-size: 18px;
    font-family: SpoqaHanSansNeo;
    font-size: 18px;
    font-weight: 500;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: 0.36px;
    text-align: center;
    color: #ffffff;
}

#login-text {
    position:absolute;
    top:48px;
    right:50px;
    font-size: 18px;
    font-family: SpoqaHanSansNeo;
    font-size: 18px;
    font-weight: 500;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: 0.36px;
    text-align: center;
    color: #ffffff;
}

.alert-danger {
    color: #721c24;
    background-color: #f8d7da;
    border-color: #f5c6cb;
}

.alert {
    width: 300px;
    margin-top: 22px;
    padding: 1.75rem 1.25rem;
    border: 1px solid transparent;
    border-radius: .25rem;
}

 

그리고 basic에는 자바스크립트 파일을 만든다.

let host = 'http://' + window.location.host;

$(document).ready(function () {
    const auth = getToken();
    if(auth === '') {
        window.location.href = host + "/api/user/login-page";
    } else {
        $('#login-true').show();
        $('#login-false').hide();
    }
})

function logout() {
    // 토큰 삭제
    Cookies.remove('Authorization', { path: '/' });
    window.location.href = host + "/api/user/login-page";
}

function getToken() {
    let auth = Cookies.get('Authorization');

    if(auth === undefined) {
        return '';
    }

    return auth;
}

 

 

이렇게 하고

localhost:8080

로들어가면

 

이렇게 나온다.

 

이제 유저를 만든다.

 

package com.teamsparta.springauth.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;
}

 

 

그리고

package com.teamsparta.springauth.service;


import com.teamsparta.springauth.dto.SignupRequestDto;
import com.teamsparta.springauth.entity.User;
import com.teamsparta.springauth.entity.UserRoleEnum;
import com.teamsparta.springauth.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    // ADMIN_TOKEN
    private final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";

    public void signup(SignupRequestDto requestDto) {
        String username = requestDto.getUsername();
        String password = passwordEncoder.encode(requestDto.getPassword());

        // 회원 중복 확인
        Optional<User> checkUsername = userRepository.findByUsername(username);
        if (checkUsername.isPresent()) {
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

        // email 중복확인
        String email = requestDto.getEmail();
        Optional<User> checkEmail = userRepository.findByEmail(email);
        if (checkEmail.isPresent()) {
            throw new IllegalArgumentException("중복된 Email 입니다.");
        }

        // 사용자 ROLE 확인
        UserRoleEnum role = UserRoleEnum.USER;
        if (requestDto.isAdmin()) {
            if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) {
                throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
            }
            role = UserRoleEnum.ADMIN;
        }

        // 사용자 등록
        User user = new User(username, password, email, role);
        userRepository.save(user);
    }
}

 

이렇게 서비스를 만들어주지만, 그것만으론 작동하지 않기에 리포지터리를 만든다.

package com.teamsparta.springauth.repository;

import com.teamsparta.springauth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    Optional<User> findByEmail(String email);
}

 

그리고 회원가입을 구현하기 위해

@Getter
@Setter
public class SignupRequestDto {
    private String username;
    private String password;
    private String email;
    private boolean admin = false;
    private String adminToken = "";
}

 

 

@PostMapping("/user/signup")
    public String signup(SignupRequestDto requestDto) {
        userService.signup(requestDto);
        return "redirect:/api/user/login-page";
    }

 

이것들을 추가해준다. 

그리고 유저 엔티티는

public User(String username, String password, String email, UserRoleEnum role) {
    this.username = username;
    this.password = password;
    this.email = email;
    this.role = role;
}

 

이걸 추가해야 서비스의

User user = new User(username, password, email, role);
userRepository.save(user);

 

이부분을 작동시킬수있다.

 

그럼 이제 회원가입이 작동한다.

 

 

여기서 가입을 하면

이렇게 가입이 된다. 

 

만약 관리자로 로그인을 하려면

AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC

 

이걸 넣어주면

이렇게 멀쩡히 관리자 가입이 된다.

 

로그인은

 

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class LoginRequestDto {
    private String username;
    private String password;
}

 

이걸 추가하고 서비스도 바꾼다.

 

 

package com.teamsparta.springauth.service;


import com.teamsparta.springauth.dto.LoginRequestDto;
import com.teamsparta.springauth.dto.SignupRequestDto;
import com.teamsparta.springauth.entity.User;
import com.teamsparta.springauth.entity.UserRoleEnum;
import com.teamsparta.springauth.jwt.JwtUtil;
import com.teamsparta.springauth.repository.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;//로그인을 위해 추가

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtUtil = jwtUtil;//여기도
    }

    // ADMIN_TOKEN
    private final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";

    public void signup(SignupRequestDto requestDto) {
        String username = requestDto.getUsername();
        String password = passwordEncoder.encode(requestDto.getPassword());

        // 회원 중복 확인
        Optional<User> checkUsername = userRepository.findByUsername(username);
        if (checkUsername.isPresent()) {
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

        // email 중복확인
        String email = requestDto.getEmail();
        Optional<User> checkEmail = userRepository.findByEmail(email);
        if (checkEmail.isPresent()) {
            throw new IllegalArgumentException("중복된 Email 입니다.");
        }

        // 사용자 ROLE 확인
        UserRoleEnum role = UserRoleEnum.USER;
        if (requestDto.isAdmin()) {
            if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) {
                throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
            }
            role = UserRoleEnum.ADMIN;
        }

        // 사용자 등록
        User user = new User(username, password, email, role);
        userRepository.save(user);
    }
    //로그인 기능 구현

    public void login(LoginRequestDto requestDto, HttpServletResponse res) {
        String username = requestDto.getUsername();
        String password = requestDto.getPassword();

        // 사용자 확인
        User user = userRepository.findByUsername(username).orElseThrow(
                () -> new IllegalArgumentException("등록된 사용자가 없습니다.")
        );

        // 비밀번호 확인
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        // JWT 생성 및 쿠키에 저장 후 Response 객체에 추가
        String token = jwtUtil.createToken(user.getUsername(), user.getRole());
        jwtUtil.addJwtToCookie(token, res);
    }
}

 

 

컨트롤러는

@PostMapping("/user/login")
public String login(LoginRequestDto requestDto, HttpServletResponse res) {
    try{
        userService.login(requestDto, res);
    }catch (Exception e){
        return "redirect:/api/user/login-page?error";//예외가 발생하면 login-page?error 경로로 리다이렉트하여 사용자에게 오류 메시지
    }
    return"redirect:/";//로그인 성공 시 / 경로로 리다이렉트하여 메인 페이지로 이동
}

 

이렇게 로그인 실패시를 위한 예외도 넣어준다.

 

그리고 앱을 가동시켜 로그인을 하면

 

이렇게 메인화면이 잘 뜬다.

 

그다음엔 필터를 만든다.

 

package com.teamsparta.springauth.filter;

import com.teamsparta.springauth.entity.User;
import com.teamsparta.springauth.jwt.JwtUtil;
import com.teamsparta.springauth.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;

@Slf4j(topic = "AuthFilter")
@Component
@Order(2)
public class AuthFilter implements Filter {

    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;

    public AuthFilter(UserRepository userRepository, JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String url = httpServletRequest.getRequestURI();

        if (StringUtils.hasText(url) &&
                (url.startsWith("/api/user") || url.startsWith("/css") || url.startsWith("/js"))
        ) {
            // 회원가입, 로그인 관련 API 는 인증 필요없이 요청 진행
            chain.doFilter(request, response); // 다음 Filter 로 이동
        } else {
            // 나머지 API 요청은 인증 처리 진행
            // 토큰 확인
            String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest);

            if (StringUtils.hasText(tokenValue)) { // 토큰이 존재하면 검증 시작
                // JWT 토큰 substring
                String token = jwtUtil.substringToken(tokenValue);

                // 토큰 검증
                if (!jwtUtil.validateToken(token)) {
                    throw new IllegalArgumentException("Token Error");
                }

                // 토큰에서 사용자 정보 가져오기
                Claims info = jwtUtil.getUserInfoFromToken(token);

                User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
                        new NullPointerException("Not Found User")
                );

                request.setAttribute("user", user);
                chain.doFilter(request, response); // 다음 Filter 로 이동
            } else {
                throw new IllegalArgumentException("Not Found Token");
            }
        }
    }

}

 

 

package com.teamsparta.springauth.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j(topic = "LoggingFilter")
@Component
@Order(1)
public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 전처리
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String url = httpServletRequest.getRequestURI();
        log.info(url);

        chain.doFilter(request, response); // 다음 Filter 로 이동

        // 후처리
        log.info("비즈니스 로직 완료");
    }
}
  • @Order(1) 로 필터의 순서를 지정합니다.
  • chain.doFilter(request, response); 다음 Filter로 이동시킵니다.
  • log.info("비즈니스 로직 완료");
    • 작업이 다 완료된 후 Client에 응답 전 로그가 작성된 것을 확인할 수 있습니다.

 

그리고 JwtUtil에

public String getTokenFromRequest(HttpServletRequest req) {
    Cookie[] cookies = req.getCookies();
    if(cookies != null) {
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                try {
                    return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                } catch (UnsupportedEncodingException e) {
                    return null;
                }
            }
        }
    }
    return null;
}

 

이걸 추가한다.