공통 프로젝트 때, 스프링 시큐리티를 사용할 것 같아 기본부터 알아보기로 했다.
개발자 유미님 유튜브를 참고하여 나름대로 정리해봤다.
특히, Spring Security 버전 별로 코드가 다른 것이 제일 인상깊었다.
AI에 Spring Security에 대해 물어볼 일이 있었는데, 23년 9월까지 학습된 AI임에도 불구하고
사용을 권장하지 않는 코드를 짜서 주길래 의아했었는데 현재 버전으로 올라오면서 코드가 바뀌었다고 한다.
개발자는 끊임없이 공부해야하는 직업이라는 말을 많이 들었는데 그 말을 조금 이해하게 된 것 같다.
이후, Spring Security와 JWT, 소셜 로그인까지 계속해서 공부할 계획이다.
Spring Security 간단한 동작 원리
- Servlet Container → filter → controller에 도착
- SecurityConfig를 만들어 놓으면 특정 filter를 만듦
- 클라이언트의 요청을 가로챈 후, 클라이언트의 권한 분석
- 권한이 없다면, filter에서 막게 됨
- 권한이 있다면, 그대로 진행
- 로그인을 할 때에는 모두 다 접근할 수 있게 해야함
Spring Security 로그인
- ID : user
- PW : 프로젝트 실행 시 콘솔에 비밀번호 출력됨
- 초기에는 모두 다 Security가 적용됨
인가
- 특정한 경로에 요청이 오면 controller 클래스에 도 달하기 전 필터에서 Spring Security가 검증을 함
- 해당 경로의 접근은 누구에게 열려 있는지
- 로그인이 완료된 사용자인지
- 해당되는 role을 가지고 있는지
- Security Configuration
- 인가 작업을 커스텀 할 수 있음
- @EnableWebSecurity, @Configuration
시큐리티 버전별 특성
- 세부 버전별로 구현 방법이 많이 다름
- spring 공식 github 레포지토리에서 spring security에서 확인 가능
- 스프링부트 2.2 ~ 2.6
- extends WebSecurityConfigurerAdapter 해서 썼음 authroizedRequest() .antMatchers .anyRequest. 로 썼음
- 스프링부트 2.7 ~ 3.0
- @Bean 빈등록 해서 사용 http .authorizedHttpRequest() .requestMatchers .anyRequest로 사용
- 스프링부트 3.1 ~
- 무조건 람다 형식으로 내부를 구현해줘야 함 http .authorizedHttpRequest((auth) -> auth .requestMatchers .anyRequest로 사용 );
Config 설정 후 로그인 페이지
- 자동으로 로그인 페이지로 리다이렉팅 되지 않고 오류 페이지가 발생함
BCrypt 암호화 메서드
시큐리티 암호화
- 스프링 시큐리티는 사용자 인증(로그인) 시 비밀번호에 대해 단방향 해시 암호화를 진행하여 저장되어 있는 비밀번호와 대조
- 따라서 회원가입 시 비밀번호 항목에 대해서 암호화를 진행해야 함
- 스프링 시큐리티는 암호화를 위해 BCrypt Password Encoder를 제공하고 권장
- 따라서 해당 클래스를 return하는 메소드를 만들어 @Bean으로 등록하여 사용하면 됨
DB 연결
- 데이터베이스 종류와 ORM
- 회원 정보를 저장하기 위한 데이터베이스는 MySQL 데이터베이스 사용
- 접근 할 때에는 Spring Data JPA 사용
- application.properties에 설정
회원 가입 로직
- 회원정보를 통해 인증/인가 작업을 진행하기 때문에 사용자로부터 회원 가입을 진행한 뒤 데이터베이스에 회원 정보를 저장해야 함
- UserEntity, JoinService, JoinController, JoinDto, UserRepository 작성
- h2를 이용한 Table 생성
- none을 update로 바꾸면, 자동으로 Entity를 기반으로 테이블이 생성됨
- 한 번 하고나서 none으로 바꿔줘야 초기화 안됨
- User 저장하기
// 자꾸 유니코드로 깨지고 한글이 안 나와서 고생했는데
// application.properties에 아래 코드를 추가하니까
// 한글이 잘 나오더라.. 별 짓 다 했는데..
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.force=true
회원 중복 검증
- existsByUsername을 이용하여 해당 유저이름이 존재하는지 확인
- 프론트 단에서도 회원가입 아이디 중복 확인 로직 추가 필요
- 프론트에서 검증하지만 백엔드에도 검증 로직은 필수
- 가입 불가 문자 정규식 처리
- 아이디, 비밀번호에 대한 정규식 처리도 필요
- 아이디 자리수
- 아이디 특수문자 포함 불가
- admin과 같은 아이디 사용 불가
- 비밀번호 자리수
- 비밀번호 특수문자 포함 필수
- 아이디, 비밀번호에 대한 정규식 처리도 필요
로그인 검증 로직
인증
- 시큐리티를 통해 인증을 진행하는 방법은 사용자가 Login 페이지를 통해 아이디, 비밀번호를 POST 요청 시 스프링 시큐리티가 데이터베이스에 저장된 회원 정보를 조회 후 비밀번호를 검증하고 서버 세션 저장소에 해당 회원 아이디를 저장함
CustomUserDetailService 작성
- UserDetailsService를 implements 함
- CustomUserDetailService는 SpringSecurityConfig가 달라고 한 CumstomUserDetails를 전달해서 검증하도록 할 것임
세션 정보
로그인한 사용자의 세션 정보 확인 방법
// 세션 현재 사용자 아이디
String id = SecurityContextHolder.getContext().getAuthentication().getName();
// 세션 현재 사용자 role
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iter = authorities.iterator();
GrantedAuthority auth = iter.next();
String role = auth.getAuthority();
- role 값은 사용자의 role을 확인한 후 특정한 API 실행 여부를 판단하는 데 사용
세션 설정
로그인 정보
- 사용자가 로그인을 진행한 뒤 사용자 정보는 SecurityContextHolder에 의해 서버 세션에 관리
- 이때 세션에 관해 세션의 소멸 시간, 아이디당 세션 생성 개수를 설정하는 방법에 대해서 알기
- stateless하게 관리됨
세션 소멸 시간 설정
- 세션 타임아웃 설정을 통해 로그인 이후 세션이 유지되고 소멸하는 시간을 설정할 수 있음
- 세션 소멸 시점은 서버에 마지막 특정 요청을 수행한 뒤 설정한 시간 만큼 유지됨(기본 시간 1800초)
- application.properties // 초 기반 server.servlet.session.timeout=1800 // 분 기반 server.servlet.session.timeout=90m
다중 로그인 설정 (공식 문서 session manager 참고)
- 동일한 아이디로 다중 로그인을 진행할 경우에 대한 설정 방법은 세션 통제를 통해 진행
@Bean
public Security
세션 고정 보호
- 해커가 우리의 admin 계정 같은 세션 아이디를 탈취 해서 서버에 특정한 권한을 날리는 것을 방지
- 세션 고정 공격 보호 방법은 sessionManagement() 메소드의 sessionFixation() 메소드를 통해서 설정 가능
csrf 설정
- CSRF란?
- 요청을 위조하여 사용자가 원하지 않아도 서버측으로 특정 요청을 강제로 보내는 방식 (회원 정보 변경, 게시글 CRUD를 사용자 모르게 요청)
- 개발 환경에서 csrf disable()
- 개발 환경에서는 Security Config 클래스를 통해 csrf 설정을 disable()
- 배포 환경에서는 공격 방지를 위해 disable() 설정을 제거하고 추가 설정 진행 필요
- 배포 환경에서 진행 사항
- SpringSecurityConfig에서 disable 설정을 하지 않으면 자동으로 enable 설정이 진행
- enable 설정 시 스프링 시큐리티는 CsrfFilter를 통해 POST, PUT, DELETE 요청에 대해 CSRF 토큰 여부를 검증함
- SecurityConfig 클래스 설정
- csrf.disable() 삭제
- POST 요청에서 설정 방법
- _csrf 토큰을 넣어서 보내줘야 함
- ajax 요청 시
- HTML <head> 구획에 요청 추가
- <meta name="_csrf" content="{{_csrf.token}}"/> <meta name="_csrf_header" content="{{_csrf.headerName}}"/>
- ajax 요청 시 위의 content 값을 가져온 후 함께 요청
- XMLHttpRequest 요청시 setRequestHeader를 통해 csrf, csr_header Key에 대한 토큰 값 넣어 요청
- GET 방식 로그아웃을 진행할 경우 설정 방법
- security config 클래스 로그아웃 설정
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ http .logout((auth) -> auth.logoutUrl("/logout") .logoutSuccessUrl("/")); return http.build(); }
- LogoutController
@Controller public class logoutController { @GetMapping("/logout") public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(authentication != null) { new SecurityContextLogoutHandler().logout(request, response, authentication); } return "redirect:/"; } }
- mustache에서 csrf 토큰 변수 오류 발생시 아래 구문을 변수 설정 파일에 추가
- spring.mustache.servlet.expose-request-attributes=true
InMemory 유저 정보 저장
- 소수의 유저를 저장할 좋은 방법
- 토이 프로젝트를 진행하는 경우 혹은 시큐리티 로그인 환경이 필요하지만 소수의 회원 정보만 가지며 데이터베이스라는 자원을 투자하기 힘든 경우 회원가입 없는 InMemmory 유저를 저장하면 됨
- 이 경우 InMemoryUserDetailsManager 클래스를 통해 유저를 등록하며 ㄴ됨
- Spring 공식 문서 - In-Memory Authentication 참고
- SpringBoot 3.2 버전부터 달라져서 이 코드 참고 불가
HttpBasic 인증
로그인 방식
- formLogin
- httpBasic
- 아이디와 비밀번호를 Base64 방식으로 인코디안 뒤 HTTP 인증 헤더에 부착하여 서버측으로 요청을 보내는 방식
- 일반 웹사이트도 종종 하지만 마이크로서버 아키텍처를 구축할 때 사용함
- 더 엄격한 보안 상황에서 진행할 때
- spring 공식 문서 basic-authentication
Role Hierarchy
- 스프링 공식 문서 Role Hierarchy 참조
- 계층 권한
- 권한 A, B, C 가 존재하고 권한의 계층은 A < B < C라고 설정을 진행하고 싶은 경우 RoleHierarchy 설정을 진행할 수 있음
- 기존 방식
- @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ http .csrf((auth) -> auth.disable()); http .authorizeHttpRequests((auth) -> auth .requestMatchers("/login").permitAll() .requestMatchers("/").hasAnyRole("A", "B", "C") .requestMatchers("/manager").hasAnyRole("B", "C") .requestMatchers("/admin").hasAnyRole("C") .anyRequest().authenticated() ); http .formLogin((auth) -> auth.loginPage("/login") .loginProcessingUrl("/loginProc") .permitAll() ); return http.build(); } // 권한이 많아지면 쓰기 어려움, 가독성 안 좋아짐
- 계층 권한 메소드
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ http .csrf((auth) -> auth.disable()); http .authorizeHttpRequests((auth) -> auth .requestMatchers("/login").permitAll() .requestMatchers("/").hasAnyRole("A") .requestMatchers("/manager").hasAnyRole("B") .requestMatchers("/admin").hasAnyRole("C") .anyRequest().authenticated() ); http .formLogin((auth) -> auth.loginPage("/login") .loginProcessingUrl("/loginProc") .permitAll() ); return http.build(); }
- @Bean public RoleHierarchy roleHierarchy() { RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); // 계층이 여러 개일 경우 \\n으로 개행 해주면 됨 hierarchy.setHierarchy("ROLE_C > ROLE_B\\n" + "ROLE_B > ROLE_A"); return hierarchy; }
'개발' 카테고리의 다른 글
[OAuth2] X(구 트위터) 로그인 API 사용기 (1) | 2024.11.17 |
---|---|
Docker 공부하기 (기초) (1) | 2024.09.01 |
JWT 심화 (1) | 2024.08.11 |
OAuth2.0 + SpringSecurity (0) | 2024.08.04 |
스프링 시큐리티(Spring Security) + JWT (0) | 2024.07.21 |