내가 생각하고 있던 로그인 방법은
1. controller에서 HttpSession 인터페이스를 이용해서 직접 세션값을 구현
2.인터셉터를 이용해서 세션값을 확인하는 방법으로 구현.
3.spring 시큐리티를 이용해서 필터로 로그인처리를 구현하고, 보안까지 잡는 방법이다.
원래는 스프링시큐리티를 이용해서 이번 프로젝트를 해보려고했는데 반나절정도 시간을 들여서 공부하고나니
음.. 할 수는 있겠지만 조금 헤맬거같은 느낌이 확 들길래 좀 더 숙련도를 쌓고 로직에 대한 이해를 하고
다음에 스프링부트로 프로젝트를 만들때, 스프링 시큐리티를 활용하기로 했다.
그래서 2번, 인터셉터를 이용해서 로그인을 구현해보려고 한다..
중복체크나 이메일인증을 하다가 입력한 정보가 다 날아가버리면 웹사이트를 닫고 욕하며 나가버리는 사태가 많이 발생하니(경험상) 두 부분은 비동기방식으로 구현해 볼 계획이다.
일단 기능에는
아이디, 비밀번호, 닉네임, 이메일을 넣으려고 한다.
후에 회원별 추천기능과,중보추천 제한,회원별 추천을 받은 총 개수(OKKY시스템처럼), 게시글 추천 수 정렬등..
추천에 관한 기능을 넣을 예정인데,
일단 기본적인 것부터 구현하고 나중에 추천시스템을 구현할때 한번에 처리하도록 해보려고 한다.
테이블 생성
create table MP_MEMBER (
member_id VARCHAR2(50) NOT NULL,
member_pw VARCHAR2(100) NOT NULL,
member_name VARCHAR2(100) NOT NULL,
member_email VARCHAR2(50) NOT NULL,
member_id_yn varchar(20),
session_key VARCHAR2(50) DEFAULT 'none' ,
session_limit TIMESTAMP,
member_join_date TIMESTAMP,
member_point INT DEFAULT 0,
member_img VARCHAR2(1000),
PRIMARY KEY(member_id)
);
일단 나중에 기능구현에 필요한 속성들은 따로넣으면 귀찮으니 null을 허용해서 미리 넣어줬다.
당장 필요한 부분만 not null로 설정해놓았다.
member_id_yn은 아이디 중복체크를 할 때 사용할 계획이다.
MemberVO
public class MemberVO{
private String memberId;
private String memberPw;
private String memberName;
private String memberEmail;
private Date memberJoinDate;
private String memberImg;
private int memberPoint;
private String memberId_yn;
//getter&setter 생성
}
<?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="memberMapper">
<insert id="register">
INSERT INTO MP_MEMBER(
MEMBER_ID,
MEMBER_PW,
MEMBER_NAME,
MEMBER_EMAIL,
MEMBER_ID_YN
) VALUES(
#{memberId},
#{memberPw},
#{memberName},
#{memberEmail},
#{memberId_yn}
)
</insert>
<select id="idCnt" parameterType="kr.co.vo.MemberVO" resultType="java.lang.Integer">
<![CDATA[
select
count(*)
from
MP_MEMBER
where
member_Id = #{memberId}
]]>
</select>
idCnt는 ID가 존재하면 Count한 int값 1을 (중복을 막아놓으면 1개밖에 생성하지 못하기 때문) 결과값으로 주고
그 값이 1이면 ajax부분에서 아이디가 중복됐다는 alert창을 띄워서 유효성 검사를 할 예정이다.
이건 뒤에서 설명하겠다.
DAO
public interface MemberDAO {
public void register(MemberVO memberVO) throws Exception;
public int idCnt(MemberVO memberVO)throws Exception;
}
@Repository
public class MemberDAOImpl implements MemberDAO {
@Inject
private SqlSession sqlsession;
@Override
public void register(MemberVO memberVO) throws Exception{
sqlsession.insert("memberMapper.register", memberVO);
}
@Override
public int idCnt(MemberVO memberVO)throws Exception{
return sqlsession.selectOne("memberMapper.idCnt", memberVO);
}
}
Service
public interface MemberService {
public void register(MemberVO memberVO) throws Exception;
public int idCnt(MemberVO memberVO)throws Exception;
}
@Service
public class MemberServiceImpl implements MemberService {
@Inject
private MemberDAO memberDAO;
@Override
public void register(MemberVO memberVO) throws Exception{
memberDAO.register(memberVO);
}
@Override
public int idCnt(MemberVO memberVO)throws Exception{
return memberDAO.idCnt(memberVO);
}
}
import java.io.PrintWriter;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import org.mindrot.jbcrypt.BCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import kr.co.service.MemberService;
import kr.co.vo.MemberVO;
import net.sf.json.JSONObject;
@Controller
@RequestMapping("/member")
public class MemberController {
private static final Logger logger = LoggerFactory.getLogger(MemberController.class);
@Inject
private MemberService memberService;
@RequestMapping(value="/registerView",method= RequestMethod.GET)
public String registerView() throws Exception{
logger.info("registerView");
return "/member/registerView";
}
@RequestMapping(value = "/register", method=RequestMethod.POST)
public String register(MemberVO memberVO, RedirectAttributes rttr)throws Exception{
logger.info("register");
String hashedPw = BCrypt.hashpw(memberVO.getMemberPw(), BCrypt.gensalt());
memberVO.setMemberPw(hashedPw);
memberService.register(memberVO);
rttr.addFlashAttribute("msg", "가입이 완료되었습니다.");
return "redirect:/board/list";
}
@RequestMapping(value="/idCnt", method=RequestMethod.POST)
@ResponseBody
public String idCnt(@RequestBody String filterJSON,HttpServletResponse response, ModelMap model)throws Exception{
JSONObject resMap= new JSONObject();
try {
ObjectMapper mapper = new ObjectMapper();
MemberVO searchVO = (MemberVO) mapper.readValue(filterJSON, new TypeReference<MemberVO>()
{});
int idCnt = memberService.idCnt(searchVO);
logger.info("idCnt"+idCnt);
resMap.put("res", "ok");
resMap.put("idCnt", idCnt);
}catch(Exception e) {
System.out.println(e.toString());
resMap.put("res","error");
}
logger.info("idCnt"+resMap);
response.setContentType("text/html: charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(resMap);
return null;
}
}
일단 회원가입페이지로 넘어갈 registerView 와 회원가입처리를 할 register 컨트롤러이다.
유효성검사 컨트롤러 inCnt는 뷰를 먼저 작성하고 같이 설명하겠다.
회원가입 뷰
<form id="join" method="post" action="/member/register">
<input type="hidden" id="memberId_yn" name="memberId_yn" value="N"/>
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user" name="memberId"
placeholder="아이디를 입력하세요" id="memberId">
</div>
<div class="col-sm-4 mb-3 mb-sm-0">
<a href="#" class="btn btn-success btn-icon-split" style="text-align:center;" onclick="duplicate(); return false;">
<span class="icon text-white-30">
<i class="fas fa-check"></i>
</span>
<span class="text">중복체크</span>
</a>
</div>
</div>
<div class=" mb-4 ">
<input type="text" class="form-control form-control-user" name="memberPw"
placeholder="비밀번호를 입력하세요" id="memberPw">
</div>
<div class=" mb-4 ">
<input type="text" class="form-control form-control-user" name="memberPw2"
placeholder="비밀번호 확인" id="memberPw2">
</div>
<div class="form-group">
<input type="text" class="form-control form-control-user" name ="memberName"
placeholder="이름을 입력하세요" id="memberName">
</div>
<div class="form-group">
<input type="email" class="form-control form-control-user" name="memberEmail"
placeholder="이메일을 입력하세요" id="memberEmail">
</div>
<div class="checkbox icheck">
<label>
<input type="checkbox"> <a href="#">약관</a>에 동의
</label>
</div>
<button class="btn btn-primary btn-user btn-block" type="button" onclick="fnSubmit(); return false;">
가입
</button>
회원가입 뷰 <script>
<script>
$(document).ready(function(){
});
function duplicate(){
var memberId=$("#memberId").val();
var submitObj = new Object();
submitObj.memberId=memberId;
$.ajax({
url : "/member/idCnt",
type : "POST",
contentType : "application/json; charset-utf-8",
data : JSON.stringify(submitObj),
dataType : "json"
}).done(function(resMap) {
if (resMap.res == "ok") {
if (resMap.idCnt == 0) {
alert("사용할 수 있는 아이디입니다.");
$("#memberId_yn").val("Y");
} else {
alert("중복된 아이디 입니다.");
$("#memberId_yn").val("N");
}
}
}).fail(function(e) {
alert("등록 시도에 실패하였습니다." + e);
}).always(function() {
pass = false;
});
}
function fnSubmit() {
var email_rule = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
// var tel_rule = /^\d{2,3}-\d{3,4}-\d{4}$/; 전화번호용
if ($("#memberName").val() == null || $("#memberName").val() == "") {
alert("이름을 입력해주세요.");
$("#memberName").focus();
return false;
}
if ($("#memberId").val() == null || $("#memberId").val() == "") {
alert("아이디를 입력해주세요.");
$("#memberId").focus();
return false;
}
if ($("#memberId_yn").val() != 'Y') {
alert("아이디 중복체크를 눌러주세요.");
$("#memberId_yn").focus();
return false;
}
if ($("#memberEmail").val() == null || $("#memberEmail").val() == "") {
alert("이메일을 입력해주세요.");
$("#memberEmail").focus();
return false;
}
if ($("#memberPw").val() == null || $("#memberPw").val() == "") {
alert("비밀번호를 입력해주세요.");
$("#memberPw").focus();
return false;
}
if ($("#memberPw2").val() == null || $("#memberPw2").val() == "") {
alert("비밀번호 확인을 입력해주세요.");
$("#memberPw2").focus();
return false;
}
if ($("#memberPw").val() != $("#memberPw2").val()) {
alert("비밀번호가 일치하지 않습니다.");
$("#memberPw2").focus();
return false;
}
if(!email_rule.test($("#memberEmail").val())){
alert("이메일을 형식에 맞게 입력해주세요. ex) 1234@naver.com");
$("#memberEmail").focus();
return false;
}
if (confirm("회원가입하시겠습니까?")) {
$("#join").submit();
return false;
}
}
</script>
필요없는 부분을 제외하고, 기능을 구현하는데 필요한 부분을 가져왔다.
부트스트랩 CDN, ajax 라이브러리(pom.xml)와 CDN추가 ,JSTL 태그를 추가해야 한다.
일단 뷰 부분을 보면 어려울게 없다.
form 과 input태그로만 구성되있고, 중복체크, 회원가입 button으로 script로 연결되서 유효성 검사를 진행하고,
form태그의 내용을 submit한다.
id , name 속성과 매핑주소만 잘 확인하자.
이제 ajax부분과 유효성검사에 대해서 설명해보겠다.
내가 공부하는 식으로 코드를 한줄한줄 해석해가며 로직을 읽어보려고 한다.
일단
@RequestMapping(value="/idCnt", method=RequestMethod.POST)
@ResponseBody
public String idCnt(@RequestBody String filterJSON,HttpServletResponse response, ModelMap model)throws Exception{
JSONObject resMap= new JSONObject();
//resMap이라는 JSONObject를 만듬 기본사용법과 특징은 Map와 유사하다 key,value형식으로 데이터를 관리.
try {
ObjectMapper mapper = new ObjectMapper();
MemberVO searchVO = (MemberVO) mapper.readValue(filterJSON, new TypeReference<MemberVO>()
{});
int idCnt = memberService.idCnt(searchVO);
logger.info("idCnt"+idCnt);
resMap.put("res", "ok");
resMap.put("idCnt", idCnt);
}catch(Exception e) {
System.out.println(e.toString());
resMap.put("res","error");
}
logger.info("idCnt"+resMap);
response.setContentType("text/html: charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(resMap);
return null;
}
@RequestBody String filterJSON
RequestBody는 클라이언트와 서버간의 통신에 사용하는 http프로토콜에서 header와 body부분 중,
body부분의 내용을 어노테이션 다음에 명시하는 클래스에 매핑해준다.
중복체크 버튼을 누르면 onclick="duplicate();" 으로 script의 duplicate 함수로 연결되고,
입력한 <input id=memberId>의 내용이 var memberId에 담긴다.
그리고 var submitObj에 그 입력한 id값이 담기고,ajax부분이 실행되어 /member/inCnt컨트롤러로 json문자열로 값을 변환한다.
JSON.stringify(value, replacer, space)
value(필수): JSON 문자열로 변환할 값이다.(배열, 객체, 또는 숫자, 문자 등이 될 수 있다.)
replacer(선택): 함수 또는 배열이 될 수 있다. 이 값이 null 이거나 제공되지 않으면, 객체의 모든 속성들이 JSON 문자열 결과에 포함된다.
그럼 이제부터 컨트롤러로 json문자열값이 넘어오고, @RequestBody로 filterJSON에 그 값이 담긴다.
Map과 유사한 key,value 형식으로 json데이터를 관리하는 JSONObject 객체 resMap을 선언하고,
ObjectMapper를 통해
MemberVO 객체인 searchVO에 filterJSON에 담긴 json문자열의 값을 javaObject로 제네릭한다. (변환)
Map-json간의 변환에 사용하는 readValue는 readValue(arg,type)
arg : 지정된 타입으로 변환할 대상
type: 대상을 어던 타입으로 변활할 것인지 클래스를 명시( Class객체, TypeReference객체 등..)
넘어온 입력한 id값(JSON) 을 MemberVO 형식으로 JAVA로 넘겨받는 것이다.
그럼 이제 searchVO엔 Id가 담겼다. 그 값을 아까 유효성검사시 사용한 Mapper에 넘겨주고
그 결과값은 int idCnt에 담긴다. (아까 설정한 resultType을 기억)
logger은 제대로 값이 출력되는지 보려고 찍었고,
resMap에 "res"라는 키의 ok라는 String값이 담기고
idCnt라는 key엔 1인지, 0인지가 담긴다.
이제 그 값을 다시 뷰로 보내서 유효성 처리를 하면 된다.
처음 데이터를 넘겨받을때, Post 방식으로 Request(요청)을 받았다.
그럼 response(응답)을 해줘야하는데, 파라미터에 HttpServletResponse가 있으면 서블릿 컨테이너가 자동으로 값을 매핑해서 넘겨준다.
응답할 ContentType을 set하고, out이라는 텍스트기반text/html:UTF-8 형식의 출력스트림 객체에 resMap을 넣는다.
출력스트림과 ContentType은 별도의 스트림이라는 개념의 공부와 HTTP에 대한 공부가 필요하다.
글이 너무 길어질 것 같으니 따로 적지는 않겠지만. 해당 코드를 이해하기 위해선 필요하다.
전 글이 HTTP에 관한 것이였는데 도움이 될지는 모르겠고..
이 방법 말고도 resMap의 파라미터를 뷰로 넘기는 방법은 여러가지가 있다...
JSON객체인 resMap을 뷰로 넘기기만 하면 되니 다른 방법으로 구현해도 괜찮겠다.
이제 다시 뷰로 넘어와서 resMap이라는 결과값을 done 함수에 파라미터로 넣어주고,
아까 키로 설정했던 res와 iniCnt를 이용해서 중복여부를 검사한다.
Y가 담기면 뒤의 y n여부를 따지는 fuSubmit()에서 넘어가게 된다 .
뒤의 fuSubmit부분은 딱히 설명할게 없을 정도로 간단하니 설명은 생략하겠다
input의 id값과 잘 매칭시켜 비교연산자를 활용하면 된다.
나중에 좀 더 유효성 검사 항목을 추가 할 예정이다.(좀 노가다 ㅜㅜ)
이제 유효성 검사를 지나서 등록부분이다.
@RequestMapping(value = "/register", method=RequestMethod.POST)
public String register(MemberVO memberVO, RedirectAttributes rttr)throws Exception{
logger.info("register");
String hashedPw = BCrypt.hashpw(memberVO.getMemberPw(), BCrypt.gensalt());
memberVO.setMemberPw(hashedPw);
memberService.register(memberVO);
rttr.addFlashAttribute("msg", "가입이 완료되었습니다.");
return "redirect:/board/list";
}
pom.xml에 추가
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
비밀번호는 암호화해서 DB에 저장하기 위해 해싱함수를 사용해서 암호화하는 BCrypt를 사용했다.
사용을위해 우선 pom.xml에 라이브러리를 추가해준다.
스프링 스큐리티 사용시에는 BCryptPasswordEncoder로 사용 할 수 있다.
우선 , 기본적인 로직은 회원가입페이지에서 정보를 입력 후, 등록버튼을 누르면
입력한 값이 유효성검사를 거친 후 , form태그를 이용해 MemberVO에 파라미터가 담겨 해당 컨트롤러로 넘어오게 된다.
BCyprt의 (hashpw([비밀번호], BCrypt.getsalt()) 를 이용해 비밀번호를 암호화하고,
VO에 set으로 넣어주고, 암호화된 비밀번호가 담긴 VO를 insert로직에 파라미터로 넣어준다.
그 후 리다이렉트로 로그인페이지나 메인페이지등으로 이동하면서, 가입완료창을 띄워준다.
BCrypt는 해시 알고리즘으로 비밀번호를 암호화하는 기법이다.
해시 알고리즘으로 몇십자리의 수가 되는 임의의 값으로 원래 문자를 암호화한다.
하지만 해시함수는 원래 빠른 검색을 목적으로 설계 된 것이다.
때문에 해커들은 무차별 대입을 통해서 비밀번호를 알아내는 경우도 있다.
무차별 대입 시 ,고성능 그래픽카드를 이용하면 초당 10억~50억번에 달하는 결과를 낼 수도 있다.
(하지만 한번당 0.2~0.5초가 걸리게 설계하면 해당 해킹방법을 막을 수 있다, 사용자 입장에서는 0.2초가 체감이 안됨)
물론, 78자리의 수를 사용하는 SHA-256 을 무차별대입으로 해독하려면 어마어마한 시간이 걸린다..
대략 1년을 해독하면 3경 1536조 번을 할 수 있는데,
2의 256승인 256비트의 경우의 수를 따져서 1년단위로 나눠보면
3,671,743,063,080,802,746,815,416,825,491,118,336,290,905,145,409,708,398,004,109 년이 걸린다..
우주의 나이가 대략 137억년이라고 한다..ㅎ..
(512비트를 사용하는 SHA-512도 있다.)
하지만 누구나 자신이 기억하기 쉽도록 상징성이 있거나( 생일, 전화번호, 주민등록번호 등등) 이해하기 쉬운 비밀번호를 사용하지 때문에 해커들도 일정 문자를 조합해보면서 시간을 단축시킬 수 있다.
또, 같은 비밀번호를 사용하는 사용자들 또한 많기 때문에 하나의 결과로 다수의 password를 알아낼 수도 있을 것이다.
이를 방지하기 위한것이 솔트 이다. (BCrypt.gensalt())
해시함수를 돌리기 전에 password마다 임의의 값(salt)을 붙이기 때문에,
같은 패스워드를 사용하는 다른 사용자도 비교적 안전해진다.
이제 리다이렉트 부분인데, 리다이렉트 하고자하는 페이지의 컨트롤러의
HttpServletRequest request
Map<String, ?> inputFlashMap = RequestContextUtils.getInputFlashMap(request);
if(null != inputFlashMap) {
model.addAttribute("msg",(String) inputFlashMap.get("msg"));
}
매개변수에 HttpServletRequest를 추가해주고
HttpServletRequest로 넘어온 값의 FlashMap에 저장되어 전달된 값을 가져온다.
그 후 ModeladdAttribute로 msg라는 key에 담긴 값을 "/register" 컨트롤러로 보내고,
다시 msg라는 key에 값(가입이 완료되었습니다)을 담아서 리다이렉트 한 페이지에서 msg를 출력하도록 해서 구현 할 수 있다.
HttpServlet이나 redirect, model 등은 이전게시글에서도 많이 다뤘기 때문에 따로 설명을 하진 않겠다.
이렇게 DB를 확인해보면 암호화 된 PW가 저장되어있고,
넘어간 페이지에서는 지정해놓은 문장이 출력된다.
다음엔 로그인/로그아웃구현을 해보겠다.
'SPRING > IceWater Community' 카테고리의 다른 글
[스프링]로그인 권한설정과(인터셉터) 로그인,로그아웃 전 페이지 기억 기능 - 로그인 3 (1) | 2021.09.16 |
---|---|
[스프링]인터셉터를 활용한 로그인구현 (세션부여) - 로그인2 (0) | 2021.09.15 |
[스프링]게시판 조회수,댓글수 순 정렬과 게시글 10개,20개씩 보기 구현 (3) | 2021.09.07 |
[스프링] 다중 게시판 구현 (CRUD) (1) | 2021.09.06 |
[스프링]게시판 이전글 다음글 구현 (5) | 2021.09.03 |