주의) 이글은 미천한 영어 실력으로 인해 다음의 원문을 의역과 오역을 통해 작성된 글입니다. 거기에 주관적인 의견이 중간중간 섞여 있으므로 Shiro가 이러이러한 느낌이구나 정도만 참고해 주시면 감사하겠습니다.
이전글
Authentication, 인증
인증은 회원관리를 하는 어플리케이션이라면 핵심으로 구현 되어야 하는 기능입니다. 즉 자신이 A라면 A라는것을 증명해야 하며, 우리는 이를 로그인이라 합니다. 자신이 네이버의 a회원이라면 a라는것을 아이디와 비밀번호로 증명해야 하는것입니다.
인증은 다음의 3단계로 이루어진다.
1. principals라 불리우는 사용자의 식별정보와 자격증명이라 불리우는 신원증명을 수집합니다. (보통은 아이디와 패스워드)
2. 시스템에 principals와 신원증명를 제출합니다.
3. 시스템은 제출된 신원증명이 principals에 대해 기대하는것과 일치하면 인증된것으로 간주하며, 일치하지 않으면 인증되지 않은것으로 간주합니다.
추상적인 설명이라 이해하기 어렵습니다. 쉽게 설명하자면 principals 는 아이디 신원증명은 비밀번호라고 생각할 수 있습니다.
shiro는 사용자가 로그인하려 입력한 아이디와 비밀번호를 담고 있는 AuthenticationToken을 login() 메서드에 전달하여 인증(로그인)을 수행합니다. AuthenticationToken은 어플리케이션에 따라 여러가지로 세분화됩니다. 예를 들어 대부분의 어플리케이션은 아이디와 비밀번호를 인증에 사용하는데, 이때는 UsernamePasswordToken을 사용할 수 있습니다.
코드5. Subject 로그인
//1. Acquire submitted principals and credentials: 사용자가 입력한 아이디, 비밀번호. 즉 로그인 정보 취득
AuthenticationToken token = new UsernamePasswordToken(username, password);
//2. Get the current Subject: 현재 접속한 사용자에 대한 Subject를 얻음
Subject currentUser = SecurityUtils.getSubject();
//3. Login: 로그인 수행. AuthenticationToken에 담긴 정보가 실제 DB상에 있는 정보와 일치하지 않으면 로그인에 실패할것입니다. 일치하는지 여부는 설정을 통해 구성된 Realm를 통해 수행
currentUser.login(token); |
cs |
만약 사용자가 입력한 정보가 잘못되어 로그인에 실패했다면..?
즉 사용자가 입력한 AuthenticationToken에 담긴 정보가 잘못되어 로그인에 실패했다면 shiro는 AuthenticationException 예외와 이것을 상속하여 세분화 한그 하위 예외들을 던지게 되므로 이를 바탕으로 처리하면 됩니다.
코드6 로그인 실패시 처리
//3. Login:
try {
currentUser.login(token);
} catch (IncorrectCredentialsException ice)
//비밀번호가 제대로 되지 않았을 경우의 처리
} catch (LockedAccountException lae) {
//계정이 잠금처리 되었을때의 경우
} catch (AuthenticationException ae) {
//로그인에 실패했지만 앞의 예외들에 해당되지 않는 원인인 경우 묶어서 처리
} |
cs |
Authorization, 권한
앞서 인증에 대해 설명했는데, 사용자가 인증(로그인)에 성공했다고 하더라도 고려해야 할 점이 있습니다. 바로 특정 정보에 대한 접근이나 특정 기능에 대한 실행 권한입니다.
예를 들어 관리자만 접근할 수 있는 정보와 일반 사용자가 접근할 수 있는 정보가 나뉠 수 있습니다. 또한 일반 사용자는 다른 사용자들의 계정을 생성하거나 변경할 수 있어서는 안될것입니다.
아래 코드7는 Subject의 Role(역할)을 통해 관리자인지 검사하는 모습입니다. 관리자 역할인 경우 사용자 계정을 생성할 수 있습니다.
코드7. 역할 검사
//현재 사용자의 Subject를 통해 Subject가 관리자 역할인지 확인
if ( subject.hasRole("administrator") ) {
//관리자만 접근할 수 있는 기능 실행
userService.createUser(userModel);
} else {
//관리자 역할이 아니라면 예외 발생
throw new AuthorizationException("사용자를 생성할 권한이 없습니다.");
} |
cs |
위 코드7에는 결함이 하나 있습니다. 역할을 체크할때 "administrator" 라는 String 타입의 이름으로 하드코딩 되어있습니다는 것입니다. 만약 어플리케이션에서 관리하는 역할의 이름이 "administrator" 에서 "admin"으로 변경된다거나 다른 역할들의 이름이 변경되면 코드를 매번 수정해야 하는 일이 발생합니다.
따라서 shiro는 역할(Role)개념뿐 아니라 허가권한(Permission) 개념을 지원합니다. 허가 권한 개념을 사용하면 코드7은 아래 코드8과 같은 모양새로 변경됩니다.
코드8. 권한 검사
//현재 사용자의 Subject를 통해 사용자 - 생성 권한이 있는지 확인
if ( subject.isPermitted("user : create") ) {
//사용자 생성 권한이 있습니다면 실행
userService.createUser(userModel);
} else {
//사용자 생성 권한이 없다면 예외 발생
throw new AuthorizationException("사용자를 생성할 권한이 없습니다.");
} |
cs |
shiro는 기본적으로 WildcardPermission 라는 녀석을 사용하여 "user : create" 와 같은 어플리케이션 개발시 대부분 사용하는 기능들에 대해 권한 검사 표현식을 제공합니다. 또한 이 WildcardPermission 이라는 녀석은 개체 레벨에서의 섬세한 권한검사까지 제공합니다.
예를 들어 단순히 "user : delete" 와 같이 사용자 정보를 삭제할 수 있는 권한을 검사하는 것이 아닌 "kim" 이라는 아이디를 가진 계정을 삭제할 수 있는 권한이 있나? 라는 식의 섬세한 개체 레벨 권한 검사를 제공합니다.
코드9. 객체 레벨 권한 검사
if ( subject.isPermitted("user : delete : kim") ) {
// "kim" 사용자를 삭제할 수 있는 권한이 있음
} else {
// "kim" 사용자를 삭제할 수 있는 권한이 없음
} |
cs |
또한 당연하게도! 정해진 권한 구문 표현식 뿐만 아니라 자신의 어플리케이션에 맞는 권한 구문을 커스터마이징 할수도 있습니다.
세션 관리
Apache Shiro는 모든 아키텍쳐에서 일관된 API를 통해 세션(Session)을 다룰 수 있습니다. 즉 간단한 웹 어플리케이션이든, 독립형 어플리케이션이든, 거대한 클러스터링된 웹 어플리케이션이든 모두 일관된 API를 사용할 수 있다는 뜻입니다. 마치 우리가 JDBC를 사용하면 어떤 DBMS와 연동을 하든 같은 API를 사용하는것과 똑같습다.
따라서 바꿔 말하면 shiro 프레임워크가 적용되어 있는 Sevlet/JSP 어플리케이션을 EJB로 아키텍쳐가 변경된다 하더라도 API가 변경되지 않는다는 뜻입니다.
또한 shiro는 컨테이너에 독립적입니다. 예를 들어 세션 클러스터링이 필요한 경우 Tomcat, Jetty, Websphere는 클러스터링 구성 방법이 모두 다르기때문에 컨테이너별로 따로 구현을 해야 합니다. 그러나 shiro는 Enterprise Cache, RDBMS, NoSQL 등의 연결 가능한 세션 저장소를 제공하므로 컨테이너에 독립적인 클러스터링을 구현할 수 있습니다.
그럼 웹 환경에서의 shiro 세션과 스윙등을 사용한 독립형 어플리케이션에서의 shiro 세션을 얻는 방법을 알아보도록 합니다.
독립형 어플리케이션이 무슨 세션이 필요하냐고 할 수 있지만 예를 들어 학생때 개발했던 채팅서버(웹서버 사용 안하고)를 생각해보자. 당연히 서버와 클라이언트 개념이 있기 때문에 연결된 사용자들에 대한 세션을 제어할 필요가 있습니다.
본론으로 들어가서 아래 코드10을 보세요. 웹 환경과 독립형 어플리케이션에서는 아래 코드 10에서처럼 사용자의 Subject의 getSession() 메서드를 통해 세션을 얻어냅니다.
코드10. Subject 세션
Session session = subject.getSession();
Session session = subject.getSession(boolean create); |
cs |
Servlet/JSP 프로그래밍을 해봤다면 코드10에서 두 개의 API가 HttpServletRequest 객체를 통해 세션을 얻는것과 같다고 생객했을 것입니다. 첫번째 메서드는 Subject가 가지고 있는 세션이 있으면 그것을 리턴해주고 없다면 생성하여 리턴해줍니다. 두번째 메서드는 false를 매개변수로 넘겨주는 경우 세션이 존재하지 않는 경우 세션 객체를 생성하지 않고 null을 리턴합니다.
그럼 이번에는 얻어낸 세션을 사용하는 API를 보도록 하자. 이번에도 역시 매우 익숙합니다. 우리가 사용해왔던 HttpSession API와 익숙하지 않습니까? 사실 Shiro 개발팀은 Java 개발자가 Servlet/JSP에 익숙하기 때문에 shiro의 세션 API또한 똑같이 설계 했다고 합니다. 그래도 Servlet/JSP의 세션 API와는 다르게 shiro의 세션은 웹 어플리케이션이 아니더라도 사용이 가능합니다.
코드11. 세션 메서드
Session session = subject.getSession();
session.getAttribute("key", someValue);
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime();
session.setTimeout(millis);
... |
cs |
Cryptography, 암호화
암호화는 데이터를 감추거나 난독화 하여 해석하지 못하도록 하는것입니다. Shiro에서는 자체적으로 암호화를 지원하는데, JDK에 포함되어 있는 암호화가 너무나 복잡하다고 판단했기 때문입니다. 그래서 JDK의 암호화 지원을 심플하게 사용할 수 있도록 하였습니다.
또한 Shiro의 Subject나 세션 기능을 사용하지 않는다 하더라도 암호화 기능만을 이용할 수 있습니다. 자신의 어플리케이션이 암호화 기능이 필요하다면 JDK의 암호화 모듈이 아닌 Shiro의 암호화 기능만을 빌려 사용해도 좋은 선택일 수 있습니다.
Shiro 암호화 기능은 두가지 중점이 있습니다. 암호화 해싱과 해독입니다.
해싱
JDK의 MessageDigest 클래스를 사용해보았다면 작업하기가 번거롭다는 점을 알 수 있습니다. API들이 객체지향적인 장점을 무시한채 정적 메서드와 팩토리 메서드를 많이 이용하고 있어 어색해 보이고 많은 예외를 처리해 주어야 하기 때문에 코드가 복잡해 보이기 때문입니다.
예를 들어 파일을 해싱하고 해당 16진수 값을 결정하는 MD5-hashing 하는 경우를 생각해보세요. 체크섬이라 불리우는 이 파일은 보통 파일을 전송하는 과정에서 변조되었는지를 검증하기 위해 사용됩니다. 만약 이러한 기능을 Shiro 없이 시도하는 경우 아래와 같은 절차를 거쳐야 합니다.
1. 파일을 바이트 배열로 변환하고, JDK에는 지원되지 않는 기능이므로 FileInputStream 을 통해 바이트 버퍼를 사용하면서 IOException을 던지는 Helper 메서드를 직접 구현해 주어야 합니다.
3. 해싱된 바이트 배열을 16진수 character로 인코딩합니다. JDK에는 지원되는 api가 없으므로 Helper 메서드를 구현하고 bitwise 연산과 bitshifting 연산을 해야 합니다다.
코드12. JDK의 MessageDigest 이용
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.digest(bytes);
byte[] hashed = md.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} |
cs |
이제 Shiro에서 똑같은 일을 어떤식으로 처리하는지 보도록 합니다. so simple 합다.
String hex = new Md5Hash(myFile).toHex(); |
cs |
그리고 SHA512 해시 및 BASE64 인코딩 방법입니다.
String encodedPassword = new Sha512Hash(password, salt, count).toBase64(); |
cs |
Ciphers
Ciphers는 키를 사용하여 데이터를 암호화 하는 알고리즘입니다. 우리는 중요한 데이터를 전송하거나 저장할때 노출되지 않도록 암호화를 사용합니다.
그러나 JDK의 Cryptography API를 사용해 보았다면 특히 javax.crypto.Cipher 클래스를 사용해 보았다면 얼마나 코드가 복잡한지 알 수 있을것입니다(고통..). 또한 문자열 기반으로 알고리즘 등을 지정하기에 타입 세이프하지 않으며, 여러가지 예외 처리도 해주어야 합니다. javax.crypto.Cipher는 오래 전에 Java API의 표준이었지만 시간에 지남에 따라 훨씬 쉬운 접근 방식이 필요해졌습니다.
Shiro에서는 CipherService API를 통해 암호화 개념을 단순화 하려고 합니다. CipherService는 데이터를 암호화 처리할때 대부분의 개발자가 원하는 기능을 가지고 있습니다. 하나의 메서드를 호출해 전체적으로 암호화하거나 해독할 수 있고, 상태를 가지고 있지 않기(stateless) 때문에 thread-safe 합니다.
예를 들어 코드13은 256비트 AES 암호화 하는 경우의 예제입니다.
코드13. Shiro의 암호화 API
AesCipherService cipherService = new AesCipherService();
cipherService.setKeySize(256);
//create a test key: 테스트 키 생성
byte[] testKey = cipherService.generateNewKey();
//encrypt a file’s bytes: //파일의 바이트를 암호화
byte[] encrypted = cipherService.encrypt(fileBytes, testKey); |
cs |
Shiro의 암호화 API는 JDK의 Chiper API와 다음의 차이점이 있습니다.
-
혼동을 일으키는 팩토리 메서드가 아닌 CipherService를 new로 인스턴스화 하여 사용할 수 있습니다.
-
암호화 설정 옵션은 JavaBean 규칙에 호환되는 getter, setter 메서드를 사용합니다.
-
암호화 및 해독은 단일 메서드를 호출해 실행됩니다.
-
API에서 강제로 처리해야 하는 checked 예외를 던지지 않기때문에 예외 처리가 선택적입니다. 만약 예외를 굳이 처리하고 싶습니다면 CryptoException을 처리하면 됩니다.
웹 환경 지원
마지막으로 Shiro의 웹 환경 지원에 대해 간략하게 알아보도록 합니다. Shiro는 웹 어플리케이션에서 보안을 유지할 수 있는 강력한 모듈을 함께 제공합니다. Java의 웹 어플리케이션을 설정할때 익숙한 web.xml 에 Shiro Servlet Filter를 정의해두면 됩니다. 코드14는 그 예입니다.
코드14. web.xml에 ShiroFilter 설정 정의
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>
org.apache.shiro.web.servlet.IniShiroFilter
</filter-class>
<!-- init-param을 사용하지 않는 경우 classpath:shiro.ini 로 설정파일로 로드합니다. -->
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping> |
cs |
등록해둔 ShiroFilter는 shiro.ini 설정을 읽을 수 있기 때문에 배포 환경에 관계없이 일관된 설정을 제공합니다. url-pattern을 /*로 설정했기 때문에 모든 요청에 대해 Shiro가 필터링하여 정책에 따라 보안기능(인증, 권한)을 수행할 수 있습니다.
URL-Specific 필터 체인
Shiro에서는 혁신적인 URL 필터 체인 규칙을 통해 필터 체인을 구성하도록(?말이 이상하다) 지원합니다. URL 규칙에 따른 필터규칙을 설정해 두면 규칙에 일치하는 요청이 있는 경우 설정을 참고하여 필터체인이 동작합니다. Shiro의 필터체인은 기존의 Servlet 환경에서 사용하는 web.xml을 이용하지 않고 INI같은 설정을 통해 구성할 수 있습니다. 이는 web.xml만으로 필터를 정의하는 것보다 많은 유연성을 제공합니다. 코드15는 Shiro INI 설정파일의 일부분입니다.
코드15. URL에 따른 필터체인 정책 설정
[urls]
/assets/** = anon
/user/signup = anon
/user/** = user
/rpc/rest/** = perms[rpc:invoke], authc
/** = authc |
cs |
보다시피 코드15에는 웹 어플리케이션에서 사용할 수 있는 섹션인 [urls] 섹션이 있습니다. 각 설정들은 등호(=)를 기준으로 왼쪽이 어플리케이션 컨텍스트 경로를 시작기준으로 하는 url 패턴이고 오른쪽(anon, user, authc..)은 해당 URL 규칙에 만족하는 요청이 왔을 경우에 거쳐가게 될 Shiro 필터이름입니다. 당연히 필터체인이므로 하나의 url 패턴에 대해 정책에 따라 여러개의 필터를 지정할 수 있고 ,(쉼표) 를 통해 구분할 수 있습니다.
URL패턴 = 필터이름, 필터이름, ... |
cs |
눈치 챘겠지만 anon 필터의 경우 anonymous(익명) 필터로 로그인등을 통해 인증되지 않는 사용자도 접근할 수 있는 정책입니다. 즉 http://블라블라/assets/somthing.jsp 로 접근합니다고 했을때 로그인이 되어있지 않아도 됩니다는 뜻입니다.
코드15에서 보이는 (anon, user, perms, authc)는 웹 어플리케이션에 shiro를 적용했을때 곧바로 사용할 수 있도록 기본적으로 제공되는 보안관련 필터입니다. 또한 기존에 사용중이던 Servlet Filter가 있습니다면 그것을 지정하여 사용할수도 있습니다. 이렇게 Shiro는 필터체인 정책들을 한눈에 볼 수 있기때문에 web.xml을 통한 필터 설정보다 전체적인 흐름을 쉽게 파악할 수 있고, 정책을 변경하기에도 용이하다.
JSP Tag Library
Shiro는 현재 접속한 사용자의 Subject가 가진 상태를 기반으로 하는 JSP Taglib을 제공합니다. 예를들어 로그인된 사용자면 "안녕하세요! xx님!" 을 로그인되지 않는 guest 사용자이면 로그인 페이지 링크와 함께 "로그인 해주세요" 를 출력하도록 할 수 있습니다.
코드16. JSP Taglib 예제
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
...
<p>안녕하세요!
<shiro:user>
<!-- 아래의 shiro:principal은 현재 Subject의 main principal 을 출력합니다. -->
<shiro:principal/>
</shiro:user>
님!
<shiro:guest>
<!-- 로그인되지 않은 경우 -->
! <a href="login.jsp">로그인 해주세요!</a>
</shiro:guest>
</p> |
cs |
자신이 가지고 있는 역할이나 할당된 사용권한, 인증 여부, 익명의 guest 등 상황에 따라 다르게 출력할 수도 있습니다. Shiro가 지원하는 더 많은 웹 관련 기능에 대해 알고싶은 경우 Apache Shiro 웹 문서 를 참고하도록 합니다.
웹 세션 관리
마지막으로 알아볼 것은 웹 환경에서 Shiro의 세션관리에 대한 흥미로운점입니다.
기본 HTTP세션
Shiro는 웹 어플리케이션 환경에서 동작하는 경우 Session 클래스를 사용하여 기존에 사용하던 Servlet Container(톰캣같은) 의 세션을 이용합니다. 즉 subject.getSession()이나 subject.getSession(boolean) 메서드를 호출했을 때 Shiro가 내부적으로 Servlet Container의 HttpSession 객체가 지원하는 세션 객체를 반환된다는 것입니다. 바꿔 말하면 HttpServletReqeust.getSession()을 이용하는 것이 아닌 subject.getSession()을 통해 세션을 얻기 때문에 웹 기반 HttpSession 객체로 작업합니다는 느낌이 들지 않는다. 즉 아키텍처 계층 사이에 독립적이고 깔끔한 분리를 유지할 수 있습니다는것입니다.
웹 계층에서 Shiro의 네이티브 세션
Shiro를 적용하게 되면 Shiro를 적용한 웹 어플리케이션에서 사용하는 세션은 기존에 우리가 사용하던 HttpServletRequest.getSession()을 통해 얻은 HttpSession이 아닐것입니다. 그렇다고 해서 기존에 잘 개발되어 있는 웹 어플리케이션을 Shiro를 도입합니다고 해서 수많은 HttpSession API들을 리팩토링 해야 했다면 개발자에게 엄청난 삽질(또 야근이야?)이 될것입니다.
우리는 물론 Shiro도 이런 상황을 기대하지 않기에 Shiro는 한가지 대책을 내놓았다. Shiro는 기존에 사용하던 네이티브 세션 API를 지원하도록 Servlet 사양의 HttpSession 부분을 완벽하게 구현해 놓았다. 즉 HttpServletRequest 또는 HttpSession API를 호출을 호출 할 때마다 Shiro는 이러한 호출을 내부 네이티브 Session API에 위임하도록 한것입니다.(극복~)
추가적인 기능
Apache Shiro는 아래와 같이 Java 어플리케이션을 보호하는 데 유용한 기능들을 추가적으로 제공합니다.
-
thread 간 Subject를 관리하기위한 스레딩 및 동시성 지원 (Executor 및 ExecutorService 지원)
-
특정 Subject로서의 논리를 실행하기 위한 Callable 및 Runnable 인터페이스 지원
-
다른 Subject의 식별을 가정하기 위한 "Run As" 지원 (예 : 관리 응용 프로그램에서 유용)
-
단위 및 통합 테스트에서 Shiro 코드의 전체 테스트를 매우 쉽게 수행 할 수 있도록 테스트 하네스(Test harness) 지원
Shiro 프레임워크의 한계
우리가 생각하는 것만큼이나 Apache Shiro가 만능 해결사(silver bullet) 가 되지는 못합니다. Shiro또한 당연히 해결하지 못하는 부분들이 존재할수밖에 없습니다.
-
Virtual Machine 레벨에서의 지원 - Apache Shiro가 액세스 제어 정책에 따라 클래스 로더에서 특정 클래스를로드하지 못하도록하는 기능과 같이 상당히 낮은 수준의 보안을 처리하지는 못합니다.
-
다단계 인증 - 이 기능은 향후 Shiro 버전에서 지원될 가능성이 크지만 현재까지 다단계 인증은 지원하지 않습니다.
-
Realm 쓰기 작업 - 현재 모든 Realm의 구현은 로그인 및 접근 제어를 수행하기위한 인증 및 권한 부여 데이터를 얻기위한 '읽기'작업을 지원하고 있습니다. 그러나 사용자 계정, 그룹 및 역할 만들기 또는 역할 그룹 및 사용 권한과 사용자 연결과 같은 '쓰기'작업은 지원되지 않습니다. 이러한 작업들은 개인이 개발하는 어플리케이션에 따라 요구사항이 크게 달라지기 때문입니다.
이제부터 실제로 시로 프레임워크의 API를 어떤식으로 사용하는지 기초부터 알아보도록 합니다.
'자바[Java]' 카테고리의 다른 글
[Logback] 로그백(logback) 다운로드 및 사용해보기 (0) | 2019.04.03 |
---|---|
[Logback] Logback이란? log4J의 후속작 로그백(Logback) 살펴보기 및 비교 (0) | 2019.04.02 |
[Apache Shiro] 아파치 시로란 무엇인가? JAVA 보안 프레임워크 아파치 시로 간략 개념 (0) | 2019.03.22 |
[JAVA] java에서 JSON 데이터 다루기. google의 json-simple 사용 방법 (5) | 2019.03.17 |
[JAVA] 메서드 오버라이딩(Method Overriding)시에 throws문 규칙에 대해 (1) | 2019.01.22 |