1. 개요

이 기사에서는 다재다능한 Java Security 프레임워크인 Apache Shiro 를 살펴보겠습니다 .

프레임워크는 인증, 권한 부여, 암호화 및 세션 관리를 제공하므로 사용자 정의가 가능하고 모듈식입니다.

2. 의존성

Apache Shiro에는 많은 모듈 이 있습니다. 그러나 이 사용방법(예제)에서는 shiro-core 아티팩트만 사용합니다.

pom.xml 에 추가해 보겠습니다 .

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

Apache Shiro 모듈의 최신 버전은 Maven Central에서 찾을 수 있습니다.

3. Security 관리자 설정

SecurityManager 는 Apache Shiro 프레임워크의 중심 부분입니다 . 응용 프로그램에는 일반적으로 실행 중인 단일 인스턴스가 있습니다.

이 사용방법(예제)에서는 데스크톱 환경에서 프레임워크를 살펴봅니다. 프레임워크를 구성하려면 리소스 폴더에 다음 콘텐츠가 포함 된 shiro.ini 파일 을 만들어야 합니다 .

[users]
user = password, admin
user2 = password2, editor
user3 = password3, author

[roles]
admin = *
editor = articles:*
author = articles:compose,articles:save

shiro.ini 구성 파일 의 [users] 섹션은 SecurityManager 에서 인식하는 사용자 자격 증명을 정의합니다 . 형식은 다음과 같습니다. p rincipal(사용자 이름) = 암호, 역할1, 역할2, …, 역할 .

역할 및 관련 권한은 [roles] 섹션에서 선언됩니다. 관리자 역할에는 애플리케이션의 모든 부분에 대한 권한과 액세스 권한이 부여됩니다 . 이는 와일드카드 (*) 기호로 표시됩니다.

편집자 역할 은 기사 와 관련된 모든 권한을 가지며 작성자 역할은 기사 를 작성 하고 저장할 수만 있습니다 .

SecurityManager 는 SecurityUtils 클래스 를 구성하는 데 사용됩니다 . SecurityUtils 에서 시스템과 상호 작용하는 현재 사용자를 얻고 인증 및 권한 부여 작업을 수행할 수 있습니다.

IniRealm을 사용하여 shiro.ini 파일 에서 사용자 및 역할 정의를 로드 한 다음 이를 사용하여 DefaultSecurityManager 개체 를 구성해 보겠습니다 .

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

이제 shiro.ini 파일에 정의된 사용자 자격 증명과 역할을 인식 하는 SecurityManager 가 있으므로 사용자 인증 및 권한 부여를 진행하겠습니다.

4. 인증

Apache Shiro의 용어에서 주체 는 시스템과 상호 작용하는 모든 엔터티입니다. 사람, 스크립트 또는 REST 클라이언트일 수 있습니다.

SecurityUtils.getSubject() 를 호출 하면 현재 Subject 의 인스턴스 , 즉 currentUser 가 반환 됩니다.

이제 currentUser 개체가 있으므로 제공된 자격 증명에 대한 인증을 수행할 수 있습니다.

if (!currentUser.isAuthenticated()) {               
  UsernamePasswordToken token                       
    = new UsernamePasswordToken("user", "password");
  token.setRememberMe(true);                        
  try {                                             
      currentUser.login(token);                       
  } catch (UnknownAccountException uae) {           
      log.error("Username Not Found!", uae);        
  } catch (IncorrectCredentialsException ice) {     
      log.error("Invalid Credentials!", ice);       
  } catch (LockedAccountException lae) {            
      log.error("Your Account is Locked!", lae);    
  } catch (AuthenticationException ae) {            
      log.error("Unexpected Error!", ae);           
  }                                                 
}

먼저 현재 사용자가 이미 인증되지 않았는지 확인합니다. 그런 다음 사용자의 주체 (사용자 이름) 와 자격 증명 (암호)을 사용하여 인증 토큰을 만듭니다.

다음으로 토큰으로 로그인을 시도합니다. 제공된 자격 증명이 정확하면 모든 것이 잘 진행됩니다.

다른 경우에는 다른 예외가 있습니다. 애플리케이션 요구 사항에 더 잘 맞는 사용자 정의 예외를 발생시키는 것도 가능합니다. 이는 AccountException 클래스를 서브클래싱하여 수행할 수 있습니다.

5. 승인

인증은 사용자의 신원을 확인하려고 하는 반면 권한 부여는 시스템의 특정 리소스에 대한 액세스를 제어하려고 합니다.

shiro.ini 파일 에서 생성한 각 사용자에게 하나 이상의 역할을 할당했음을 기억하십시오. 또한 역할 섹션에서 각 역할에 대해 서로 다른 권한 또는 액세스 수준을 정의합니다.

이제 응용 프로그램에서 이를 사용하여 사용자 액세스 제어를 시행하는 방법을 살펴보겠습니다.

shiro.ini 파일 에서 관리자에게 시스템의 모든 부분에 대한 전체 액세스 권한을 부여합니다.

편집자는 기사 와 관련된 모든 리소스/작업에 완전히 액세스할 수 있으며 작성자는 기사 작성 및 저장 만 할 수 있습니다.

역할에 따라 현재 사용자를 환영합니다.

if (currentUser.hasRole("admin")) {       
    log.info("Welcome Admin");              
} else if(currentUser.hasRole("editor")) {
    log.info("Welcome, Editor!");           
} else if(currentUser.hasRole("author")) {
    log.info("Welcome, Author");            
} else {                                  
    log.info("Welcome, Guest");             
}

이제 현재 사용자가 시스템에서 수행할 수 있는 작업을 살펴보겠습니다.

if(currentUser.isPermitted("articles:compose")) {            
    log.info("You can compose an article");                    
} else {                                                     
    log.info("You are not permitted to compose an article!");
}                                                            
                                                             
if(currentUser.isPermitted("articles:save")) {               
    log.info("You can save articles");                         
} else {                                                     
    log.info("You can not save articles");                   
}                                                            
                                                             
if(currentUser.isPermitted("articles:publish")) {            
    log.info("You can publish articles");                      
} else {                                                     
    log.info("You can not publish articles");                
}

6. 영역 구성

실제 애플리케이션에서는 shiro.ini 파일 이 아닌 데이터베이스에서 사용자 자격 증명을 얻는 방법이 필요 합니다. 여기에서 Realm의 개념이 작용합니다.

Apache Shiro의 용어에서 Realm 은 인증 및 권한 부여에 필요한 사용자 자격 증명 저장소를 가리키는 DAO입니다.

영역을 만들려면 Realm 인터페이스만 구현하면 됩니다. 지루할 수 있습니다. 그러나 프레임워크는 하위 클래스로 분류할 수 있는 기본 구현과 함께 제공됩니다. 이러한 구현 중 하나는 JdbcRealm 입니다.

JdbcRealm 클래스 를 확장하고 doGetAuthenticationInfo() , doGetAuthorizationInfo() , getRoleNamesForUser()getPermissions() 메서드를 재정의 하는 사용자 정의 영역 구현을 만듭니다 .

JdbcRealm 클래스 를 서브클래싱하여 영역을 생성해 보겠습니다 .

public class MyCustomRealm extends JdbcRealm {
    //...
}

단순화를 위해 java.util.Map 을 사용하여 데이터베이스를 시뮬레이트합니다.

private Map<String, String> credentials = new HashMap<>();
private Map<String, Set<String>> roles = new HashMap<>();
private Map<String, Set<String>> perm = new HashMap<>();

{
    credentials.put("user", "password");
    credentials.put("user2", "password2");
    credentials.put("user3", "password3");
                                          
    roles.put("user", new HashSet<>(Arrays.asList("admin")));
    roles.put("user2", new HashSet<>(Arrays.asList("editor")));
    roles.put("user3", new HashSet<>(Arrays.asList("author")));
                                                             
    perm.put("admin", new HashSet<>(Arrays.asList("*")));
    perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
    perm.put("author", 
      new HashSet<>(Arrays.asList("articles:compose", 
      "articles:save")));
}

계속 진행하여 doGetAuthenticationInfo() 를 재정의합니다 .

protected AuthenticationInfo 
  doGetAuthenticationInfo(AuthenticationToken token)
  throws AuthenticationException {
                                                                 
    UsernamePasswordToken uToken = (UsernamePasswordToken) token;
                                                                
    if(uToken.getUsername() == null
      || uToken.getUsername().isEmpty()
      || !credentials.containsKey(uToken.getUsername())) {
          throw new UnknownAccountException("username not found!");
    }
                                        
    return new SimpleAuthenticationInfo(
      uToken.getUsername(), 
      credentials.get(uToken.getUsername()), 
      getName()); 
}

먼저 UsernamePasswordToken 에 제공된 AuthenticationToken 을 캐스팅합니다 . uToken 에서 사용자 이름( uToken.getUsername() )을 추출하고 이를 사용하여 데이터베이스에서 사용자 자격 증명(암호)을 가져옵니다.

레코드가 발견되지 않으면 UnknownAccountException 을 발생시킵니다. 그렇지 않으면 자격 증명과 사용자 이름을 사용 하여 메서드에서 반환되는 SimpleAuthenticatioInfo 개체를 생성합니다.

사용자 자격 증명이 소금으로 해시된 경우 연결된 소금과 함께 SimpleAuthenticationInfo 를 반환해야 합니다.

return new SimpleAuthenticationInfo(
  uToken.getUsername(), 
  credentials.get(uToken.getUsername()), 
  ByteSource.Util.bytes("salt"), 
  getName()
);

또한 doGetAuthorizationInfo() 뿐만 아니라 getRoleNamesForUser()getPermissions() 도 재정의해야 합니다 .

마지막으로 사용자 정의 영역을 securityManager 에 연결해 보겠습니다 . 우리가 해야 할 일은 위의 IniRealm 을 사용자 정의 영역으로 교체하고 DefaultSecurityManager 의 생성자에 전달하는 것입니다.

Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);

코드의 다른 모든 부분은 이전과 동일합니다. 이것이 사용자 정의 영역으로 securityManager 를 적절하게 구성하는 데 필요한 전부입니다.

이제 문제는 프레임워크가 자격 증명과 어떻게 일치하는가입니다.

기본적으로 JdbcRealmSimpleCredentialsMatcher 를 사용하며 이는 AuthenticationTokenAuthenticationInfo 의 자격 증명을 비교하여 동등성을 확인하기만 합니다 .

암호를 해시하는 경우 대신 HashedCredentialsMatcher 를 사용하도록 프레임워크에 알려야 합니다 . 해시된 암호가 있는 영역에 대한 INI 구성은 여기 에서 찾을 수 있습니다 .

7. 로그아웃

이제 사용자를 인증했으므로 로그아웃을 구현할 차례입니다. 사용자 세션을 무효화하고 사용자를 로그아웃시키는 단일 메서드를 호출하면 됩니다.

currentUser.logout();

8. 세션 관리

프레임워크는 자연스럽게 세션 관리 시스템과 함께 제공됩니다. 웹 환경에서 사용하는 경우 기본적으로 HttpSession 구현입니다.

독립 실행형 애플리케이션의 경우 엔터프라이즈 세션 관리 시스템을 사용합니다. 데스크톱 환경에서도 일반적인 웹 환경에서와 마찬가지로 세션 개체를 사용할 수 있다는 이점이 있습니다.

간단한 예를 살펴보고 현재 사용자의 세션과 상호 작용해 보겠습니다.

Session session = currentUser.getSession();                
session.setAttribute("key", "value");                      
String value = (String) session.getAttribute("key");       
if (value.equals("value")) {                               
    log.info("Retrieved the correct value! [" + value + "]");
}

9. Spring을 사용한 웹 애플리케이션용 Shiro

지금까지 Apache Shiro의 기본 구조를 개략적으로 설명하고 데스크톱 환경에서 구현했습니다. 프레임워크를 Spring Boot 애플리케이션에 통합하여 진행해 보겠습니다.

여기서 주요 초점은 Spring 애플리케이션이 아니라 Shiro입니다. 간단한 예제 앱을 구동하는 데만 사용할 것입니다.

9.1. 의존성

먼저 pom.xml 에 Spring Boot 상위 의존성을 추가해야 합니다 .

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
</parent>

다음으로 동일한 pom.xml 파일 에 다음 의존성을 추가해야 합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${apache-shiro-core-version}</version>
</dependency>

9.2. 구성

pom.xml 에 shiro-spring-boot-web-starter 의존성을 추가하면 기본적으로 SecurityManager 와 같은 Apache Shiro 애플리케이션의 일부 기능이 구성됩니다 .

그러나 여전히 Realm 및 Shiro Security 필터를 구성해야 합니다. 위에서 정의한 것과 동일한 사용자 지정 영역을 사용합니다.

따라서 Spring Boot 애플리케이션이 실행되는 메인 클래스에 다음과 같은 Bean 정의를 추가해 보겠습니다.

@Bean
public Realm realm() {
    return new MyCustomRealm();
}
    
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter
      = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/secure", "authc");
    filter.addPathDefinition("/**", "anon");

    return filter;
}

ShiroFilterChainDefinition 에서 /secure 경로 에 authc 필터를 적용 하고 Ant 패턴을 사용하여 다른 경로에 anon 필터를 적용했습니다.

authc anon 필터는 웹 응용 프로그램에 대해 기본적으로 함께 제공됩니다 . 다른 기본 필터는 여기 에서 찾을 수 있습니다 .

Realm 빈을 정의하지 않은 경우 ShiroAutoConfiguration 은 기본적으로 src/main/resources 또는 src/main/resources/META-INF 에서 shiro.ini 파일 을 찾을 것으로 예상하는 IniRealm 구현을 제공합니다.

ShiroFilterChainDefinition 빈을 정의하지 않으면 프레임워크는 모든 경로를 보호하고 로그인 URL을 login.jsp 로 설정합니다 .

application.properties 에 다음 항목을 추가하여 이 기본 로그인 URL 및 기타 기본값을 변경할 수 있습니다 .

shiro.loginUrl = /login
shiro.successUrl = /secure
shiro.unauthorizedUrl = /login

authc 필터가 /secure 에 적용 되었으므로 해당 경로에 대한 모든 요청에는 양식 인증이 필요합니다.

9.3. 인증 및 승인

/index , /login, /logout/secure 경로 매핑을 사용하여 ShiroSpringController 를 생성해 보겠습니다 .

login() 메서드는 위에서 설명한 대로 실제 사용자 인증을 구현하는 곳입니다 . 인증에 성공하면 사용자는 Security 페이지로 리디렉션됩니다.

Subject subject = SecurityUtils.getSubject();

if(!subject.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(
      cred.getUsername(), cred.getPassword(), cred.isRememberMe());
    try {
        subject.login(token);
    } catch (AuthenticationException ae) {
        ae.printStackTrace();
        attr.addFlashAttribute("error", "Invalid Credentials");
        return "redirect:/login";
    }
}

return "redirect:/secure";

이제 secure() 구현 에서 SecurityUtils.getSubject() 를 호출하여 currentUser 를 얻었습니다 . 사용자의 역할 및 권한은 Security 페이지와 함께 사용자의 Security 페이지로 전달됩니다.

Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";

if(currentUser.hasRole("admin")) {
    role = role  + "You are an Admin";
} else if(currentUser.hasRole("editor")) {
    role = role + "You are an Editor";
} else if(currentUser.hasRole("author")) {
    role = role + "You are an Author";
}

if(currentUser.isPermitted("articles:compose")) {
    permission = permission + "You can compose an article, ";
} else {
    permission = permission + "You are not permitted to compose an article!, ";
}

if(currentUser.isPermitted("articles:save")) {
    permission = permission + "You can save articles, ";
} else {
    permission = permission + "\nYou can not save articles, ";
}

if(currentUser.isPermitted("articles:publish")) {
    permission = permission  + "\nYou can publish articles";
} else {
    permission = permission + "\nYou can not publish articles";
}

modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);

return "secure";

그리고 끝났습니다. 이것이 Apache Shiro를 Spring Boot 애플리케이션에 통합하는 방법입니다.

또한 프레임워크는 애플리케이션을 보호하기 위해 필터 체인 정의와 함께 사용할 수 있는 추가 어노테이션 을 제공합니다.

10. JEE 통합

Apache Shiro를 JEE 애플리케이션에 통합하는 것은 web.xml 파일을 구성하기만 하면 됩니다. 평소와 같이 구성은 shiro.ini 가 클래스 경로에 있을 것으로 예상합니다. 자세한 예제 구성은 여기에서 볼 수 있습니다 . 또한 JSP 태그는 여기 에서 찾을 수 있습니다 .

11. 결론

이 사용방법(예제)에서는 Apache Shiro의 인증 및 권한 부여 메커니즘을 살펴보았습니다. 또한 사용자 정의 영역을 정의하고 이를 SecurityManager 에 연결하는 방법에 중점을 두었습니다 .

항상 그렇듯이 전체 소스 코드는 GitHub에서 사용할 수 있습니다 .

Security footer banner