SpringSecurityとApacheShiro
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エンドポイントでのメソッド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を比較しました。
これらのフレームワークが提供するものの表面を把握したばかりであり、さらに調査することがたくさんあります。 JAASやOACCなど、かなりの数の選択肢があります。 それでも、その利点により、 SpringSecurityが現時点で勝っているようです。
いつものように、ソースコードはGitHubでから入手できます。