2022 07 10
2022-07-10¶
인프링 시큐리티 일반 로그¶
- 모든 주소가 막힌다
- 스프링 시큐리티를 쓴다면 기본적으로 모든 주소가 막혀 인증이 필요해짐
- localhost:8080/login
- 최초 아이디: user, 비밀번호: 스프링 뜰 때 비밀번호
- 인증을 완료하면 그제서야 Controller API 호출 가능
- GET: /login => 이거 만들어도 스프링 시큐리티가 낚아챔
SecurityConfig파일 만들면 login Controller로 흘러들어감
- GET: /login => 이거 만들어도 스프링 시큐리티가 낚아챔
- 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 // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨 @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 두 가지를 모두 상속받은 것
- 따라서 추상화를 시키자!
JWT를 위한 네트워크 지식¶
- TCP
- 통신 OSI 7계층
- 응용 : 롤 프로그램
- 프레젠테이션 : 암호화, 압축
- 세션 : 인증 체크
- 트랜스포트 : TCP/UDP
- 네트워크
- 데이터링크
- 물리
- TCP (신뢰성 있는 통신) => 컴퓨터가 이해가능
- UDP (전화, 동영상) => 사람이 이해가능
- 통신 OSI 7계층
- CIA
- A나라 ---(문서)---> B나라
- C나라가 문서 가로챌수도!
- CIA => C: Confidentiality, I: Integrity, A: Availability
- 문서 암호화 but 몇가지 문제!
- 열쇠 전달 문제
- 문서가 누구로부터 왔는가?
- A나라 ---(문서)---> B나라
- RSA
- Public Key : 공개키
- Private Key : 개인키
- A ------A개인키(B공개키(사랑해))------> B
- B가 문서를 받으면
- A의 공개키로 열어본다
- B의 개인키로 열어본다
- RFC
- 내부망끼리 통신하기 위해 약속된 규칙이 필요했음
- 이게 RFC
- 이게 쭉쭉쭉쭉 RFC 문서화되니까 HTTP 프로토콜이 되었음
- 이게 RFC
- 내부망끼리 통신하기 위해 약속된 규칙이 필요했음
JWT¶
- JWT?
- JSON 객체로 안전하게 전송하기 위한 개방형 표준
- 서명된 토큰에 중점을 둔 것
- Base64로 인코딩/디코딩 가능
- 구조
- header
- payload (claim: iss, exp, sub, aud 등)
- signature
- JSON 객체로 안전하게 전송하기 위한 개방형 표준
- 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); } } }