진짜 개발자
본문 바로가기

FrameWork/Spring Security

SpringSecurity - SpringSecurity 간단한 설정과 예제

728x90
SpringSecurity sample 공식 문서 _ https___spring

SpringSecurity 란 : https://postitforhooney.tistory.com/entry/SpringSecurity-%EC%B4%88%EB%B3%B4%EC%9E%90%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-Spring-Security-%ED%8D%BC%EC%98%B4

SpringSecurity sample 공식 문서 : https://spring.io/guides/topicals/spring-security-architecture/

SpringSecurity Reference : https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#web-app-security

백기선님 SpringSecurity 강좌 : https://www.youtube.com/watch?v=fG21HKnYt6g


1. SpringSecurity 로그인 과정

1.1 사용자가 요청을 한다.

1.2 사용자가 요청한 서비스가 로그인이 필요하다.

1.3 SpringSecurity는 SpringSecurityContext에서 Authentication이라는 객체를 찾는다.

1.4 이때 Authentication 객체가 존재하지 않는다면 사용자에게 Login 페이지를 보여준다.

1.5 사용자가 로그인 정보를 입력하고 로그인을 하면 사용자가 입력한 Id에 해당하는 UserDetails를 읽어와서 사용자가 입력한 정보들과 비교를 한다. (UserDetails는 각기 다른 어플리케이션의 User Model을 추상화한 것.)

1.6 로그인에 성공하면 Authentication객체를 SpringSecuritContext에 담는다.

 

 

 

2. SpringSecurity 관련 객체

2.1 UserDetails

앞서 말한 것처럼 우리 어플리케이션내의 User에 해당하는 Model에 UserDetails 구현하여 SpringSecurity가 이해할 수 있는 형태의 User로 만들어주어야 합니다.

 

2.2 UserDetailsService

UserDetailsService는 DataBase로부터 사용자를 가져와 로그인처리 로직을 구현하는 역할을 합니다.

 

UserDetailsService를 구현하면 loadUserByUsername() 을 구현하게 되며, 이때 당연히 return 값으로 UserDetails를 구현한 객체를 return해야 합니다.

 

이때 우리 어플리케이션의 User를 나타내는 객체에 UserDetails를 구현해도 되고, 이를 좀더 편리하게 사용할 수 있도록 SpringSecurity에서 UserDetails를 구현하여 만든 클래스로 User라는 클래스가 존재하는데 이를 확장하여 그것을 리턴해도 됩니다. (이때 Authority의 경우에는 DB의 정보를 보고 직접 만들어 전달해주어야 합니다.)

 

 

 

3. Role

허가(Permission)을 다루기 위해서 SpringSecurity에서는 Authority를 사용합니다. 예를 들어 User의 Role을 가지는 계정은, /admin/**이하의 요청이 불가능하며, ADMIN Role 을 가지는 계정만이 요청이 가능한다던지의 처리를 할 때 필요한 개념입니다. 예제에서 다룰 것입니다.

 

 

 

4. PasswordEncoder

패스워드 저장시 암호화를 돕는 객체입니다.

 

Spring 5.X 버전 이후 부터 변화된 암호화 정책에 의해서 Password의 암호화 알고리즘을 변환시 이 암호화를 풀어서 다시 암호화 해야하는데 이것을 돕기 위해 SpringSecurity 에서 DelegatingPasswordEncoder를 사용하게 되었습니다.

 

이러한 변화 때문에 암호화시 패스워드의 앞단에 {id}가 붙어서 저장이 되게 됩니다. 그런데 이때 패스워드를 입력받은 그대로 저장한다면 당연히 패스워드 앞에 {id}가 존재하지 않기 때문에, 위와 같은 에러를 내뱉습니다.

 

 

5. 예제

5.1 SpringMVC Config

우선 테스트를 위한 것이므로 핸들러를 직접 생성하여 여러 설정하는 것을 생략하기 위해, WebMvcConfigurer를 구현하고, addViewControllers를 구현하여, 간단히 테스트에 사용될 요청들을 추가합니다.

 

 

5.2 Template 작성

위에서 SpringMvcConfig에서 추가한 Controller에 대응하는 Template들을 생성합니다. login.html은 추후에 작성하고, 나머지 home, hello를 먼저 작성합니다.

 

home.html

 

hello.html

 


우선 여기까지 작성한 후 실행한 결과입니다. 성공적으로 페이지가 나타납니다.

 

 

 

5.3 SpringSecurity Config

이제 본격적으로 SpringSecurity 관련 설정을 진행하겠습니다. 우선 build.gradlespring-boot-starter-security 를 추가합니다.

 

그 후, SpringSecurity에게 우리 어플리케이션이 어느 요청에는 인증이 필요한지 안한지를 알려주어야 합니다. SpringSecurity는 Servlet의 Filter를 기반으로 동작하는데, 위의 설정이 즉 custom filter를 생성하는 것 입니다.

 

우선 SpringSecurityConfig 클래스를 생성합니다. 그 후 @Configuration, @EnableWebSecurity 어노테이션을 부여합니다. 그 후 설정을 위해 WebSecurityConfigurerAdapter를 확장합니다.

 

configure(HttpSecurity http)를 오버라이딩 하여 설정을 진행합니다. antMatchers().permitAll()는 인증없이 사용자의 접근을 허용하는 url을 작성합니다.

.anyRequest().authenticated()의 경우 나머지 모든 요청에 대해서 인증을 요구하도록 하는 코드 입니다.

 

formLogin().loginPage("/login").permitAll()은 사용자가 인증이 필요한 페이지에 접근하여 리다이렉팅 되는 url입니다. 우리는 앞서 viewController를 설정할 때 /login으로 접근하는 사용자를 우리가 작성한 login.html을 보여주도록 했습니다. 또한 permitAll()을 꼭해주어야 사용자가 /login url로 접근이 가능합니다.

 

 

5.4 login.html 폼

username, password 를 name으로 가지는 input을 통해서 사용자가 로그인정보를 spring security에게 전달하도록 합니다. 또한 요청하는 url로 앞서 SpringSecurityConfig에서 지정한 /login으로 폼을 제출하도록 합니다.

 

 

5.5 우리 App에서 사용할 User Model 생성

우리 App에서 사용자를 의미하는 모델입니다. 기본적으로 식별자로 사용될 id를 가지며, userid 로 사용될 이메일과 password, 그리고 허가를 부여할 때 사용될 authority를 추가합니다.

 

 

 

5.6 회원 추가 로직 구현

AccountRepository

임시 Database의 역할을 하게 될 Repository 클래스 입니다. Map을 통해서 사용자들을 가지게 됩니다.

 

회원 가입 페이지와 로직을 구현하기 귀찮기 때문에, GetMapping을 통해 /create로 요청이 오게 되면 임시로 사용자를 생성하여 Repository에 저장하도록 합니다. 중요한 점은 authority에는 꼭 ROLE_*과 같은 형태로 저장을 해야한다는 것 입니다. Spring Security에서 권한을 다룰때 자체적으로 PREFIX로 ROLE_을 추가하기 때문입니다.

 

사용자가 /create로 요청을 보낼 수 있도록 SpringSecurityConfig클래스에 /create를 추가합니다.

 


여기까지 작성한 후 테스트를 해보면 성공적으로 사용자가 생성되어 리턴되는것을 확인할 수 있습니다.

 

 

 

5.7 UserDetailsService를 이용하여 로그인 처리 구현

UserDetailsService를 구현하면 SpringSecurity 이용시 우리의 DataBase에서 User들을 찾아서 로그인을 하도록 구현할 수 있습니다.

 

별도의 로그인 처리 클래스를 생성하여 UserDetailsService를 구현해도 되지만 Service클래스에 구현하여 처리를 한다고도 합니다. 저는 Service 클래스에 구현하였습니다.

 

우선 로그인이 어떻게 되어야 할지 생각을 해봅시다. 사용자가 email을 전달한다면 해당 email과 매칭되는 우리 DataBase에 저장되어있는 계정의 정보를 가져와야 할 것입니다.

 

AccountRepository

때문에 accountRepository에 email을 통해 계정을 가져올 수 있는 findByEmail() 메소드를 구현했습니다.

 

AccountService

UserDetailsService를 구현하면 loadUserByUsername()을 구현하게 되는데 이 메소드에서 우리 App의 계정 모델을 SpringSecurity에서 처리가능한 계정의 형태인 UserDetails 형태로 변환하여 반환을 해야합니다.

 

저는 SpringSecurity에서 제공하는 UserDetails를 구현한 User를 이용했습니다. User는 app에서 사용되는 계정 모델을 UserDetails로의 변환을 조금더 수월하게 할 수 있도록 도와주며 생성자에는 3가지 매개변수를 필요하는데 각각 아이디, 비밀번호, 권한리스트 입니다.

 

우선 클래스에서 방금 생성한 findByEmail() 메소드를 이용해 Account객체를 가져옵니다. 그후 가져온 account에서 email, password를 꺼내어 User의 첫번째, 두번째 매개변수로 전달합니다.

 

User의 마지막 매개변수인 List<GrantedAuthority>는 사용자가 가지고있는 권한들이 담긴 리스트로 GrantedAuthority 객체들이 담겨있습니다. 권한리스트에는 GrantedAuthority를 구현한 SimpleGrantedAuthority를 생성하여 담아주면 됩니다.

 

 


여기까지 진행한 후 /create로 다시 요청을 보내어 계정을 생성한 뒤 로그인을 시도합니다. 이때 아무런 반응이 나타나지 않을 것인데 이때 Spring 을 확인하면..

 

위 그림과 같은 에러가 타나났을 것입니다. 이부분이 바로 4.PasswordEncoder부분에서 다루었던 내용입니다.

 

 

5.8 PasswordEncoder

위의 에러를 제거하기 위해서는 PasswordEncoder를 사용하여 사용자가 입력한 password에 암호화를 거쳐서 {id}가 자동으로 붙도록 만들거나, Encoder를 사용하지 않을 것이라면 암호 앞에 임의로 {noop}을 추가로 부여하면 됩니다.

 

5.8.1 {noop}

위와 같이 계정을 생성하는 요청을 처리하는 핸들러에서 암호 앞부분에 {noop}을 추가하면 끝입니다.

 

성공적으로 로그인이 됩니다.

 

5.8.2 PasswordEncoder 사용

SpringSecurityConfig

SpringSecurityConfig 클래스에 PasswordEncoder Bean을 추가적으로 등록하는 코드를 작성합니다.

 

AccountService

AccountService클래스에 위와같이 PasswordEncoder를 사용하여 Password를 암호화하는 로직을 추가합니다. 암호화 결과를 보기 위해 account의 password를 콘솔에 출력합니다.

 

console을 확인하면 위 그림과 같이 암호 앞쪽에 자동으로 {bcrypt}가 추가된것을 볼 수 있으며, 로그인에 성공하는 것을 볼 수 있습니다.