콘텐츠로 이동

2022 07 10

2022-07-10

참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/dashboard

인프링 시큐리티 일반 로그

  • 모든 주소가 막힌다
    • 스프링 시큐리티를 쓴다면 기본적으로 모든 주소가 막혀 인증이 필요해짐
    • localhost:8080/login
    • 최초 아이디: user, 비밀번호: 스프링 뜰 때 비밀번호
  • 인증을 완료하면 그제서야 Controller API 호출 가능
    • GET: /login => 이거 만들어도 스프링 시큐리티가 낚아챔
      • SecurityConfig 파일 만들면 login Controller로 흘러들어감
  • SecurityConfig Bean 정의하기
    @Configuration
    @EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
            http.authorizeRequests()
                    // 이 세가지 주소가 아니면
                    .antMatchers("/user/**").authenticated()
                    .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN' or hasRole('ROLE_MANAGER')")
                    .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN)")
                    // 누구나 들어갈 수 있다
                    .anyRequest().permitAll()
                    // 권한이 없는 페이지로 이동하려고 할 때 로그인을 받아야겠지?
                    .and()
                    .formLogin()
                    .loginPage("/login");
        }
    }
    
  • 패스워드 인코딩
    @Configuration
    @EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Bean
        public BCryptPasswordEncoder encodePwd() {
          return new BCryptPasswordEncoder();
        }
    }
    
  • 시큐리티 로그인

    @Configuration
    @EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨
    @EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화! Controller에서 @Secured("ROLE_ADMIN")
    @EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize, @PostAuthorize 어노테이션 활성화! Controller에서 @PreAuthorized("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
            http.authorizeRequests()
                    // 이 세가지 주소가 아니면
                    .antMatchers("/user/**").authenticated() // 인증만 되면 들어갈 수 있어
                    .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN' or hasRole('ROLE_MANAGER')")
                    .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN)")
                    // 누구나 들어갈 수 있다
                    .anyRequest().permitAll()
                    // 권한이 없는 페이지로 이동하려고 할 때 로그인을 받아야겠지?
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .loginProcessingUrl("/login") // login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인 진행
                    .defaultSuccessUrl("/");
        }
    }
    

    • 시큐리티 /login 주소 요청이 오면 낚아채서 로그인을 진행시킴
    • 로그인을 진행이 완료가 되면 시큐리티 session을 만들어줍니다
    • 오브젝트 => Authentication 타입 객체 => Authentication 안에 User 정보가 있어야 함
      • UserDetails 타입 객체로 지정되어 있음
    • SecuritySession => Authentication => UserDetails 타입이여야 함
      // PrincipalDetails를 UserDetails에 넣을 수 있
      public class PrincipalDetails implements UserDetails {
          private User user;
      
          public PrincipalDetails(User user) {
              this.user = user;
          }
      
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              Collection<GrantedAuthority> collect = new ArrayList<>();
              collect.add(new GrantedAuthority() {
                  @Override
                  public String getAuthority() {
                      return user.getRole();
                  }
              });
              return collect;
          }
      
          @Override 
          public String getPassword() {
              return user.getPassword();
          }
      
          @Override
          public String getUsername() {
            return user.getUsername();
          }
      
          // 기타 등등... 활성화 여부, expire 여부 등등...
      }
      
    • 자, 이번엔 authentication 객체는 어떻게 만드는가?
      // 시큐리티 설정에서 loginProcessingUrl("/login");
      // login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어있는 loadUserByUsername 함수 실행
      @Service
      public class PrincipalDetailsService implements UserDetailService {
      
          @Autowired
          private UserRepository userRepository;
      
          // 시큐리티 Session = Authentication = UserDetails
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              User userEntity = userRepository.findByUsername(username);
              if (userEntity != null) {
                  return new PrincipalDetails(userEntity);
              }
              return null;
          }
      }
      

인프런 스프링 시큐리티 OAuth

  • 시큐리티 OAuth
    @Configuration
    @EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨
    @EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화! Controller에서 @Secured("ROLE_ADMIN")
    @EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize, @PostAuthorize 어노테이션 활성화! Controller에서 @PreAuthorized("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private PrincipalOauth2UserService principalOauth2UserService;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
            http.authorizeRequests()
                    // 이 세가지 주소가 아니면
                    .antMatchers("/user/**").authenticated() // 인증만 되면 들어갈 수 있어
                    .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN' or hasRole('ROLE_MANAGER')")
                    .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN)")
                    // 누구나 들어갈 수 있다
                    .anyRequest().permitAll()
                    // 권한이 없는 페이지로 이동하려고 할 때 로그인을 받아야겠지?
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .loginProcessingUrl("/login") // login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인 진행
                    .defaultSuccessUrl("/")
                    .and()
                    .oauth2Login()
                    .loginPage("/loginForm") // 구글 로그인 완료된 후 후처리가 필요하다 1. 코드받기(인증) => 2. 액세스토큰(권한) => 3. 사용자 프로필 정보 가져오고 => 4. 그 정보 토대로 회원가입 자동 진행
                    .userInfoEndpoint()
                    .userService(principalOauth2UserService);
        }
    }
    
    @Service
    public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) {
            return super.loadUser(userRequest);
        }
    }
    
    @Controller
    public class UserController {
      // 그냥 회원가입한 유저
      @GetMapping("/test/login")
      public @ResponseBody String testLogin(Authentication authentication, 
                                            @AuthenticationPrincipal PrincipalDetails userDetails) {
          System.out.println("/test/login ====== ");
          PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
          System.out.println("authentication : " + principalDetails.getUser());
          System.out.println("userDetails : " + userDetails.getUsername());
          return "세션 정보 확인하기";
      }
    
      @GetMapping("/test/oauth/login")
      public @ResponseBody String testOAuthLogin(Authentication authentication, 
                                                 @AuthenticationPrincipal OAuth2User oauth) {
          System.out.println("/test/oauth/login ====== ");
          OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
          System.out.println("authentication : " + oAuth2User.getAttributes());
          System.out.println("oauth2User : " + oauth.getAttributes());
          return "OAuth 세션 정보 확인";
      }
    }
    
  • 정리
    • 스프링 시큐리티는 자신만의 세션을 들고 있어
    • 여기 안에 들어갈 수 있는 타입은 Authentication 객체 밖에 없음
      • 이걸 DI 할 수 있음
      • 해당 객체는 두가지 타입이 있음
        • UserDetails : 일반 로그인
        • OAuth2User : OAuth 로그인
      • 요게 들어가 있어야 로그인 처리가 되는 거임
    • 필요할 때 꺼내써야 하는데 일반 로그인, OAuth 로그인 둘 다 지원해야하는데...?
      • 따라서 추상화를 시키자!
        • UserDetails와 OAuth2User 두 가지를 모두 상속받은 것
          @Data
          public class PrincipalDetails implements UserDetails, OAuth2User {
          }
          

JWT를 위한 네트워크 지식

  • TCP
    • 통신 OSI 7계층
      • 응용 : 롤 프로그램
      • 프레젠테이션 : 암호화, 압축
      • 세션 : 인증 체크
      • 트랜스포트 : TCP/UDP
      • 네트워크
      • 데이터링크
      • 물리
    • TCP (신뢰성 있는 통신) => 컴퓨터가 이해가능
    • UDP (전화, 동영상) => 사람이 이해가능
  • CIA
    • A나라 ---(문서)---> B나라
      • C나라가 문서 가로챌수도!
      • CIA => C: Confidentiality, I: Integrity, A: Availability
      • 문서 암호화 but 몇가지 문제!
        1. 열쇠 전달 문제
        2. 문서가 누구로부터 왔는가?
  • RSA
    • Public Key : 공개키
    • Private Key : 개인키
    • A ------A개인키(B공개키(사랑해))------> B
      1. B가 문서를 받으면
      2. A의 공개키로 열어본다
      3. B의 개인키로 열어본다
  • RFC
    • 내부망끼리 통신하기 위해 약속된 규칙이 필요했음
      • 이게 RFC
        • 이게 쭉쭉쭉쭉 RFC 문서화되니까 HTTP 프로토콜이 되었음

JWT

  • JWT?
    • JSON 객체로 안전하게 전송하기 위한 개방형 표준
      • 서명된 토큰에 중점을 둔 것
    • Base64로 인코딩/디코딩 가능
    • 구조
      • header
      • payload (claim: iss, exp, sub, aud 등)
      • signature
  • JWT with Spring
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final CorsFilter corsFilter;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .addFilter(corsFilter)
                    .formLogin().disable()
                    .httpBasic().disable()
                    .authorizeRequest()
                    .antMatchers("/api/v1/user/**")
                    .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                    .antMatchers("/api/v1/manager/**")
                    .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                    .antMatchers("/api/v1/admin/**")
                    .access("hasRole('ROLE_ADMIN')")
                    .anyRequest().permitAll();
        }
    }
    
    @Configuration
    public class CorsConfig {
    
        @Bean
        public CorsFilter corsFilter() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(true);
            config.addAllowedOrigin("*");
            config.addAllowedHeader("*");
            config.addAllowedMethod("*");
            source.registerCorsConfiguration("/api/**", config);
            return new CorsFilter(source);
        }
    }
    
  • Filter 만들기
    public class MyFilter1 implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
            System.out.println("필터1");
            chain.doFilter(request, response);
        }
    }
    
    public class MyFilter2 implements Filter {
      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        System.out.println("필터2");
        chain.doFilter(request, response);
      }
    }
    
    public class MyFilter3 implements Filter {
      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        System.out.println("필터3");
        chain.doFilter(request, response);
      }
    }
    
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final CorsFilter corsFilter;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .addFilter(corsFilter)
                    .formLogin().disable()
                    .httpBasic().disable()
                    .authorizeRequest()
                    .antMatchers("/api/v1/user/**")
                    .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                    .antMatchers("/api/v1/manager/**")
                    .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                    .antMatchers("/api/v1/admin/**")
                    .access("hasRole('ROLE_ADMIN')")
                    .anyRequest().permitAll();
        }
    }
    
    // IoC를 통해 필터 실행 순서 정해줄 수 있음
    @Configuration
    public class FilterConfig {
        @Bean
        public FilterRegistrationBean<MyFilter1> filter() {
            FilterRegistrationBean<MyFilter1> bean = new FilterRegistrationBean<>(new MyFilter1());
            bean.addUrlPatterns("/*");
            bean.setOrder(0);
            return bean;
        }
    
        @Bean
        public FilterRegistrationBean<MyFilter2> filter() {
          FilterRegistrationBean<MyFilter2> bean = new FilterRegistrationBean<>(new MyFilter2());
          bean.addUrlPatterns("/*");
          bean.setOrder(1);
          return bean;
        }
    }
    
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
      private final CorsFilter corsFilter;
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new MyFilter3(), BasicAuthenticationFilter.class);
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilter(corsFilter)
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeRequest()
                .antMatchers("/api/v1/user/**")
                .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/manager/**")
                .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/admin/**")
                .access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll();
      }
    }
    // 요 상황이라면 필터3=>필터2=>필터1 순서 출력
    
  • 필터에서 Authorization 뽑아 토큰 검증
    public class MyFilter1 implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse res = (HttpServletResponse) response;
    
            // 토큰: cos에 대응되는 토큰을 만들어 줄 것. id/pw 정상적으로 들어와 로그인 완료되면 토큰 만들어주고 그걸 응답
            // 요청할 때 마다 header에 Authorization에 value값으로 토큰을 가지고 옴
            // 그때 토큰 넘어오면 내가 만든 토큰이 맞는지 검증하면 됨
            if (req.getMethod().equals("POST")) {
                System.out.println("POST 요청됨");
                String headerAuth = req.getHeader("Authorization");
                if (headerAuth.equals("cos")) {
                    chain.doFilter(req, res);
                } else {
                    PrintWriter out = res.getWriter();
                    out.print("실패!");
                }
            }
        }
    }
    
  • JWT 로그인 만들어보기
    • 유저네임, 패스워드 => 로그인 정상
    • 서버쪽 세션ID 생성, 클라이언트 쿠키 세션 ID를 응답
    • 요청할 대마다 쿠키값 세션ID를 항상 들고 서버쪽으로 요청하기 때문에, 서버는 세션 ID가 유효한지 판단하여 유효하면 인증이 필요한 페이지로 접근
    • 유저네임, 패스워드 로그인 정상
    • JWT 토큰 생성하여 클라이언트에 응답
    • 요청시마다 JWT 토큰을 가지고 요청
    • 서버가 JWT 토큰 유효한지 판단 (TODO)
      public class PrincipalDetails implements UserDetails {
          private User user;
      
          public PrincipalDetails(User user) {
              this.user = user;
          }
      
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              Collection<GrantedAuthority> authorities = new ArrayList<>();
              user.getRoleList().forEach(r -> {
                  authorities.add(() -> r);
              });
              return authorities;
          }
      }
      
      @Service
      @RequiredArgsConstructor
      public class PrincipalDetailsService implements UserDetailsService {
          private final UserRepository userRepository;
      
          @Override
          public UserDetails loadUserByUsername(String username) {
              User user = userRepository.findByUsername(username);
              return new PrincipalDetails(user);
          }
      }
      
      // /login 요청해서 username, password 동작하면 해당 필터 (formlogin 때 사용)
      @RequiredArgsConstructor
      public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
          private final AuthenticationManager authenticationManager;
      
          // login 요청을 하면 로그인 시도를 위해 실행되는 함수
          @Override
          public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationExample {
              System.out.println("JwtAuthenticationFilter : 로그인 시도 중");
              // 1. username, password 받아서
              try {
                  ObjectMapper om = new ObjectMapper();
                  User user = om.readValue(request.getInputStream(), User.class);
                  UsernamePasswordAuthenticationToken authenticationToken = 
                          new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
                  // 2. authenticationManager를 통해 정상인지 로그인 시도 (PrincipalDetailService.loadUserByUsername())
                  // PrincipalDetailsservice의 loadUserByUsername() 함수가 실행된 후 정상이라면 authentication이 리턴됨
                  // DB에 있는 username과 password가 일치
                  Authentication authentication = authenticationManager.authenticate(authenticationToken);
                  // 3. PrincipalDetails를 세션에 담고
                  PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
                  // Authentication 객체가 session 영역에 저장을 해야하고 그 방법이 return 해주면 됨
                  // 리턴의 이유는 권한 관리를 security가 대신 해주기에 편하려고 하는 것
                  // jwt 토큰 쓰면서 세션 만들이유가 없는데... 단지 권한 때문에 session 넣어주기
                  return authentication;
              } catch (IOException e) {
              }
              return super.attemptAuthentication(request, response);
          }
      
          // AttemptAuthentication 실행 후 인증이 정상적으로 되었다면 successfulAuthentication 함수가 실행됨
          // JWT 토큰을 만들어 request 요청한 사용자에게 JWT 토큰을 response 해주면 됨
          @Override
          protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authResult) {
              PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
              String jwtToken = JWT.create()
                      .withSubject("cos")
                      .withExpiresAt(new Date())
                      .withClaim("id", principalDetails.getUser().getId())
                      .withClaim("username", principalDetails.getUser().getUsername())
                      .sign(Algorithm.HMAC512(JwtProperties.SECRET));
      
              response.addHeader("Authorization", "Bearer" + jwtToken);
              super.successfulAuthentication(request, response, chain, authResult);
          }
      }
      
      @Configuration
      @EnableWebSecurity
      @RequiredArgsConstructor
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
      
        private final CorsFilter corsFilter;
      
        @Override
        protected void configure(HttpSecurity http) throws Exception {
          http.addFilterBefore(new MyFilter3(), BasicAuthenticationFilter.class);
          http.csrf().disable();
          http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                  .and()
                  .addFilter(corsFilter)
                  .formLogin().disable()
                  .httpBasic().disable()
                  .addFilter(new JwtAuthenticationFilter(authenticationManager)) // AuthenticationManager
                  .authorizeRequest()
                  .antMatchers("/api/v1/user/**")
                  .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                  .antMatchers("/api/v1/manager/**")
                  .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                  .antMatchers("/api/v1/admin/**")
                  .access("hasRole('ROLE_ADMIN')")
                  .anyRequest().permitAll();
        }
      }
      
  • JWT 토큰을 기반으로 개인정보 접근 수락하도록
    // 시큐리티가 filter 가지고 있는데 그 필터중에 BasicAuthenticationFilter가 있음
    // 권한이나 인증이 필요한 특정 주소를 요청했을 때 해당 필터를 필히 탐
    // 만약에 권한이 인증이 필요한 주소가 아니라면 이 필터 안탐
    @RequiredArgsConstructor
    public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
    
        private final UserRepository userRepository;
    
        public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
            super(authenticationManager);
        }
    
        // 인증이나 권한이 필요한 주소요청이 있을때 해당 필터를 타게 됨
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
            String jwtHeader = request.getHeader("Authorization");
            // JWT 토큰을 검증하여 정상적인 사용자인지 확인
            // 헤더 있니?
            if (jwtHeader == null || !jwtHeader.startsWith("Bearer")) {
                chain.doFilter(request, response);
                return;
            }
            // JWT 토큰을 검증하여 정상적인 사용자인지 확인
            String jwtToken = request.getHeader("Authorization").replace("Bearer", "");
            String username = JWT.require(Algorithm.HMAC512("cos")).build().verify(jwtToken).getClaim("username").asString();
            // 서명이 정상적으로 된 경우
            if (username != null) {
                User user = userRepository.findByUsername(username);
                PrincipalDetails principalDetails = new PrincipalDetails(user);
                Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
                // 강제로  시큐리티 세션에 접근하여 저장
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
    }