ㅁ 암호화
- 어떠한 값(평문)을 다른사람들이 알아볼 수 없는 암호문으로 변경하는 과정
- DB 같은 저장소에 개인정보 중 보호해야되는 값들은 암호문으로 보관해야됨.
- 암호화: 평문 -> 암호문
- 복호화: 암호문 -> 평문
- 양방향 암호화 방식: 암호화, 복호화 둘 다 가능.
- 단방향 암호화 방식: 암호화만 가능.
- 옛날에는 양방향 암호화 방식으로 비밀번호를 처리했다.
그래서 비밀번호 찾기하면 기존 비번을 알려줄 수 있었다.
평문을 유추할 수 있다는 것은 보안에 취약하다는 뜻이다.
그래서 단방향 암호화 방식으로 보안정책이 바뀌었다.
이때 많이 사용하던 방식이 SHA 암호화 방식이다. (해쉬알고리즘)
- 그런데 SHA방식도 취약점이 있었다.
매번 똑같은 평문을 입력하면 매번 똑같은 암호문을 만들어준다.
ex) 1111 => Axasdf23@#52a..
ex) 1111 => Axasdf23@#52a..
많은 데이터가 쌓이다 보니까 평문을 가지고 암호문을 유추할 수 있게 되었다.
암호문을 집어넣으면 그걸로 평문을 만들어 낼 수 있는 레인보우 테이블이 만들어졌다.
- 샘플 데이터가 많이 취합되면서 평문을 가지고 암호문을 유추할 수 있고 역으로 암호문만 넣으면 평문을 알려주는 레인보우 테이블이 만들어졌다.
- 그래서 솔팅 기법이 만들어졌다.
저 평문에다가 매번 랜덤한 숫자를 넣어서 암호문을 만들게 해서 매번 다르게 암호문이 만들어진다.
- SHA 방식의 문제점을 보완하기 위해서 솔팅기법(salting)이 추가되었다.
- 솔팅기법은 평문에 매번 랜덤값을 덧붙여서 암호화한다.
똑같은 평문을 입력해도 암호문이 달라진다.
ex) 1111 + salt값(45211) => Axasdf23@#52a..
ex) 1111 + salt값(45211) => Axasdf2341asdf2@$1
- 스프링 시큐리티 모듈에서 제공하는 Bcrypt 암호화방식(솔팅기법이 추가된 암호화 방식)이 있다.
(1) 스프링 시큐리티 라이브러리 추가 (pom.xml)
(2) BcryptPasswordEncodr 빈으로 등록 (spring-security.xml파일)
(3) web.xml에서 해당 spring-security.xml 파일 pre-loading ~
ㅁ pom.xml
- 저 3개 전부 추가한다.
- <!-- Spring -->과 <!-- AspectJ (AOP를 위한 라이브러리)--> 사이에 추가한다.
- 빨간 글씨는 이슈가 있었던 것. 큰 문제는 아님. 있어도 많이 씀. 5.7.5로 간다. usage가 많아서.
<?xml version="1.0" encoding="UTF-8"?>
<modelVersion>4.0.0</modelVersion>
<groupId>com.br</groupId>
<artifactId>spring</artifactId>
<name>springWeb</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>11</java-version>
<org.springframework-version>5.3.27</org.springframework-version>
<org.aspectj-version>1.9.19</org.aspectj-version>
<org.slf4j-version>2.0.7</org.slf4j-version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- Spring Security 모듈 라이브러리 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.7.5</version>
</dependency>
<!-- AspectJ (AOP를 위한 라이브러리)-->
<dependency> <!-- 원래 있던 것. 이게 있어야 "Advice 동작 시점"의 어노테이션들을 사용할 수 있다. -->
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- 위빙 : Advice(공통로직)를 PointCut(핵심로직)에 로딩되도록 함 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
<scope>runtime</scope>
</dependency>
<!-- Logging (logback) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
<!-- <scope>test</scope> 이건 지워야 함 -->
</dependency>
<!-- (2) slf4j -->
<dependency> <!-- 기존 dependency 중 이것만 남기고 다 지운다.-->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<!-- (3) log4jdbc -->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
<version>1.16</version>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework-version}</version>
<scope>test</scope>
</dependency>
<!-- Lombok 라이브러리 추가 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!-- Jackson 라이브러리 추가 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
<!-- db관련 라이브러리 -->
<!-- (1) ojdbc8 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>23.2.0.0</version>
</dependency>
<!-- (2) spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- (3) mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.14</version>
</dependency>
<!-- (4) mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- (5) commons-dbcp -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 파일 업로드 관련 라이브러리 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java-version}</source>
<target>${java-version}</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
- core, web, config 부분만 다르다. 복붙해서 저것만 고쳤다.
ㅁ root-context.xml
- 여기다 해도 되는데 안함.
ㅁ 새 spring bean configuration xml 파일 생성
- 근데 보통 스프링 시큐리티 관련해서는 따로 빼서 xml 파일을 만든다.
Spring Bean Configuration 파일을 만든다.
- 저곳에 저 이름으로 만든다. finish.
- (확인필요) security를 체크한다.
- 전화면에서 finish말고 next해서 체크하고 만들어도 된다.
<?xml version="1.0" encoding="UTF-8"?>
<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="bcryptPwdEncoder"></bean>
</beans>
- root-context.xml뿐 아니라 지금 이 파일ㄷ ㅗ강제로 서버 스타트시 읽혀직 ㅔ만들어야 한다.
- web.xml로 간다.
ㅁ web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/root-context.xml
/WEB-INF/spring/spring-security.xml
</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 인코딩 필터 등록 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
- "/WEB-INF/spring/spring-security.xml"를 추가했다.
ㅁ MemberController
package com.br.spring.controller;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
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.ResponseBody;
import com.br.spring.dto.MemberDto;
import com.br.spring.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequestMapping("/member")
@RequiredArgsConstructor
@Controller
public class MemberController {
private final MemberService memberService;
private final BCryptPasswordEncoder bcryptPwdEncoder;
@PostMapping("/signin.do")
public void signin(MemberDto m, HttpServletResponse response, HttpSession session, HttpServletRequest request) throws IOException {
MemberDto loginUser = memberService.selectMember(m);
// 로그인 성공시 => 세션에 회원정보를 담고, alert와 함께 메인 페이지로 이동
// 로그인 실패시 => alert와 함께 기존에 보던 페이지 유지(아이디, 비번 입력하는 로그인 폼 유지. 작성하던 입력데이터도 그대로.)
// script문을 응답데이터로 돌려줘서 흐름제어
response.setContentType("text/html; charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script>");
if(loginUser != null) { // 로그인 성공
session.setAttribute("loginUser", loginUser);
out.println("alert('" + loginUser.getUserName() + "님 환영합니다~');");
out.println("location.href = '" + request.getHeader("referer") + "';"); // 메인페이지가 아닌 이전에 보던 페이지로 이동
}else { // 로그인 실패
out.println("alert('로그인에 실패하였습니다. 아이디 및 비밀번호를 다시 확인해주세요.');");
out.println("history.back();"); // modal이 띄워진 상태였으면 modal도 유지된다.
}
out.println("</script>");
}
@GetMapping("/singout.do")
public String signout(HttpSession session) {
session.invalidate();
return "redirect:/";
}
@GetMapping("/signup.do")
public void signupPage() { } //WEB-INF/views/member/signup.jsp
@ResponseBody
@GetMapping("/idcheck.do")
public String idCheck(String checkId) {
return memberService.selectUserIdCount(checkId) == 0 ? "YYYY" : "NNNN";
}
@PostMapping("/insert.do")
public void signup(MemberDto m) {
log.debug("암호화 전 member: {}", m);
m.setUserPwd( bcryptPwdEncoder.encode(m.getUserPwd()) );
log.debug("암호화 후 member: {}", m);
}
}
- 전역변수를 뒀다.
- m.setUserPwd( bcryptPwdEncoder.encode(m.getUserPwd()) ); log.debug("암호화 후 member: {}", m);를 작성했다.
=======================================================================
- 비번 pass07!!이다.
- 커밋한다.
- 3계정 다 $2a$10$YG8TWlRA7J2U5HQaf/u/6OehDRKcdo4E2Eja41yuoBx0/3jBbP7/O
- 로그인하려면 다 pass07!! 입력해야 한다.
ㅁ MemberController
package com.br.spring.controller;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
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.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.br.spring.dto.MemberDto;
import com.br.spring.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequestMapping("/member")
@RequiredArgsConstructor
@Controller
public class MemberController {
private final MemberService memberService;
private final BCryptPasswordEncoder bcryptPwdEncoder;
@PostMapping("/signin.do")
public void signin(MemberDto m, HttpServletResponse response, HttpSession session, HttpServletRequest request) throws IOException {
MemberDto loginUser = memberService.selectMember(m);
// 로그인 성공시 => 세션에 회원정보를 담고, alert와 함께 메인 페이지로 이동
// 로그인 실패시 => alert와 함께 기존에 보던 페이지 유지(아이디, 비번 입력하는 로그인 폼 유지. 작성하던 입력데이터도 그대로.)
// script문을 응답데이터로 돌려줘서 흐름제어
response.setContentType("text/html; charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script>");
if(loginUser != null) { // 로그인 성공
session.setAttribute("loginUser", loginUser);
out.println("alert('" + loginUser.getUserName() + "님 환영합니다~');");
out.println("location.href = '" + request.getHeader("referer") + "';"); // 메인페이지가 아닌 이전에 보던 페이지로 이동
}else { // 로그인 실패
out.println("alert('로그인에 실패하였습니다. 아이디 및 비밀번호를 다시 확인해주세요.');");
out.println("history.back();"); // modal이 띄워진 상태였으면 modal도 유지된다.
}
out.println("</script>");
}
@GetMapping("/singout.do")
public String signout(HttpSession session) {
session.invalidate();
return "redirect:/";
}
@GetMapping("/signup.do")
public void signupPage() { } //WEB-INF/views/member/signup.jsp
@ResponseBody
@GetMapping("/idcheck.do")
public String idCheck(String checkId) {
return memberService.selectUserIdCount(checkId) == 0 ? "YYYY" : "NNNN";
}
@PostMapping("/insert.do")
public String signup(MemberDto m, RedirectAttributes rdAttributes) {
log.debug("암호화 전 member: {}", m);
m.setUserPwd( bcryptPwdEncoder.encode(m.getUserPwd()) );
log.debug("암호화 후 member: {}", m);
int result = memberService.insertMember(m);
// 성공시 alert와 함께 메인페이지 이동
// 실패시 alert와 함께 기존에 작업중이던 페이지 유지
if(result > 0) {
rdAttributes.addFlashAttribute("alertMsg", "성공적으로 회원가입 되었습니다.");
}else {
rdAttributes.addFlashAttribute("alertMsg", "회원가입에 실패하였습니다.");
rdAttributes.addFlashAttribute("historyBackYN", "Y");
}
return "redirect:/";
}
}
- redirect하고 포워딩하는 곳까지 데이터를 유지하려면 RedirectAttributes 객체를 써야 한다.
매개변수에 추가한다.
ㅁ header.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!-- Bootstrap 사용을 위한 CDN -->
<!-- ------------------------- -->
<style>
header{height: 50px}
header a{color:black;}
header .profile-img{width:30px;}
</style>
<script>
if('${alertMsg}' != ''){
alert('${alertMsg}');
if('${historyBackYN}' == 'Y'){
history.back();
}
}
</script>
<header class="row m-3">
<div class="col-3 d-flex justify-content-center align-items-center">
<a href=""><img src="${ contextPath }/resources/images/goodee_logo.png" width="100px"></a>
</div>
<div class="col-5"></div>
<div class="col-4 d-flex justify-content-center align-items-center">
<c:choose>
<c:when test="${ empty loginUser }">
<!-- case1. 로그인전 -->
<a href="${contextPath}/member/signup.do">회원가입</a> |
<a href="#" data-toggle="modal" data-target="#loginModal">로그인</a>
</c:when>
<c:otherwise>
<!-- case2. 로그인후 -->
<div>
<img class="profile-img" src="${ contextPath }<c:out value='${loginUser.profileURL}' default='/resources/images/defaultProfile.png' />">
<a href="">${loginUser.userName}님</a> |
<a href="${contextPath}/member/signout.do">로그아웃</a>
</div>
</c:otherwise>
</c:choose>
</div>
</header>
<nav class="navbar m-3 navbar-expand-sm bg-dark navbar-dark d-flex justify-content-center">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">공지사항</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">일반게시판</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">사진게시판</a>
</li>
</ul>
</nav>
<!-- 로그인 클릭 시 뜨는 모달 (기존에는 안보이다가 위의 a 클릭시 보임) -->
<div class="modal fade" id="loginModal">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">Login</h4>
<button type="button" class="close" data-dismiss="modal">×</button>
</div>
<form action="${contextPath}/member/signin.do" method="post">
<!-- Modal Body -->
<div class="modal-body">
<label for="userId" class="mr-sm-2">ID :</label>
<input type="text" class="form-control mb-2 mr-sm-2" placeholder="Enter ID" id="userId" name="userId" required> <br>
<label for="userPwd" class="mr-sm-2">Password:</label>
<input type="password" class="form-control mb-2 mr-sm-2" placeholder="Enter password" id="userPwd" name="userPwd" required>
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="submit" class="btn btn-primary">로그인</button>
<button type="button" class="btn btn-danger" data-dismiss="modal">취소</button>
</div>
</form>
</div>
</div>
</div>
- 상단에 <style> 태그 다음에 <script> 태그를 작성했다.
- 이전 페이지로 이동해야하는 경우가 있다면 앞으로 alertMsg 라는 key값에 "Y"를 담으면 된다.
- id는 user09, 비번은 위의 3계정과 같이 pass07!!로 회원가입 요청을 보냈다.
- 같은 평문이지만 암호문은 다른걸 볼 수 있다.
(위의 세 계정의 비번은 sql developer에서 수동으로 다 같게 수정한 것)
- 참고로 이제 로그인이 안된다. 로그인 시에도 Bcrypt~를 써야 한다.