본문 바로가기
Java/Spring

Spring) Security - 인증된 사용자 정보 가져오기, 처리 순서 파악

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

 

Security - 인증된 사용자 정보 가져오기

아이디를 클릭하면 해당 회원의 정보를 랜더링 해주도록 하겠습니다.


security 처리 순서 파악하기

사용자 인증 요청이 들어오면, 위와 같은 순서로 처리가 됩니다.

AuthenticationFilter에서 사용자가 입력한 username과 password를 통해 UsernamePasswordAuthenticationToken 생성

해당 객체를 AuthenticationManager에게 보내줌 (구현체는 providerManager)

<authentication-manager> 
    <authentication-provider user-service-ref="memberSecurityService">
        <password-encoder ref="bcryptPasswordEncoder"/>
    </authentication-provider> 
</authentication-manager>

③ 이를 관리하는 AuthenticationProvider가 여러개 있고, 그 중 security가 현재 username, password를 가지고 인증을 처리할 수 있는 provider를 찾음 (UserDetailsService를 가지고 있는 Provider가 처리할 수 있다고 나섬!!)

UserDetailsService를 통해 실제 하나의 User를 가져옴

⑤ 이를 DB에서 조회하여 데이터를 가져오게 되면, 이를 AuthenticationFilter에서 세션에 저장

⑥ security가 관리하는 SecurityContextHolder가 있고, SecurityContext가 따로 있으며, 그 안에 Authentication에서 사용자 정보 관리

- Authentication 내부적으로 principal, credentials, authorities 를 구분하여 관리해줍니다.

즉, 인증된 사용자 정보는 SecurityContextHolder가 가지고 있습니다.


Security - 사용자 정보 상세 조회

 

이러한 처리 과정을 이해한 후 인증된 사용자 정보를 Controller에서 가져와 랜더링 처리해주겠습니다.

 

인증된 사용자 정보 가져오기

방법 ① - SecurityContextHolder로부터 직접 가져오는 방법 (비추)

 

Controller

MemberSecurityController

/**
 * security가 관리하는 인증된 사용자 정보
 */
@GetMapping("/memberDetail.do")
public ModelAndView memberDetail(ModelAndView mav) {
    // SecurityContextHolder로부터 직접 가져오는 방법
    SecurityContext securityContext = SecurityContextHolder.getContext();
    Authentication authentication = securityContext.getAuthentication();
    Object principal = authentication.getPrincipal();
    Object credentials = authentication.getCredentials();
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    log.debug("principal = {}", principal);
    log.debug("credentials = {}", credentials);
    log.debug("authorities = {}", authorities);

    mav.setViewName("member/memberDetail");
    return mav;
}

@콘솔출력값
DEBUG: com.ce.spring2.member.controller.MemberSecurityController - principal = Member(super=MemberEntity(memberId=honggd, password=$2a$10$G7F3EfcXE8wJWUAsCznaoOqttim4MwJBNlWey5yuL5ZRMwRSdPvEi, name=홍길동그랑땡, gender=M, birthday=1999-09-09, email=honggd12@abc.com, phone=01045674567, address=경기 성남시 분당구 대왕판교로 688, hobby=[등산, 게임], createdAt=2022-08-22T12:23:43, updatedAt=2022-08-23T12:47:25, enabled=true), authorities=[ROLE_USER])
DEBUG: com.ce.spring2.member.controller.MemberSecurityController - credentials = null // 실제 열람 불가!
DEBUG: com.ce.spring2.member.controller.MemberSecurityController - authorities = [ROLE_USER]

 

※ intercetor에서 발생하는 403 오류

지금까지 포스팅 과정을 거치면 servlet-context에 loginInterceptor를 통한 login여부를 검사했을 것입니다.

이제는 security가 처리해주기 때문에 해당 interceptor는 주석 처리해주겠습니다.

servlet-context.xml

<!-- 
<interceptor>
    <mapping path="/member/memberDetail.do"/>
    <mapping path="/member/memberUpdate.do"/>
    <mapping path="/board/**"/>
    <exclude-mapping path="/board/boardList.do"/>
    <exclude-mapping path="/board/boardDetail.do"/>
    <exclude-mapping path="/board/fileDownload.do"/>
    <beans:bean id="loginInterceptor" class="com.ce.spring2.common.interceptor.LoginInterceptor"></beans:bean>
</interceptor> 
-->
public class LoginInterceptor extends HandlerInterceptorAdapter {
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// 로그인 여부 확인
		HttpSession session = request.getSession();
		Member loginMember = (Member)session.getAttribute("loginMember");
		
		
		if(loginMember == null) {
			// Redirect Attribute 대신 사용할 FlashMap
			FlashMap flashMap = new FlashMap();
			flashMap.put("msg", "로그인 후 이용할 수 있습니다.");
			FlashMapManager manager = RequestContextUtils.getFlashMapManager(request);
			manager.saveOutputFlashMap(flashMap, request, response);

			response.sendRedirect(request.getContextPath() + "/member/memberLogin.do");
			return false;
		}
		
		return super.preHandle(request, response, handler);
	}
}

해당 intercetpor를 주석하지 않는다면, session에 "loginMember"를 저장하지 않고 security로 관리하므로 loginMember는 null이므로 해당 if문을 타게 됩니다.

그 때, redirect가 "/member/memberLogin.do"로 되는데 이 때 /member/memberLogin.do는 isAnonymous() 즉! 인증이 안되었을 때만 접근할 수 있으므로 403 오류가 발생하게 됩니다.

 

다만!!! 위와 같은 방법으로 인증된 사용자 정보를 가져오는 것은 비추입니다.

(하나하나 다 꺼내온 것이기 때문)

 

인증된 사용자 정보 가져오기

방법 ② - Authentication 주입 받기

 

Controller

MemberSecurityController

@GetMapping("/memberDetail.do")
public ModelAndView memberDetail(Authentication authentication, ModelAndView mav) {
    Object principal = authentication.getPrincipal();
    Object credentials = authentication.getCredentials();
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    log.debug("principal = {}", principal);
    log.debug("credentials = {}", credentials);
    log.debug("authorities = {}", authorities);

    mav.setViewName("member/memberDetail");
    return mav;
}

@콘솔출력값
DEBUG: com.ce.spring2.member.controller.MemberSecurityController - principal = Member(super=MemberEntity(memberId=honggd, password=$2a$10$G7F3EfcXE8wJWUAsCznaoOqttim4MwJBNlWey5yuL5ZRMwRSdPvEi, name=홍길동그랑땡, gender=M, birthday=1999-09-09, email=honggd12@abc.com, phone=01045674567, address=경기 성남시 분당구 대왕판교로 688, hobby=[등산, 게임], createdAt=2022-08-22T12:23:43, updatedAt=2022-08-23T12:47:25, enabled=true), authorities=[ROLE_USER])
DEBUG: com.ce.spring2.member.controller.MemberSecurityController - credentials = null
DEBUG: com.ce.spring2.member.controller.MemberSecurityController - authorities = [ROLE_USER]

잘 가져오는 것을 확인할 수 있습니다.

 

memberDetail.jsp

<input type="text" class="form-control" placeholder="아이디 (4글자이상)" name="memberId" id="memberId" value='<sec:authentication property="principal.memberId"/>' readonly required/>
<input type="text" class="form-control" placeholder="이름" name="name" id="name" value="<sec:authentication property="principal.name"/>" required/>
<input type="date" class="form-control" placeholder="생일" name="birthday" id="birthday" value="<sec:authentication property="principal.birthday"/>" />
<input type="email" class="form-control" placeholder="이메일" name="email" id="email" value="<sec:authentication property="principal.email"/>"/>
<input type="tel" class="form-control" placeholder="전화번호 (예:01012345678)" name="phone" id="phone" maxlength="11" value="<sec:authentication property="principal.phone"/>" required/>
<input type="text" class="form-control" placeholder="주소" name="address" id="address" value="<sec:authentication property="principal.address"/>"/>

<sec:authentication> 태그의 property를 이용하여 principal를 통해 인증된 사용자 정보에 대해 접근하여 값을 가져와주었습니다.

 

select 혹은 checkbox 등 값을 비교해야하는 상황에서는 principal를 변수로 저장하여 사용하는 것이 편리합니다.

<sec:authentication property="principal" var="loginMember" scope="page"/>

<select class="form-control" name="gender" required>
  <option value="" disabled selected>성별</option>
  <option value="M" ${loginMember.gender eq 'M' ? 'selected' : ''}>남</option>
  <option value="F" ${loginMember.gender eq 'F' ? 'selected' : ''}>여</option>
</select>
<%
//Member loginMember = (Member) pageContext.getAttribute("loginMember");
Member loginMember = (Member) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String[] hobbies = loginMember.getHobby();
List<String> hobbyList = hobbies != null ? Arrays.asList(hobbies) : null;
pageContext.setAttribute("hobbyList", hobbyList);

%>
<div class="form-check-inline form-check">
    취미 : &nbsp; 
    <input type="checkbox" class="form-check-input" name="hobby" id="hobby0" value="운동" ${hobbyList.contains('운동') ? 'checked' : ''}>
    <label for="hobby0" class="form-check-label" >운동</label>&nbsp;
    <input type="checkbox" class="form-check-input" name="hobby" id="hobby1" value="등산" ${hobbyList.contains('등산') ? 'checked' : ''}>
    <label for="hobby1" class="form-check-label" >등산</label>&nbsp;
    <input type="checkbox" class="form-check-input" name="hobby" id="hobby2" value="독서" ${hobbyList.contains('독서') ? 'checked' : ''}>
    <label for="hobby2" class="form-check-label" >독서</label>&nbsp;
    <input type="checkbox" class="form-check-input" name="hobby" id="hobby3" value="게임" ${hobbyList.contains('게임') ? 'checked' : ''}>
    <label for="hobby3" class="form-check-label" >게임</label>&nbsp;
    <input type="checkbox" class="form-check-input" name="hobby" id="hobby4" value="여행" ${hobbyList.contains('여행') ? 'checked' : ''}>
    <label for="hobby4" class="form-check-label" >여행</label>&nbsp;

<sec:authentication property="principal" var="loginMember" scope="page"/>

principal를 loginMember라는 변수에 저장해주며, 기본적으로 page scope에 저장됩니다.

이를 이용하여 값을 비교할 수 있습니다.