개발자 미니민의 개발스터디

[spring] security-context 스프링 시큐리티 설정 (접근권한, 로그인)

by mini_min
[spring] security-context 스프링 시큐리티 설정 (접근권한, 로그인)

✔️ 스프링 시큐리티 설정

: web.xml 에서 환경 설정할 수 있다.

<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>

<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>


<!-- 스프링 시큐리티 -->
<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>

 

- 시큐리티 설정은 웹 사이트 경로에 대한 권한을 설정하는 것이다. auto-config="true" 를 통해 일반적인 웹어플리케이션에 필요한 기본 보안 서비스를 자동으로 설정한다.

<context:component-scan base-package="com.sp.app"/>

 

<intercept-url pattern="...." access="...." /> 

위에 패턴은 url pattern 의 권한에 따른 보안을 설정한다.

 

리소스와 uploads/photo 의 경우, 메인이나 다른 부분에서는 모두가 접근해야하기 때문에 따로 보안을 설정한다.

⭐ hasRole : 하나의 권한을 설정

⭐ hasAnyRole : 하나 이상의 권한을 설정

⭐ permitAll : 모두 통과시킨다.

⭐ denyAll : 모두 거절한다.

⭐ isAuthenticated() : 인증한 사용자인지 확인한다. (isAnonymous() : 익명 사용자인지 확인한다.)

<!-- 권한이 없는 페이지를 접근할 경우 접근 불가 메시지 출력  -->
<http auto-config="true">
    <!-- spring 4.x때 추가된 옵션으로 ssl을 사용하지 않을 경우 csrf는 disalbed=true로 설정. -->
    <csrf disabled="true"/>

    <!-- 모든 접근자 접근 허용-->
    <intercept-url pattern="/" access="permitAll"/>
    <intercept-url pattern="/index.jsp" access="permitAll"/>
    <intercept-url pattern="/member/login" access="permitAll"/>
    <intercept-url pattern="/member/member" access="permitAll"/>
    <intercept-url pattern="/member/userIdCheck" access="permitAll"/>
    <intercept-url pattern="/member/complete" access="permitAll"/>
    <intercept-url pattern="/member/pwdFind" access="permitAll"/>
    <intercept-url pattern="/member/expired" access="permitAll"/>

    <intercept-url pattern="/resources/**" access="permitAll"/>
    <intercept-url pattern="/uploads/photo/**" access="permitAll"/>


    <!-- 관리자 페이지 -->
    <intercept-url pattern="/admin" access="hasAnyRole('ROLE_ADMIN', 'ROLE_EMP')"/>

    <!-- 모든 페이지 -->
    <intercept-url pattern="/**" access="hasAnyRole('ROLE_ADMIN', 'ROLE_EMP', 'ROLE_USER')"/>

 

 

✔️ 로그인 페이지 설정하기

: <form-login .... />

로그인 페이지를 설정한다.

<form-login login-page="/member/login"
    login-processing-url="/member/login"
    username-parameter="userId"
    password-parameter="userPwd"
    authentication-failure-url="/member/login?login_error"
    default-target-url="/"
    always-use-default-target="false"
    authentication-success-handler-ref="loginSuccessHandler"
    authentication-failure-handler-ref="loginFailureHandler"/>
<beans:bean id="loginSuccessHandler" class="com.sp.app.security.LoginSuccessHandler">
    <beans:property name="defaultUrl" value="/"/>
</beans:bean>

<beans:bean id="loginFailureHandler" class="com.sp.app.security.LoginFailureHandler">
    <beans:property name="defaultFailureUrl" value="/member/login?login_error"/>
</beans:bean>
⭐ login-page : 로그인 페이지. 세션 인증이 없는 경우 호출될 URL 이다.
⭐ login-processing-url : 로그인 처리를 호출할 때 사용할 URL 이다.
⭐ username-parameter : 로그인 페이지에서 사용될 사용자 계정 파라미터명이다.
⭐ password-parameter : 로그인 페이지에서 사용될 사용자 암호 파라미텨명이다.
- authentication-failure-url : 로그인 실패시 호출할 URL
- default-target-url : 로그인 후 호출될 URL
- always-use-default-target : 로그인 후 디폴트 타겟 url 을 호출할지 결정하는 여부!
- authentication-success-handler-ref : 로그인 후 호출될 핸들러이다. 직접 후처리 작업할 경우 사용
- authentication-failure-handler-ref : 로그인 실패 후 호출될 핸들러이다. 직접 후처리 작업할 경우 사용

 

 

✔️ 로그아웃 설정하기

: <logout .... />

로그아웃을 설정한다.

<logout logout-url="/member/logout"
    invalidate-session="true"
    logout-success-url="/"/>
⭐ logout-url : 로그아웃 URL
⭐ invalidate-session : 세션 삭제 여부
⭐ logout-success-url : 로그아웃 후 호출될 URL 이다.

 

 

✔️ 접근 권한 없는 경우 & 접속 제한

<!-- 접근 권한 없는 경우 -->
<access-denied-handler error-page="/member/noAuthorized"/>
<!-- 동일 아이디로 한명만 로그인 했을 경우. 기존 세션 만료하기 -->
<session-management>
    <concurrency-control max-sessions="1" expired-url="/member/expired"/>
</session-management>

 

 

✔️ 시큐리티 데이터베이스 인증 설정

: 사용자 정보를 데이터베이스에서 조회하는 인증제공자 설정이다!

<jdbc-user-service > 엘리먼트를 사용한다. (userId 에 따라 사용자 정보를 DB에서 조회한다.)

⭐ users-by-username-query : 로그인 페이지에서 사용자가 제공한 계정을 "?" 변수에 받아 사용자의 username, password, enabled 를 검색한다. 해당 쿼리에서는 반드시 username, password, enabled 컬럼을 리턴 받아야한다. 컬럼 이름이 다르면! 별칭을 사용한다.

⭐ authorities-by-username-query : 사용자가 제공한 계정을 "?" 변수에 받아 username, authority 를 검색한다. 마찬가지로 컬럼 이름이 다르면! 별칭을 사용한다.

<jdbc-user-service data-source-ref="dataSource"
    id="userService"
    users-by-username-query="SELECT userId AS username, userPwd AS password, enabled FROM member1 WHERE userId = ?"
    authorities-by-username-query="SELECT userId AS username, authority FROM memberAuthority WHERE userId= ?"/>

 

 

✔️ 암호화

: SHA 알고리즘은 미국 NSA 에서 고안한 암호화 알고리즘이다. 스프링은 기본적으로 SHA256 알고리즘을 사용한다.

 

📓 스프링 시큐리티 암호화 클래스 종류

: bcryptEncoder

= 스프링 시큐리티에서 기본적으로 사용하는 암호화 방식으로 암호화가 될 때 마다 새로운 값을 생성한다. 임의적 값을 추가해서 암호화 하지 않아도 된다.

 

⭐환경설정

<authentication-manager>
    <authentication-provider user-service-ref="userService">
        <password-encoder ref="bcryptEncoder"/>
    </authentication-provider>
</authentication-manager>

 

⭐ 암호화 설정 - 패스워드를 인코딩하는 빈을 설정한다. 

<!-- bcrypt : 패스워드 암호화에 특화된 password hashing function  -->
<beans:bean id="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></beans:bean>

 

나중에 @Autowired PasswordEncoder passwordEncoder ; 

를 오토와이드 하여 암호화 시킨다. (ServiceImpl 에서)

@Autowired
private BCryptPasswordEncoder bcrypt;


//패스워드 암호화
String encPassword = bcrypt.encode(dto.getUserPwd());
dto.setUserPwd(encPassword);

//패스워드 비교 
return bcrypt.matches(userPwd, dto.getUserPwd());

 

 

✔️ LoginSuccessHandler

: SavedRequestAwareAuthenticationSuccessHandler 클래스를 상속받는다. 

빈객체에 있는 value 값을 가져와서 디폴트url set 에서 받는다!

(빈 프로퍼티 값 가져옴 -> 로그인석세스핸들러 set에 저장) 

1) 로그인시, 로그인 날짜를 변경한다. 최근 로그인 날짜는 SYSDATE,  failure_cnt (비밀번호 틀린횟수) = 0

2) 세션에 로그인 유저 정보를 저장

3) 로그인 Id 로 readMember 한다.

4) 그래서 얻은 로그인 정보를 세션에 저장한다.

5) if 패스워드 변경날짜가 90일 이상인 경우 updatePwd 페이지로 연결시킨다. 

6) 해당되지 않으면 리다이렉트 시킨다.

 

: resultRedirectStrategy 는 리다이렉트 설정이다.

1) 만약, 로그인이 되지 않은 상태에서 권한이 필요한 페이지에 접근한 경우 리다이렉트 한다.

public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
	
	@Autowired
	private MemberService service;
	
	private String defaultUrl;
	
	private RequestCache requestCache = new HttpSessionRequestCache();
	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
	

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws ServletException, IOException {

		// 로그인 날짜 변경
		try {
			//authentication.getName() : 로그인 아이디
			service.updateLastLogin(authentication.getName());
		} catch (Exception e) {
		}
		
		//세션에 로그인 유저 정보 저장
		HttpSession session = request.getSession();
		
		Member member = service.readMember(authentication.getName());

		// 로그인 정보를 세션에 저장
		SessionInfo info = new SessionInfo();
		info.setUserName(member.getUserName());
		info.setUserId(member.getUserId());
		info.setMemberIdx(member.getMemberIdx());
		info.setMembership(member.getMembership());
		
		session.setAttribute("member", info);
		
		//패스워드 변경이 90일이 지난 경우 패스워드 변경 창으로 이동
		try {
			 Date endDate = new Date();
			 long gap;
			 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			 Date modifyDate = sdf.parse(member.getModify_date());
			 gap = (endDate.getTime() - modifyDate.getTime()) / (24*60*60*1000);
			 
			 if(gap >= 90) {
				 String targetUrl = "/member/updatePwd";
				 redirectStrategy.sendRedirect(request, response, targetUrl);
				 return;
			 }
			
		} catch (Exception e) {
		}
		
		//redirect 설정
		resultRedirectStrategy(request, response, authentication);
	}
	
	
	public void resultRedirectStrategy(HttpServletRequest request,
			HttpServletResponse response,
			Authentication authentication) throws ServletException, IOException {
		
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if(savedRequest != null) {
			//로그인이 되지 않은 상태에서 권한이 필요한 페이지에 접근한 경우
			String targetUrl = savedRequest.getRedirectUrl();
			redirectStrategy.sendRedirect(request, response, targetUrl);
			
		} else {
			//직접 로그인 주소를 클릭한 경우
			redirectStrategy.sendRedirect(request, response, defaultUrl);
			
		}
	}
	
	
	public String getDefaultUrl() {
		return defaultUrl;
	}


	public void setDefaultUrl(String defaultUrl) {
		this.defaultUrl = defaultUrl;
	}

}

 

 

✔️ LoginFailureHandler

: AuthenticationFailureHandler 구현

1) InternalAuthenticationServiceException : 존재하지 않는 아이디

2) DisabledException : 인증이 거부되었을 때 예외처리

public class LoginFailureHandler implements AuthenticationFailureHandler {
	
	@Autowired
	private MemberService service;
	
	private String defaultFailureUrl;
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		
		String userId = request.getParameter("userId");
		
		String s = "아이디 또는 패스워드가 일치하지 않습니다.";
		
		try {
			if(exception instanceof BadCredentialsException) {
				// 패스워드가 일치하지 않는 경우 
				
				//실패횟수 누적
				service.updateFailureCount(userId);
				int count = service.checkFailureCount(userId);

				if(count >= 5) {
					//계정 비활성화
					Map<String, Object> map = new HashMap<String, Object>();
					map.put("enabled", 0);
					map.put("userId", userId);
					service.updateMemberEnabled(map);
					
					//계정 비활성화 상태 저장하기
					Member dto = new Member();
					dto.setUserId(userId);
					dto.setRegisterId(userId);
					dto.setStateCode(1); //패스워드 틀린사람 (계정 잠금) = 1
					dto.setMemo("패스워드 5회 이상 오류!!");
					service.insertMemberState(dto);
				}
						
						
			} else if(exception instanceof InternalAuthenticationServiceException) {
				//존재하지 않는 아이디
			} else if(exception instanceof DisabledException ) {
				//인증 거부 : enabled=0
				s = "계정이 비활성화 되어 있습니다. 관리자에게 문의하세요.";
			}
			
		} catch (Exception e) {
			// TODO: handle exception
		}
		
		request.setAttribute("message", s);
		
		request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
		
	}

	public void setDefaultFailureUrl(String defaultFailureUrl) {
		this.defaultFailureUrl = defaultFailureUrl;
	}
	
}

 

 

 

블로그의 정보

개발자 미니민의 개발로그

mini_min

활동하기