[Spring] Spring Security(4)

99winnmin·2022년 7월 28일
0

Spring

목록 보기
17/17

여러가지 로그인 방식

DefaultLoginPageGeneratingFilter(기본 로그인 화면)

  • GET /login 을 처리
  • 별도의 로그인 페이지 설정을 하지 않으면 제공되는 필터
  • 기본 로그인 폼을 제공
  • OAuth2 / OpenID / Saml2 로그인과도 같이 사용할 수 있음.

UsernamePasswordAuthenticationFilter

  • POST /login 을 처리. processingUrl 을 변경하면 주소를 바꿀 수 있음.
  • form 인증을 처리해주는 필터로 스프링 시큐리티에서 가장 일반적으로 쓰임.
  • 주요 설정 정보
    • filterProcessingUrl : 로그인을 처리해 줄 URL (POST)
    • username parameter : POST에 username에 대한 값을 넘겨줄 인자의 이름
    • password parameter : POST에 password에 대한 값을 넘겨줄 인자의 이름
    • 로그인 성공시 처리 방법
      • defaultSuccessUrl : alwaysUse 옵션 설정이 중요
      • successHandler
    • 로그인 실패시 처리 방법
      • failureUrl
      • failureHandler
    • authenticationDetailSource : Authentication 객체의 details 에 들어갈 정보를 직접 만들어 줌.
    • 기본예제.java
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        // username,passwd로 통행증 만들기
        // 원래 UsernamePasswordAuthenticationToken 안에 authenticated = false임
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        // 인증토큰을 가지고 manager에게 내 인증을 처리해달라고 요청함
        // 연결되어 있는 authenticationProvider들에게 물어보고 누구든지 처리해주면 인증완료!
        return this.getAuthenticationManager().authenticate(authRequest);
    }

DefaultLogoutPageGeneratingFilter(기본 로그아웃 화면)

  • GET /logout 을 처리
  • POST /logout 을 요청할 수 있는 UI 를 제공
  • DefaultLoginPageGeneratingFilter 를 사용하는 경우에 같이 제공됨.

LogoutFilter

  • POST /logout 을 처리. processiongUrl 을 변경하면 바꿀 수 있음.
  • 로그 아웃을 처리
    • session, SecurityContext, csrf, 쿠키, remember-me 쿠키 등을 삭제처리 함.
    • (기본) 로그인 페이지로 redirect
  • 로그아웃시 실행되는 부분
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	if (requiresLogout(request, response)) {
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Logging out [%s]", auth));
		}
		this.handler.logout(request, response, auth);
		this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
		return;
	}
	chain.doFilter(request, response);
}
  • LogoutHandler
    • void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication); -> LogoutHandler에서 실제로 구현되는 유일한 메서드
    • 그 밖에 logout handler
      • SecurityContextLogoutHandler : 세션과 SecurityContext 를 clear 함.
      • CookieClearingLogoutHandler : clear 대상이 된 쿠키들을 삭제함.
      • CsrfLogoutHandler : csrfTokenRepository 에서 csrf 토큰을 clear 함.
      • HeaderWriterLogoutHandler
      • RememberMeServices : remember-me 쿠키를 삭제함.
      • LogoutSuccessEventPublishingLogoutHandler : 로그아웃이 성공하면 이벤트를 발행함.
  • LogoutSuccessHandler
    • void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
    • SimpleUrlLogoutSuccessHandler

Basic login 직접해보기

기본 index.html의 설정을 해놓는 코드이다. url에 따라 권한을 설정하기도 하고 login 성공/실패에 따라 동작하는 방식을 정하기도 한다. 또한 CustomAuthDetails 객체를 만들어서 detail한 정보를 직접 볼 수 있도록 세팅해놓을 수 있다.

  • SecurityConfig.java
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomAuthDetails customAuthDetails;

    public SecurityConfig(CustomAuthDetails customAuthDetails) {
        this.customAuthDetails = customAuthDetails;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication() // 해당 설정을 하게되면 .yaml의 user는 더이상 접속 불가
                .withUser(User.withDefaultPasswordEncoder()
                        .username("user1")
                        .password("1111")
                        .roles("USER")
                )
                .withUser(User.withDefaultPasswordEncoder()
                        .username("admin")
                        .password("2222")
                        .roles("ADMIN")
                );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests(request->{
                    request
                            .antMatchers("/").permitAll() // 메인화면은 권한 필요x
                            .anyRequest().authenticated();
                })
                .formLogin(// 기본 로그인/로그아웃 화면 나오게 해줌
                        login->login.loginPage("/login")
                                .permitAll() // 로그인화면을 만든 것으로 바꿈
                                .defaultSuccessUrl("/",false) // 로그인하고 메인 페이지로 안가겠다
                                .failureUrl("/login-error")
                                .authenticationDetailsSource(customAuthDetails) // 필요에 따라서 유용하게 쓰일 수 있음
                )
                .logout(logout -> logout.logoutSuccessUrl("/"))
                .exceptionHandling(exception -> exception.accessDeniedPage("/access-denied"))
        ;
    }

    @Bean
    RoleHierarchy roleHierarchy(){ // admin은 유저가 할 수 있는 기능을 다 할 수 있음
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        return roleHierarchy;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .requestMatchers( // resource 파일들은 일단 적용되게 해주는 메서드
                        PathRequest.toStaticResources().atCommonLocations()
                );
    }
}

정말 말 그대로 기초 로그인인데 내용이 이렇게 어려운거봐서는 역시 spring이 쉽지 않다...

Authentication 메커니즘

인증 (Authentication)

  • Authentication 는 인증된 결과만 저장하는 것이 아니고, 인증을 하기 위한 정보와 인증을 받기 위한 정보가 하나의 객체에 동시에 들어 있습니다. 왜냐하면, 인증을 제공해줄 제공자(AuthenticationProvider)가 어떤 인증에 대해서 허가를 내줄 것인지 판단하기 위해서는 직접 입력된 인증을 보고 허가된 인증을 내주는 방식이기 때문입니다.
  • 그래서 AuthenticationProvider 는 처리 가능한 Authentication에 대해 알려주는 support 메소드를 지원하고, authenticate() 에서 Authentication을 입력값과 동시에 출력값으로도 사용합니다.
    • Credentials : 인증을 받기 위해 필요한 정보, 비번등 (input)
    • Principal : 인증된 결과. 인증 대상 (output)
    • Details : 기타 정보, 인증에 관여된 된 주변 정보들
    • Authorities : 권한 정보들,
  • Authentication 을 구현한 객체들은 일반적으로 Token(버스 토큰과 같은 통행권) 이라는 이름의 객체로 구현됩니다. 그래서 Authentication의 구현체를 인증 토큰이라고 불러도 좋습니다.
  • Authentication 객체는 SecurityContextHolder 를 통해 세션이 있건 없건 언제든 접근할 수 있도록 필터체인에서 보장해 줍니다.

인증 제공자(AuthenticationProvider)

  • 인증 제공자(AuthenticationProvider)는 기본적으로 Authentication 을 받아서 인증을 하고 인증된 결과를 다시 Authentication 객체로 전달 합니다.
  • 그런데 인증 제공자는 어떤 인증에 대해서 도장을 찍어줄지 AuthenticationManager 에게 알려줘야 하기 때문에 support() 라는 메소드를 제공합니다. 인증 대상과 방식이 다양할 수 있기 때문에 인증 제공자도 여러개 올 수 있습니다.

인증 관리자(AuthenticationManager)

  • 인증 제공자들을 관리하는 인터페이스가 AuthenticationManager (인증 관리자)이고, 이 인증 관리자를 구현한 객체가 ProviderManager 입니다.
  • ProviderManager 도 복수개 존재할 수 있습니다.
  • 개발자가 직접 AuthenticationManager를 정의해서 제공하지 않는다면, AuthenticationManager 를 만드는 AuthenticationManagerFactoryBean 에서 DaoAuthenticationProvider 를 기본 인증제공자로 등록한 AuthenticationManage를 만든다.
  • DaoAuthenticationProvider 는 반드시 1개의 UserDetailsService 를 발견할 수 있어야 한다. 만약 없으면 InmemoryUserDetailsManager 에 [username=user, password=(서버가 생성한 패스워드)]인 사용자가 등록되어 제공됩니다.
  • 선생권한ProviderManager.java
@Component
public class TeacherManager implements AuthenticationProvider, InitializingBean {
    // 통행증을 다룰 객체(통행증을 발급해주는 역할)
    private HashMap<String, Teacher> teacherDB = new HashMap<>();

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        TeacherAuthenticationToken token = (TeacherAuthenticationToken) authentication;
        if(teacherDB.containsKey(token.getCredentials())){
            Teacher teacher = teacherDB.get(token.getCredentials());
            return TeacherAuthenticationToken.builder()
                    .principal(teacher)
                    .details(teacher.getUsername())
                    .authenticated(true)
                    .build();
        }
        // 처리할 수 없는 token을 false로해서 넘기면 handling했다는 것이 때문에 문제가 됨
        return null; // 처리할 수 없는 Authentication은 null로 넘김
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // TeacherAuthenticationToken가 Token을 발행해서 인증을 해줄 provider를 찾는데
        // authentication이 그 토큰이라면?
        // 해당 메서드로 provider로 동작을 하겠다!라고 선언
        // 즉 인증을 위임하겠다!
        return authentication == TeacherAuthenticationToken.class;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Set.of(
                new Teacher("ryuT","류선생", Set.of(new SimpleGrantedAuthority("ROLE_TEACHER"))),
                new Teacher("leeT","이선생", Set.of(new SimpleGrantedAuthority("ROLE_TEACHER"))),
                new Teacher("parkT","박선생", Set.of(new SimpleGrantedAuthority("ROLE_TEACHER")))
        ).forEach(t ->
                teacherDB.put(t.getId(),t));
    }
}

학생, 선생님 로그인 구현

  • StudentAuthenticationToken 과 TeacherAuthenticationToken 을 각각 Authentication 으로 한다.
  • StudentManager와 TeacherManager 를 각각 AuthenticationProvider 로 구현한다.

출처 : 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online.

profile
功在不舍

0개의 댓글