1. 회원가입 구현하기
먼저, 회원가입을 구현하기 위해 테이블부터 생성합니다.
CREATE TABLE `spb_member` (
`member_id` varchar(30) NOT NULL COMMENT '아이디',
`member_pw` varchar(300) DEFAULT NULL COMMENT '비밀번호',
`member_name` varchar(100) DEFAULT NULL COMMENT '이름',
`member_contact` varchar(50) NOT NULL COMMENT '연락처',
`signup_time` datetime DEFAULT NULL COMMENT '가입시간',
`deleted_yn` char(1) NOT NULL DEFAULT 'N' COMMENT '탈퇴 여부',
PRIMARY KEY (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
중복된 아이디가 존재할 순 없으므로, member_id를 PK로 정해줬습니다.
다음으로 회원가입 화면을 살펴봅니다.
localhost:8080/signin
부트스트랩 템플릿에서 가져온 화면을 조금 수정했습니다.
/templates/ubooks/pages/signin.html
<!--
THEME: Aviato | E-commerce template
VERSION: 1.0.0
AUTHOR: Themefisher
HOMEPAGE: https://themefisher.com/products/aviato-e-commerce-template/
DEMO: https://demo.themefisher.com/aviato/
GITHUB: https://github.com/themefisher/Aviato-E-Commerce-Template/
WEBSITE: https://themefisher.com
TWITTER: https://twitter.com/themefisher
FACEBOOK: https://www.facebook.com/themefisher
-->
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<!-- 공통 헤드-->
<th:block th:replace="/ubooks/common/header :: headFragment"></th:block>
<body id="body">
<section class="signin-page account">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="block text-center">
<a class="logo" href="/index">
<img th:src="@{images/ubooks/logo.png}" alt="">
</a>
<h2 class="text-center">Create Your Account</h2>
<form id="signinForm" name="signinForm" class="text-left clearfix" action="/signin" method="post">
<div>
<div class="form-group form-inline">
<input id="memberId" name="memberId" type="text" class="form-control" placeholder="아이디" style="width:72.9%">
<button id="idCheckBtn" type="button" class="btn btn-main text-center">ID check</button>
</div>
</div>
<div class="form-group">
<input id="memberPw" name="memberPw" type="password" class="form-control" placeholder="비밀번호">
</div>
<div class="form-group">
<input id="pwCheck" name="pwCheck" type="password" class="form-control" placeholder="비밀번호 확인">
</div>
<div class="form-group">
<input id="memberName" name="memberName" type="text" class="form-control" placeholder="이름">
</div>
<div class="form-group">
<input id="memberContact" name="memberContact" type="text" class="form-control" placeholder="연락처">
</div>
<div id="alert" style="display:none;" class="alert alert-warning alert-common" role="alert">
<i class="tf-ion-alert-circled"></i><span id="alertType">Warning!</span> <span id="alertMessage">Better check yourself.You are not looking too good</span>
</div>
<div class="text-center">
<button id="signinBtn" name="signinBtn" type="button" class="btn btn-main text-center" disabled>Sign In</button>
</div>
</form>
<p class="mt-20">이미 계정이 있으신가요?<a href="/login"><i class="tf-ion-log-in"> </i> 로그인</a></p>
<p><a href="/forget-password"> Forgot your password?</a></p>
</div>
</div>
</div>
</div>
</section>
<!-- 공통 스크립트-->
<th:block th:replace="/ubooks/common/script :: scriptFragment"></th:block>
</body>
</html>
테이블과 이름을 맞춰서 input의 name을 설정했습니다. form의 action 은 똑같이 /signin이지만 method를 POST로 하여서 컨트롤러 단에서 회원가입을 하는 화면을 불러오는 메서드와 회원가입을 처리하는 메서드를 분리할 수 있습니다.
이제 DTO를 만들어봅니다.
MemberDto.java
package spb.ubooks.dto;
import lombok.Data;
@Data
public class MemberDto {
String memberId;
String memberPw;
String memberName;
String memberContact;
String signupTime;
String deletedYn;
}
DTO를 만들었으니, Controller를 만들어봅니다.
UbooksController.java
@RequestMapping(value = "/signin", method = RequestMethod.GET)
public ModelAndView ubooksSignin() throws Exception {
ModelAndView mv = new ModelAndView("/ubooks/pages/signin");
return mv;
}
@RequestMapping(value = "/signin", method = RequestMethod.POST)
public String insertMember(MemberDto member) throws Exception {
memberService.insertMember(member);
return "redirect:/login";
}
/signin 의 GET방식은 단순히 화면을 표시하는 메서드이고, POST방식은 유저로부터 받은 회원 정보를 DB에 insert 하고 /login으로 redirect 합니다. 이제 service부분을 만들어봅니다.
MemberService.java
package spb.ubooks.service;
import javax.servlet.http.HttpServletRequest;
import spb.ubooks.dto.MemberDto;
public interface MemberService {
public void insertMember(MemberDto member) throws Exception;
}
MemberServiceImpl.java
@Service
@Slf4j
public class MemberServiceImpl implements MemberService{
@Autowired
private MemberMapper memberMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void insertMember(MemberDto member) throws Exception {
if(memberMapper.selectIdCheck(member.getMemberId()) > 0) { // 이미 아이디가 존재할 때 (front 에서 선처리해서 일반적인 방법으로는 탈 일이 없다.)
throw new DuplicatedIdException("아이디가 중복되어 회원가입에 실패했습니다.");
} else {
member.setMemberPw(passwordEncoder.encode(member.getMemberPw())); // BCryptPasswordEncoder
memberMapper.insertMember(member);
}
}
}
impl 코드를 살펴보면 PasswordEncoder, memberMapper.selectIdCheck, insertMember 와 같은 아직 구현하지 않은 소스들이 많습니다. 하나하나 살펴보겠습니다.
먼저 PasswordEncoder.encode 메서드는 비밀번호를 암호화 하기 위해 사용합니다. PasswordEncoder를 사용하기 위해선 Spring Security를 build.gradle에 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-security'
그다음, @Configuration 어노테이션을 사용하여 클래스를 만들어줍니다.
SecurityConfiguration.java
package spb.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().disable() // cors 비활성화
.csrf().disable() // csrf 비활성화
.formLogin().disable() //기본 로그인 페이지 없애기
.headers().frameOptions().disable();
}
}
WebSecurityConfigurerAdapter를 상송받아서 클래스를 만듭니다.
@EnableWebSecurity 어노테이션은 스프링 부트를 사용하기 위한 어노테이션입니다.
getPasswordEncoder 빈은 PasswordEncoder를 return 하는데 PasswordEncoder는 4가지 종류가 지원됩니다.
BcryptPasswordEncoder | Bcrypt 해시 함수를 사용하여 비밀번호 암호화 |
Argon2PasswordEncoder | Argon2 해시 함수를 사용하여 비밀번호 암호화 |
Pbkdf2PasswordEncoder | Pbkdf2 해시 함수를 사용하여 비밀번호 암호화 |
ScryptPasswordEncoder | Scrypt 해시 함수를 사용하여 비멀번호 암호화 |
저는 BcryptPasswordEncoder를 사용하여 개발해 보겠습니다.
다음으로 Mapper를 설정합니다.
MemberMapper.java
@Mapper
public interface MemberMapper {
public int selectIdCheck(String memberId) throws Exception;
public void insertMember(MemberDto member) throws Exception;
}
sql-member.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="spb.ubooks.mapper.MemberMapper">
<select id="selectIdCheck" parameterType="string" resultType="int">
<![CDATA[
select count(*) from spb_member where member_id=#{memberId} and deleted_yn = 'N'
]]>
</select>
<insert id="insertMember" parameterType="spb.ubooks.dto.MemberDto">
<![CDATA[
insert into spb_member
(
member_id,
member_pw,
member_name,
member_contact,
signup_time
)
values
(
#{memberId},
#{memberPw},
#{memberName},
#{memberContact},
NOW()
)
]]>
</insert>
</mapper>
selectIdCheck는 파라미터로 받은 아이디와 동일한 아이디가 DB에 존재하는지 확인을 위해 그 개수를 return 합니다. 없다면 0이 return 되겠죠??
insertMember는 회원 가입을 위해 파라미터로 받은 MemberDto로 들어온 값을 DB에 insert합니다. signup_time은 NOW() 함수를 이용해 현재 시간이 insert 되도록 합니다.
다음으로는 DuplicatedIdException입니다.
DuplicatedIdException.java
package spb.ubooks.exception;
public class DuplicatedIdException extends Exception{
/**
* 사용자 정의 에러 - 아이디 중복 에러
*/
private static final long serialVersionUID = 1L;
public DuplicatedIdException() {};
public DuplicatedIdException(String message) {
super(message);
}
}
Exception 클래스를 상속받아서 사용자 정의 Exception을 만듭니다. 생성자로 message를 받습니다.
결과 확인하기
memberId = signinTest
memberPw = 1234 로 input들을 채워 넣고 submit 버튼을 클릭하여 결과를 확인해 보겠습니다.
select * from spb_member where member_id='signinTest';
성공적으로 회원가입이 완료되었습니다. 이제 로그인 및 로그아웃을 구현해보겠습니다.
2. 로그인 구현하기
Controller 부터 살펴봅시다.
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView ubooksLogin() throws Exception {
ModelAndView mv = new ModelAndView("/ubooks/pages/login");
return mv;
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String memberLogin(MemberDto member, HttpServletRequest request) throws Exception {
return memberService.loginMember(member, request);
}
로그인 페이지의 url은 /login입니다. GET방식의 메서드는 단순 화면을 보여주는 메서드이고, POST방식의 메서드에서는 로그인 로직을 처리합니다.
이제 서비스 부분을 살펴봅니다.
MemberService.java에 loginMember를 추가합니다.
public String loginMember(MemberDto member, HttpServletRequest request) throws Exception;
유저로부터 입력받은 id, pw를 받을 MemberDto, 로그인 성공 시 session에 값을 등록하기 위한 HttpServletRequest를 파라미터로 추가하였습니다.
MemberServiceImpl.java
@Override
public String loginMember(MemberDto member, HttpServletRequest request) throws Exception {
MemberDto m = memberMapper.selectMemberCheck(member.getMemberId());
if(m!= null) {
if(passwordEncoder.matches(member.getMemberPw(), m.getMemberPw())) { // 로그인 성공
HttpSession session = request.getSession();
session.setAttribute("memberId", m.getMemberId());
session.setAttribute("memberName", m.getMemberName());
return "redirect:/";
}
}
return "redirect:/loginFail";
}
먼저, selectMemberCheck를 mapper에 등록시켜야합니다. selectMemberCheck를 사용하여 아이디와 매칭 되는 row를 가져옵니다.
selectMemberCheck의 결과가 있다면, passwordEncoder.matches(평문 비밀번호, 암호화된 비밀번호) 메서드를 이용하여 유저로부터 입력된 평문 비밀번호와 DB에 저장된 암호화된 비밀번호를 비교합니다. 비밀번호가 일치한다면 세션에 값을 등록하고 redirect:/를 리턴하여 로그인이 성공할 시에는 홈주소로 이동합니다. 로그인이 실패한다면 /loginFail로 이동합니다.
이제 mapper를 살펴봅니다.
MemberMapper.java에 다음 코드를 추가합니다.
public MemberDto selectMemberCheck(String memberId) throws Exception;
sql-member.xml에도 다음 select를 추가합니다.
<select id="selectMemberCheck" parameterType="string" resultType="spb.ubooks.dto.MemberDto">
<![CDATA[
select * from spb_member where member_id=#{memberId} and deleted_yn = 'N'
]]>
</select>
로그인에 실패할 경우의 로직을 위해 컨트롤러 및 뷰를 추가합니다.
@RequestMapping(value = "/loginFail")
public ModelAndView memberLoginFail() throws Exception {
ModelAndView mv = new ModelAndView("/ubooks/pages/loginFail");
return mv;
}
단순 html페이지를 열어주는 메서드입니다.
loginFail.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<!-- 공통 헤드-->
<th:block th:replace="/ubooks/common/header :: headFragment"></th:block>
<body id="body">
<script>
alert("아이디 또는 비밀번호가 오류")
location.href="/login";
</script>
<!-- 공통 스크립트-->
<th:block th:replace="/ubooks/common/script :: scriptFragment"></th:block>
로그인에 실패시에는 javascript를 이용하여 오류 메시지를 사용자에게 알려주고, 다시 login화면으로 이동합니다.
결과 확인하기
로그인 성공시
정상적으로 홈 화면으로 이동하였고, 세션에 값도 설정한 대로 잘 들어있습니다.
로그인 실패 시
loginFail로 이동하였고, alert 메시지가 떴습니다. 확인을 클릭하면 다시 login 페이지로 이동합니다.
3. 로그아웃 구현하기
저희는 Spring Security를 추가했기 때문에 로그아웃은 단순히 링크만 걸어주면 됩니다.
<a href="/logout"><i class="tf-ion-log-out"></i>로그아웃</a>
spring security에서 기본값으로 /logout 경로를 요청하면 /login?logout url이 리턴되면서 로그아웃이 됩니다.
결과 확인하기
홈으로 돌아가서 session의 값이 잘 지워졌나 확인해봅니다.
정상적으로 로그아웃 되었습니다
'Backend-Programming > Spring Boot' 카테고리의 다른 글
SpringBoot - data - mongodb(MongoRepository)를 사용한 몽고디비 CRUD (0) | 2022.03.26 |
---|---|
Spring Boot - JPA를 사용하여 게시판구현하기 (0) | 2021.10.08 |
Spring Boot - 스프링 데이터 JPA 사용하기(MariaDB) (0) | 2021.10.07 |
Spring Boot - AOP사용하기(Aspect Oriented Programming) (0) | 2021.10.01 |