본문 바로가기
Java/Spring

Spring) Security - 커스텀 로그인/로그아웃 페이지, CSRF 공격대비 설정(403에러), error처리

by 박채니 2022. 9. 11.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.

 

security 커스텀 로그인 페이지

 

security-context.xml

    <form-login
        login-page="/member/memberLogin.do"
        login-processing-url="/member/memberLogin.do"
        default-target-url="/"
        always-use-default-target="false"
        username-parameter="memberId"
        password-parameter="password" />

(http 태그는 생략함! http 태그 안에 작성해야합니다.)

 

login-page → 로그인 폼 요청 (작성필요)

login-processing-url → security 로그인 처리 (작성불필요)

username-parameter → 기본값 : username

password-parameter → 기본값 : password

default-target-url → 로그인 성공 후 리다이렉트 url (기본 값 root)

always-use-default-target → 항상 root로 리다이렉트? false : 이전 요청 페이지로 리다이렉트 처리

 

username-parameter, password-parameter로그인 폼에서 아이디/비밀번호를 작성하는 폼의 name값을 입력해줍니다.

memberLogin.jsp

<input 
    type="text" class="form-control" name="memberId"
    placeholder="아이디" required> 
<br /> 
<input
    type="password" class="form-control" name="password"
    placeholder="비밀번호" required>

로그인 폼의 아이디 인풋의 name이 memberId 이므로 지정해주었습니다.

(이 경우 password-parameter는 생략 가능. 기본 값이 password이므로!)

 

Controller

MemberSecurityController

@Controller
@RequestMapping("/member")
@Slf4j
public class MemberSecurityController {
	@Autowired
	private MemberService memberService;
	
	@Autowired
	private BCryptPasswordEncoder bcryptPasswordEncoder;
	
	@GetMapping("/memberEnroll.do")
	public String memberEnroll() {
		
		return "member/memberEnroll";
	}

	@PostMapping("/memberEnroll.do")
	public String memberEnroll(Member member, RedirectAttributes redirectAttr) {
		try {
			// 비밀번호 암호화
			String rawPassword = member.getPassword();
			String encodePassword = bcryptPasswordEncoder.encode(rawPassword);
			member.setPassword(encodePassword);
			log.debug("encodePassword = {}", encodePassword);
			
			int result = memberService.insertMember(member);
			redirectAttr.addFlashAttribute("msg", "회원가입이 정상적으로 처리 되었습니다.");
			
			return "redirect:/";
		} catch(Exception e) {
			log.error("회원 가입 오류 : " + e.getMessage(), e);
			throw e;
		}
	}

	@GetMapping("/memberLogin.do")
	public void memberLogin() {
		
	}
}

로그인 폼을 연결하였으니, 더이상 security에서 제공하는 로그인 폼이 나오지 않는 것을 확인할 수 있습니다.

하지만 여기서 메모리 상에 저장해뒀던 honggd, 1234를 입력 후 로그인을 하면 403 에러가 발생합니다.

<authentication-manager> <!-- provider 관리 -->
    <authentication-provider>
        <user-service>
            <user 
                name="honggd" 
                password="$2a$10$G7F3EfcXE8wJWUAsCznaoOqttim4MwJBNlWey5yuL5ZRMwRSdPvEi" 
                authorities="ROLE_USER"/> <!-- 메모리상에서 관리! -->
            <user 
                name="admin" 
                password="$2a$10$G7F3EfcXE8wJWUAsCznaoOqttim4MwJBNlWey5yuL5ZRMwRSdPvEi" 
                authorities="ROLE_USER, ROLE_ADMIN"/>
        </user-service>
        <password-encoder ref="bcryptPasswordEncoder"/>
    </authentication-provider> <!-- 인증관련 처리(인증주체) -->
</authentication-manager>

honggd 로그인 시

 

그 이유는 Post 요청으로 실행되는 로그인 → csrf 토큰 값이 없어서 발생한 것입니다.

 

 

CSRF (Cross-site Request Forgery) 공격 대비 설정

- 사용자의 특정 권한을 이용하여 공격자의 의도된 행동을 실행하게 만드는 공격법

- 특정 사용자의 권한수정, 등록/수정/삭제

- 모든 post 요청에 spring이 제공하는 token 값 검증

<csrf disabled="false"/>

(http 태그는 생략함! http 태그 안에 작성해야합니다.)

 

post 요청 시 csrf 토큰 값 제출

방법 ① - <form:form> 태그 이용

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<form:form
    action="${pageContext.request.contextPath}/member/memberLogin.do"
    method="post">
    <div class="modal-body">
        <input 
            type="text" class="form-control" name="memberId"
            placeholder="아이디" required> 
        <br /> 
        <input
            type="password" class="form-control" name="password"
            placeholder="비밀번호" required>
    </div>
    <div class="modal-footer">
        <button type="submit" class="btn btn-outline-success">로그인</button>
        <button type="button" class="btn btn-outline-success" data-dismiss="modal">취소</button>
    </div>
</form:form>

 

폼 태그 내부에 input:hidden으로 csrf 토큰 값을 value로 가지고 있는 인풋 태그가 생성되었습니다.

해당 토큰 값이 일치하지 않다면, 공격자의 폼 제출로 간주하여 403 에러가 발생하게 되는 것이죠.

 

※ 만일 form taglib 오류가 난다면, 마찬가지로 해당 경로 하위에 jar파일을 lib 폴더 하위에 위치시키면 해결!
C:\Users\사용자\.m2\repository\org\springframework\spring-webmvc\5.2.22.RELEASE(본인 버전에 맞게 경로따라가기)

 

로그인 성공


security 커스텀 로그아웃 페이지

- security의 로그아웃은 반드시 POST 요청

 

security-context.xml

<logout
    logout-url="/member/memberLogout.do"
    logout-success-url="/" />

(http 태그는 생략함! http 태그 안에 작성해야합니다.)

 

post 요청 시 csrf 토큰 값 제출

방법 ② - input:hidden 이용

header.jsp

<form action="${pageContext.request.contextPath}/member/memberLogout.do" method="post">			    	
    <button class="btn btn-outline-success my-2 my-sm-0" type="submit" >로그아웃</button>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />			    
</form>

 

Controller

security가 처리해주므로 MemberSecurityController에 핸들러를 생성하지 않아도 정상적으로 잘 작동합니다.

 

로그아웃 성공

 

※ logout intercepr-url 설정

<intercept-url pattern="/member/memberLogout.do" access="permitAll"/>

기본 세션 관리하는 시간인 30분이 지난 다음 로그아웃을 누르면, 이미 세션이 해제된 상태에서 로그아웃 버튼을 누르게 됩니다.

그렇게 되면 logout 요청을 'isAnonymous' 상태에서 하게 되므로 오류가 발생하게 됩니다.

따라서 permitAll로 열어두어 오류 발생을 방지해줍니다.

 


error 처리

만일 아이디/비밀번호가 틀렸다면, url에 ?error가 붙게 되며 해당 로그인 페이지로 다시 리다이렉트 처리가 됩니다.

error가 발생하였다면, 그에 따른 사용자 알림이 있어야할 것이고, 이에 따른 스크립트 처리를 해주겠습니다.

 

memberLogin.jsp

<div class="modal-body">
    <c:if test="${param.error != null}">
        <div class="alert alert-danger alert-dismissible fade show" role="alert">
          아이디 또는 비밀번호가 일치하지 않습니다.
          <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
    </c:if>
    <input 
        type="text" class="form-control" name="memberId"
        placeholder="아이디" required> 
    <br /> 
    <input
        type="password" class="form-control" name="password"
        placeholder="비밀번호" required>
</div>

param.error가 존재한다면, 해당 alert가 띄워지도록 처리해보았습니다.