Spring - 37.2 Spring Security

갓김치·2021년 1월 11일
0

JSP+Spring

목록 보기
41/43

Spring Security

DB설계

유저 관련

  • 다대다
  • 한명이 여러역할가능, 한 역할에 여러명가능

자원 관련

  • ROLES_HIERARCHY는 빼고 3개가 자원관련
  • 다대다
  • 한 자원이 여러 역할가능, 한 역할에 여러 자원 접근가능

관리자 권한 관련

  • 슈퍼 관리자와 일반 관리자
  • 그룹웨어, pms, erp에서 중요한 것 : 접근제어
  • 그룹웨어안에서 사용할 수 있는 부분도 직급에 따라 나뉘어짐
    • 대리가 하는 일중 일반 사원이 못하는 일이 있을 수 있음
    • 사원이 부모, 사원의 권한을 이어받아 대리가 만들어짐
    • 대리가 부모, 대리의 권한을 이어받아 과장이 만들어짐

사용 예제

1. Spring Security 의존성 추가

2. web.xml 수정

  • 시큐리티는 필터의 집합
  • 하지만 필터로 등록하자니 컨테이너 밖에있어 인젝션을 할 수 없다.
  • 하지만 컨테이너 안으로 넣자니 톰캣이 필터의 존재를 모를텐데 어쩌지?
<filter>
  <!-- BeanIds 에 filter-name 나와있음 -->
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  <init-param>
  <param-name>targetBeanName</param-name>
  <param-value>springSecurityFilterChain</param-value>
  </init-param>
</filter>
<filter-mapping>
  <filter-name>springSecurityFilterChain</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
  • web.xml에 프록시를 등록하는 이유
    • DelegatingFilterProxy가 요청을 받고, 컨테이너 안 진짜에게 넘겨야될텐데 어떤 빈에게 넘겨야할지를 어떻게 선택할까? filter-name으로 구별하여 넘긴다.

3. container에 security-context.xml 추가

  • 상위 컨테이너에 넣어야 제일 먼저 생성될 수 있다 접근제어는 제일 먼저 해야하는일이기때문에

4. 필요없는 컨트롤러 삭제

LoginProcessController

@Controller
public class LoginProcessServlet {
	@Inject
	IAuthenticateService service;
	
	@RequestMapping(value="/login/loginProcess.do", method=RequestMethod.POST)
	public String doPost(@RequestParam("mem_id") String mem_id
						 ,@RequestParam("mem_pass")String mem_pass
						 ,HttpServletRequest req
						 ,RedirectAttributes redirectAttributes) {
		HttpSession session = req.getSession();
		Object result = service.authenticate(MemberVO.builder()
												     .mem_id(mem_id)
												     .mem_pass(mem_pass)
												     .build());
		if(result instanceof MemberVO) {
			// 인증 성공 -> req에 들어있는 정보가 더이상 필요 없음 -> redirect
			MemberVO authMember = (MemberVO) result;
			session.setAttribute("authMember", authMember);
			return "redirect:/";
		}else {
			String message = null;
			if(ServiceResult.NOTEXIST.equals(result)) {
				message = "아이디 오류, 그런 사람 없음";
			}else if(ServiceResult.INVALIDPASSWORD.equals(result)) { // INVALIDPASSWORD
				message = "비번 오류, 다시 입력하셈요.";
				session.setAttribute("mem_id", mem_id);
			}else { // DISABLE
				message = "탈퇴한 회원입니다.";
			}
			redirectAttributes.addFlashAttribute("message", message);
			return "redirect:/login/loginForm.do";
		}
		
	}
}
  • authMember는 스프링이 자동으로 만들어주지않음
  • authMember를 대체할 애가 필요함

LogoutController

@Controller
public class LogoutServlet{
	
	@RequestMapping(value="/login/logout.do",method=RequestMethod.POST)
	public String logout(HttpServletRequest req) throws UnsupportedEncodingException {
		 HttpSession session = req.getSession();
		 session.removeAttribute("authMember"); 
		 // 로그아웃된것처럼 보임 왜냐 - authMember 하나만 지웠으니까 - 로그인된 중에 세션에 여러가지 정보 저장했을 수도 있음 

		 session.invalidate();
		 // 모든 어트리뷰트를 하나하나 다 지워주고, 세션 만료시키고, 세션아이디까지 없애줌
		 
		 String encoded = URLEncoder.encode("로그아웃성공", "UTF-8");
		 return "redirect:/?message="+encoded;
	}
}

5. security-context.xml

  • authentication-manager

6. membervo대신 wrapper 생성

  • security framework은 기존의 MemberVO를 몰라
  • 근데 바꾸기는 싫어
    • Security를 위해 MemberVO를 UserDetails를 implements하면 간단하지만, MemberVO를 사용하던 다른 모듈에서 수정사항이 발생할 수 있으니 adapter패턴을 사용하여 수정사항을 최소화하자
  • 그래서 어댑터패턴 사용 : MemberWrapper
  • MemberWrapper를 security 프레임웤내에서 사용할수 있도록 extends User를 해준 것
  • 근데 이제 문제는 memberdao에서 membervo로 돌려주고있음
  • 그래서 중간에 membervo를 wrapper로 바꿔주는게필요
  • 7번으로 ㄱㄱ

Users.class

private String password; // mem_pass
private final String username; // mem_id
private final Set<GrantedAuthority> authorities; // mem_role
private final boolean accountNonExpired; // 계정 만료
private final boolean accountNonLocked; // 계정 잠금
private final boolean credentialsNonExpired; // 비번만료
private final boolean enabled; // 현재 계정 사용 가능한지? -> mem_delete가 null이면 전부 true, mem_delete가 'Y'면 전부 false

MemberWrapper.class

/*
 * Security를 위해 UserDetails를 implements하면 간단하지만,
 * MemberVO를 사용하던 다른 모듈에서 수정사항이 발생할 수 있으니 adapter패턴을 사용하여 수정사항을 최소화하자
 */
public class MemberWrapper extends User{
	
	public MemberWrapper(MemberVO realMember) {
		super(realMember.getMem_id()
			, realMember.getMem_pass()
			, Collections.singleton(new SimpleGrantedAuthority(realMember.getMem_role())));
	}
	
	private MemberVO realMember;
}

7. AuthenticateServiceImpl

  • member dao selectMember에서 리턴으로 돌려주는 MemberVO는 security가 갖고놀수없음
  • 기존의 코드는 지우고 UserDetailsService를 구현하여 MemberVO를 MemberWrapper로 바꿔주자
  • security가 이 결과값을 받아 session에 집어넣음

8. security-context.xml

  • authentication
  • login에 관련된 명령들 설정
  • 이러고 테스트를 해보니 csrf 공격이라고 간주하여 막아버리네요 -> disabled="true"로 설정
	<authentication-manager id="authenticationManager">
		<!-- provider가 db에서 가져와서 비밀번호까지 비교하는 작업을 함-->
		<authentication-provider user-service-ref="customUserService"><!-- authServImpl -->
			<password-encoder ref="passwordEncoder"/>
		</authentication-provider>
  • auth provider가 평문을 암호문으로 바꾼후에 db정보와 비교
  • 비교 끝나면 authentication 객체를 만들어 session scope에 auth manager가 집어넣어줌

9. MypageController

  • 근데 세션스코프안에 인증객체가 필요한데 그걸 어떻게찾죠?
  • @AuthenticationPrincipal(expression="realMember") MemberVO authMember 이렇게 쓰면 된단다~~~
  • 이 어노테이션의 퀄러파이드네임을 보면 security 소속임 그럼 핸들러어댑터가 알수없음 그럼 오또캐?

10. servlet-context.xml

	<annotation-driven validator="validator"><!-- 커스터마이징한 벨리데이터 연결 -->
		<argument-resolvers>
			<beans:bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver"></beans:bean>
		</argument-resolvers>
	</annotation-driven>
  • 핸들러어댑터가 추가적인 아규먼트리졸버를 가질 수 있도록 추가적으로 등록해준것
  • 이 리졸버를 통하여 마이페이지컨트롤러에 @Authenticationprincipal이 작동할 수 있는거
  • 그럼이제 auth manager가 세션스코프안에 어떤이름으로 넣던지는 우리랑 상관없는일이됨

11. security-context.xml 이용해 로그인

		<form-login
			login-page="/login/loginForm.do"
			username-parameter="mem_id"
			password-parameter="mem_pass"
			login-processing-url="/login/loginProcess.do"
			always-use-default-target="true"
			default-target-url="/"
		/>
  • 로그인시 들어오는 데이터를 매핑 -> auth manager에게 나중에 넘겨서 인증이 처리됨

12. security-context.xml 이용해 로그아웃

<!-- security를 타기위해서는 밖에서 다시 filter를 타고 들어와야하는데 탈퇴후 로그아웃할때는 forward로 와서 여길 타지 않음 -->
<logout
        logout-url="/login/logout.do"
        invalidate-session="true"
        logout-success-url="/"
        />
  • 로그아웃 성공후 웰컴페이지로 가도록 매핑
  • 여기까지는 인증구조에 해당

13. (인가) security-context.xml

<!-- 		<intercept-url pattern="/mypage.do" access="isAuthenticated()"/> -->
<!-- 		<intercept-url pattern="/member/memberList.do" access="hasRole('ROLE_ADMIN')"/> -->
  • 원칙적으로는 인가를 하려면 request uri 기준으로 필터이용해서 처리할 수 있어야함 (옛날에 auth filter로 하던 방식)
  • 이렇게 하자니 단위업무따라 uri가 늘어날때마다 이게 늘어나야해서불편
  • 사실 근데 이거 모두다 db에들어가야되는데 편하게하려고 인메모리한거
<!-- 비즈니스 로직대상으로 한번더체크하고싶다면 그대로 두어도되지만 컨트롤러단 대상으로 접근제어를 하기위해 servlet-context.xml으로 옮겨놓음 -->
	<global-method-security
		pre-post-annotations="enabled"
	/>
  • 인터셉트 대신 쓸 수 있는 구조
  • 이걸로 처음에는 서비스에서 인가처리를 했었는데 이걸 서비스에서 인가처리하면 잘못된유저가 일단 컨트롤러는 지나서 서비스까지 가게되는 것이기때문에 말이 안됨
  • 그래서 servlet-context.xml으로 빼면서 컨트롤러단에서 쓸수있도록 옮김

14. servlet-context.xml

<!-- AOP 활용하여 보호자원 관리 -->
<!-- 컨트롤러에 인터페이스가 없음에도 Interface대상의 proxy가 만들어졌다? -> 그렇다면 실구현체 대상으로 proxy가 만들어지는 설정이 적용되었다는것 (proxy-target-class="true" == cglib 방식 적용됨) -->
<security:global-method-security
                                 pre-post-annotations="enabled"
                                 proxy-target-class="true"
                                 />
  • 하위컨테이너에따라 프록시가 생성되고 aop 위빙따라 인가처리를 하려고함
  • 근데 컨트롤러단에서는 인터페이스를 만든적이없음
  • 그래서 인터페이스없이도 프록시가 만들어질 수 있는 설정을 추가한 것

15. MypageController

  • @PreAuthorize("isAuthenticated()")
  • @Before처럼 위빙처리

16. MemberServiceImpl

@Inject
private AuthenticationManager authenticationManager;
  • 주입받아 아이디, 비밀번호으로 존재여부 확인 인증
@Override
	public ServiceResult modifyMember(MemberVO member) {
		
		ServiceResult result = null;
		try {
			authenticationManager.authenticate(
					new UsernamePasswordAuthenticationToken(member.getMem_id(), member.getMem_pass()));
					
			int rowcnt = dao.updateMember(member);
			if (rowcnt>0) {
					result = ServiceResult.OK;
			}else {
				result = ServiceResult.FAIL;
			}
		} catch (BadCredentialsException e) { // 비번 오류가 난 상황 (없는 경우 알아서 예외)
			result = ServiceResult.INVALIDPASSWORD;
		}
		return result;
  	}

탈퇴시 현 상황의 문제점

  • 비번이 오류나도 별일없음..
  • 그동안은 loginprocesscontroller를 직접구현하여 오류나는걸 우리가 직접 처리했었음 근데 지금은 그 구조가 없어짐
  • 로그인하는과정에서 비번이오류가났을때 security로 어떻게 view단까지 전달을 해주는지를 봐야함
  • 이럴려면 exception을 더 가지고 놀아야함
  • exception가지고 놀려면 현재까지의 구조를 이해하고있어야함

SecurityContextLogoutHandler을 구현체로 이용

  • session invalidate는 잘되지만 여전히 탈퇴한 사람이 로그인이 가능함

MemberWrapper에서 Users의 생성자를 이용

로그인시의 exception 처리

  • AuthServImpl에서 UsernameNotFoundException이 발생하면 authServImpl을 사용하는 authentication-provider가 가져간다
  • 아이디가 틀려도 UNFE가 발생하는것이아닌 BadCredentialExcpeion이 뜨고있다
  • 그럼 provider가 예외를 바꿔치기 하고 있다는 것
  • 우리가 provider를 정의해줘야함
<!-- UsernameNotFoundException 살리기 -->
<beans:bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"
            p:userDetailsService-ref="customUserService"
            p:passwordEncoder-ref="passwordEncoder"
            p:hideUserNotFoundExceptions="false"
            />

<authentication-manager id="authenticationManager">
  <authentication-provider ref="authenticationProvider"/>
</authentication-manager>

로케일 처리

  • servlet-context.xml
<beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource" 
            p:basenames="kr.or.ddit.msg.message,org.springframework.security.messages"
            />
  • 메세지번들 함께 로딩
  • 근데 servlet-context는 하위여서 상위에있는 security-context에서 메시징 처리를 못하고 있음
  • 그럼 상위로 옮겨줘야겠지?
  • 근데 하위에서는 메시징처리가되는데 상위에서는 잘안됨

처리하기위해 문서참고합시다

RememberMe + 아이디 기억하기

RememberMe

security-context.xml

  • <remember-me remember-me-parameter="rememberMe" remember-me-cookie="rememberMeCookie"/>
    • remember-me-parameter: The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'.

아이디 기억하기

쉬운방법1. authentication-provider내에서 처리

  • 근데 아이디 기억하는게 인증이랑 관련있는것도아닌데 provider에서 하는건 좀 그래...

방법2. Eventhandler

security-context.xml

  • <form-login>authentication-success-handler-ref를 활용
  • 문서를 읽어보면 AuthenticationSuccessHandler가 필요하다고함
    • 구현체 중 SavedRequestAwareAuthenticationSuccessHandler 이게 우리에게 적합. 이걸 변경해서 쓸 예정

스프링el


스프링 el기반의 접근제어를 하기위해 use-expression="true"로 설정해놓은것

AOP기반의 접근제어

  • 비즈니스 로직에 @PreAuthorize("hasRole('ROLE_ADMIN')")
  • BeforeAdvice와 같은거
  • 근데 비즈니스로직까지 와서 접근제어를 하는게 맞는가? -> 컨트롤러에서 부터 막았어야지.. -> 사실 프론트에서 막았어야지,, -> 아냐 그보다 먼저 필터에서 막았어야지.. (인메모리 url패턴 활용)
  • 근데 필터에서 막으려고 url 리스트를 늘일 수는 없으니 url패턴으로 막는게 정석이긴하지만 컨트롤러단에서 막는것으로 정책을 바꾼것



내일

  • 오전에 security 마무리
  • websocket
    • websocket과 security의 연동
  • 오후에 여유가 있다면 spring batch쓰거나 비슷한 프레임워크를 사용

오늘 미션

  • 지난 주말 미션이 알바관리
  • aop 갖다붙여서 security?
  • 알바생관리하려면 로그인관리
  • 그럼 알바생안에서도 멤버라는 테이블을 짜야겠지
profile
갈 길이 멀다

0개의 댓글