이번 게시물에서는 Spring Security 공부 차원에서 이를 활용하여 로그인 페이지, 회원가입 페이지가 있는 소규모 프로젝트를 만들어 볼 것이다.
전체 코드는 Github에서 확인해 볼 수 있다.
해당 게시물은 해어린 블로그님의 게시물과 코딩스타트님의 게시물, 레퍼런스를 참조하여 작성했다.
1. Spring Security란?
Spring Security는 스프링 기반 에플리케이션의 보안을 담당해주는 스프링 하위 프레임워크이다. 이를 활용하면 개발자가 직접 보안 관련 로직을 짜는 수고를 덜 수가 있다. 스프링 시큐리티에 대해 공부하기 위해 여러 포스팅과 블로그, 레퍼런스들을 둘러보았고, 우선 다음의 4가지 용어를 이해한 후 넘어갈 것이다.
1) 접근 주체(Principal)
- 보호되어 있는 리소스에 접근하고자 하는 대상
2) 인증(Authentication)
- '유저'를 확인하는 작업, 접근한 대상이 어떤 종류의 유저인지 확인하는 과정
3) 인가(Authorize)
- 해당 리소스에 접근한 대상이 권한이 있는지 확인하는 과정
4) 권한(Authorization)
- 어떤 리소스에 대한 접근 제한을 의미, 모든 리소스는 각각 권한이 걸려있으며, 인가 과정에서 최소한의 권한을 확인하는 것
이 모든 로직을 직접 구현하기 위해서는 많을 노력과 시간이 필요하지만, Spring Security를 활용하고 해당 프레임워크의 메커니즘을 지킨다면 비교적 쉽게 처리할 수 있다.
2. Spring Security가 제공하는 기능
Spring Security를 활용하여 WebSecurityConfig 클래스를 생성하게 되면, 다음과 같은 기능을 제공한다.
- 애플리케이션의 모든 URL에 대해 인증을 요구함
- 로그인 양식을 생성해 줌
- 아이디 및 암호를 가진 사용자의 양식 기반 인증 기능
- 로그아웃 기능
- CSRF 공격 방지
- Session Fixation 보호해줌
- 보안 헤더 통합
- HTTP Strict Transport Security (보안 요청 위함)
- X-Content-Type-Options 통합
- 캐시 컨트롤 (정적 리소스 캐싱 가능)
- X-XSS-Protection 통합
- 클릭재킹 예방을 위한 X-Frame-Options
- 아래의 서블릿 API 메소드들과 통합
참조: 원본링크
3. 페이지 설계
이번에 만들 소규모 프로젝트는 Spring Security를 활용해 로그인 및 회원가입 기능을 만들어보고, 권한에 따라 접근 가능한 페이지를 만들어보는 것이다. 따라서 아래의 페이지만 제작해 볼 예정이다.
1) 로그인 페이지(첫 화면)
- 로그인을 할 수 있는 페이지
- 누구나 접속 가능
- 로그아웃시 돌아오는 페이지
2) 회원가입 페이지
- 회원가입 할 수 있는 페이지
- 누구나 접속 가능
- 토이 프로젝트임으로 여기서 User, Admin 권한 선택 기능 추가
3) 유저 페이지
- 로그인 성공시 넘어오는 페이지
- 유저, 관리자만 접근 가능
- 로그아웃 가능
- 관리자 페이지 넘어가기 가능
4) 관리자 페이지
- 유저페이지에서 넘어오는 페이지
- 관리자만 접근 가능
- 로그아웃 가능
4. 의존성 추가
우선 build.gradle에 의존성을 추가한다.
+ web, lombok, JPA, H2도 기본적으로 추가
<로그인 구현>
1. Config 파일 작성
Spring Security 관련 설정들을 변경하기 위하여, config 파일을 만들 것이다. 우선 config.auth 패키지를 만들고, 그 안에 WebSecurityConfig 클래스를 생성한다. 앞으로 시큐리티 관련 클래스는 모두 이 패키지에 담을 것이다.
첫 설계 시에는 로그인 페이지를 "/"으로 지정하였지만, 권한이 없을 경우 자동으로 로그인 페이지로 이동됨을 알아 유저페이지를 "/"으로 지정하였다. 위 클래스에 대한 설명은 다음과 같다.
1) @EnableWebSecurity #13
- 이 어노테이션으로 인해 Spring Security 설정들을 활성화 시켜준다.
2) @Configuration #14
- Bean을 관리하는 어노테이션, 아직 Bean에 대한 정확한 공부가 부족함, 추후에 공부할 예정
3) extends WebSecurityConfigurerAdapter #16
- 해당 클래스가 Spring Security 설정 파일로서 역할을 하기 위해선, 이 클래스를 상속해야 함
- 이로 인해 아래의 메소드들을 Override해서 변경할 수 있음
4) private final UserService userService #18
- 유저 정보를 가져올 클래스, 추후에 만들 예정
5) configure(WebSecurity web) #22
- 인증을 무시할 경로 설정
- css, js, img, h2-console은 인증을 하지 않아도 접근이 가능해야 하므로 설정
6) configure(HttpSecurity http) throws Exception #28
- http 관련한 인증 설정 가능
6-1) csrf().disable().headers().frameOptions().disable() #30
- h2-console 화면을 사용하기 위해 해당 옵션들을 disable() 해줌
6-2) authorizeRequests() #32
- URL별 권한 관리를 설정하기 위한 시작점 지정
- authorizeRequests()가 선언되어야 antMatchers 옵션 사용 가능
6-3) antMatchers() #33~
- 권한 관리 대상을 지정하는 옵션
- URL 혹은 HTTP 메소드 별로 관리를 가능하게 해 줌
- permitAll(): 누구나 접근 가능하게 함
- hasRole(): 해당 권한 있어야 접근 가능
- anyRequest(): anyMatchers()에서 설정하지 않은 나머지 주소들
- authenticated(): 권한이 있기만 하면 접근 가능
6-4) formLogin() #38
- 로그인 관련 설정하는 옵션
- loginPage(): 로그인 페이지 링크
- defaultSuccessUrl(): 로그인 성공시 연결되는 주소
6-5) logout() #42
- 로그아웃 관련 설정하는 옵션
- logoutSuccessUrl(): 로그아웃 성공시 연결되는 주소
- invalidateHttpSession(true): 로그아웃 성공시 저장해 두었던 세션 모두 날리기
7) configure(AuthenticationManagerBuilder auth) throws Excetion #50
- 로그인 시 필요한 정보를 가져옴
7-1) userDetailsService() #51
- 유저 정보를 어느 서비스에서 가져올지 결정
7-2) passwordEncoder() #52
- 패스워드 인코더 설정, 여기선 BCryptPasswordEncoder 이용
+ BCryptPasswordEncoder는 Spring Security에서 제공하는 클래스 중 하나
+ 비밀번호 인코딩 메소드, 제출된 비밀번호와 저장되어있는 비밀번호 일치여부 알려주는 메소드 제공
2. User Entity
이제 UserService를 위해 User Entity와 UserRepository를 작성할 것이다. User 엔티티는 UserDetails를 상속받아서 진행하는데, 이때 필수로 override해야 하는 메소드들은 다음과 같다. 만일에 override을 하지 않는 경우, 아예 에러가 발생한다.
메소드 | return 타입 | 설명 | default |
getAuthorities() | Collection<? extends GrantedAuthority> | 계정의 권한 목록 return | - |
getPassword() | String | 계정의 비밀번호 return | - |
getUsername() | String | 계정의 고유값 return (unique한 값이어야 함) (보통 DB의 PK값) |
- |
isAccountNonExpired() | boolean | 계정의 만료 여부 return | true (만료 X) |
isAccountNonLocked() | boolean | 계정의 잠김 여부 return | true (잠기지 X) |
isCredentialsNonExpired() | boolean | 비밀번호 만료 여부 return | true (만료 X) |
isEnabled() | boolean | 계정의 활성화 여부 return | true (활성화 됨) |
여기서 중요한 부분은 getAuthorities() #46 이다. 해당 메소드는 사용자의 권한 목록을 Collection 형태로 반환해야 하며, 그 자료형은 무조건 GrandtedAuthority(스프링시큐리티의 자료형인듯 함)여야 한다.
우선 권한이 중복되는 일은 없으므로 set 형태의 roles를 선언하여 반환한다. 또한 Admin은 그보다 아래인 일반 유저의 권한도 동시에 가지고 있으므로, auth는 "ROLE_ADMIN, ROLE_USER"와 같은 형태로 해당 db에 저장될 것이다. 따라서 split(",")을 활용하여 ROLE_ADMIN과 ROLE_USER 둘 모두 roles에 추가한 후 반환하는 형식이다.
3. UserRepository
UserInfo entity를 만들었으니 이와 짝궁인 UserRepository를 만들어 준다.
JpaRepository를 상속받아준다.(pk가 Long이므로 Long) 또한 이메일을 통해 회원을 조회하기 위한 findByEmail()을 추가한다.
4. UserService
아까 WebSecurityConfig에서 필요한 UserService를 service 패키지를 만들어 그 안에 만들어 준다.
UserService는 UserDetailsService 클래스를 상속하며, 이에 따라 loadUserByUsername() #19 을 무조건 override 해야 한다. 기본 반환타입은 UserDetails 인데, UserInfo가 이를 상속하고 있으므로 자동으로 다운캐스팅 된다.
위에서 만든 UserRepository의 findByEmail을 활용하여 찾고, null일 경우 UsernameNotFoundException을 통해 예외 처리를 해준다.
<회원가입 구현>
1. UserInfoDto
우선 받은 회원정보를 전달하기 위한 객체 Dto를 web.dto 패키지를 생성한 후 만들어 준다.
추후 UserService에서 비밀번호를 가져와 암호화하기 때문에 생성자 대신 @Setter를 추가한다.
2. UserService 수정
UserService에 회원정보를 저장하기 위해 save() 메소드를 추가한다.
이때, infoDto로 전달받은 password를 Bcrypt으로 암호화시킨후에 다시 저장해준다.
3. UserController
이제 UserController를 web 패키지에 생성한다.
/user 로 POST 요청을 할 경우 회원 정보를 저장한 후, login 페이지로 이동하게 만든다. 또한 회원가입 시 아직 유저는 어떠한 권한도 얻지 못한 상태이므로, 맨 앞에서 만들었던 WebSecurityConfig의 permitAll()에 "/user"를 추가한다.
#34에 "/user"를 추가, 그리고 #35의 주소를 "/main"으로 변경하였다.
<로그아웃 구현>
WebSecurityConfig에서 로그아웃 설정을 하였으나, 이는 POST요청에 csrf을 보내는 경우만 해당된다. 따라서 GET 요청으로 로그아웃을 해도 로그아웃이 가능하도록 구현한다.
<나머지 구현>
처음에는 현재 공부하고 있는 mustache를 활용하여 view 영역을 개발하고자 하였으나, 머스테치는 form 전송시 csrf 토큰을 함께 전송하는 기능을 제공해 주지 않아 Thymeleaf를 사용하고자 한다.
1. 의존성 추가
우선 build.gradle 에 Thymeleaf 의존성과 뷰 페이지에서 로그인된 사용자 정보를 가져오기 위한 의존성을 추가한다.
2. 뷰, 요청 연결
Config 파일을 하나 만든 후 요청과 뷰를 연결해준다.
3. 뷰 작성
이제 첫 화면인 로그인 페이지, 회원가입 페이지, 유저 페이지, 관리자 페이지를 작성해 볼 것이다. 구체적인 코드는 github에 올라가있으며, 해당 게시물에서는 시큐리티 관련 설정만 올릴 것이다.
로그인을 하기 위해 thymeleaf 문법인 th:action="@{/login}"을 사용한다.
4. 실행
이제 실제로 SpringSecurityTestApplication을 실행하여 잘 작동된는지 확인해 본다.
localhost:8080에 접속하면 자동으로 localhost:8080/login으로 넘어가진다. 우선 회원가입 페이지로 넘어가서 회원가입을 해본다.
우선 user 권한을 가진 계정을 하나 생성한다.
방금 만든 계정으로 로그인을 해본다.
성공적으로 로그인이 완료되었으며, 현재 사용자의 아이디(이메일)과 소유하고 있는 권한이 유저뿐임을 알 수 있다.
위 페이지에서 관리자 페이지로 이동을 클릭할 경우, admin 권한이 없어 에러 메세지가 뜨게 된다.
그럼 admin 권한을 가진 계정을 다시 회원가입 해본다.
해당 계정으로 로그인 해볼 경우, 소유 권한이 user와 admin 2개 모두 가지고 있음을 알 수 있다.
이 계정은 admin 권한을 가지고 있으므로, 관리자 전용 페이지도 들어감을 알 수 있다.
마지막으로 localhost:8080/h2-console에 들어가 확인해 본 결과, 2개의 계정이 잘 저장되어 있으며, 비밀번호 또한 암호화되어 있는 것을 볼 수 있다.
<후기>
이번에 소규모 프로젝트를 진행하게 되면서 스프링 시큐리티를 활용해야 하여 공부를 진행하게 되었다. 하지만 아직 구현을 한번 해보았을 뿐, 스프링 시큐리티에 대한 더 자세한 공부는 하지 못하였다. 확실히 스프링에 대한 공부가 필요함을 느끼게 되었다.
또한, 기존에 학습하던 머스테치가 csrf 토큰 전송을 지원해주지 않아 또다른 유명한 엔진인 thymeleaf을 사용해보았다. 하지만 처음 사용해 봄에 따라 많은 오류가 발생하였다. 나중에 시간이 되면 thymeleaf도 한번 공부해서 정리할 계획이다.
우선 스프링 시큐리티를 활용해 로그인, 회원가입 서비스까지 구현해 보았지만, 스스로가 많이 부족함을 다시 느낀 것 같다. 따라서 스프링 부트를 우선 익숙하게 익히고, 그 후 스프링 공부를 진행하여 자세히 공부할 예정이다.
이 소규모 프로젝트의 모든 코드는 github에 올라가 있습니다.
https://github.com/imgzon3/spring-blog/tree/master/spring-security-test
'Web > etc.' 카테고리의 다른 글
[Intellij] cdn으로 불러온 js 라이브러리를 인식하지 못할 경우(Unresolved) (1) | 2021.08.09 |
---|---|
Postman 설치, 사용법 (0) | 2021.07.28 |
댓글