1. 概要

セキュリティは、アプリケーション開発の世界、特にエンタープライズWebおよびモバイルアプリケーションの分野における主要な関心事です。

このクイックチュートリアルでは、 2つの一般的なJavaセキュリティフレームワーク、ApacheShiroとSpringSecurityを比較します。

2. 少し背景

Apache Shiroは2004年にJSecurityとして生まれ、2008年にApacheFoundationに受け入れられました。 これまでに多くのリリースがあり、これを書いている時点での最新のものは1.5.3です。

Spring Securityは2003年にAcegiとして開始され、2008年に最初の公開リリースでSpringFrameworkに組み込まれました。 開始以来、それはいくつかの反復を経ており、これを書いている時点での現在のGAバージョンは5.3.2です。

どちらのテクノロジーも、暗号化およびセッション管理ソリューションとともに、認証および承認サポートを提供します。 さらに、Spring Securityは、CSRFやセッション固定などの攻撃に対する一流の保護を提供します。

次のいくつかのセクションでは、2つのテクノロジーが認証と承認を処理する方法の例を示します。 簡単にするために、FreeMarkerテンプレートで基本的なSpring BootベースのMVCアプリケーションを使用します。

3. ApacheShiroの構成

まず、2つのフレームワーク間で構成がどのように異なるかを見てみましょう。

3.1. Mavenの依存関係

Spring BootアプリでShiroを使用するため、スターターとshiro-coreモジュールが必要です。

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.3</version>
</dependency>

最新バージョンは、 MavenCentralにあります。

3.2. レルムの作成

ユーザーの役割と権限をメモリ内で宣言するには、ShiroのJdbcRealmを拡張するレルムを作成する必要があります。 トムとジェリーの2人のユーザーを定義し、それぞれUSERとADMINの役割を果たします。

public class CustomRealm extends JdbcRealm {

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

    {
        credentials.put("Tom", "password");
        credentials.put("Jerry", "password");

        roles.put("Jerry", new HashSet<>(Arrays.asList("ADMIN")));
        roles.put("Tom", new HashSet<>(Arrays.asList("USER")));

        permissions.put("ADMIN", new HashSet<>(Arrays.asList("READ", "WRITE")));
        permissions.put("USER", new HashSet<>(Arrays.asList("READ")));
    }
}

次に、この認証と承認の取得を有効にするには、いくつかのメソッドをオーバーライドする必要があります。

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
  throws AuthenticationException {
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;

    if (userToken.getUsername() == null || userToken.getUsername().isEmpty() ||
      !credentials.containsKey(userToken.getUsername())) {
        throw new UnknownAccountException("User doesn't exist");
    }
    return new SimpleAuthenticationInfo(userToken.getUsername(), 
      credentials.get(userToken.getUsername()), getName());
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    Set roles = new HashSet<>();
    Set permissions = new HashSet<>();

    for (Object user : principals) {
        try {
            roles.addAll(getRoleNamesForUser(null, (String) user));
            permissions.addAll(getPermissions(null, null, roles));
        } catch (SQLException e) {
            logger.error(e.getMessage());
        }
    }
    SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(roles);
    authInfo.setStringPermissions(permissions);
    return authInfo;
}

メソッドdoGetAuthorizationInfoは、いくつかのヘルパーメソッドを使用して、ユーザーの役割と権限を取得しています。

@Override
protected Set getRoleNamesForUser(Connection conn, String username) 
  throws SQLException {
    if (!roles.containsKey(username)) {
        throw new SQLException("User doesn't exist");
    }
    return roles.get(username);
}

@Override
protected Set getPermissions(Connection conn, String username, Collection roles) 
  throws SQLException {
    Set userPermissions = new HashSet<>();
    for (String role : roles) {
        if (!permissions.containsKey(role)) {
            throw new SQLException("Role doesn't exist");
        }
        userPermissions.addAll(permissions.get(role));
    }
    return userPermissions;
}

次に、このCustomRealmをBeanとしてブートアプリケーションに含める必要があります。

@Bean
public Realm customRealm() {
    return new CustomRealm();
}

さらに、エンドポイントの認証を構成するには、別のBeanが必要です。

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/home", "authc");
    filter.addPathDefinition("/**", "anon");
    return filter;
}

ここでは、 DefaultShiroFilterChainDefinition インスタンスを使用して、 /homeエンドポイントにアクセスできるのは認証されたユーザーのみであることを指定しました。

構成に必要なのはこれだけです。残りはShiroが行います。

4. SpringSecurityの構成

それでは、Springで同じことを実現する方法を見てみましょう。

4.1. Mavenの依存関係

まず、依存関係:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

最新バージョンは、 MavenCentralにあります。

4.2. 構成クラス

次に、Springセキュリティ構成をクラス SecurityConfig で定義し、WebSecurityConfigurerAdapterを拡張します。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeRequests(authorize -> authorize
            .antMatchers("/index", "/login").permitAll()
            .antMatchers("/home", "/logout").authenticated()
            .antMatchers("/admin/**").hasRole("ADMIN"))
          .formLogin(formLogin -> formLogin
            .loginPage("/login")
            .failureUrl("/login-error"));
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
          .withUser("Jerry")
            .password(passwordEncoder().encode("password"))
            .authorities("READ", "WRITE")
            .roles("ADMIN")
            .and()
          .withUser("Tom")
            .password(passwordEncoder().encode("password"))
            .authorities("READ")
            .roles("USER");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

ご覧のとおり、 AuthenticationManagerBuilder オブジェクトを作成して、ユーザーに役割と権限を宣言しました。 さらに、BCryptPasswordEncoderを使用してパスワードをエンコードしました。

Spring Securityは、さらに構成するためのHttpSecurityオブジェクトも提供します。 この例では、次のことを許可しています。

  • インデックスおよびログインページにアクセスするすべての人
  • ホームページとログアウトに入る認証済みユーザーのみ
  • adminページにアクセスするためのADMINロールを持つユーザーのみ

また、ユーザーをloginエンドポイントに送信するためのフォームベースの認証のサポートも定義しました。 ログインに失敗した場合、ユーザーは /login-errorにリダイレクトされます。

5. コントローラとエンドポイント

次に、2つのアプリケーションのWebコントローラーマッピングを見てみましょう。 それらは同じエンドポイントを使用しますが、一部の実装は異なります。

5.1. ビューレンダリングのエンドポイント

ビューをレンダリングするエンドポイントの場合、実装は同じです。

@GetMapping("/")
public String index() {
    return "index";
}

@GetMapping("/login")
public String showLoginPage() {
    return "login";
}

@GetMapping("/home")
public String getMeHome(Model model) {
    addUserAttributes(model);
    return "home";
}

コントローラの実装であるShiroとSpringSecurityはどちらも、ルートエンドポイントで index.ftl 、ログインエンドポイントで login.ftl home.ftl[ X181X]ホームエンドポイント。

ただし、 /homeエンドポイントでのメソッドaddUserAttributesの定義は、2つのコントローラー間で異なります。 このメソッドは、現在ログインしているユーザーの属性をイントロスペクトします。

Shiroは、現在の Subject とその役割および権限を取得するために、 SecurityUtils#getSubjectを提供しています。

private void addUserAttributes(Model model) {
    Subject currentUser = SecurityUtils.getSubject();
    String permission = "";

    if (currentUser.hasRole("ADMIN")) {
        model.addAttribute("role", "ADMIN");
    } else if (currentUser.hasRole("USER")) {
        model.addAttribute("role", "USER");
    }
    if (currentUser.isPermitted("READ")) {
        permission = permission + " READ";
    }
    if (currentUser.isPermitted("WRITE")) {
        permission = permission + " WRITE";
    }
    model.addAttribute("username", currentUser.getPrincipal());
    model.addAttribute("permission", permission);
}

一方、Spring Securityは、この目的のために、SecurityContextHolderのコンテキストからAuthenticationオブジェクトを提供します。

private void addUserAttributes(Model model) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null && !auth.getClass().equals(AnonymousAuthenticationToken.class)) {
        User user = (User) auth.getPrincipal();
        model.addAttribute("username", user.getUsername());
        Collection<GrantedAuthority> authorities = user.getAuthorities();

        for (GrantedAuthority authority : authorities) {
            if (authority.getAuthority().contains("USER")) {
                model.addAttribute("role", "USER");
                model.addAttribute("permissions", "READ");
            } else if (authority.getAuthority().contains("ADMIN")) {
                model.addAttribute("role", "ADMIN");
                model.addAttribute("permissions", "READ WRITE");
            }
        }
    }
}

5.2. POSTログインエンドポイント

Shiroでは、ユーザーが入力した資格情報をPOJOにマップします。

public class UserCredentials {

    private String username;
    private String password;

    // getters and setters
}

次に、 UsernamePasswordToken を作成して、ユーザーまたはSubjectを次の場所に記録します。

@PostMapping("/login")
public String doLogin(HttpServletRequest req, UserCredentials credentials, RedirectAttributes attr) {

    Subject subject = SecurityUtils.getSubject();
    if (!subject.isAuthenticated()) {
        UsernamePasswordToken token = new UsernamePasswordToken(credentials.getUsername(),
          credentials.getPassword());
        try {
            subject.login(token);
        } catch (AuthenticationException ae) {
            logger.error(ae.getMessage());
            attr.addFlashAttribute("error", "Invalid Credentials");
            return "redirect:/login";
        }
    }
    return "redirect:/home";
}

Spring Security側では、これはホームページへのリダイレクトの問題です。 Springのログインプロセスは、UsernamePasswordAuthenticationFilterによって処理され、私たちには透過的です

@PostMapping("/login")
public String doLogin(HttpServletRequest req) {
    return "redirect:/home";
}

5.3. 管理者専用エンドポイント

次に、役割ベースのアクセスを実行する必要があるシナリオを見てみましょう。 / admin エンドポイントがあり、そのエンドポイントへのアクセスはADMINロールに対してのみ許可されているとします。

Shiroでこれを行う方法を見てみましょう:

@GetMapping("/admin")
public String adminOnly(ModelMap modelMap) {
    addUserAttributes(modelMap);
    Subject currentUser = SecurityUtils.getSubject();
    if (currentUser.hasRole("ADMIN")) {
        modelMap.addAttribute("adminContent", "only admin can view this");
    }
    return "home";
}

ここでは、現在ログインしているユーザーを抽出し、それらがADMINロールを持っているかどうかを確認し、それに応じてコンテンツを追加しました。

Springセキュリティでは、プログラムで役割を確認する必要はありません。 SecurityConfig で、このエンドポイントに到達できるユーザーをすでに定義しています。 だから今、それはビジネスロジックを追加するだけの問題です:

@GetMapping("/admin")
public String adminOnly(HttpServletRequest req, Model model) {
    addUserAttributes(model);
    model.addAttribute("adminContent", "only admin can view this");
    return "home";
}

5.4. ログアウトエンドポイント

最後に、ログアウトエンドポイントを実装しましょう。

Shiroでは、単に Subject#logoutと呼びます。

@PostMapping("/logout")
public String logout() {
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "redirect:/";
}

Springの場合、ログアウトのマッピングは定義されていません。 この場合、デフォルトのログアウトメカニズムが作動します。これは、構成でWebSecurityConfigurerAdapterを拡張したため自動的に適用されます。

6. ApacheShiroとSpringSecurity

実装の違いを見てきたので、他のいくつかの側面を見てみましょう。

コミュニティサポートに関しては、 Spring Frameworkには一般に、開発者の巨大なコミュニティがあり、その開発と使用に積極的に関わっています。 Spring Securityは傘の一部であるため、同じ利点を享受する必要があります。 シロは人気がありますが、それほど大きな支持はありません。

ドキュメントに関しては、Springが再び勝者です。

ただし、Springセキュリティに関連する学習曲線が少しあります。 一方、シロはわかりやすい。 デスクトップアプリケーションの場合、shiro.iniを介した構成がはるかに簡単です。

しかし、繰り返しになりますが、スニペットの例で見たように、 Spring Securityは、ビジネスロジックとセキュリティ 個別を維持する優れた機能を果たし、横断的関心事としてセキュリティを提供します。

7. 結論

このチュートリアルでは、ApacheShiroとSpringSecurityを比較しました。

これらのフレームワークが提供するものの表面を把握したばかりであり、さらに調査することがたくさんあります。 JAASOACCなど、かなりの数の選択肢があります。 それでも、その利点により、 SpringSecurityが現時点で勝っているようです。

いつものように、ソースコードはGitHubから入手できます。