- Develop OS : Windows10 Ent, 64bit - WEB/WAS Server : Tomcat v9.0 - DBMS : MySQL 5.7.29 for Linux (Docker) - Language : JAVA 1.8 (JDK 1.8) - Framwork : Spring 3.1.1 Release - Build Tool : Maven 3.6.3 - ORM : Mybatis 3.2.8 |
커스터마이징 순서대로 총 9개의 포스팅으로 나눠져 있습니다. 순서대로 보면 쉽게 적용할 수 있습니다.
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_기본 컨텍스트 설정 [1/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_DB 연동 로직 작성 [2/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_로그인 실패 대응 로직 [3/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_로그인 성공 대응 로직 [4/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_DB 패스워드 암호화 [5/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_암호화된 DB 패스워드로 인증 [6/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_Principal 객체 [7/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_자동 로그인(Remember-me) [8/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_security 태그 라이브러리 [9/9]
스프링 Security를 이용한 로그인 인증 백엔드 로직의 마지막 포스팅은 Remember-me(자동 로그인) 방법입니다. Provider까지 커스터마이징 했다면 자동 로그인 인증도 직접 구현할 수 있지만, 매우 간단하게 높은 수준의 보안성을 가진 자동 로그인 로직을 완성시켜줍니다. 개발자는 할 일이 거의 없습니다.
스프링 Security에서 제공하는 Remember-me 구현 방식은 두 가지가 있습니다.
1. 쿠키만을 이용하는 방법 (Simple Hash Cookie)
2. DB에 저장된 데이터를 이용해 쿠키를 이중 인증하는 방법 (Persistent 기반)
하나씩 살펴보도록 하겠습니다.
[ 심플 해시 기반 쿠키를 이용한 자동 로그인 구현 ]
이 방법은 우리가 직접 쿠키를 구워서 사용자에게 보내 저장시킨 뒤 사이트 접속 시에 쿠키 정보가 있다면 자동 로그인을 시켜주는 것과 동일합니다. 다만 사용자 정보를 해시 방식으로 암호화해서 쿠키에 저장해 보안성을 높였습니다.
레퍼런스가 없어서 확실치는 않지만 코드를 까보고 테스트 해본 결과의 인증 과정은 아래와 같습니다. 더 복잡한 구조로 동작하겠지만 표면적인 부분들만 대략적으로 살펴보겠습니다.
1. Remember-me 기능을 활성화했다면, 사용자 인증 요청 시 쿠키를 먼저 검색
- <s:http> 태그 사이에 설정
- key : Hash 암/복호화에 사용할 키 값
- token-validity-seconds : 토큰 유효 기간
- authentication-success-handler-ref : 핸들러를 커스마이징 했다면 로그인 성공 후 수행할 로직
- user-service-ref : UserDetailsService를 커스터마이징 했을 경우 주입
Success Handler는 수동 로그인할 때 커스터마이징 했던 Bean(클래스)을 그대로 사용해도 되고 따로 하나 만들어서 분리해도 됩니다. 안해주면 핸들러를 커스터마이징 했더라도 성공 후 아무 로직도 수행하지 않습니다.
또 user-service-ref에는 DB에서 인증 데이터를 가져오는 UserDetailsService 구현체를 커스터마이징 했을 경우 Bean 주입해주는 설정인데, 만약 인증 로직의 Provider 구현체를 커스터마이징하면서 @AutoWired 등으로 자동 DI를 했다면 해당 Bean을 가져다 사용합니다. 따라서 Provider 커스터마이징 했을 경우 굳이 등록해주지 않아도 되는 설정입니다.
<!-- 자동 로그인(토큰 유효기간 일주일) -->
<s:remember-me key="hsweb" token-validity-seconds="60000"
authentication-success-handler-ref="userLoginSuccessHandler"/>
로그인 뷰의 자동로그인 checkbox <input> 태그에서는 아래와 같이 파라미터 name과 value를 설정해주면 됩니다. 스프링 부트에서는 파라미터명과 쿠키 이름을 바꿀 수 있도록 설정을 추가할 수 있는데 스프링 3.0에서는 컨텍스트에서 해당 설정을 할 수 없습니다. 나중에 커스터마이징을 하게 되면 바꿀 수 있습니다.
* name="_spring_security_remember_me"
* value="True"
<!-- 자동 로그인 -->
<div class="custom-control custom-checkbox mb-1">
<input type="checkbox" class="custom-control-input"
name="_spring_security_remember_me" value="True"
id="customCheck1">
<label class="custom-control-label" for="customCheck1">로그인 유지</label></div>
2. Remember-me 쿠키가 있다면 토큰 처리 디폴트 Bean에게 처리 위임
- 쿠키가 없거나 유효하지 않다면 기존 로그인 로직으로 처리
- 쿠키가 있다면 TokenBasedRememberMeServices 객체 등에서 처리
Remember-me 쿠키가 발견될 경우, 기존에 우리가 커스터마이징 했던 Provider를 사용하지 않습니다. 이를 담당하는 객체에서 처리하는데, 이 클래스 코드를 보면 아래와 같이 UserDetailsService의 메소드를 실행해서 UserDetails 객체를 생성합니다. 위에서 설명했듯이 이 구현체의 타입을 AutoWired 해두거나 설정에서 Bean으로 등록해 주입해줘야 합니다. Provider를 커스터마이징 한 뒤라면 신경쓰지 않아도 됩니다.
쿠키 값을 복호화해서 username(ID)와 password를 가져온 뒤, DB에서 가져온 인증 정보와 비교해 둘 다 맞으면 UserDetails를 Authentication 객체에 넣은 뒤 로그인을 마무리합니다. 따라서 계정 정보가 바뀌면 자동 로그인도 적용되지 않습니다.
수동 로그인에서 사용하는 Provider를 사용하지 않기 때문에 로그인 인증과정에서 꼭 필요한 로직이 있다면 다른 방법을 강구해야 합니다. 예를 들어 Provider에서 인증이 끝난 후에는 UserDetails 객체에서 패스워드 부분을 null 처리하도록 했는데, 자동 로그인을 하면 패스워드 정보가 포함되어 있습니다. 이 경우 저는 Success Handler에서 패스워드 부분을 지워주도록 로직을 수정했습니다. 나중에 Remember-me도 커스터마이징하면 로직을 추가할 수도 있습니다.
* 심플 해시 쿠키(Simple Hash Cookie) 방식의 문제점
쿠키는 브라우저에 저장되기 때문에 보안에 취약합니다. 누군가 쿠키를 탈취한다면 암호화되어 있어 원문을 알아내기는 어려워도 이 쿠키를 가지고 자동 로그인 인증을 할 수는 있습니다. 유효기간이 다 될 때까지 몰래 로그인해서 정보를 훔쳐갈 수 있다는 것이죠.
이를 보완하기 위한 방법이 아래에서 설명할 Persistent 쿠키 인증 방식입니다.
[ 쿠키 및 Persistent Repository 방식을 이용한 자동 로그인 구현 ]
Persistent란 지속적이라는 의미로 일반적으로는 메모리 외 저장장치에 저장되는 방식을 의미합니다. 대표적으로 DB를 들 수 있습니다. 쿠키를 사용한 인증의 보안 취약점을 보완하기 위해 고안된 방식입니다. 과정은 아래와 같습니다.
쿠키의 값으로 자동 로그인을 해주는 방식은 동일한데, 쿠키의 값이 유효한지 DB를 통해 한번 더 검증하고 자동 로그인될 때마다 새로 발급해주는 과정이 추가됩니다. 추가된 부분만 살펴보겠습니다.
1. Remember-me 쿠키를 생성해 브라우저에 전달하기 전 쿠키 값을 DB에 값을 저장해둔다.
이 과정을 위해 먼저 DB에 테이블을 하나 생성해줍니다. 커스터마이징하지 않는다면 정해진 규칙에 맞게 생성해야 하고, 아래와 같은 코드로 생성해주시면 됩니다. 저는 MySQL을 쓰는데 SQL 쿼리는 대부분 비슷해서 그대로 사용 가능할 것 같습니다.
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null);
그리고 컨텍스트 파일에 remeber-me 설정 부분을 아래와 같이 고쳐줍니다. DB를 연결할 때 사용했던 dataSource의 Bean ID를 레퍼런스로 주입해주면 됩니다. remeber-me 설정은 <http> 태그 안에 위치합니다.
key값은 권한 객체의 검증을 위한 값으로 필수 설정은 아닙니다. 설정을 하지 않으면 디폴트로 적용됩니다. 권한 객체의 변조를 막기위한 장치인데 커스터마이징 할 때 좀 더 자세히 다루도록 하겠습니다. 그리고 설정할 부분은 딱 여기까지입니다. 더 이상 무언가 작업할 필요는 없습니다. 아래에 이어지는 글들은 부가 설명들입니다.
<!-- 자동 로그인 -->
<s:remember-me data-source-ref="dataSource"
key="codevang"
authentication-success-handler-ref="userLoginSuccessHandler" />
JdbcTokenRepositoryImpl 클래스의 내용을 보면 위 테이블 구조와 동일하게 SQL 쿼리문이 작성되어 있습니다. DB와 연결되는 dataSource 객체만 제공해주면 JDBC를 사용해서 알아서 쿼리 및 로직을 수행하기 때문에 더 이상 개발자가 관여하지 않아도 되도록 돼있습니다.
최초로 자동 로그인을 한번 해봅니다. 위에서 생성한 테이블에 자동으로 값이 insert된 것을 확인할 수 있습니다. 최초 로그인이기 때문에 인증에 성공하면 해당 계정과 아래 정보를 생성해서 DB에 넣어준 뒤, "series : token"으로 문자열을 붙여서 암호화를 한 뒤 쿠키를 생성해서 브라우저에 보내줍니다.
* Series : 기기, 브라우저별 쿠키를 구분할 고유 값
* token : 브라우저가 가지고 있는 쿠키의 값을 검증할 인증값
* last_used : 가장 최신 자동 로그인 시간
고유한 구분자로 username(ID)이 아닌 series를 사용하는 이유는 다중 기기나 브라우저에서 자동 로그인 기능을 사용하기 위함입니다. 크롬과 익스플로러에서 둘 다 자동 로그인을 체크하고 로그인하면 username(ID)은 두 개가 생기지만 각각 고유한 series값으로 구분됩니다. 따라서 토큰이 신규 발급된 이후부터는 series 값으로 사용자 정보를 검색합니다.
토큰 유효시간에 대한 판단은 마지막 등록시점인 last_used 값에서 설정된 유효시간(디폴트 2주)을 더한 값이 현재 서버의 시간보다 이전이면 유효한 것으로, 이후이면 만료된 것으로 판단합니다.
2. 다시 자동 로그인을 하면 쿠키와 token의 값을 비교한 뒤 새로운 값으로 업데이트
이제 다시 자동 로그인을 해보겠습니다. Remember-me 쿠키가 발견되면 쿠키가 가지고 있는 값을 다시 복호화해서 Series 값이 DB에 있는지 찾아봅니다. 그리고 일치하는 값이 있다면 다시 새로운 token값을 생성해 DB에 업데이트하고 새로운 쿠키를 만들어 다시 브라우저에게 보내줍니다. 위에서 본 token 값과 달라져 있는 것을 확인할 수 있습니다. 만약 series 값이 같은 데이터를 찾았는데 token이 다르면 도난당한 쿠키로 인지하고 데이터를 지워버립니다. 물론 로그인도 안시켜줍니다.
나중에 커스터마이징하게 되면 새로 쿠키를 발급할 때 유효기간을 늘려줄 것인지 그대로 사용할 것인지 등을 결정할 수 있습니다. 디폴트에서는 token이 업데이트될 때 last_used도 같이 업데이트 되기 때문에 계속 자동 로그인 기간이 연장됩니다.
만약 누군가 쿠키를 탈취해 자동 로그인을 하면 DB의 값이 바뀌고 새로운 쿠키(token)가 발행되기 때문에 원래 주인의 자동 로그인이 풀리게 됩니다. 이 경우 원래 주인이 로그인을 다시 하면서 자동 로그인을 체크하거나 로그아웃을 할 경우 훔쳐간 쪽에서는 더 이상 쿠키를 사용할 수 없게 됩니다. 쿠키를 훔쳐서 사용하는 것 자체는 막을 수가 없어도 유효 기한 내에 무한정 사용할 수 없도록 하는 것이죠. 물론 원래 주인이 계속 사용하지 않는다면 심플 해시 쿠키 방식과 다를바가 없습니다.
3. persistent_logins 테이블에 저장된 ID값으로 Authentication 객체를 생성해 인증을 완료한다.
심플 해시 쿠키를 사용했을 때는 ID와 패스워드를 복호화해서 둘 다 맞는지 확인했지만, Persistent 방식에서는 쿠키의 안전성이 높다고 판단했는지 그냥 DB에 저장된 ID값을 가지고 인증 데이터(UserDetails)를 만들고 인증을 완료합니다.
따라서 심플 해시 기반에서는 DB에서 비밀번호를 바꾸면 자동 로그인이 무효화됐지만 Persistent에서는 DB에서 비밀번호를 바꿔도 자동 로그인이 유지됩니다.
4. 로그아웃 시에는 DB의 token 값을 삭제한다.
로그아웃을 하면 브라우저의 쿠키도 지워지고, DB의 token 값도 같이 삭제됩니다. 이 부분도 디폴트 설정에서는 스프링 Security가 자동으로 로직을 수행해주는 부분입니다.
PersistentTokenBasedRememberMeServices 클래스의 코드를 보면 username으로 찾아서 DB의 값을 지워주기 때문에 모든 series/token 정보가 삭제됩니다. 모든 자동로그인이 한 번에 풀린다는 것이죠. 왜 이렇게 해놨는지는 모르겠지만 좀 불편할 것 같습니다. 이 부분은 조만간 커스터마이징을 추가로 해서 수정하도록 하겠습니다.
@Override
public void logout(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) {
super.logout(request, response, authentication);
if (authentication != null) {
tokenRepository.removeUserTokens(authentication.getName());
}
}
디폴트 설정으로 사용한다면 위 내용 정도로 쉽게 사용이 가능할 것 같습니다. 설정 두세줄만 추가해주면 자동 로그인 기능이 구현되니 엄청 편리하네요. 다만 로그아웃이나 쿠키 이름 등을 변경하는 얕은 커스터마이징이 필요할 수도 있고, 또는 자동 로그인 등록 시 2차 인증을 하도록 하는 등의 로직을 추가하려면 좀 더 깊게 커스터마이징이 필요합니다.
Remember-me 커스터마이징은 아래 링크글을 참조하시면 됩니다.
[Spring Security] - 스프링 Security_Remember-me 커스터마이징 [1/3]
[Spring Security] - 스프링 Security_Remember-me 커스터마이징 [2/3]
[Spring Security] - 스프링 Security_Remember-me 커스터마이징(커스텀 필터) [3/3]
'▸Spring Security' 카테고리의 다른 글
스프링 필터와 스프링 시큐리티(Spring Security)의 동작 구조 (0) | 2020.04.22 |
---|---|
스프링 Security_로그인_security 태그 라이브러리 [9/9] (2) | 2020.03.31 |
스프링 Security_로그인_Principal 객체 [7/9] (1) | 2020.03.30 |
스프링 Security_로그인_암호화된 DB 패스워드로 인증 [6/9] (0) | 2020.03.29 |
스프링 Security_로그인_DB 패스워드 암호화 [5/9] (1) | 2020.03.29 |
댓글