본문 바로가기
Spring|Spring-boot/Spring Security

Spring Security Form Login

by oncerun 2021. 3. 28.
반응형

 

스프링 시큐리티는 DelegatingFilterProxy를 사용해 추상화된 필터를 사용합니다.

 

그중 Form으로 Login 하는 경우를 살펴봅니다.

 

Form으로 login을 하는경우에는 UsernamePasswordAuthenticationFilter를 사용합니다.

이때 Token을 발급해주며 해당 토큰으로 원하는 페이지에 접근할 수 있습니다.

 

보통 새로운 SecurityConfig클래스에 WebSecurityConfigurerAdapter를 상속받아서 security의 설정 파일을 만들어 줍니다. 

dependencies { 
compile 'org.springframework.security:spring-security-web:4.2.2.RELEASE' 
compile 'org.springframework.security:spring-security-config:4.2.2.RELEASE' 
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.1.8.RELEASE'
}

 

스프링 혹은 스프링 부트에 맞는 자신에게 적합한 라이브러리를 추가해주어야합니다.

 

스프링 시큐리티를 적용했다면 자신이 만들지 않은 login창이 나타나는데 이것은 DefaultLoginPageGenerationFilter에 의해 자동적인 로그인 폼페이지로 리다이렉트 되기 때문입니다.

 

이 필터는 GET/login을 처리하며 별도의 로그인 페이지를 설정하지 않은 경우 제공되는 필터입니다.

private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
		String errorMsg = "Invalid credentials";
		if (loginError) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				AuthenticationException ex = (AuthenticationException) session
						.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
				errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials";
			}
		}
		String contextPath = request.getContextPath();
		StringBuilder sb = new StringBuilder();
		sb.append("<!DOCTYPE html>\n");
		sb.append("<html lang=\"en\">\n");
		sb.append("  <head>\n");
		sb.append("    <meta charset=\"utf-8\">\n");
		sb.append("    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
		sb.append("    <meta name=\"description\" content=\"\">\n");
		sb.append("    <meta name=\"author\" content=\"\">\n");
		sb.append("    <title>Please sign in</title>\n");
		sb.append("    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" "
				+ "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n");
		sb.append("    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" "
				+ "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
		sb.append("  </head>\n");
		sb.append("  <body>\n");
		sb.append("     <div class=\"container\">\n");
		if (this.formLoginEnabled) {
			sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath
					+ this.authenticationUrl + "\">\n");
			sb.append("        <h2 class=\"form-signin-heading\">Please sign in</h2>\n");
			sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n");
			sb.append("          <label for=\"username\" class=\"sr-only\">Username</label>\n");
			sb.append("          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter
					+ "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
			sb.append("        </p>\n");
			sb.append("        <p>\n");
			sb.append("          <label for=\"password\" class=\"sr-only\">Password</label>\n");
			sb.append("          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter
					+ "\" class=\"form-control\" placeholder=\"Password\" required>\n");
			sb.append("        </p>\n");
			sb.append(createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request));
			sb.append("        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
			sb.append("      </form>\n");
		}
		if (this.openIdEnabled) {
			sb.append("      <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath
					+ this.openIDauthenticationUrl + "\">\n");
			sb.append("        <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n");
			sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n");
			sb.append("          <label for=\"username\" class=\"sr-only\">Identity</label>\n");
			sb.append("          <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter
					+ "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
			sb.append("        </p>\n");
			sb.append(createRememberMe(this.openIDrememberMeParameter) + renderHiddenInputs(request));
			sb.append("        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
			sb.append("      </form>\n");
		}
		if (this.oauth2LoginEnabled) {
			sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
			sb.append(createError(loginError, errorMsg));
			sb.append(createLogoutSuccess(logoutSuccess));
			sb.append("<table class=\"table table-striped\">\n");
			for (Map.Entry<String, String> clientAuthenticationUrlToClientName : this.oauth2AuthenticationUrlToClientName
					.entrySet()) {
				sb.append(" <tr><td>");
				String url = clientAuthenticationUrlToClientName.getKey();
				sb.append("<a href=\"").append(contextPath).append(url).append("\">");
				String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue());
				sb.append(clientName);
				sb.append("</a>");
				sb.append("</td></tr>\n");
			}
			sb.append("</table>\n");
		}
		if (this.saml2LoginEnabled) {
			sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>");
			sb.append(createError(loginError, errorMsg));
			sb.append(createLogoutSuccess(logoutSuccess));
			sb.append("<table class=\"table table-striped\">\n");
			for (Map.Entry<String, String> relyingPartyUrlToName : this.saml2AuthenticationUrlToProviderName
					.entrySet()) {
				sb.append(" <tr><td>");
				String url = relyingPartyUrlToName.getKey();
				sb.append("<a href=\"").append(contextPath).append(url).append("\">");
				String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue());
				sb.append(partyName);
				sb.append("</a>");
				sb.append("</td></tr>\n");
			}
			sb.append("</table>\n");
		}
		sb.append("</div>\n");
		sb.append("</body></html>");
		return sb.toString();
	}

 

다음과 같이 미리 page가 만들어져 있는 것을 확인할 수 있는데, 잘 보면 if문에 OAuth2 / OpenID /Saml2 로그인과도 사용할 수 있는 것을 확인할 수 있습니다.

 

LogoutFilter 또한 마찬가지입니다. DefaultLogoutPageGeneratingFilter가 요청을 가로채어 GET /logout을 처리하며 UI 또한 제공해줍니다.

 

 

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(request -> {
                    request
                            .antMatchers("/").permitAll()
                            .anyRequest().authenticated();
                })
                .formLogin(
                        login -> login.loginPage("/login")
                                .permitAll()
                )

    }

configure 메서드의 HttpSecurity를 인자로 받는 메서드는 스프링 시큐리티의 규칙을 정의합니다. 

이때는 UsernamePasswordAuthenticationFilter가 사용되는데, POST /login을 처리하며 processingUrl을 변경하면 해당 주소를 변경할 수 있습니다. 

 

 

 

UsernamePasswordAuthenticationFilter : username, password를 사용하는 form기반 인증을 처리하는 필터입니다.

 

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
  • AuthenticationManager를 통한 인증 실행
  • 성공하면, Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
  • 실패하면 AuthenticationFailureHandler 실행

이 필터는 id와 password를 가지고 Token을 생성하며 해당 토큰으로 ProviderManger에게 인증을 진행하도록 해당 토큰을 넘겨주는 것을 볼 수 있습니다.

 

 

- AuthenticationManager (interface)

 : Authentication 객체를 받아 인증하고 인증되었다면 인증된 Authentication 객체를 돌려주는 메서드를 구현하도록 하는 인터페이스다.

이 메서드를 통해 인증되면 isAuthenticated(boolean) 값을 TRUE로 바꿔준다.

 

-ProviderManager (class)

: AuthenticationManager의 구현체로 스프링에서 인증을 담당하는 클래스로 볼 수 있다. 스프링 빈으로 등록이 되어서 제공된다.

 

-AuthenticationProvider

:  인증과정이 이 메서드를 통해서 진행된다.

 

위 과정을 글로 풀면 다음과 같습니다. 해당 토큰이 구현체인 ProviderManager에게 전달됩니다. 그러면 이 매니저는 AuthenticationProvider 목록을 여러 개 가지고 있는데 이 토큰을 주면서 true를 리턴해주는 provider의 authenticate() 메서드를 실행하여 인증을 진행합니다. 

 

AuthenticationProvider 은 인터페이스이기 때문에 내가 usernamepasswordToken을 처리할 클래스를 생성해서 처리해 줄 수 도 있다.

   @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

그러면 반복문을 돌다가 내가 만든 토큰 처리 클래스가 true를 반환하고  만든 토큰을 돌려주게 할 수 있습니다.

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userId = authentication.getName(); 
        String userPassword = authentication.getCredentials().toString(); //userPassword
			
            내가 처리하고 토큰을 리턴
      		
        return new myToken(userId, userPassword, null, user);
    }

 

 

또한 이러한 인증 과정 중에 내가 ip 나 브라우저 세션 아이디 등을 부가적으로 얻을 수 있는 방법이 존재하는데

 

authenticationDetailsSource()를 사용하여 처리할 수 있습니다.

 

이 메서드는 details를 생성하여 Authentication 객체의 멤버에 넣어 줄 수 있습니다.

@Component
public class CustomAuthDetails implements AuthenticationDetailsSource<HttpServletRequest,RequestInfo> {

    //login
     @Override
    public RequestInfo buildDetails(HttpServletRequest context) {
         return RequestInfo.builder()
                 .remoteIp(context.getRemoteAddr())
                 .sessionId(context.getSession().getId())
                 .loginTime(LocalDateTime.now())
                 .build();
    }
}

 

인터페이스를 impl 하고 첫 번 째는 HttpServletRequest를 2번째는 내가 필요할 정보를 가질 VO를 생성하여 넣어주고

buildDetails를 오버라이드 하여 request에서 정보를 뽑을 수 있습니다.

 

또한 Resource파일은 이 인증절차를 무시하고 진행해야 하기 때문에 다음과 같이 진행할 수 있습니다.

 @Override
    public void configure(WebSecurity web) throws Exception {

        web.ignoring()
                .requestMatchers(
                        PathRequest.toStaticResources().atCommonLocations()

                );
    }

뭐 실제로 antMatcher를 사용해 특정 주소를 추가할 수 있는데, 이것은 src/main/resource/static내의 자원들에 대해서 ignoring 됩니다.

 

만약 ROLE에 따른 요청 주소가 존재한다면 Controller에서 다음과 같은 설정을 진행할 수 있습니다.

@PreAuthorize("hasAnyAuthority('ROLE_USER')") 이후 config파일에 다음과 같은 어노테이션을 추가합니다.

@EnableGlobalMethodSecurity(prePostEnabled = true)

이 경우에 불편한 점은 관리자가 유저의 정보에 접근을 못한다는 것인데, 이때는 RoleHierarchy설정을 통한 변경이 가능합니다.

  @Bean
    public RoleHierarchy roleHierarchy(){
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        return roleHierarchy;
    }

 

반응형

'Spring|Spring-boot > Spring Security' 카테고리의 다른 글

Spring Security(4)  (0) 2021.04.04
Spring Security Basic  (0) 2021.03.28
Spring Security(2)  (0) 2021.03.21
Spring Security(1)  (0) 2021.03.21
Spring Security  (0) 2021.03.21

댓글