Chapter#01 : [Spring] IntelliJ를 사용한 Spring Project 생성( Gradle )
Chapter#02 : [Spring] MVC 패턴 및 MyBatis를 사용한 게시판 제작
Chapter#03 : [Spring] Log4j 설정 및 사용하기( log 파일 저장하기 )
Chapter#04 : [Spring] SpringSecurity를 이용한 사용자 인증 프로세스 구축
Chapter#05 : [Spring] JWT 토큰 발급 및 토큰 인증 받기
Chapter#06 : [Spring] Swagger 웹 서비스 RESTful API 문서 자동 생성
1. Spring Security 라이브러리 추가
build.gradle을 열고 Dependencies에 스프링 시큐리티 라이브러리 추가하여 준다.
build.gradle
~~ 이 하 생 략 ~~
dependencies {
~~ 이 하 생 략 ~~
// Spring Security
implementation "org.springframework.security:spring-security-acl:5.5.0"
implementation "org.springframework.security:spring-security-config:5.5.0"
implementation "org.springframework.security:spring-security-core:5.5.0"
implementation "org.springframework.security:spring-security-crypto:5.5.0"
implementation "org.springframework.security:spring-security-taglibs:5.5.0"
implementation "org.springframework.security:spring-security-web:5.5.0"
~~ 이 하 생 략 ~~
}
~~ 이 하 생 략 ~~
plugins {
id "java"
id "war"
}
apply plugin : "war"
group "org.example"
version "0.0.1-SNAPSHOT"
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:5.8.1"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.8.1"
// Servelt API
providedCompile "javax.servlet:servlet-api:2.5"
// JSTL
implementation "javax.servlet:jstl:1.2"
// SpringFramework
implementation "org.springframework:spring-aop:5.2.22.RELEASE"
implementation "org.springframework:spring-beans:5.2.22.RELEASE"
implementation "org.springframework:spring-context:5.2.22.RELEASE"
implementation "org.springframework:spring-core:5.2.22.RELEASE"
implementation "org.springframework:spring-expression:5.2.22.RELEASE"
implementation "org.springframework:spring-jcl:5.2.22.RELEASE"
implementation "org.springframework:spring-tx:5.2.22.RELEASE"
implementation "org.springframework:spring-web:5.2.22.RELEASE"
implementation "org.springframework:spring-webmvc:5.2.22.RELEASE"
implementation "org.springframework:spring-jdbc:5.2.22.RELEASE"
// Spring Security
implementation "org.springframework.security:spring-security-acl:5.5.0"
implementation "org.springframework.security:spring-security-config:5.5.0"
implementation "org.springframework.security:spring-security-core:5.5.0"
implementation "org.springframework.security:spring-security-crypto:5.5.0"
implementation "org.springframework.security:spring-security-taglibs:5.5.0"
implementation "org.springframework.security:spring-security-web:5.5.0"
// log4j
implementation "org.apache.logging.log4j:log4j-api:2.14.1"
implementation "org.apache.logging.log4j:log4j-core:2.14.1"
implementation "org.apache.logging.log4j:log4j-slf4j-impl:2.14.1"
// slf4j
implementation "org.slf4j:slf4j-api:1.7.25"
// MariaDB Connect
implementation "org.mariadb.jdbc:mariadb-java-client:2.5.4"
// MyBatis
implementation "org.mybatis:mybatis:3.5.8"
implementation "org.mybatis:mybatis-spring:2.0.6"
// Jackson
implementation "com.fasterxml.jackson.core:jackson-core:2.11.4"
implementation "com.fasterxml.jackson.core:jackson-annotations:2.11.4"
implementation "com.fasterxml.jackson.core:jackson-databind:2.11.4"
}
test {
useJUnitPlatform()
}
2. Spring Security 기본 로그인 폼을 이용한 사용자 로그인
Spring Security 필터는 웹 어플리케이션의 요청과 응답을 처리하여 보안 관련 작업을 수행한다.
이러한 필터들은 웹 요청이나 응답을 가로채서 필터 체인을 통해 전달되며,
각각의 필터는 특정한 보안 작업을 수행한다.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 보안 절차를 위해서는 모든 요청이 spring security 필터를 거치도록 설정 -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
~~ 이 하 생 략 ~~
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 보안 절차를 위해서는 모든 요청이 spring security 필터를 거치도록 설정 -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:context/context-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.do</url-pattern>
<url-pattern>/</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<filter>
<filter-name>characterEncoding</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>characterEncoding</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
</web-app>
src/main/resources/context 디렉토리에 context-security.xml 파일을 생성한다.
context-security.xml 파일이 생성되면 아래의 코드를 추가하여 준다.
context-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- auto-config="true" : Spring Security 기본 로그인 폼 사용 -->
<security:http auto-config="true">
<security:intercept-url pattern="/logIn.do" access="permitAll()"></security:intercept-url>
<security:intercept-url pattern="/**" access="isAuthenticated()"></security:intercept-url>
<security:logout logout-url="/logOut.do" logout-success-url="/logIn?logout" invalidate-session="true"></security:logout>
<!-- CSRF 보호 비활성화 -->
<security:csrf disabled="true"></security:csrf>
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}1q2w3e" authorities="ROLE_USER"></security:user>
<security:user name="admin" password="{noop}q1w2e3" authorities="ROLE_ADMIN"></security:user>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
<security:intercept-url> - pattern
pattern | 로그인 페이지 URL 설정 |
access | · permitAll( ) : 모든 사용자에 한하여 접근을 허용 · hasRole( '권한' ) : 특정 '권한'을 가진 사용자에 한하여 접근을 허용 · isAuthenticated( ) : 인증된 사용자에 한하여 접근을 허용 · denyAll( ) : 모든 사용자의 접근을 차단함 |
모든 설정을 마무리하고 Apache Tomcat을 실행한다.
Web Browser를 열고 localhost:8181/login.do 페이지로 접속을 하면 Spring Security 기본 로그인 폼 페이지가 오픈된다.
context-security.xml 파일에서 user, admin 이라는 계정을 생성해 두었다.
name | password | authorities |
user | {noop}1q2w3e | ROLE_USER |
admin | {noop}1q2w3e | ROLE_ADMIN |
2. 커스텀 로그인 폼 제작 및 사용자 로그인 처리
1) context-security.xml 수정
이번에는 개발자가 직접 제작한 로그인 폼을 사용하여 로그인 하도록 설정을 해보도록 하자.
context-security.xml 파일의 설정을 변경한다.
context-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- auto-config="false" : 로그인 폼을 직접 만들어서 사용 -->
<security:http auto-config="false">
<!-- 로그인 페이지와 로그인 처리 URL 설정 -->
<security:form-login login-page="/member/logIn.do" login-processing-url="/member/authenticationProcess.do" username-parameter="memberId" password-parameter="memberPw" default-target-url="/member/main.do" always-use-default-target="true"></security:form-login>
<!-- 로그인 페이지는 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/logIn.do" access="permitAll()"></security:intercept-url>
<!-- 인증된 사용자에 한해 isAuthenticated() 설정 -->
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')"></security:intercept-url>
<!-- 로그아웃 설정 -->
<security:logout logout-url="/member/logOut.do" logout-success-url="/member/logIn.do?logout" invalidate-session="true" delete-cookies="JSESSIONID"></security:logout>
<!-- CSRF 보호 비활성화 -->
<security:csrf disabled="true"></security:csrf>
</security:http>
<security:authentication-manager id="authenticationManager">
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}1q2w3e" authorities="ROLE_USER"></security:user>
<security:user name="admin" password="{noop}q1w2e3" authorities="ROLE_ADMIN"></security:user>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
<security:form-login> 설정
login-page | 로그인 페이지의 URL을 설정하여 사용자가 제작한 로그인 레이아웃을 사용한다. |
login-processing-url | 실제 로그인 처리를 진행하는 URL 해당 URL을 통해 사용자 ID, 사용자 Password를 넘겨받아 인증 프로세스를 수행 |
default-target-url | 로그인이 성공하였을 경우 이동할 URL 주소 |
always-use-default-target | 로그인이 성공하였을 경우 default-target-url을 따를 것인지 설정 기본값 true |
username-parameter | 사용자 계정 ID를 받는 파라미터 명칭을 직접 지정한다. 사용하지 않으면 기본값은 username이라는 명칭이 기본 파라미터 명칭으로 설정된다. |
password-parameter | 사용자 계정의 비밀번호를 받는 파라미터 명칭을 직접 지정한다. 사용하지 않으면 기본값은 password라는 명칭이 기본 파라미터 명칭으로 설정된다. |
failureHandler | 로그인 인증 실패한 사용자에 대한 처리를 담당하는 커스텀 핸들러를 설정 |
authentication-success-handler-ref | 로그인 성공시에 지정한 클래스를 호출하여, 별도의 로직을 수행할 수 있다. 예) jwt 토큰 생성 |
authentication-failure-handler-ref | 로그인 실패시에 해당 클래스를 호출하여, 별도의 로직을 수행할 수 있다. 예) 로그인 실패 횟수에 따른 자동 로그인 방지 |
<security:logout> 설정
logout-url | 로그아웃 처리에 대한 URL |
logout-success-url | 로그아웃 성공시, 리다이렉트 할 URL 주소 |
invalidate-session | 로그아웃 시 세션을 무효화할지 여부를 지정한다. 기본값 : true |
delete-cookies | 로그아웃 시 제거할 쿠키의 이름 목록을 지정한다. 여러 쿠키를 제거하려면 쉼표(,)로 구분하여 나열하면 됩니다. ( ※ JSESSIONID 쿠키는 기본적으로 세션 ID를 저장하는 JSESSIONID 쿠키이다. ) |
logout-handler | 커스텀 로그아웃 핸들러를 지정한다. 이 속성을 사용하면 기본 로그아웃 처리 대신에 사용자 정의 로직을 실행한다. |
2) 사용자 로그인 컨트롤러 ( Controller ) 생성
org.example 패키지 경로 에 member 패키지를 추가한다.
member 패키지가 추가되면 다시 org.example.member 패키지에 controller 패키지를 또 생성한다.
org.example.member.controller 패키지가 완성되면 MemberController.java 컨트롤러를 생성한다.
MemberController 컨트롤러가 생성되면 아래와 같이 Spring Security 로그인 처리를 수행하는 코드를 작성한다.
MemberController.java
package org.example.member.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
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 javax.servlet.http.HttpServletRequest;
@Controller
@RequestMapping("/member")
public class MemberController {
@Autowired
@Qualifier("authenticationManager")
private AuthenticationManager authenticationManager;
@RequestMapping(value="/main.do", method = RequestMethod.GET)
public String firstWelcomePage() {
return "member/memberMain";
}
@RequestMapping(value="/logIn.do", method = RequestMethod.GET)
public String showLoginForm() {
return "member/memberLogIn";
}
@ResponseBody
@RequestMapping(value="/authenticationProcess.do", method = RequestMethod.POST)
public String processLogin(HttpServletRequest request, RedirectAttributes redirectAttributes) {
String memberId = request.getParameter("memberId");
String memberPw = request.getParameter("memberPw");
try {
// 사용자 인증을 위한 UsernamePasswordAuthenticationToken 객체 생성
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(memberId, memberPw);
// AuthenticationManager를 사용하여 인증 수행
Authentication authentication = authenticationManager.authenticate(token);
// 인증 성공 후 SecurityContext에 인증 객체 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/main.do";
} catch (Exception e) {
redirectAttributes.addAttribute("error", true);
return "redirect:/login.do?logout";
}
}
}
3) 사용자 로그인 폼 뷰 ( View ) 제작
src/main/webapp/WEB-INF/views 디렉토리에 member라는 디렉토리를 생성한다.
member 디렉토리가 생성되면 memberLogIn.jsp 파일을 생성한다.
memberLogIn.jsp 파일이 생성되면 사용자 로그인 폼 JavaScript 코드와 HTML 도큐먼트를 생성한다.
memberLogIn.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>회원 로그인</title>
</head>
<style type="text/css">
table, thead, tbody, tfoot { border:1px solid #000000;border-collapse:collapse; }
tfoot { text-align:right; }
th, td { border:1px solid #000000;padding:10px; }
tbody > tr > td { cursor:pointer;cursor:hand; }
tbody > tr > td:first-child { text-align:center; }
button { cursor:pointer;cursor:hand; }
</style>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("keydown", function(event) {
if(event.keyCode === 13) {
accessMemberRequest();
}
});
document.getElementById("memberLogIn").addEventListener("click", function() {
accessMemberRequest();
});
});
function accessMemberRequest() {
if(document.getElementsByName("memberId")[0].value.replace(/\s/gi, "") == "") {
alert("ID가 입력되지 않았습니다.\nID를 입력해 주세요.");
document.getElementsByName("memberId")[0].focus();
return false;
}
if(document.getElementsByName("memberPw")[0].value.replace(/\s/gi, "") == "") {
alert("비밀번호가 입력되지 않았습니다.\n비밀번호를 입력해 주세요.");
document.getElementsByName("memberPw")[0].focus();
return false;
}
document.getElementById("formMemberAccess").method = "POST";
document.getElementById("formMemberAccess").action = "/member/authenticationProcess.do";
document.getElementById("formMemberAccess").submit();
}
</script>
<body>
<h1>회원 로그인</h1>
<c:if test="${param.error != null}">
<p style="color:#FF0000;">Login failed.</p>
</c:if>
<form id="formMemberAccess">
<table>
<tbody>
<tr>
<th>사용자 ID</th>
<td><input type="text" name="memberId" required/></td>
</tr>
<tr>
<th>비밀번호</th>
<td><input type="password" name="memberPw" required/></td>
</tr>
</tbody>
<tfoot style="border:0px">
<tr>
<td colspan="2">
<button type="button" id="memberLogIn">Log In</button>
</td>
</tr>
</tfoot>
</table>
</form>
</body>
</html>
로그인 페이지가 제작되었다면 다음으로 로그인 처리 후 보여질 memberMain.jsp 페이지를 생성한다.
memberMain.jsp 페이지가 생성되면 JavaScript 코드와 HTML 도큐먼트를 추가한다.
memberMain.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Home 화면</title>
</head>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("memberLogout").addEventListener("click", function() {
denialMemberRequest();
});
});
function denialMemberRequest() {
window.location.href = "/member/logOut.do"
}
</script>
<body>
<h1>메인 페이지</h1>
<button type="button" id="memberLogout">로그아웃</button>
</body>
</html>
3. Spring Security를 이용한 다중 로그인 방지 처리 프로세스
context-security.xml 파일을 열고 아래 코드를 추가하여 준다.
context-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<security:http auto-config="false">
~~ 이 하 생 략 ~~
<!-- 동시 세션 제어 설정 -->
<security:session-management>
<security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" expired-url="/member/login.do?expired=true"></security:concurrency-control>
</security:session-management>
</security:http>
~~ 이 하 생 략 ~~
</beans>
<security:concurrency-control> 설정
max-sessions
max-sessions 속성을 1로 설정했다.
이는 사용자가 한 번에 한 개의 세션만 가질 수 있도록 한다는 의미이다.
다른 장치나 브라우저에서 로그인하려고 하면 이전 세션이 무효화됩니다.
error-if-maximum-exceeded
max-sessions가 1로 설정되어 있고 error-if-maximum-exceeded가 true로 설정되어 있다면,
사용자가 로그인하고 있을 때 다른 장치나 브라우저에서 로그인을 시도하면 나중에 로그인을 요청한 브라우저에서 메시지가 발생한다.
만약 error-if-maximum-exceeded를 false로 설정한다면, 사용자가 동시 세션 수를 초과해도 에러 메시지를 보여주지 않고, 새로운 세션을 허용하게 된다.
이때, 먼저 로그인 하고 있던 브라우저에서는 기존의 세션은 만료되고 나중에 로그인 요청한 브라우저에서 사용자는 새로운 세션으로 로그인할 수 있게 됩니다.
expired-url
error-if-maximum-exceeded 값이 true인 경우 나중에 로그인한 브라우저에서 리다이렉트 될 페이지의 URL을 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- auto-config="false" : 로그인 폼을 직접 만들어서 사용 -->
<security:http auto-config="false">
<!-- 로그인 페이지와 로그인 처리 URL 설정 -->
<security:form-login login-page="/logIn.do" login-processing-url="/authenticationProcess.do" username-parameter="memberId" password-parameter="memberPw" default-target-url="/main.do" always-use-default-target="true"></security:form-login>
<!-- 로그인 페이지는 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/logIn.do" access="permitAll()"></security:intercept-url>
<!-- 인증된 사용자에 한해 isAuthenticated() 설정 -->
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')"></security:intercept-url>
<!-- 로그아웃 설정 -->
<security:logout logout-url="/logOut.do" logout-success-url="/logIn.do?logout" invalidate-session="true" delete-cookies="JSESSIONID"></security:logout>
<!-- CSRF 설정 -->
<security:csrf disabled="true"></security:csrf>
<!-- 동시 세션 제어 설정 -->
<security:session-management>
<security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" expired-url="/member/login.do?expired=true"></security:concurrency-control>
</security:session-management>
</security:http>
<!-- Spring Security의 사용자 인증을 수행 -->
<security:authentication-manager id="authenticationManager">
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}1q2w3e" authorities="ROLE_USER"></security:user>
<security:user name="admin" password="{noop}q1w2e3" authorities="ROLE_ADMIN"></security:user>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
4. DataBase를 사용한 회원가입 및 로그인 처리
1) MariaDB 사용자 로그인 처리를 위한 테이블 생성
MariaDB 데이터베이스에 아래 Query를 실행하여 준다.
① 사용자 계정 테이블 생성
CREATE TABLE member_info (
member_uuid CHAR(36) NOT NULL PRIMARY KEY
, member_id VARCHAR(25) NOT NULL
, member_pw CHAR(60) NOT null
, member_name VARCHAR(30) NOT NULL
, member_phone VARCHAR(14) NOT NULL
, member_email VARCHAR(50) NOT NULL
, member_other_matters text NULL
, member_status enum ('ACTIVE', 'DISABLED') DEFAULT 'ACTIVE' NOT NULL
, registry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP() NOT NULL
, CONSTRAINT member_id UNIQUE (member_id)
) DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
② 사용자 계정에 대한 권한 정보를 가지는 테이블 생성
CREATE TABLE member_authority (
seq INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY
, member_uuid VARCHAR(36) NOT NULL
, member_authority enum('ROLE_USER', 'ROLE_ADMIN') DEFAULT NULL
, CONSTRAINT fk_member_uuid FOREIGN KEY (member_uuid) REFERENCES member_info (member_uuid)
) DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
2) context-security.xml - 사용자 계정 생성 및 비밀번호 암호화 및 검증 프로세스 추가
사용자 계정 생성을 위한 프로세스를 추가하기위 context-security.xml 파일을 수정한다.
context-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- auto-config="false" : 로그인 폼을 직접 만들어서 사용 -->
<security:http auto-config="false">
~~ 이 하 생 략 ~~
<!-- 회원가입 페이지는 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/singUp.do" access="permitAll()"></security:intercept-url>
<!-- 회원가입시 회원 신청 ID 중복 확인 프로세스 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/duplicateId.do" access="permitAll()"></security:intercept-url>
<!-- 회원가입 로직처리 프로세스 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/memberRegistry.do" access="permitAll()"></security:intercept-url>
<!-- 인증된 사용자에 한해 isAuthenticated() 설정 -->
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')"></security:intercept-url>
~~ 이 하 생 략 ~~
</security:http>
~~ 이 하 생 략 ~~
</beans>
전체 코드
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- auto-config="false" : 로그인 폼을 직접 만들어서 사용 -->
<security:http auto-config="false">
<!-- 로그인 페이지와 로그인 처리 URL 설정 -->
<security:form-login login-page="/member/logIn.do" login-processing-url="/member/authenticationProcess.do" username-parameter="memberId" password-parameter="memberPw" default-target-url="/member/main.do" always-use-default-target="true"></security:form-login>
<!-- 로그인 페이지는 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/logIn.do" access="permitAll()"></security:intercept-url>
<!-- 회원가입 페이지는 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/singUp.do" access="permitAll()"></security:intercept-url>
<!-- 회원가입시 회원 신청 ID 중복 확인 프로세스 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/duplicateId.do" access="permitAll()"></security:intercept-url>
<!-- 회원가입 로직처리 프로세스 접근권한 permitAll() 설정 -->
<security:intercept-url pattern="/member/memberRegistry.do" access="permitAll()"></security:intercept-url>
<!-- 인증된 사용자에 한해 isAuthenticated() 설정 -->
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')"></security:intercept-url>
<!-- 로그아웃 설정 -->
<security:logout logout-url="/member/logOut.do" logout-success-url="/member/logIn.do?logout" invalidate-session="true" delete-cookies="JSESSIONID"></security:logout>
<!-- CSRF 보호 비활성화 -->
<security:csrf disabled="true"></security:csrf>
<!-- 동시 세션 제어 설정 -->
<security:session-management>
<security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" expired-url="/member/login.do?expired=true"></security:concurrency-control>
</security:session-management>
</security:http>
<!-- Spring Security의 사용자 인증을 수행 -->
<security:authentication-manager id="authenticationManager">
<security:authentication-provider user-service-ref="securityUserDetailsService">
<security:password-encoder ref="passwordEncoder"></security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<bean id="securityUserDetailsService" class="org.example.security.SecurityUserDetailsService"></bean>
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
</beans>
3) 사용자 정보를 가져오는 UserDetailService 추가
Spring Security의 UserDetaieService는 사용자 정보를 가져오는 인터페이스이다.
이 인터페이스는 주로 사용자의 인증( authentication ) 작업에 활용된다.
UserDetaieService를 구현하여 사용자 정보를 DataBase, Memory, 외부 서비스 등에서 가져와서
Spring Securiy 메커니즘에 활용할 수 있다.
org.example 패키지를 선택하고 security 패키지를 추가한다.
org.example.security 패키지가 생성되면 SecurityUserDetailsService.java 클래스 파일을 생성한다.
SecurityUserDetailsService 클래스 파일을 생성하고 UserDetaieService를 implements 해준다.
SecurityUserDetailsService.java
package org.example.security;
import org.example.member.service.MemberDAO;
import org.example.member.service.MemberVO;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
public class SecurityUserDetailsService implements UserDetailsService {
@Resource(name="memberDaoMyBatis")
private MemberDAO memberDAO;
// loadUserByUsername(String memberId)는 사용자 정보를 조회하고 `UserDetails`를 반환한다.
@Override
public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException {
try {
// 입력받은 사용자 ID로 사용자 UUID를 조회
MemberVO memberVO = memberDAO.findByUsername(memberId);
if(memberVO == null) {
throw new UsernameNotFoundException("User not found with username: " + memberId);
}
// 조회한 사용자 UUID로 인증 수행
List<String> authorities = memberDAO.findAuthoritiesByUsername(memberVO.getMemberUuid());
List<GrantedAuthority> grantedAuthorities = authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// getMemberUuid() : 사용자 UUID
// getMemberPw() : 사용자의 비밀번호
// grantedAuthorities : 사용자의 권한 목록
return new User(memberVO.getMemberUuid(), memberVO.getMemberPw(), grantedAuthorities);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
4) 사용자 로그인 및 회원가입 - 모델( Modle ) VO 및 ENUM 생성
① 사용자 정보값을 가지는 MemberVO 생성
org.example.member 패키지에 service 패키지를 하나 더 추가한다.
org.example.member.service 패키지가 생성되면 service 패키지에 MemberVO.java 클래스 파일을 생성한다.
MemberVO 파일이 생성되면 아래 코드와 같이 멤버 변수를 생성하고 Getter와 Setter 메서드를 생성한다.
MmeberVO.java
package org.example.member.service;
import org.example.member.status.MemberStatus;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
public class MemberVO {
private String memberUuid;
private String memberId;
private String memberPw;
private String memberName;
private String memberPhone;
private String memberEmail;
private String memberOtherMatters;
private MemberStatus memberStatus;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date registryDate;
public String getMemberUuid() {
return memberUuid;
}
public void setMemberUuid(String memberUuid) {
this.memberUuid = memberUuid;
}
public String getMemberId() {
return memberId;
}
public void setMemberId(String memberId) {
this.memberId = memberId;
}
public String getMemberPw() {
return memberPw;
}
public void setMemberPw(String memberPw) {
this.memberPw = memberPw;
}
public String getMemberName() {
return memberName;
}
public void setMemberName(String memberName) {
this.memberName = memberName;
}
public String getMemberPhone() {
return memberPhone;
}
public void setMemberPhone(String memberPhone) {
this.memberPhone = memberPhone;
}
public String getMemberEmail() {
return memberEmail;
}
public void setMemberEmail(String memberEmail) {
this.memberEmail = memberEmail;
}
public String getMemberOtherMatters() {
return memberOtherMatters;
}
public void setMemberOtherMatters(String memberOtherMatters) {
this.memberOtherMatters = memberOtherMatters;
}
public MemberStatus getMemberStatus() {
return memberStatus;
}
public void setMemberStatus(MemberStatus memberStatus) {
this.memberStatus = memberStatus;
}
public Date getRegistryDate() {
return registryDate;
}
public void setRegistryDate(Date registryDate) {
this.registryDate = registryDate;
}
@Override
public String toString() {
return "MemberVO { "
+ "memberUuid=\'" + memberUuid + "\'"
+ ", memberId=\'" + memberId + "\'"
+ ", memberPw=\'" + memberPw + "\'"
+ ", memberName=\'" + memberName + "\'"
+ ", memberPhone=\'" + memberPhone + "\'"
+ ", memberEmail=\'" + memberEmail + "\'"
+ ", memberOtherMatters=\'" + memberOtherMatters + "\'"
+ ", memberStatus=\'" + memberStatus + "\'"
+ ", registryDate=\'" + registryDate + "\'"
+ " }";
}
}
② 사용자 상태값을 지정하는 MemberStatus ( Enum ) 생성
org.example.member 패키지에 회원 상태 값을 가지는 status 패키지를 추가한다.
org.example.member.status 패키지가 생성되면 MemberStatus.java Enum 파일을 생성한다.
MemberStatus는 ACTIVE( 계정 활성화 ), DISABLED( 계정 비활성화 ) 상태의 값을 가진다.
MemberStatus.java
package org.example.member.status;
public enum MemberStatus {
ACTIVE // 활성화
, DISABLED // 사용안함
}
③ 사용자 권한에 대한 값을 가지는 AuthorityVO 생성
org.example.member.service 패키지에 AuthorityVO.java 파일을 생성한다.
AuthorityVO 파일이 생성되면 아래 코드와 같이 멤버 변수를 생성하고 Getter와 Setter 메서드를 생성한다.
AuthorityVO.java
package org.example.member.service;
import org.example.member.status.MemberAuthority;
public class AuthorityVO {
private int seq;
private String memberUuid;
private MemberAuthority memberAuthority;
public int getSeq() {
return seq;
}
public void setSeq(int seq) {
this.seq = seq;
}
public String getMemberUuid() {
return memberUuid;
}
public void setMemberUuid(String memberUuid) {
this.memberUuid = memberUuid;
}
public MemberAuthority getMemberAuthority() {
return memberAuthority;
}
public void setMemberAuthority(MemberAuthority memberAuthority) {
this.memberAuthority = memberAuthority;
}
@Override
public String toString() {
return "AuthorityVO { "
+ "seq=\'" + seq + "\'"
+ ", memberUuid=\'" + memberUuid + "\'"
+ ", memberAuthority=\'" + memberAuthority + "\'"
+ " }";
}
}
④ 사용자 권한의 상태값을 지정하는 MemberAuthority ( Enum ) 생성
org.example.member.status 패키지에 MemberAuthority.java Enum 파일을 생성한다.
MemberAuthority Enum 파일은 ROLE_USER( 사용자 ), ROLE_ADMIN( 관리자 ) 권한을 지정한다.
MemberAuthority.java
package org.example.member.status;
public enum MemberAuthority {
ROLE_USER // 사용자
, ROLE_ADMIN // 관리자
}
5) 사용자 로그인 및 회원가입 - VO 클래스 객체를 SQL 맵핑하는 XML 파일 생성
sql-mapper-config.xml 파일을 생성하고 위에서 선언한 MemberVO와 AuthorityVO를 맵핑하여 준다.
sql-mapper-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias type="org.example.board.service.BoardVO" alias="board"></typeAlias>
<typeAlias type="org.example.member.service.MemberVO" alias="member"></typeAlias>
<typeAlias type="org.example.member.service.AuthorityVO" alias="authority"></typeAlias>
</typeAliases>
</configuration>
6) 사용자 로그인 및 회원가입 - 컨트롤러( Controller ) 수정
org.example.member.controller 패키지의 MemberController.java 컨트롤러를 수정한다.
MemberController.java
package org.example.member.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.member.service.AuthorityVO;
import org.example.member.service.MemberService;
import org.example.member.service.MemberVO;
import org.example.member.status.MemberAuthority;
import org.example.member.status.MemberStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
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 javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Controller
@RequestMapping("/member")
public class MemberController {
@Resource(name="memberService")
private MemberService memberService;
@Autowired
@Qualifier("authenticationManager")
private AuthenticationManager authenticationManager;
@RequestMapping(value="/logIn.do", method = RequestMethod.GET)
public String showLoginForm() {
return "member/memberLogIn";
}
@ResponseBody
@RequestMapping(value="/authenticationProcess.do", method = RequestMethod.POST)
public String authenticate(HttpServletRequest request, RedirectAttributes redirectAttributes) {
String memberId = request.getParameter("memberId");
String memberPw = request.getParameter("memberPw");
try {
// 사용자 인증을 위한 UsernamePasswordAuthenticationToken 객체 생성
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(memberId, memberPw);
// AuthenticationManager를 사용하여 인증 수행
Authentication authentication = authenticationManager.authenticate(token);
// 인증 성공 후 SecurityContext에 인증 객체 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/main.do";
} catch (Exception e) {
redirectAttributes.addAttribute("error", true);
return "redirect://logIn.do?logout";
}
}
@RequestMapping(value="/main.do", method = RequestMethod.GET)
public String firstWelcomePage() {
return "member/memberMain";
}
@RequestMapping(value="/singUp.do", method = RequestMethod.GET)
public String memberSingUp() {
return "member/memberSingUp";
}
@RequestMapping(value="/memberRegistry.do", method = RequestMethod.POST)
public void singUpProcessing(HttpServletRequest request, HttpServletResponse response) throws Exception {
boolean move = true;
String errorMessage = null;
String memberUuid = null;
// @step#01 회원가입
MemberVO memberVo = new MemberVO();
if(request.getParameter("memberId").isEmpty() == false) {
memberVo.setMemberId(request.getParameter("memberId"));
}
if(request.getParameter("memberPw").isEmpty() == false) {
// BCryptPasswordEncoder 생성
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String memberPw = request.getParameter("memberPw");
// 비밀번호 암호화
String encodedPassword = passwordEncoder.encode(memberPw);
memberVo.setMemberPw(encodedPassword);
}
if(request.getParameter("memberName").isEmpty() == false) {
memberVo.setMemberName(request.getParameter("memberName"));
}
if(request.getParameter("memberPhone").isEmpty() == false) {
memberVo.setMemberPhone(request.getParameter("memberPhone"));
}
if(request.getParameter("memberEmail").isEmpty() == false) {
memberVo.setMemberEmail(request.getParameter("memberEmail"));
}
if(request.getParameter("memberOtherMatters").isEmpty() == false) {
memberVo.setMemberOtherMatters(request.getParameter("memberOtherMatters"));
}
memberUuid = String.valueOf(UUID.randomUUID());
memberVo.setMemberUuid(memberUuid);
memberVo.setMemberStatus(MemberStatus.ACTIVE);
if(memberService.insertSingUpMember(memberVo) > 0) {
move = true;
} else {
move = false;
errorMessage = "회원가입에 실패하였습니다.\n다시 시도하여 주시기 바랍니다.";
}
// @step#02. 회원 권한 설정
if(move == true) {
AuthorityVO authorityVo = new AuthorityVO();
authorityVo.setMemberUuid(memberUuid);
authorityVo.setMemberAuthority(MemberAuthority.ROLE_USER);
if(memberService.insetAuthoritySetting(authorityVo) > 0) {
move = true;
} else {
move = false;
errorMessage = "회원 권한 설정에 실패하였습니다.\n다시 시도하여 주시기 바랍니다.";
}
}
// @step#03. 회원 가입 완료
if(move == true) {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script type='text/javascript'>");
out.println("alert('회원가입에 성공였습니다.\\n로그인하여 다시 사용해 주시기 바랍니다.');");
out.println("window.location.replace('/logIn.do')");
out.println("</script>");
out.flush();
} else {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script type='text/javascript'>");
out.printf("alert(%s);", errorMessage);
out.println("window.location.replace('/singUp.do');");
out.println("</script>");
out.flush();
}
}
@RequestMapping(value="/duplicateId.do", method = RequestMethod.POST)
public void duplicateId(HttpServletRequest request, HttpServletResponse response) throws Exception {
// @step#01 아이디 중복 조회
MemberVO memberVo = new MemberVO();
if(request.getParameter("memberId").isEmpty() == false) {
memberVo.setMemberId(request.getParameter("memberId"));
}
int resultCount = memberService.selectDuplicateId(memberVo);
// @step#01 아이디 중복 조회 결과 반환
Map<String, Object> responseBody = new HashMap<>();
if(resultCount > 0) {
responseBody.put("status", "failure");
responseBody.put("message", request.getParameter( "memberId" ) + "는\n이미 사용중인 아이디 입니다.");
} else {
responseBody.put("status", "success");
responseBody.put("message", "사용가능한 아이디 입니다.");
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody));
response.getWriter().flush();
}
}
7) 사용자 로그인 및 회원가입 - Service 구성
① Service 인터페이스 생성
org.example.member.service 패키지에 MemberService.java 인터페이스 생성한다.
MemberService 인터페이스가 생성되면 아래와같이 코드를 작성한다.
MemberService.java
package org.example.member.service;
public interface MemberService {
int selectDuplicateId(MemberVO memberVo) throws Exception;
int insertSingUpMember(MemberVO memberVo) throws Exception;
int insetAuthoritySetting(AuthorityVO authorityVo) throws Exception;
}
② ServiceImpl 클래스 생성
org.example.member.service 패키지 경로에 impl 패키지를 생성한다.
org.example.member.service.impl 패키지가 생성되면 MemberServiceImpl.java 클래스 파일을 생성한다.
MemberServiceImpl 클래스 파일이 생성되면 아래 코드를 추가하여 준다.
MemberServiceImpl.java
package org.example.member.service.impl;
import org.example.member.service.AuthorityVO;
import org.example.member.service.MemberDAO;
import org.example.member.service.MemberVO;
import org.example.member.service.MemberService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service("memberService")
public class MemberServiceImpl implements MemberService {
@Resource(name="memberDaoMyBatis")
private MemberDAO memberDAO;
@Override
public int selectDuplicateId(MemberVO memberVo) throws Exception {
return memberDAO.selectDuplicateId(memberVo);
}
@Override
public int insertSingUpMember(MemberVO memberVo) throws Exception {
return memberDAO.insertSingUpMember(memberVo);
}
@Override
public int insetAuthoritySetting(AuthorityVO authorityVO) throws Exception {
return memberDAO.insetAuthoritySetting(authorityVO);
}
}
8) 사용자 로그인 및 회원가입 - Data Access 로직 구성
① DAO 인터페이스 생성
org.example.member.service 패키지에 MemberDAO.java 인터페이스를 추가한다.
MemberDAO 인터페이스가 생성되면 아래 코드를 추가한다.
MemberDAO.java
package org.example.member.service;
import java.util.List;
public interface MemberDAO {
int selectDuplicateId(MemberVO vo) throws Exception;
MemberVO selectMemberInfo(String memberUuid) throws Exception;
int insertSingUpMember(MemberVO vo) throws Exception;
int insetAuthoritySetting(AuthorityVO authorityVo) throws Exception;
MemberVO findByUsername(String memberId) throws Exception;
List<String> findAuthoritiesByUsername(String memberId) throws Exception;
}
② DAOMyBatis 클래스 생성
org.example.member.service.impl 패키지에 MemberDAOMyBatis.java 클래스 파일을 생성한다.
MemberDAOMyBatis 클래스 파일이 생성되면 아래 코드를 작성하여 준다.
MemberDAOMyBatis.java
package org.example.member.service.impl;
import org.example.member.service.AuthorityVO;
import org.example.member.service.MemberDAO;
import org.example.member.service.MemberVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.List;
@Repository("memberDaoMyBatis")
public class MemberDAOMyBatis implements MemberDAO {
private static final Logger logger = LoggerFactory.getLogger(MemberDAOMyBatis.class);
@Resource(name="memberMapper")
private MemberMapper memberMapper;
public MemberDAOMyBatis() {
logger.info("===> MemberDAOMyBatis 생성");
}
@Override
public int selectDuplicateId(MemberVO memberVo) throws Exception {
logger.info("===> MyBatis로 selectCountMember() 기능 처리");
return memberMapper.selectDuplicateId(memberVo);
}
@Override
public MemberVO selectMemberInfo(String memberUuid) throws Exception {
logger.info("===> MyBatis로 selectMemberInfo() 기능 처리");
return memberMapper.selectMemberInfo(memberUuid);
}
@Override
public int insertSingUpMember(MemberVO memberVo) throws Exception {
logger.info("===> MyBatis로 insertSingUpMember() 기능 처리");
return memberMapper.insertSingUpMember(memberVo);
}
@Override
public int insetAuthoritySetting(AuthorityVO authorityVO) throws Exception {
logger.info("===> MyBatis로 insetAuthoritySetting() 기능 처리");
return memberMapper.insetAuthoritySetting(authorityVO);
}
@Override
public MemberVO findByUsername(String memberId) throws Exception {
logger.info("===> MyBatis로 findByUsername() 기능 처리");
return memberMapper.findByUsername(memberId);
}
@Override
public List<String> findAuthoritiesByUsername(String memberId) throws Exception {
logger.info("===> MyBatis로 findAuthoritiesByUsername() 기능 처리");
return memberMapper.findAuthoritiesByUsername(memberId);
}
}
9) 사용자 로그인 및 회원가입 - Mapper 객체 생성
org.example.member.service.impl 패키지에 MemberMapper.java 인터페이스 파일을 추가하여 준다.
MemberMapper 인터페이스 파일이 추가되면 아래 코드를 작성한다.
MemberMapper.java
package org.example.member.service.impl;
import org.apache.ibatis.annotations.Mapper;
import org.example.member.service.AuthorityVO;
import org.example.member.service.MemberVO;
import java.util.List;
@Mapper
public interface MemberMapper {
int selectDuplicateId(MemberVO vo) throws Exception;
MemberVO selectMemberInfo(String memberUuid);
int insertSingUpMember(MemberVO vo) throws Exception;
int insetAuthoritySetting(AuthorityVO authorityVO) throws Exception;
MemberVO findByUsername(String memberId);
List<String> findAuthoritiesByUsername(String memberId);
}
10) 사용자 로그인 및 회원가입 - Mapper XML 맵핑
org/main/resources/sqlmap/mapping 디렉토리에 member-mapping.xml 파일을 생성한다.
member-mapping.xml 파일을 생성되면 Mapper 객체와 연동할 Query문을 작성한다.
member-mapping.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="org.example.member.service.impl.MemberMapper">
<resultMap id="memberResult" type="member">
<id property="memberUuid" column="member_uuid"></id>
<result property="memberId" column="member_id"></result>
<result property="memberPw" column="member_pw"></result>
<result property="memberName" column="member_name"></result>
<result property="memberPhone" column="member_phone"></result>
<result property="memberEmail" column="member_email"></result>
<result property="memberOtherMatters" column="member_other_matters"></result>
<result property="memberStatus" column="member_status"></result>
<result property="registryDate" column="registry_date"></result>
</resultMap>
<resultMap id="authorityResult" type="authority">
<id property="seq" column="seq"></id>
<result property="memberUuid" column="member_uuid"></result>
<result property="memberAuthority" column="member_authority"></result>
</resultMap>
<select id="selectDuplicateId" parameterType="member" resultType="int">
SELECT COUNT(member_id) AS countNumber FROM member_info WHERE 1 = 1
<if test="memberId!=null and !memberId.equals('')">
AND member_id = #{memberId}
</if>
</select>
<select id="selectMemberInfo" parameterType="String" resultMap="memberResult">
SELECT member_id, member_name, member_phone, member_email, member_other_matters, member_status, registry_date FROM member_info WHERE 1 = 1
<if test="memberUuid!=null and !memberUuid.equals('')">
AND member_uuid = #{memberUuid}
</if>
</select>
<insert id="insertSingUpMember" useGeneratedKeys="true" keyProperty="memberUuid" parameterType="member">
INSERT INTO member_info(member_uuid, member_id, member_pw, member_name, member_phone, member_email, member_other_matters, member_status)
VALUE(#{memberUuid}, #{memberId}, #{memberPw}, #{memberName}, #{memberPhone}, #{memberEmail}, #{memberOtherMatters}, #{memberStatus});
</insert>
<insert id="insetAuthoritySetting" useGeneratedKeys="true" keyProperty="seq" parameterType="authority">
INSERT INTO member_authority(member_uuid, member_authority)
VALUE(#{memberUuid}, #{memberAuthority});
</insert>
<select id="findByUsername" parameterType="String" resultMap="memberResult">
SELECT member_uuid, member_id, member_pw, member_name FROM member_info WHERE 1 = 1
<if test="memberId!=null and !memberId.equals('')">
AND member_id = #{memberId}
</if>
</select>
<select id="findAuthoritiesByUsername" parameterType="String" resultType="java.lang.String">
SELECT member_authority FROM member_authority WHERE 1 = 1
<if test="memberUuid!=null and !memberUuid.equals('')">
AND member_uuid = #{memberUuid}
</if>
</select>
</mapper>
11) 사용자 로그인 및 회원가입 - 뷰 ( View ) 생성
① 사용자 로그인 페이지
로그인 페이지에서 회원가입 페이지로 이동할 수 있는 버튼을 추가한다.
기존에 만들어둔 memberLogin.jsp 파일의 코드를 아래와 같이 수정하여준다.
memberLogIn.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>회원 로그인</title>
</head>
<style type="text/css">
table, thead, tbody, tfoot { border:1px solid #000000;border-collapse:collapse; }
tfoot { text-align:right; }
th, td { border:1px solid #000000;padding:10px; }
tbody > tr > td { cursor:pointer;cursor:hand; }
tbody > tr > td:first-child { text-align:center; }
button { cursor:pointer;cursor:hand; }
</style>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("keydown", function(event) {
if (event.keyCode === 13) { // Enter 키의 키 코드는 13입니다.
accessMemberRequest();
}
});
document.querySelector("#memberSingUp").addEventListener("click", function() {
window.location.href = "/member/singUp.do";
});
document.querySelector("#memberLogin").addEventListener("click", function() {
accessMemberRequest();
});
});
function accessMemberRequest() {
if(document.querySelector("#memberId").value.replace(/\s/gi, "") === "") {
alert("ID가 입력되지 않았습니다.\nID를 입력해 주세요.");
document.querySelector("#memberId").value = "";
document.querySelector("#memberId").focus();
return false;
}
if(document.querySelector("#memberPw").value.replace(/\s/gi, "") === "") {
alert("비밀번호가 입력되지 않았습니다.\n비밀번호를 입력해 주세요.");
document.querySelector("#memberPw").value = "";
document.querySelector("#memberPw").focus();
return false;
}
document.querySelector("#formMemberAccess").method = "POST";
document.querySelector("#formMemberAccess").action = "/member/authenticationProcess.do";
document.querySelector("#formMemberAccess").submit();
}
</script>
<body>
<h1>회원 로그인</h1>
<form id="formMemberAccess">
<table>
<tbody>
<tr>
<th>사용자 ID</th>
<td><input type="text" id="memberId" name="memberId" value="" required/></td>
</tr>
<tr>
<th>비밀번호</th>
<td><input type="password" id="memberPw" name="memberPw" value="" required/></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<button type="button" id="memberSingUp">회원가입</button>
|
<button type="button" id="memberLogin">로그인</button>
</td>
</tr>
</tfoot>
</table>
</form>
</body>
</html>
② 회원가입 페이지
이제 Spring Security의 비밀번호 암호화 로직을 사용하는 회원가입 페이지를 생성한다.
src/main/webapp/WEB-INF/views/member 디렉토리에 memberSingUp.jsp 파일을 생성하여 준다.
memberSingUp.jsp 파일이 생성되면 회원가입 프로세스를 수행하는 페이지 코드를 작성한다.
memberSingUp.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>회원가입</title>
</head>
<style type="text/css">
table, thead, tbody, tfoot { border:1px solid #000000;border-collapse:collapse; }
tfoot { text-align:right; }
th, td { border:1px solid #000000;padding:10px; }
tbody > tr > td { cursor:pointer;cursor:hand; }
tbody > tr > td:first-child { text-align:center; }
button { cursor:pointer;cursor:hand; }
textarea { width:350px;height:150px;resize:none; }
</style>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("keydown", function(event) {
if(event.keyCode === 13) { // Enter 키의 키 코드는 13입니다.
singUpMemberRequest();
}
});
document.querySelector("#btnDuplicateId").addEventListener("click", function() {
duplicateIdValidation();
});
document.querySelector("#memberPhone").addEventListener("keyup", function(event) {
inputPhoneNumber(event.target);
});
document.querySelector("#memberLogIn").addEventListener("click", function() {
window.location.href = "/login.do";
});
document.querySelector("#memberSingUp").addEventListener("click", function() {
singUpMemberRequest();
});
});
// 회원 신청 아이디 중복 확인
function duplicateIdValidation() {
if(document.querySelector("#memberId").value.replace(/\s/gi, "") === "") {
alert("ID가 입력되지 않았습니다.\nID를 입력해 주세요.");
document.querySelector("#memberId").focus();
return false;
}
if(validateId(document.querySelector("#memberId").value) === false) {
alert("ID는 영문으로 시작하는 영문 소문자, 숫자, 특수문자(_, -, .)로 이루어진\n6자 이상 20자 이상의 문자여야 합니다.");
document.querySelector("#memberId").value = "";
document.querySelector("#memberId").focus();
return false;
}
jQuery.ajax({
url : "/member/duplicateId.do"
, type : "POST"
, async : false
, dataType : "json"
, data : { memberId : document.querySelector("#memberId").value.replace(/\s/gi, "") }
, success : function(response) {
alert(response.message);
if(response.result === "failure") {
document.querySelector("#memberId").value = "";
document.querySelector("#memberId").focus();
return false;
}
}
});
}
// 회원가입
function singUpMemberRequest() {
if(document.querySelector("#memberId").value.replace(/\s/gi, "") === "") {
alert("ID가 입력되지 않았습니다.\nID를 입력해 주세요.");
document.querySelector("#memberId").focus();
return false;
}
if(validateId(document.querySelector("#memberId").value) === false) {
alert("ID는 영문으로 시작하는 영문 소문자, 숫자, 특수문자(_, -, .)로 이루어진\n6자 이상 20자 이상의 문자여야 합니다.");
document.querySelector("#memberId").value = "";
document.querySelector("#memberId").focus();
return false;
}
if(document.querySelector("#memberPw").value.replace(/\s/gi, "") === "") {
alert("비밀번호가 입력되지 않았습니다.\n비밀번호를 입력해 주세요.");
document.querySelector("#memberPw").focus();
return false;
}
if(document.querySelector("#confirmPw").value.replace(/\s/gi, "") === "") {
alert("비밀번호 확인이 입력되지 않았습니다.\n비밀번호 확인을 입력해 주세요.");
document.querySelector("#confirmPw").focus();
return false;
}
if(document.querySelector("#memberPw").value !== document.getElementById("confirmPw").value) {
alert("비밀번호가 일치하지 않았습니다.\n입력한 비밀번호를 다시 확인해 주세요.");
document.querySelector("#memberPw").value = "";
document.querySelector("#confirmPw").value = "";
document.querySelector("#memberPw").focus();
return false;
}
if(validatePassword(document.querySelector("#memberPw").value) === false) {
alert("비밀번호는 8자 이상, 20자 이하의\n대문자, 소문자, 숫자, 특수문자를 혼합한\n비밀번호만 사용이 가능합니다.");
document.getElementById("memberPw").value = "";
document.getElementById("confirmPw").value = "";
document.getElementById("memberPw").focus();
return false;
}
if(document.querySelector("#memberPhone").value.replace(/\s/gi, "") === "") {
alert("휴대폰 번호가 입력되지 않았습니다.\n휴대폰 번호를 입력해 주세요.");
document.querySelector("#memberEmail").focus();
return false;
}
if(validatePhoneNumber(document.querySelector("#memberPhone").value) === false) {
alert("휴대폰 번호 형식이 올바르지 않습니다.\n정확한 휴대폰 번호를 입력해 주세요.");
document.querySelector("#memberPhone").value = "";
document.querySelector("#memberPhone").focus();
return false;
}
if(document.querySelector("#memberEmail").value.replace(/\s/gi, "") === "") {
alert("Email 주소가 입력되지 않았습니다.\nEmail 주소를 입력해 주세요.");
document.querySelector("#memberEmail").focus();
return false;
}
if(validateEmail(document.querySelector("#memberEmail").value) === false) {
alert("Email 형식이 올바르지 않습니다.\n정확한 Email 주소를 입력해 주세요.");
document.querySelector("#memberEmail").value = "";
document.querySelector("#memberEmail").focus();
return false;
}
document.querySelector("#formMemberSingUp").method = "POST";
document.querySelector("#formMemberSingUp").action = "/member/memberRegistry.do";
document.querySelector("#formMemberSingUp").submit();
}
function inputPhoneNumber(phone) {
if(event.keyCode !== 8) {
const regExp = new RegExp(/^[0-9]{2,3}-^[0-9]{3,4}-^[0-9]{4}/g);
if(phone.value.replace( regExp, "").length !== 0) {
if(checkPhoneNumber( phone.value ) === true) {
let number = phone.value.replace(/[^0-9]/g, "");
let tel = "";
let seoul = 0;
if(number.substring(0, 2).indexOf("02") === 0) {
seoul = 1;
phone.setAttribute("maxlength", "12");
} else {
phone.setAttribute("maxlength", "13");
}
if(number.length < (4 - seoul)) {
return number;
} else if(number.length < (7 - seoul)) {
tel += number.substr(0, (3 - seoul) );
tel += "-";
tel += number.substr(3 - seoul);
} else if(number.length < (11 - seoul)) {
tel += number.substr(0, (3 - seoul));
tel += "-";
tel += number.substr((3 - seoul), 3);
tel += "-";
tel += number.substr(6 - seoul);
} else {
tel += number.substr(0, (3 - seoul));
tel += "-";
tel += number.substr(( 3 - seoul), 4);
tel += "-";
tel += number.substr(7 - seoul);
}
phone.value = tel;
} else {
const regExp = new RegExp(/[^0-9|^-]*$/);
phone.value = phone.value.replace(regExp, "");
}
}
}
}
function checkPhoneNumber(number) {
const regExp = new RegExp(/^[0-9|-]*$/);
if( regExp.test( number ) === true ) { return true; }
else { return false; }
}
// ID 형식 검사
function validateId(id) {
// 영문 소문자로 시작하고, 영문 소문자, 숫자, 특수문자(_, -, .)로 이루어진 6자 이상 20자 이하의 ID 형식을 검사하는 정규 표현식
const idPattern = /^[a-z][a-z0-9_.-]{6,19}$/;
return idPattern.test(id);
}
// 비밀번호 형식 검사
function validatePassword(password) {
// 비밀번호 길이 체크
if(password.length < 8 || password.length > 20) {
return false;
}
// 대문자, 소문자, 숫자, 특수문자 포함 여부 체크
const uppercaseRegex = /[A-Z]/;
const lowercaseRegex = /[a-z]/;
const digitRegex = /[0-9]/;
const specialCharRegex = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/;
// 비밀번호 형식 검사
if(uppercaseRegex.test(password) === false || lowercaseRegex.test(password) === false || digitRegex.test(password) === false || specialCharRegex.test(password) === false) {
return false;
}
// 모든 요구사항 충족
return true;
}
// 휴대폰 번호 형식 검사
function validatePhoneNumber(phoneNumber) {
// 유효한 휴대폰 번호 패턴 정의 (01?-0000-0000, 가운데 번호는 3자리 또는 4자리)
const pattern = /^01[0-9]{1}-[0-9]{3,4}-[0-9]{4}$/;
// 정규식과 매치되는지 확인하여 유효성 검사 결과 반환
return pattern.test(phoneNumber);
}
// 이메일 주소 형식 검사
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
</script>
<body>
<h1>회원 로그인</h1>
<form id="formMemberSingUp">
<input type="hidden" id="validationId" value="">
<table>
<tbody>
<tr>
<th>사용자 ID</th>
<td>
<input type="text" id="memberId" name="memberId" value="" required/>
<button type="button" id="btnDuplicateId">ID 중복확인</button>
</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input type="password" id="memberPw" name="memberPw" value="" required/>
</td>
</tr>
<tr>
<th>비밀번호 확인</th>
<td>
<input type="password" id="confirmPw" value=""/>
</td>
</tr>
<tr>
<th>이름</th>
<td>
<input type="text" id="memberName" name="memberName" value="" required/>
</td>
</tr>
<tr>
<th>휴대폰 번호</th>
<td>
<input type="text" id="memberPhone" name="memberPhone" value="" style="text-align:center;" maxlength="13" placeholder="000-0000-00000" pattern="[0-9]{2,3}-[0-9]{3,4}-[0-9]{3,4}"/>
</td>
</tr>
<tr>
<th>Email 주소</th>
<td>
<input type="text" id="memberEmail" name="memberEmail" value="" required/>
</td>
</tr>
<tr>
<th>기타사항</th>
<td>
<textarea name="memberOtherMatters" rows="4" cols="50"></textarea>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<button type="button" id="memberLogIn">로그인</button>
|
<button type="button" id="memberSingUp">회원가입</button>
</td>
</tr>
</tfoot>
</table>
</form>
</body>
</html>
'Spring Web > Spring Framework' 카테고리의 다른 글
[Spring] Swagger 웹 서비스 RESTful API 문서 자동생성 (0) | 2023.06.13 |
---|---|
[Spring] JWT 토큰 발급 받고 및 토큰 인증 받기 (0) | 2023.06.13 |
[Spring] Log4j 설정 및 사용하기(log 파일 저장하기) (0) | 2023.06.13 |
[Spring] MVC 패턴 및 MyBatis 사용하는 게시판 제작 (0) | 2023.06.13 |
[Spring] IntelliJ를 사용한 Spring Project 생성(Gradle) (1) | 2023.06.13 |