관리 메뉴

거니의 velog

231212_SPRING 2 (15-2) 본문

대덕인재개발원/대덕인재개발원_웹기반 애플리케이션

231212_SPRING 2 (15-2)

Unlimited00 2023. 12. 12. 13:34
package kr.or.ddit.controller;

public class SecurityController {

	/*
	 * [ 18장 : 스프링 시큐리티 ]
	 * 
	 * 		1. 스프링 시큐리티 소개
	 * 
	 * 			- 애플리케이션에서 보안 기능을 구현하는데 사용되는 프레임워크이다.
	 * 			- 스프링 시큐리티는 필터 기반으로 동작하기 때문에 스프링 MVC와 분리되어 동작한다.
	 * 
	 * 			# 기본 보안 기능
	 * 			- 인증 (Authentocation)
	 * 				> 애플리케이션 사용자의 정당성을 확인한다.
	 * 
	 * 			- 인가 (Authorization)
	 * 				> 애플리케이션의 리소스나 처리에 대한 접근을 제어한다.
	 * 
	 * 			# 시큐리티 제공 기능
	 * 			- 세션 관리
	 * 			- 로그인 처리
	 * 			- CSRF 토큰 처리
	 * 			- 암호화 처리
	 * 			- 자동 로그인
	 * 
	 * 			*** CSRF 용어 설명
	 * 			- 크로스 사이트 요청 위조는 웹 사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하기 공격자가
	 * 			의도한 행위(수정, 삭제, 등록 등)를 특정 웹 사이트에 요청하게 하는 공격을 말한다.
	 * 
	 * 				> CSRF 공격을 대비하기 위해서는 스프링 시큐리티 CSRF Token을 이용하여 인증을 진행한다.
	 * 
	 * 			# 시큐리티 인증 구조
	 * 
	 * 				클라이언트에서 타겟으로 들어가기 위해서 요청을 진행한다. 이 때 타겟에 설정되어 있는 요청 접근 권한이 '사용자'
	 * 				등급일 때로 설정되어 있다고 가정하자. 타겟으로 접근하기 위한 요청을 날렸고 요청 안에 사용자 등급에 해당하는 인가
	 * 				정보가 포함되어 있지 않으면 스프링 시큐리티는 인증을 진행할 수 있도록 인증 페이지(로그인 페이지)를 제공하여
	 * 				사용자에게 인증을 요청한다. 사용자는 아이디, 비밀번호를 입력 후 인증을 요청한다.
	 * 				클라이언트에서 서버로 요청한 HttpServletRequest의 요청 객체를 AuthenticationFilter가 요청을
	 * 				인터셉터 하는데, UsernamePasswordAuthenticationToken을 통해 인증을 진행할 토큰을 만들어
	 * 				AuthenticationManager에게 위임한다. 넘겨 받은 id, pw를 이용해 인증을 진행하는 데 성공시,
	 * 				Authentication 객체 생성과 성공을 전달하고, 그렇지 않으면 Exception 에러를 발생시킨다.
	 * 				인증에 성공 후, 만들어진 Authentication 객체를 AuthenticationProvider에게 전달하고
	 * 				UserDetailService에게 넘겨받은 Authentication 객체 정보를 이용해서 Database에 일치하는 회원 정보
	 * 				를 조회하여 꺼낸다. 꺼낸 정보를 UserDetails로 만들고 최종 User 객체에 회원 정보를 등록한다.
	 * 				등록이 되면서 User Session 정보가 생성된다. 이후 스프링 시큐리티 내 SecurityContextHolder
	 * 				(시큐리티 인메모리)에 UserDetail 정보를 저장한다. 그리고 최종적으로 JSESSIONID가 유효한지를 검증 후
	 * 				인증을 완료 후 타겟 정보로 넘어가도록 돕는다.
	 * 
	 * 		2. 스프링 시큐리티 설정
	 * 
	 * 			# 환경 설정
	 * 				- 의존 라이브러리 설정(pom.xml 설정)
	 * 				> spring-security-web
	 * 				> spring-security-config
	 * 				> spring-security-core
	 * 				> spring-security-taglibs
	 * 
	 * 			# 웹 컨테이너 설정(web.xml)
	 * 				- 스프링 시큐리티가 제공하는 서블릿 필터 클래스를 서블릿 컨테이너에 등록하나.
	 * 				- 스프링 시큐리티는 필터 기반이므로 시큐리티 필터체인을 등록한다.
	 * 					> context-param의 param-value 추가
	 * 						(추가 파라미터: /WEB-INF/spring/security-context.xml)
	 * 					> SpringSecurityFilterChain 추가
	 * 
	 * 			# 스프링 시큐리티 설정
	 * 
	 * 				- 스프링 시큐리티 컴포넌트를 빈으로 정의한다.
	 * 				- spring/security-context.xml 설정
	 * 
	 * 			# 웹 화면 접근 정책
	 * 
	 * 				- 웹 화면 접근 정책을 정한다. (테스트를 위한 각 화면 당 접근 정책을 설정한다.)
	 * 
	 * 					대상			|	화면			|	접근 정책		
	 * 				------------------------------------------------------------
	 * 				일반 게시판			|	목록 화면		|	모두가 접근 가능하다.
	 * 								|	등록 화면		|	로그인한 회원만 접근 가능하다.
	 * 				------------------------------------------------------------
	 * 				공지사항 게시판		|	목록 화면		|	모두가 접근 가능하다
	 * 								|	등록 화면		|	로그인한 관리자만 접근 가능하다.
	 * 				------------------------------------------------------------
	 * 
	 * 			# 화면 설명
	 * 				- 컨트롤러
	 * 				> controller/SecurityBoardController
	 * 				> controller/SecurityNoticeController
	 * 
	 * 				- 화면
	 * 				> board/list, register
	 * 				> notice/list, register
	 * 
	 * 		3. 접근 제한 설정
	 * 
	 * 			- 시큐리티 설정을 통해서 특정 URI에 접근을 제한할 수 있다.
	 * 
	 * 				# 환경 설정
	 * 
	 * 				- 스프링 시큐리티 설정
	 * 					> URI 패턴으로 접근 제한을 설정한다.
	 * 					> security-context.xml 설정
	 * 						<security:intercept-uri pattern="/board/list" access="permitAll" />
	 * 						<security:intercept-uri pattern="/board/register" access="hasRole('ROLE_MEMBER')" />
	 * 					>  notice도 마찬가지로 설정...
	 * 
	 * 				# 화면 설명
	 * 				- 일반 게시판 목록 화면(모두 접근 가능하도록 되어 있음 : permitAll)
	 * 				- 일반 게시판 등록 화면(회원 권한을 가진 사용자만 접근 가능 : hasRole('ROLE_MEMBER'))
	 * 					> 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지로 이동한다.
	 * 				- 공지사항 게시판 목록 화면(모두 접근 가능하도록 되어 있음 : permitAll)
	 * 				- 공지사항 게시판 등록 화면(관리자 권한을 가진 사용자만 접근 가능 : hasRole('ROLE_ADMIN'))
	 * 					> 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지로 이동한다.
	 * 
	 * 		4. 로그인 처리
	 * 
	 * 			- 메모리 상에 아이디와 패스워드를 지정하고 로그인을 처리한다.
	 * 			- 스프링 시큐리티 5버전부터는 패스워드 암호화 처리기를 반드시 이용하도록 변경 되었다.
	 * 			- 암호화 처리기를 사용하지 않도록 "{noop}" 문자열을 비밀번호 앞에 사용한다.
	 * 
	 * 				# 환경 설정
	 * 
	 * 					- 스프링 시큐리티 설정
	 * 						> security-context.xml 설정
						<security:authentication-manager>
							<security:authentication-provider>
								<security:user-service>
									<security:user name="member" password="{noop}1234" authorities="ROLE_MEMBER" />
									<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN" />
								</security:user-service>
							</security:authentication-provider>
						</security:authentication-manager>
	 * 
	 * 				# 화면 설명
	 * 
	 * 					- 일반 게시판 등록 화면
	 * 						> 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지가 연결되고,
	 * 						일반 회원 등급인 ROLE_MEMBER 권한을 가진 member 계정으로 로그인 후 해당 페이지로 접근 가능.
	 * 					- 공지사항 게시판 등록 화면
	 * 						> 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지가 연결되고,
	 * 						관리자 등급인 ROLE_ADMIN 권한을 가진 admin 계정으로 로그인 후 해당 페이지로 접근 가능.
	 * 
	 * 		5. 접근 거부 처리
	 * 
	 * 			- 접근 거부가 발생한 상황을 처리하는 접근 거부 처리자의 URI를 지정할 수 있다.
	 * 
	 * 				# 환경 설정
	 * 
	 * 					- 스프링 웹 설정(servlet-context.xml 설정)
	 * 					> <context:component-scan base-package="kr.or.ddit.security..." />
	 * 					** 필요에 의한 패키지 라인을 bean으로 등록하여 사용해야 할 때 스프링 웹 설정에서 base-package를 설정할 수 있다.
	 * 
	 * 					- 스프링 시큐리티 설정(security-context.xml 설정)
	 * 					> 접근 거부 처리자의 URI를 지정
	 * 					> <security:access-denied-handler error-page="/accessError/" />
	 * 
	 * 				# 접근 거부 처리
	 * 
	 * 					- 접근 거부 처리 컨트롤러 작성
	 * 						> security/CommonController
	 * 
	 * 				# 화면 설명
	 * 					- 일반 게시판 등록 페이지
	 * 					> 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로
	 * 						접근 시 접근 가능
	 * 					- 공지사항 게시판 등록 페이지
	 * 					> 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로
	 * 						접근 시에 공지사항 게시판 등록 페이지는 관리자 권한으로만 접근 가능하므로 접근이 거부된다.
	 * 						이 때, access-denied-handler로 설정되어 있는 URI로 이동하고 해당 페이지에서 접근이
	 * 						거부 되었을 떄 보여질 페이지의 정보가 나타난다.
	 * 
	 * 		6. 사용자 정의 접근 거부 처리자
	 * 
	 * 			- 접근 거부가 발생한 상황에 단순 메시지 처리 이상의 다양한 처리를 하고 싶다면 AccessDeniedHandler를 직접 구현한다.
	 * 
	 * 				# 환경 설정
	 * 				- 스프링 시큐리티 설정(security-context.xml 설정)
	 * 					> id가 'customAccessDenied'를 가진 빈을 등록한다.
	 * 					> <security:access-denied-handler ref="customAccessDenied" />
	 * 
	 * 				# 접근 거부 처리자 클래스 정의
	 * 				- CustomAccessDeniedHandler 클래스 정의
	 * 				> AccessDeniedHandler 인터페이스를 참조받아서 handle 메소드를 재정의하여 사용한다.
	 * 				우리는 접근이 거부되었을 때 빈으로 등록해 둔 CustomAccessDeniedHandler 클래스가 발동해 해당 메소드가
	 * 				실행되고 response 내장 객체를 활용하여 /accessError URL로 이동하여 접근 거부시 보여질 페이지로 이동하지만
	 * 				이곳에서 더 많은 로직을 처리할 수도 있다. (request, response 내장 객체를 이용하여 다양한 처리 가능)
	 * 
	 * 				# 화면 설명
	 * 				- 일반 게시판 등록 페이지
	 * 				> 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로 접근 시 접근 가능
	 * 				- 공지사항 게시판 등록 페이지
	 * 				> 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로 접근 시에 공지사항
	 * 				게시판 등록 페이지는 관리자 권한만 접근 가능하므로 접근이 거부된다. 이 때, access-denied-handler로 설정
	 * 				되어 있는 ref 속성에 부여된 클래스 메소드로 이동하고 해당 페이지에서 접근이 거부되었을 때 페이지의 정보가 나타난다.
	 * 
	 * 		7. 사용자 정의 로그인 페이지
	 * 
	 * 			- 기본 로그인 페이지가 아닌 사용자가 직접 정의한 로그인 페이지를 사용한다.
	 * 
	 * 				# 환경 설정
	 * 				- 스프링 시큐리티 설정(security-context.xml 설정)
	 * 
	 * 					<security:form-login login-page="/login" /> 설정
	 * 					: 사용자가 직접 만든 로그인 페이지로 이동할 '/login' URL을 가지고 있는 컨트롤러 메소드를 정의
	 * 
	 * 				# 로그인 페이지 정의
	 * 
	 * 				- 사용자가 정의한 로그인 컨트롤러
	 * 					> controller 패키지 안에 LoginController 생성
	 * 				- 사용자가 정의한 로그인 뷰
	 * 					> views/loginForm.jsp
	 * 
	 * 				** 시큐리티에서 제공하는 기본 로그인 페이지로 이동하지 않고, 사용자가 정의한 로그인 페이지의 URI를 요청하여
	 * 				해당 페이지에서 권한을 체크하도록 한다. 인증이 완료되면 최초의 요청된 target URI로 이동한다.
	 * 				그렇지 않은 경우 사용자가 만들어놓은 접근 거부 페이지로 이동한다.
	 * 
	 * 		8. 로그인 성공 처리
	 * 
	 * 			- 로그인을 성공한 후에 로그인 이력 로그를 기록하는 등의 동적을 하고 싶은 경우가 있다.
	 * 				이런 경우 AuthenticationSuccessHandler라는 인터페이스를 구현해서 로그인 성공 처리자로 지정할 수 있다.
	 * 
	 * 				# 환경 설정
	 * 				- customLoginSuccess Bean 등록
	 * 				<security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess" />
	 * 
	 * 				# 로그인 성공 처리자 클래스 정의
	 * 				- 로그인 성공 처리자
	 * 				> SavedRequestAwareAuthenticationSuccessHandler는 AuthenticationSuccessHandler의 구현 클래스이다.
	 * 					인증 전에 접근을 시도한 URL로 리다이렉트하는 기능을 가지고 있으며 스프링 시큐리티에서 기본적으로
	 * 					사용되는 구현 클래스이다.
	 * 				- 로그인 성공 처리자2
	 * 				> AuthenticationSuccessHandler 인터페이스를 직접 구현하여 인증 전에 접근을 시도한 URL로
	 * 					리다이렉트하는 기능을 구현한다.
	 * 
	 * 				# 화면 설명
	 * 				- 일반 게시판 등록 화면
	 * 				> 사용자가 정의한 로그인 페이지에서 회원 권한에 해당하는 계정으로 로그인 시, 성공했다면 성공 처리자인
	 * 					CustomLoginSuccess 클래스로 넘어가 넘겨받은 파라미터들 중 authentication 안에
	 * 					principal로 User 정보를 받아서 username과 password를 출력한다.
	 * 					(출력 정보는 로그인 성공 시 인증된 회원 정보이다.)
	 * 
	 * 		9. 로그아웃 처리
	 * 
	 * 			- 로그아웃을 위한 URI를 지정하고, 로그아웃 처리 후에 별도의 작업을 하기 위해서 사용자가 직접 구현한 처리자를
	 * 				등록할 수 있다.
	 * 
	 * 			# 환경 설정
	 * 
	 * 				- 스프링 시큐리티 설정(security-context.xml 설정)
	 * 				> <security:logout logout-url="/logout" invalidate-session="true" />
	 * 
	 * 				** logout 경로를 스프링에서 제공하는 /logout 경로로 설정한다.
	 * 					logout 처리 페이지에서도 action 경로를 /logout으로 설정한다.
	 * 
	 * 		10. JDBC를 이용한 인증/인가 처리
	 * 
	 * 			- 지정한 형식으로 테이블을 생성하면 JDBC를 이용해서 인증/인가를 처리할 수 있다.
	 * 			- 생성할 테이블은 사용자를 관리하는 테이블(users)과 권한을 관리하는 테이블이다.
	 * 
	 * 			# 데이터베이스 테이블 준비
	 * 
	 * 				- users, authorities 테이블 준비
	 * 
	 * 			# 환경설정
	 * 
	 * 				- 의존 라이브러리 설정
	 * 					> 데이터베이스 관련 라이브러리를 추가한다.
	 * 					> 기존 데이터베이스 연결을 위한 라이브러리를 가져와 등록(pom.xml)
	 * 
	 * 			# 스프링 설정(root-context.xml 설정)
	 * 
	 * 				- 데이터 소스 설정(기존 설정)
	 * 
	 * 			# 스프링 시큐리티 설정(security-context.xml 설정)
	 * 
	 * 				- customPasswordEncoder 빈 등록 진행
	 * 					<security:authentication-manage> 태그 설정
	 * 
	 * 			# 비밀번호 암호화 처리기 클래스 정의
	 * 				- 비밀번호 암호화 처리기
	 * 					스프링 시큐리티 5부터는 기본적으로 PasswordEncoder를 지정해야 하는데, 제대로 하려면 생성된 사용자 테이블(users)
	 * 					에 비밀번호를 암호화하여 저장해야 한다. 테스트를 위해서 생성한 데이터는 암호화를 처리하지 않으므로 로그인 하면
	 * 					당연히 로그인 에러가 발생할 것이다. (암호화된 비밀번호가 날아가는게 아니라서)
	 * 					그래서 암호화를 하지 않는 PasswordNoOpPasswordEncoder를 직접 구현하여 지정하면 로그인 시 암호화를 고려하지
	 * 					않으므로 로그인이 정상적으로 이루어지는 걸 확인할 수 있다.
	 * 					** PasswordEncoder를 참조받아서 우리가 원하는 로직으로 변경해서 사용(암호화를 사용하지 않는 루트로)
	 * 
	 * 		11. 사용자 테이블 이용한 인증/인가 처리
	 * 
	 * 			- 스프링 시큐리티가 기본적으로 이용하는 테이블 구조를 그대로 생성해서 사용해도 되지만, 기존에 구축된 회원 테이블이 있다면
	 * 				약간의 작업으로 기존 테이블을 활용할 수 있다.
	 * 
	 * 			# 데이터베이스 테이블 준비
	 * 				- member, member_auth 테이블 준비
	 * 
	 * 			# 환경 설정
	 * 				- 스프링 시큐리티 설정(security-context.xml 설정)
	 * 					> bcryptPasswordEncoder 빈 등록 진행
	 * 					> <security:jdbc-user-service> 태그 설정
	 * 					> <security:password-encoder> 태그 설정
	 * 
	 * 			# 쿼리 정의
	 * 				- 인증할 때 필요한 쿼리
	 * 					> select user_id, user_pw, enabled from member where user_id = ?
	 *
	 *				- 권한을 확인할 때 필요한 쿼리
	 *					> select m.user_id, ma.auth from member m, member_auth ma where ma.user_no = m.user_no and m.user_id = ?
	 *
	 *			# BCryptPasswordEncoder 클래스를 이용하여 직접 encode된 비밀번호를 찾아 데이터베이스에 셋팅한다.
	 *				- 12번 UserDetails 정보를 등록하면서 같이 진행
	 *
	 *					- BCryptPasswordEncoder 클래스를 활용한 단방향 비밀번호 암호화
	 *						> encode() 메소드를 통해서 SHA-2 방식의 8바이트 Hash 암호를 매번 랜덤하게 생성한다.
	 *						> 똑같은 비밀번호를 입력하더라도 암호화되는 문자열은 매번 다른 문자열을 반환한다.
	 *
	 *						비밀번호를 입력하면 암호화된 비밀번호로 인코딩하는데, 암호화된 비밀번호와 DB 테이블에 있는 암호화된 비밀번호가 
	 *						일치한지를 파악 후 일치하면 로그인 성공으로 다음 스탭을 진행한다.
	 *						> BCryptPasswordEncoder 클래스의 encode() 메소드를 통해 만들어지는 암호화된 해쉬 다이제스트들은 입력한
	 *							비밀번호 문자에 해당하는 수십억개의 다이제스트들 중에서 일치하는 다이제스트가 존재할 경우 비밀번호의 일치로 보고
	 *							인증을 성공시켜준다.
	 *
	 *		12. UserDetailsService 재정의
	 *
	 *			- 스프링 시큐리티의 UserDetailsService를 구현하여 사용자 상세 정보를 얻어오는 메서드를 재정의한다.
	 *
	 *				# 환경 설정
	 *					- 의존 라이브러리 설정(pom.xml) 설정
	 *					> 데이터베이스 관련 라이브러리
	 *					- 스프링 시큐리티 설정(security-context.xml 설정)
	 *					> customUserDetailsService 빈 등록
	 *					> security:authentication-provider 태그 설정
	 *
	 *				# 클래스 재정의
	 *					- UserDetailsService 재정의
	 *						> security/CustomUserDetailsService 클래스 생성
	 *						> 기존 사용중인 read를 기반으로 한 readById 재정의
	 *						> CustomUserDetailsService 클래스 내 loadUserByUsername 메소드에서 인코딩 된 비밀번호 확인 후
	 *							데이터베이스 비밀번호 수정
	 *							> Member 테이블 내 특정 계정들 비밀번호를 암호화된 비밀번호로 수정한다.
	 *
	 *		13. 스프링 시큐리티 표현식
	 *
	 *			- 스프링 시큐리티를 이용하면 인증 및 권한 정보에 따라 화면을 동적으로 구성할 수 있고, 
	 *				로그인 한 사용자 정보를 보여줄 수도 있다.
	 *
	 *			# 공동 표현식
	 *
	 *				- hasRole([role])
	 *					> 해당 롤이 있으면 true
	 *				- hasAnyRole([role1, role2])
	 *					> 여러 롤들 중에서 하나라도 해당하는 롤이 있으면 true
	 *				- principal
	 *					> 인증된 사용자의 사용자 정보(UserDetails 인터페이스를 구현한 클래스의 객체)를 의미
	 *				- authentication
	 *					> 인증된 사용자의 인증 정보(Authentication 인터페이스를 구현한 클래스의 객체)fmf dmlal
	 *				- permitAll
	 *					> 모든 사용자에게 허용
	 *				- denyAll
	 *					> 모든 사용자에게 거부
	 *				- isAnonymous()
	 *					> 익명의 사용자의 경우(로그인을 하지 않은 경우도 해당)
	 *				- isAuthenticated()
	 *					> 인증된 사용자면 true를 반환
	 *				- isFullyAuthenticated()
	 *					> Remember-me로 인증된 것이 아닌 일반적인 방법으로 인증된 사용자인 경우 true
	 *
	 *			# 표현식 사용
	 *
	 *				- 표현식을 이용하여 동적 화면 구성
	 *					> home.jsp 수정
	 *						: 표현식을 이용한 내용 추가
	 *
	 *				- 로그인한 사용자 정보 보여주기
	 *					> views/board/register.jsp 수정
	 *					> views/notice/register.jsp 수정
	 *
	 *		14. 자동 로그인
	 *
	 *			- 로그인하면 특정 시간 동안 다시 로그인 할 필요가 없는 기능이다.
	 *			- 스프링 시큐리티는 메모리나 데이터베이스를 사용하여 처리한다.
	 *			- 기능을 구현하기 위해 <security:remember-me> 태그를 이용하여 시큐리티 설정 파일을 수정한다.
	 *
	 *			# 데이터베이스 테이블
	 *				- persistent_logins 테이블 준비
	 *
	 *			# 환경 설정
	 *				- 스프링 시큐리티 설정(security-context.xml 설정)
	 *					<security:remember-me data-source-ref="dataSource" token-validaty="604800" /> 태그 설정
	 *					<security:logout logout-url="/logout" invalidate-session="true" 
	 *						delete-cookies="remember-me, JSESSION_ID" /> 태그 설정
	 *
	 *			# 자동 로그인
	 *				- 로그인 상태 유지 체크박스 추가
	 *					> loginForm.jsp 수정 (자동 로그인 체크 박스 만들기)
	 *
	 *			# 자동 로그인 시, 만들어지는 쿠키 정보들
	 *				- JSESSIONID와 remember-me 쿠키가 만들어진다.
	 *				- JSESSIONID를 삭제 후, 다시 로그인을 진행하더라도 로그인 후 진행될 페이지가 정상적으로 나타나는 것을 확인할 수 있음
	 *					> 자동 로그인이 remember-me 쿠키에 의해서 데이터베이스에 저장되는 각 hash token 값에 의해 재설정됨을 확인]
	 *
	 *		15. 스프링 시큐리티 어노테이션
	 *
	 *			- 스프링 시큐리티는 어노테이션을 사용하여 필요한 설정을 추가할 수 있다.
	 *
	 *				# 사용 어노테이션
	 *
	 *					- @Secured
	 *						> 스프링 시큐리티 모듈을 지원하기 위한 어노테이션으로 초기부터 사용되었다.
	 *					- @PreAuthorize
	 *						> 메서드가 실행되기 전에 적용할 접근 정책을 지정할 때 사용한다.
	 *					- @PostAuthorize
	 *						> 메서드가 실행한 후에 적용할 접근 정책을 지정할 때 사용한다.
	 */
	
}

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
	
	<context:component-scan base-package="kr.or.ddit" />
	
	<!--  
		pre-post-annotations 속성을 'enabled'로 설정하면 @PreAuthorize, @PostAuthorize를 사용할 수 있고
		secured-annotations 속성을 'enabled'로 설정하면 @Secured를 사용할 수 있다.
	-->
	<security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled" />
	
	
</beans:beans>

[security-context.xml]

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 6. 사용자 정의 접근 거부 처리자 Bean 추가 -->
	<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
	<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>
	<!-- <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean> -->
	<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
	<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>

	<security:http>
	
		<!-- 3. 접근 제한 설정 : URL 패턴으로 접근 제한을 설정한다. -->
		<!-- <security:intercept-url pattern="/board/list" access="permitAll" />
		<security:intercept-url pattern="/board/register" access="hasRole('ROLE_MEMBER')" />
		<security:intercept-url pattern="/notice/list" access="permitAll" />
		<security:intercept-url pattern="/notice/register" access="hasRole('ROLE_ADMIN')" /> -->
		<!-- 3. 접근 제한 설정 끝 -->
		
		<!-- 
			폼 기반 인증 기능을 사용한다. : 사용자 정의 로그인 페이지 추가(login-page),
			로그인 성공 처리 추가 : 로그인 성공 후 처리를 담당하는 처리자로 지정(customLoginSuccess)
		-->
		<security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess" />
		
		<!-- 5. 접근 거부 처리자 : 접근 거부 처리자의 URI를 지정 -->
		<!-- <security:access-denied-handler error-page="/accessError" /> -->
		
		<!-- 6. 사용자 정의 접근 거부 처리자 추가 : customAccessDenied를 접근 거부 처리자로 등록 -->
		<security:access-denied-handler ref="customAccessDenied" />
		
		<!--  
			14. 자동 로그인 적용
			- 데이터 소스를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록
			- 쿠키의 유효 시간을 지정한다. (604800 : 7일)
		-->
		<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
		
		<!-- 9. 로그아웃 처리 : 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화한다. -->
		<security:logout logout-url="/logout" invalidate-session="true" 
			delete-cookies="remember-me, JSESSION_ID" />
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider user-service-ref="customUserDetailsService">
			<!-- <security:user-service>
				<security:user name="member" password="{noop}1234" authorities="ROLE_MEMBER" />
				<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN" />
			</security:user-service> -->
			
			<!-- 사용자 정의 테이블을 이용한 인증/인가 처리 -->
			<!-- <security:jdbc-user-service data-source-ref="dataSource"/>
			<security:password-encoder ref="customPasswordEncoder" /> -->
			<!-- 사용자 정의 테이블을 이용한 인증/인가 처리 끝 -->
			
			<!-- 
				UserDetailsService를 설정하면서 데이터베이스 연동 후 사용자가 정의한 테이블로 mapper를 통한 데이터 바인딩으로 
				인증/인가 진행 시 주석
			-->
			<!-- <security:jdbc-user-service data-source-ref="dataSource" 
			users-by-username-query="select user_id, user_pw, enabled from member where user_id = ?" 
			authorities-by-username-query="select m.user_id, ma.auth from member m, member_auth ma where ma.user_no = m.user_no and m.user_id = ?" />
			<security:password-encoder ref="bcryptPasswordEncoder" /> -->
			
			<security:password-encoder ref="bcryptPasswordEncoder" />
			
		</security:authentication-provider>
	</security:authentication-manager>

</beans>

package kr.or.ddit.controller;

import java.security.Principal;
import java.util.Iterator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
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 kr.or.ddit.vo.CrudMember;
import kr.or.ddit.vo.CustomUser;

@Controller
@RequestMapping("/board")
public class BoardController {

	private static final Logger log = LoggerFactory.getLogger(BoardController.class);
	
	@RequestMapping(value = "/list", method = RequestMethod.GET)
	public String list() {
		log.info("list : access to all");
		return "board/list";
	}
	
	// 회원 권한을 가진 사용자만 접근이 가능하다.
	@PreAuthorize("hasAnyRole('ROLE_MEMBER', 'ROLE_ADMIN')")
	@RequestMapping(value = "/register", method = RequestMethod.GET)
	public String register(Principal principal) {
		log.info("register : access to member");
		
		// 사용자 정보 가져오기
		// [방법 1] principal 객체를 받아와서 인증된 정보 가져오기
		log.info("name : " + principal.getName());
		
		// [방법 2] User 객체 정보를 얻어와서 사용
		CustomUser user = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		log.info("username : " + user.getUsername());
		log.info("password : " + user.getPassword());
		CrudMember member = user.getMember(); 
		log.info("userId : " + member.getUserId());
		log.info("userName : " + member.getUserName());
		
		Iterator<GrantedAuthority> ite = user.getAuthorities().iterator();
		while(ite.hasNext()) {
			log.info("auth : " + ite.next().getAuthority());
		}
		
		return "board/register";
	}
	
}

package kr.or.ddit.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;


@Controller
@RequestMapping("/notice")
public class NoticeController {

	private static final Logger log = LoggerFactory.getLogger(NoticeController.class);
	
	@RequestMapping(value = "/list", method = RequestMethod.GET)
	public String list() {
		log.info("list : access to all");
		return "notice/list";
	}
	
	@PreAuthorize("hasRole('ROLE_ADMIN')")
	@RequestMapping(value = "/register", method = RequestMethod.GET)
	public String register() {
		log.info("register : access to admin");
		
		return "notice/register";
	}
	
}


이제 요기에 일괄 적용 해보자

- http://localhost/notice/login.do


[pom.xml]

<!-- 스프링 시큐리티 라이브러리 의존 관계 정의 시작 -->
		<!-- 스프링 시큐리티를 웹에서 동작하도록 해줌 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
			<version>5.0.7.RELEASE</version>
		</dependency>
		<!-- 스프링 시큐리티 설정을 도와줌 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>5.0.7.RELEASE</version>
		</dependency>
		<!-- 스프링 시큐리티 일반 기능 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-core</artifactId>
			<version>5.0.7.RELEASE</version>
		</dependency>
		<!-- 스프링 시큐리티 태그 라이브러리를 연결해 줌 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-taglibs</artifactId>
			<version>5.0.7.RELEASE</version>
		</dependency>
		<!-- 스프링 시큐리티 라이브러리 의존 관계 정의 끝 -->

[web.xml]

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_1.xsd">

	<!-- 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/security-context.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 
				/WEB-INF/spring/security-context.xml 
			</param-value>
		</init-param>
		
		<init-param>
			<param-name>throwExceptionIfNoHandlerFound</param-name>
			<param-value>true</param-value>
		</init-param>
		
		<load-on-startup>1</load-on-startup>
		
		<!--  
			web.xml의 설정은 WAS(Tomcat) 자체 설정일 뿐이다.
			multipart-config : 메모리 사이즈, 업로드 파일 저장 위치, 최대 크기 설정
			- location : 저장될 디렉토리(필수)
			- max-file-size : 업로드 파일 최대 크기(기본값 : -1L, 제한이 없다)
			- max-request-size : 한번 요청 시 업로드 파일 최대 크기
			- file-size-threshold : 설정 크기가 넘는 경우 임시 디렉토리에 저장(기본값 0, 설정하지 않는 한 무조건 저장)
			
			web.xml에서 설정하지 않을 때는 @MultipartConfig 어노테이션으로 설정이 가능하다.
			- 요청을 받는 컨트롤러에 설정이 가능하다. (메소드 라인이 아니라 컨트롤러인 클래스 라인에 설정한다)
			- @MultipartConfig(
				  location = "D:/upload",
				  maxFileSize = "24683394",
				  maxRequestSize = "478212294",
				  fileSizeThreshold = "154985741"
			  )
			  
			  임시 파일이 저장되는 경로는 다음과 같다.
			 - C:\Users\개인PC이름\AppData\Local\Temp=
		-->
<!-- 		<multipart-config>
			<location>C:\\upload</location>
			<max-file-size>24683394</max-file-size>
			<max-request-size>478212294</max-request-size>
			<file-size-threshold>154985741</file-size-threshold>
		</multipart-config> -->
	</servlet>
		
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	
	<!--  
		한글 처리를 위한 UTF-8 필터 등록
		JSP나 서블릿 처리할 때마다 넘겨받은 request를 setCharacterEncoding으로 UTF-8 설정했던 부분을 필터로 대체함
	-->
	<filter>
		<filter-name>encordingFilter</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>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encordingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	
	<!--  
		MultipartFilter 적용 : 파일 업로드를 위한 필터 적용
		- MultipartFilter의 역할은 들어온 요청이 multipart/form-data 유형의 요청인지를 확인하여 multipart 형태의 요청이면
		MultipartResolver를 통해 multipart 요청을 확인한다.
		그리고 해당 요청이 적절한 요청이면 MultipartHttpServletRequest로 랩핑한다.
		- MultipartFilter의 기본 빈 이름은 'filterMultipartResolver'이다.
	-->
	<filter>
		<filter-name>MultipartFilter</filter-name>
		<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>MultipartFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	
	<!-- 서블릿 필터 클래스를 서블릿 컨테이너에 등록함 -->
	<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>
	
	<!-- 3. 상태 코드를 사용한 이동 대상 페이지 설정 시작 -->
	<!-- <error-page>
		<error-code>400</error-code>
		<location>/WEB-INF/views/error/errorCommon400.jsp</location>
	</error-page>
	<error-page>
		<error-code>404</error-code>
		<location>/WEB-INF/views/error/errorCommon404.jsp</location>
	</error-page>
	<error-page>
		<error-code>500</error-code>
		<location>/WEB-INF/views/error/errorCommon500.jsp</location>
	</error-page> -->
	<!-- 3. 상태 코드를 사용한 이동 대상 페이지 설정 끝 -->

	<!-- 4. 예외 타입을 사용한 이동 대상 페이지 설정 시작 -->
	<!-- <error-page>
		<exception-type>java.lang.Exception</exception-type>
		<location>/WEB-INF/views/error/errorCommon.jsp</location>
	</error-page> -->
	<!-- 4. 예외 타입을 사용한 이동 대상 페이지 설정 끝 -->
	
	<!-- 5. 기본 에러 페이지 설정 시작 -->
	<!-- <error-page>
		<location>/WEB-INF/views/error/errorCommon.jsp</location>
	</error-page> -->
	<!-- 5. 기본 에러 페이지 설정 끝 -->
	
</web-app>

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
		<beans:property name="order" value="2" />
	</beans:bean>
	
	<!-- Tiles 설정을 위한 Bean 등록 시작 -->
	<beans:bean id="tilesViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
		<beans:property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView" />
		<beans:property name="order" value="1" />
	</beans:bean>
	
	<beans:bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
		<beans:property name="definitions">
			<beans:list>
				<beans:value>/WEB-INF/spring/tiles-config.xml</beans:value>
			</beans:list>
		</beans:property>
	</beans:bean>
	<!-- Tiles 설정을 위한 Bean 등록 끝 -->
	
	<context:component-scan base-package="kr.or.ddit.controller" />
	
	<!--  
		서블릿 표준용 MultipartResolver 를 스프링 빈으로 정의
		- StandardServletMultipartResolver 사용 시 설정
			> Servlet 3.0의 Part를 이용한 MultipartFile 데이터 처리
	-->
<!-- 	<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
		
	</beans:bean> -->
	
	<!--  
		pre-post-annotations 속성을 'enabled'로 설정하면 @PreAuthorize, @PostAuthorize를 사용할 수 있고
		secured-annotations 속성을 'enabled'로 설정하면 @Secured를 사용할 수 있다.
	-->
	
	<context:component-scan base-package="kr.or.ddit.security" />
	
	<security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled" />
	
	<!-- 
		인터셉터 설정 
		- loginInterceptor 클래스를 빈으로 정의한다.
			> 설정한 클래스는 해당 위치에 존재해야 함(설정에 맞는 위치에 있어야 한다)	
	-->
	<!-- <beans:bean id="loginInterceptor" class="kr.or.ddit.controller.intercept.LoginInterceptor" />
	<interceptors>
		<interceptor>
			<mapping path="/login1" />
			<beans:ref bean="loginInterceptor" />
		</interceptor>
	</interceptors>
	
	<beans:bean id="accessLoggingInterceptor" class="kr.or.ddit.controller.intercept.AccessLoggingInterceptor" />
	<interceptors>
		<interceptor>
			<mapping path="/**" />
			<exclude-mapping path="/resources/**"/>
			<beans:ref bean="accessLoggingInterceptor" />
		</interceptor>
	</interceptors> -->
	
</beans:beans>

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 6. 사용자 정의 접근 거부 처리자 Bean 추가 -->
	<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
	<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>
	<!-- <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean> -->
	<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
	<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>

	<security:http>
		
		<!-- 
			폼 기반 인증 기능을 사용한다. : 사용자 정의 로그인 페이지 추가(login-page),
			로그인 성공 처리 추가 : 로그인 성공 후 처리를 담당하는 처리자로 지정(customLoginSuccess)
		-->
		<security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess" />
		
		<!-- 6. 사용자 정의 접근 거부 처리자 추가 : customAccessDenied를 접근 거부 처리자로 등록 -->
		<security:access-denied-handler ref="customAccessDenied" />
		
		<!--  
			14. 자동 로그인 적용
			- 데이터 소스를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록
			- 쿠키의 유효 시간을 지정한다. (604800 : 7일)
		-->
		<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
		
		<!-- 9. 로그아웃 처리 : 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화한다. -->
		<security:logout logout-url="/logout" invalidate-session="true" 
			delete-cookies="remember-me, JSESSION_ID" />
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider user-service-ref="customUserDetailsService">
			<security:password-encoder ref="bcryptPasswordEncoder" />
		</security:authentication-provider>
	</security:authentication-manager>

</beans>

위에서 만들어 놓은 것 복사 붙여넣기...

package kr.or.ddit.mapper;

import java.util.Map;

import kr.or.ddit.vo.crud.NoticeMemberVO;

public interface ILoginMapper {

	public NoticeMemberVO loginCheck(NoticeMemberVO member);
	public NoticeMemberVO idCheck(String memId);
	public int signup(NoticeMemberVO memberVO);
	public NoticeMemberVO idFind(Map<String, String> map);
	public NoticeMemberVO pwFind(Map<String, String> map);
	public NoticeMemberVO readByUserId(String username);

}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.or.ddit.mapper.ILoginMapper">

	<resultMap type="noticeMemberVO" id="noticeMemberMap">
		<id property="memNo" column="mem_no" />
		<result property="memNo" column="mem_no" />
		<result property="memId" column="mem_id" />
		<result property="memPw" column="mem_pw" />
		<result property="memName" column="mem_name" />
		<result property="memGender" column="mem_gender" />
		<result property="memEmail" column="mem_email" />
		<result property="memPhone" column="mem_phone" />
		<result property="memPostCode" column="mem_postcode" />
		<result property="memAddress1" column="mem_address1" />
		<result property="memAddress2" column="mem_address2" />
		<result property="memProfileImg" column="mem_profileimg" />
		<result property="memRegDate" column="mem_regdate" />
		<result property="enabled" column="enabled" />
		<collection property="authList" resultMap="noticeAuthMap" />
	</resultMap>
	
	<resultMap type="noticeMemberAuthVO" id="noticeAuthMap">
		<result property="memNo" column="mem_no" />
		<result property="auth" column="auth" />
	</resultMap>
	
	<!-- ctrl + shift + y : 소문자 변환 -->
	<select id="loginCheck" parameterType="noticeMemberVO" resultType="noticeMemberVO">
		select 
			mem_no,
			mem_id,
			mem_pw,
			mem_name,
			mem_gender,
			mem_email,
			mem_phone,
			mem_postcode,
			mem_address1,
			mem_address2,
			mem_agree,
			mem_profileimg,
			mem_regdate
		from noticemember 
		where mem_id = #{memId} 
		and mem_pw = #{memPw}
	</select>
	
	<select id="idCheck" parameterType="string" resultType="noticeMemberVO">
		select 
			mem_no,
			mem_id,
			mem_pw,
			mem_name,
			mem_gender,
			mem_email,
			mem_phone,
			mem_postcode,
			mem_address1,
			mem_address2,
			mem_agree,
			mem_profileimg,
			mem_regdate
		from noticemember 
		where mem_id = #{memId} 
	</select>
	
	<insert id="signup" parameterType="noticeMemberVO">
		insert into noticemember (
			mem_no,
			mem_id,
			mem_pw,
			mem_name,
			mem_gender,
			mem_email,
			mem_phone,
			mem_postcode,
			mem_address1,
			mem_address2,
			mem_agree,
			mem_profileimg,
			mem_regdate,
			enabled
		) values (
			seq_noticemember.nextval,
			#{memId},
			#{memPw},
			#{memName},
			#{memGender},
			#{memEmail},
			#{memPhone},
			#{memPostCode},
			#{memAddress1},
			#{memAddress2},
			#{memAgree},
			#{memProfileImg},
			sysdate,
			1
		)
	</insert>
	
	<select id="idFind" parameterType="java.util.HashMap" resultType="noticeMemberVO">
	    select 
	        mem_no,
	        mem_id,
	        mem_pw,
	        mem_name,
	        mem_gender,
	        mem_email,
	        mem_phone,
	        mem_postcode,
	        mem_address1,
	        mem_address2,
	        mem_agree,
	        mem_profileimg,
	        mem_regdate
	    from noticemember 
	    where 1=1 
	    and mem_email = #{memEmail} 
	    and mem_name = #{memName} 
	</select>
	
	<select id="pwFind" parameterType="java.util.HashMap" resultType="noticeMemberVO">
	    select 
	        mem_no,
	        mem_id,
	        mem_pw,
	        mem_name,
	        mem_gender,
	        mem_email,
	        mem_phone,
	        mem_postcode,
	        mem_address1,
	        mem_address2,
	        mem_agree,
	        mem_profileimg,
	        mem_regdate
	    from noticemember 
	    where 1=1 
	    and mem_id = #{memId} 
	    and mem_email = #{memEmail} 
	    and mem_name = #{memName} 
	</select>
	
	<select id="readByUserId" parameterType="string" resultMap="noticeMemberMap">
		select 
			m.mem_no, mem_id, mem_pw, mem_name, mem_gender, mem_email, mem_phone,
			mem_postcode, mem_address1, mem_address2, mem_agree, mem_profileimg, mem_regdate, enabled, 
			a.auth
		from noticemember m left outer join noticemember_auth a on(m.mem_no = a.mem_no) 
		where m.mem_id = #{memId}
	</select>
	
</mapper>

package kr.or.ddit.vo;

import java.util.Collection;
import java.util.stream.Collectors;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

import kr.or.ddit.vo.crud.NoticeMemberVO;

public class CustomUser extends User {

	private NoticeMemberVO member;
	
	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}
	
	public CustomUser(NoticeMemberVO member) {
		// Java 스트림을 사용한 경우(람다 표현식)
		// - 자바 버전 8부터 추가된 기능
		// map : 컬렉션(List, Map, Set 등), 배열 등의 설정되어 있는 각 타입의 값들을 하나씩 참조하여 람다식으로 반복 처리할 수 있게 해준다.
		// collect : Stream()을 돌려 발생되는 데이터를 가공 처리 하고 원하는 형태의 자료형으로 변환을 돕는다.
		// 회원 정보 안에 들어 있는 역할명 들을 컬렉션 형태의 스트림으로 만들어서 보내준다.
		// *** 람다 표현식은 복잡한 메서드 라인을 간단한 표현식으로 출력할 수 있다는 장점이 있는 대신 디버깅을 할 수 없다는 점이 단점이다.
		super(member.getMemId(), member.getMemPw(), member.getAuthList().stream().map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList()));
		this.member = member;
	}

	public NoticeMemberVO getMember() {
		return member;
	}

	public void setMember(NoticeMemberVO member) {
		this.member = member;
	}
	
}
package kr.or.ddit.security;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import kr.or.ddit.mapper.ILoginMapper;
import kr.or.ddit.vo.CustomUser;
import kr.or.ddit.vo.crud.NoticeMemberVO;

public class CustomUserDetailsService implements UserDetailsService {
	
	private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);
	
	@Inject
	private BCryptPasswordEncoder bpe;
	
	@Inject
	private ILoginMapper loginMapper;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.info("loadUserByUsername() 실행...!");
		log.info("Load User By Username : " + username);
		
		// UserDetailService를 등록하는 과정에서 우리가 할 목표는 User 객체의 정보와
		// 인증되어 실제로 사용될 내 id에 해당하는 회원정보를 CrudMember에 담고 그 녀석을 UserDetails 정보 안에서
		// 가용할 수 있도록 만든다.
		NoticeMemberVO member;
		try {
			member = loginMapper.readByUserId(username);
			log.info("queried by member mapper : " + member);
			return member == null ? null : new CustomUser(member);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return null;
	}

}

package kr.or.ddit.controller.crud.notice;

import java.util.List;

import javax.inject.Inject;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import kr.or.ddit.service.INoticeService;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("/notice")
public class NoticeRetrieveController {

	@Inject
	private INoticeService noticeService;
	
	@PreAuthorize("hasAnyRole('ROLE_MEMBER', 'ROLE_ADMIN')")
	@RequestMapping(value = "/list.do")
	public String noticeList(
			@RequestParam(name = "page", required = false, defaultValue = "1") int currentPage,
			@RequestParam(required = false, defaultValue = "title") String searchType,
			@RequestParam(required = false) String searchWord,
			Model model
			) {
		PaginationInfoVO<NoticeVO> pagingVO = new PaginationInfoVO<NoticeVO>();
		
		// 검색 기능 추가
		// 검색을 했을 때의 조건은 키워드(searchWord)가 넘어왔을 때 정확하게 검색을 진행한 거니까
		// 이 때, 검색을 진행하기 위한 타입과 키워드를 PaginationInfoVO에 셋팅하고 목록을 조회하기 위한 조건으로
		// 쿼리를 조회할 수 있도록 보내준다.
		if(StringUtils.isNotBlank(searchWord)) {
			pagingVO.setSearchType(searchType);
			pagingVO.setSearchWord(searchWord);
			model.addAttribute("searchType", searchType);
			model.addAttribute("searchWord", searchWord);
		}
		
		// 현재 페이지 전달 후, start/endRow와 start/endPage 설정
		pagingVO.setCurrentPage(currentPage);
		
		int totalRecord = noticeService.selectNoticeCount(pagingVO); // 총 게시글 수 가져오기
		pagingVO.setTotalRecord(totalRecord);
		List<NoticeVO> dataList = noticeService.selectNoticeList(pagingVO);
		pagingVO.setDataList(dataList);
		
		model.addAttribute("pagingVO", pagingVO);
		
		return "notice/list";
	}
	
	@RequestMapping(value = "/detail.do", method = RequestMethod.GET)
	public String noticeDetail(int boNo, Model model) {
		NoticeVO noticeVO = noticeService.selectNotice(boNo);
		model.addAttribute("notice", noticeVO);
		return "notice/detail";
	}
	
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 6. 사용자 정의 접근 거부 처리자 Bean 추가 -->
	<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
	<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>
	<!-- <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean> -->
	<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
	<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>

	<security:http>
		
		<!-- 
			폼 기반 인증 기능을 사용한다. : 사용자 정의 로그인 페이지 추가(login-page),
			로그인 성공 처리 추가 : 로그인 성공 후 처리를 담당하는 처리자로 지정(customLoginSuccess)
		-->
		<security:form-login login-page="/notice/login.do" authentication-success-handler-ref="customLoginSuccess" />
		
		<!-- 6. 사용자 정의 접근 거부 처리자 추가 : customAccessDenied를 접근 거부 처리자로 등록 -->
		<security:access-denied-handler ref="customAccessDenied" />
		
		<!--  
			14. 자동 로그인 적용
			- 데이터 소스를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록
			- 쿠키의 유효 시간을 지정한다. (604800 : 7일)
		-->
		<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
		
		<!-- 9. 로그아웃 처리 : 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화한다. -->
		<security:logout logout-url="/logout" invalidate-session="true" 
			delete-cookies="remember-me, JSESSION_ID" />
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider user-service-ref="customUserDetailsService">
			<security:password-encoder ref="bcryptPasswordEncoder" />
		</security:authentication-provider>
	</security:authentication-manager>

</beans>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<div class="login-box">
    <div class="card">
        <div class="card-body login-card-body">
            <h2 class="login-box-msg"><b>DDIT</b> BOARD</h2>

            <form action="/login" method="post" id="signForm" name="signForm">
              <div class="input-group mb-3">
                    <input type="text" id="memId" name="username" class="form-control" placeholder="아이디를 입력해주세요">
                    <div class="input-group-append">
                        <div class="input-group-text">
                            <span class="fas fa-envelope"></span>
                        </div>
                    </div>
                    <span class="error invalid-feedback" style="display: block;">${errors.memId }</span>
                </div>
                <div class="input-group mb-3">
                    <input type="password" id="memPw" name="password" class="form-control" placeholder="비밀번호를 입력해주세요">
                    <div class="input-group-append">
                        <div class="input-group-text">
                            <span class="fas fa-lock"></span>
                        </div>
                    </div>
                    <span class="error invalid-feedback" style="display: block;">${errors.memPw }</span>
                </div>
                <div class="row">
                    <div class="col-8">
                        <div class="icheck-primary">
                            <input type="checkbox" id="remember" name="remember-me">
                            <label for="remember">
                                Remember Me
                            </label>
                        </div>
                    </div>
                    <div class="col-4">
                        <button type="button" class="btn btn-dark btn-block" id="loginBtn">로그인</button>
                    </div>
                </div>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
                <sec:csrfInput />
            </form>

<script type="text/javascript">
    $(function() {
        // 로그인 페이지를 요청 시, background에 이미지 삽입
        $("body").css("background-image", "url('${pageContext.request.contextPath}/resources/dist/img/background04.jpg')").css("background-size", "cover");
        
        var idFindBtn = $("#idFindBtn");
        var pwFindBtn = $("#pwFindBtn");
        var loginBtn = $("#loginBtn");
        var idFindForm = $("#idFindForm");
        var pwFindForm = $("#pwFindForm");
        
        idFindBtn.on("click", function(){
        	var memEmail = $("#memEmail").val();
        	var memName = $("#memName").val();
        	
        	if(!memEmail) {
        		alert("이메일을 입력해 주세요.");
        		$("#memEmail").focus();
        		return false;
        	}
        	
        	if(!memName) {
        		alert("이름을 입력해 주세요.");
        		$("#memName").focus();
        		return false;
        	}
        	
        	var data = {
        		memEmail : memEmail,
        		memName : memName
        	};
        	
        	$.ajax({
        		type: "post",
        		url: "/notice/idFind.do",
        		data: JSON.stringify(data),
        		contentType: "application/json;charset=utf-8",
        		/* beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
        			xhr.setRequestHeader("${_csrf.headerName}", "${_csrf.token}"");
        		}, */
        		beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
        			xhr.setRequestHeader(header, token);
        		},
        		success: function(res) {
					console.log("결과 : " + res);
					
					// 회원님의 아이디는 [<font id="id" color="red" class="h2"></font>] 입니다.
					if(!res) { // 리턴받은 결과가 없음, 즉 아이디 없음
						$(".idText").html("회원님의 아이디를 찾을 수 없습니다.");
					}else { // 아이디 있음
						var id = res.memId;
						$(".idText").html("회원님의 아이디는 [<font id='id' class='h2' style='color:red'>"+id+"</font>] 입니다.");
					}
				}
        	});
        });
        
        pwFindBtn.on("click", function(){
        	var memId = $("#memId").val();
        	var memEmail = $("#memEmail2").val();
        	var memName = $("#memName2").val();
        	
        	if(!memId) {
        		alert("아이디를 입력해 주세요.");
        		$("#memId").focus();
        		return false;
        	}
        	
        	if(!memEmail) {
        		alert("이메일을 입력해 주세요.");
        		$("#memEmail2").focus();
        		return false;
        	}
        	
        	if(!memName) {
        		alert("이름을 입력해 주세요.");
        		$("#memName2").focus();
        		return false;
        	}
        	
        	var data = {
        		memId : memId,
           		memEmail : memEmail,
           		memName : memName
           	};
           	
           	$.ajax({
           		type: "post",
           		url: "/notice/pwFind.do",
           		data: JSON.stringify(data),
           		contentType: "application/json;charset=utf-8",
           		beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
        			xhr.setRequestHeader(header, token);
        		},
           		success: function(res) {
   					console.log("결과 : " + res);
   					
 					// 회원님의 비밀번호는 [<font color="red" class="h2" id="password"></font>] 입니다.
   					if(!res) { // 리턴받은 결과가 없음, 즉 비밀번호 없음
						$(".pwText").html("회원님의 비밀번호를 찾을 수 없습니다.");
					}else { // 비밀번호 있음
						var pw = res.memPw;
						$(".pwText").html("회원님의 비밀번호는 [<font id='password' class='h2' style='color:red'>"+pw+"</font>] 입니다.");
					}
   				}
           	});
        	
        });
        
        loginBtn.on("click", function(){
        	location.href = "/notice/login.do";
        });
    });

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- csrf meta 정보를 등록 -->
    <meta id="_csrf" name="_csrf" content="${_csrf.token }" />
    <meta id="_csrf_header" name="_csrf_header" content="${_csrf.headerName }" />
    <title>AdminLTE 3 | Log in</title>

    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
    <link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/fontawesome-free/css/all.min.css">
    <link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
    <link rel="stylesheet" href="${pageContext.request.contextPath }/resources/dist/css/adminlte.min.css">
    <script src="${pageContext.request.contextPath }/resources/plugins/jquery/jquery.min.js"></script>
    <script type="text/javascript">
    	var token = "";
    	var header = "";
    	$(function(){
    		token = $("meta[name='_csrf']").attr("content");
    		header = $("meta[name='_csrf_header']").attr("content");
    	});
    </script>
</head>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- csrf meta 정보를 등록 -->
    <meta id="_csrf" name="_csrf" content="${_csrf.token }" />
    <meta id="_csrf_header" name="_csrf_header" content="${_csrf.headerName }" />
    <title>AdminLTE 3 | Simple Tables</title>

    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
    <link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/fontawesome-free/css/all.min.css">
    <link rel="stylesheet" href="${pageContext.request.contextPath }/resources/dist/css/adminlte.min.css">
    <script src="${pageContext.request.contextPath }/resources/plugins/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="${pageContext.request.contextPath }/resources/ckeditor/ckeditor.js"></script>
    <script type="text/javascript">
    	var token = "";
    	var header = "";
    	$(function(){
    		token = $("meta[name='_csrf']").attr("content");
    		header = $("meta[name='_csrf_header']").attr("content");
    	});
    </script>
</head>

- http://localhost/notice/forget.do


- http://localhost/notice/list.do

package kr.or.ddit.controller.exception;

import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;

// @ControllerAdvice 어노테이션은 스프링 컨트롤러에서 발생하는 예외를 처리하는 핸들러 클래스임을 명시한다.
@ControllerAdvice
public class CommonExceptionHandler {

//	// @ExceptionHandler 어노테이션은 괄호 안에 설정한 예외 타입을 해당 메소드가 처리한다는 것을 의미한다.
//	@ExceptionHandler(Exception.class)
//	public String Handle(Exception e, Model model) {
//		model.addAttribute("exception", e);
//		return "error/errorCommon";
//	}
//	
//	@ExceptionHandler(NoHandlerFoundException.class)
//	@ResponseStatus(value = HttpStatus.NOT_FOUND)
//	public String handle404(Exception e) {
//		return "error/custom404";
//	}
	
}

- http://localhost/notice/login.do

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
                <sec:csrfInput/>
            </form>
idCheckBtn.on("click", function(){
    var id = $("#memId").val();

    if(!id) {
        alert("아이디를 입력해 주세요!");
        return false;
    }

    var data = {
        memId : id
    };

    $.ajax({
        type: "post",
        url: "/notice/idCheck.do",
        data: JSON.stringify(data),
        contentType: "application/json;charset=utf-8",
        beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
            xhr.setRequestHeader(header, token);
        },
        success: function(res) {
            console.log("중복확인 후 넘겨받은 결과 : " + res);

            if(res == "NOTEXIST") { // 아이디 사용 가능
                alert("사용 가능한 아이디 입니다.");
                idCheckFlag = true; // 중복 확인 했다는 flag 설정
            }else { // 아이디 중복
                alert("이미 사용중인 아이디 입니다.");
                //idCheckFlag = false; // 중복 확인 했다는 flag 설정
            }
        }
    });
});

@RequestMapping(value = "/signup.do", method = RequestMethod.POST)
	public String signup(HttpServletRequest req, NoticeMemberVO memberVO, Model model, RedirectAttributes ra) {
		String goPage = "";
		
		Map<String, String> errors = new HashMap<String, String>();
		if(StringUtils.isBlank(memberVO.getMemId())) {
			errors.put("memId", "아이디를 입력해 주세요.");
		}
		if(StringUtils.isBlank(memberVO.getMemPw())) {
			errors.put("memId", "비밀번호를 입력해 주세요.");
		}
		if(StringUtils.isBlank(memberVO.getMemName())) {
			errors.put("memId", "이름을 입력해 주세요.");
		}
		
		if(errors.size() > 0) { // 넘겨받은 데이터의 에러가 존재
			model.addAttribute("errors", errors);
			model.addAttribute("member", memberVO);
			model.addAttribute("bodyText", "register-page");
			goPage = "conn/register";
		}else { // 정상적인 데이터를 받았을 때
			ServiceResult result = noticeService.signup(req, memberVO);
			if(result.equals(ServiceResult.OK)) { // 가입 성공
				ra.addFlashAttribute("message", "회원가입을 완료하였습니다!");
				goPage = "redirect:/notice/login.do";
			}else { // 가입 실패
				model.addAttribute("message", "서버에러, 다시 시도해 주세요!");
				model.addAttribute("member", memberVO);
				model.addAttribute("bodyText", "register-page");
				goPage = "conn/register";
			}
		}
		
		return goPage;
	}
@Service
public class NoticeServiceImpl implements INoticeService {

	@Inject
	private INoticeMapper noticeMapper;
	
	@Inject
	private ILoginMapper loginMapper;
	
	@Inject
	private IProfileMapper profileMapper;
	
	private TelegramSendController tst = new TelegramSendController();
	
	// 스프링 시큐리티를 활용한 비밀번호 암호화를 처리할 PasswordEncoder 의존성 주입
	@Inject
	private PasswordEncoder pe;
	@Override
	public ServiceResult signup(HttpServletRequest req, NoticeMemberVO memberVO) {
		ServiceResult result = null;

		// 회원가입 시, 프로필 이미지로 파일을 업로드 하는데 이때 업로드 할 서버 경로
		String uploadPath = req.getServletContext().getRealPath("/resources/profile");
		File file = new File(uploadPath);
		if (!file.exists()) {
			file.mkdirs();
		}

		String proFileImg = ""; // 회원정보에 추가될 프로필 이미지 경로
		try {
			// 넘겨받은 회원정보에서 파일 데이터 가져오기
			MultipartFile proFileImgFile = memberVO.getImgFile();

			// 넘겨받은 파일 데이터가 존재할 때
			if (proFileImgFile.getOriginalFilename() != null && !proFileImgFile.getOriginalFilename().equals("")) {
				String fileName = UUID.randomUUID().toString(); // UUID 파일명 생성
				fileName += "_" + proFileImgFile.getOriginalFilename(); // UUID_원본파일명으로 파일명 생성
				uploadPath += "/" + fileName; // /resources/profile/uuid_원본파일명

				proFileImgFile.transferTo(new File(uploadPath)); // 해당 위치에 파일 복사
				proFileImg = "/resources/profile/" + fileName; // 파일 복사가 일어난 파일의 위치로 접근하기 위한 URI 설정
			}

			memberVO.setMemProfileImg(proFileImg);
			
			// 비밀번호 암호화(스프링 시큐리티 적용 후)
			memberVO.setMemPw(pe.encode(memberVO.getMemPw()));
		} catch (Exception e) {
			e.printStackTrace();
		}

		int status = loginMapper.signup(memberVO);

		if (status > 0) { // 등록 성공
			// 한명의 회원이 등록될 때 하나의 권한을 무조건 가질 수 있도록 권한 등록(스프링 시큐리티 적용 이후)
			loginMapper.signupAuth(memberVO.getMemNo());
			
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}
package kr.or.ddit.mapper;

import java.util.Map;

import kr.or.ddit.vo.crud.NoticeMemberVO;

public interface ILoginMapper {

	public NoticeMemberVO loginCheck(NoticeMemberVO member);
	public NoticeMemberVO idCheck(String memId);
	public int signup(NoticeMemberVO memberVO);
	public NoticeMemberVO idFind(Map<String, String> map);
	public NoticeMemberVO pwFind(Map<String, String> map);
	public NoticeMemberVO readByUserId(String username);
	public void signupAuth(int memNo);

}

<insert id="signup" parameterType="noticeMemberVO" useGeneratedKeys="true">
		<selectKey keyProperty="memNo" order="BEFORE" resultType="int">
			select seq_noticemember.nextval from dual
		</selectKey>
		insert into noticemember (
			mem_no,
			mem_id,
			mem_pw,
			mem_name,
			mem_gender,
			mem_email,
			mem_phone,
			mem_postcode,
			mem_address1,
			mem_address2,
			mem_agree,
			mem_profileimg,
			mem_regdate,
			enabled
		) values (
			#{memNo},
			#{memId},
			#{memPw},
			#{memName},
			#{memGender},
			#{memEmail},
			#{memPhone},
			#{memPostCode},
			#{memAddress1},
			#{memAddress2},
			#{memAgree},
			#{memProfileImg},
			sysdate,
			1
		)
	</insert>
	
	<insert id="signupAuth" parameterType="int">
		insert into 
			noticemember_auth 
		values(#{memNo}, 'ROLE_MEMBER')
	</insert>

- http://localhost/notice/signup.do


 

'대덕인재개발원 > 대덕인재개발원_웹기반 애플리케이션' 카테고리의 다른 글

231216_SPRING CRUD 보강 1  (0) 2023.12.15
231213_SPRING 2 (16-1)  (0) 2023.12.12
231212_SPRING 2 (15-1)  (0) 2023.12.11
231211_SPRING 2 (14-1)  (0) 2023.12.11
231208_SPRING 2 (13-2)  (0) 2023.12.08