Apache Shiro

Javaセキュリティフレームワーク認証認可セッション管理暗号化Enterprise

ライブラリ

Apache Shiro

概要

Apache Shiroは、Javaアプリケーション向けの強力で使いやすいセキュリティフレームワークです。2025年現在でも、その設計思想である「シンプルさ」と「理解しやすさ」により、多くの企業とプロジェクトで選ばれ続けています。認証、認可、暗号化、セッション管理という「アプリケーションセキュリティの4つの基石」を統合的に提供し、複雑なセキュリティ要件を直感的なAPIで実装できます。SpringSecurityと比較して学習コストが低く、軽量でありながら、モバイルアプリからエンタープライズアプリケーションまで幅広く対応可能です。POJOベースの設計により、あらゆるJavaコンテナで即座に使用でき、フレームワークに依存しない柔軟性を提供します。

詳細

Apache Shiro 2025は、Java生態系の進化に合わせて最新のセキュリティベストプラクティスを実装しています。認証では複数プロバイダー対応(LDAP、データベース、ファイル、カスタム)、認可では細粒度の権限制御とロールベースアクセス制御(RBAC)、セッション管理ではクラスタリング対応とカスタムセッションストレージ、暗号化では業界標準アルゴリズムと鍵管理を標準サポートしています。また、Web環境ではServletフィルター統合、非Web環境でもスタンドアロン実行が可能で、マイクロサービス、ウェブアプリケーション、デスクトップアプリケーション、IoTシステムなど多様な環境で活用されています。設定ベースとアノテーションベースの両方のアプローチをサポートし、開発者の好みに応じた実装が可能です。

主な特徴

  • 4つの基石統合: 認証・認可・セッション管理・暗号化の完全統合ソリューション
  • シンプルなAPI: 直感的で理解しやすいプログラミングインターフェース
  • 柔軟なアーキテクチャ: POJOベースでコンテナ非依存の設計
  • マルチ環境対応: Web、デスクトップ、モバイル、IoTまで対応
  • 豊富な認証プロバイダー: LDAP、データベース、外部システム連携
  • 高度なセッション管理: クラスタリング、分散セッション、カスタムストレージ

メリット・デメリット

メリット

  • シンプルで直感的なAPIにより学習コストが低く、迅速な開発が可能
  • POJOベースの設計でフレームワーク依存性がなく、既存システムに容易に統合
  • 軽量でパフォーマンスが高く、リソース使用量を最小限に抑制
  • 豊富なドキュメントと活発なコミュニティによる充実したサポート
  • Web、デスクトップ、モバイルなど多様な環境での実績と安定性
  • 設定ベースとアノテーションベースの両方をサポートし、柔軟な実装が可能

デメリット

  • SpringSecurityと比較して、最新のOAuth2やOpenID Connect対応が限定的
  • 大規模エンタープライズ環境での複雑な要件には追加実装が必要
  • 他のApacheプロジェクトと比較して開発ペースがやや緩やか
  • 高度なセキュリティ機能(多要素認証、アダプティブ認証)は別途実装必要
  • 最新のマイクロサービスアーキテクチャに特化した機能が不足
  • Java以外の言語・プラットフォームでは使用不可

参考ページ

書き方の例

基本的なセットアップ

<!-- Maven依存関係 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.13.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.13.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.13.0</version>
</dependency>
# shiro.ini - 基本設定ファイル
[main]
# ユーザー定義(実際にはデータベースやLDAPから取得)
myRealm = com.example.MyRealm

# セッション管理設定
sessionManager = org.apache.shiro.session.mgt.DefaultSessionManager
sessionManager.sessionIdCookie.httpOnly = true
sessionManager.sessionIdCookie.secure = true
sessionManager.globalSessionTimeout = 1800000

# 暗号化設定
credentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName = SHA-256
credentialsMatcher.hashIterations = 100000
credentialsMatcher.storedCredentialsHexEncoded = true

myRealm.credentialsMatcher = $credentialsMatcher

securityManager.realms = $myRealm
securityManager.sessionManager = $sessionManager

[urls]
# URL別アクセス制御
/login.html = anon
/logout = logout
/admin/** = authc, roles[admin]
/user/** = authc, roles[user]
/** = authc

カスタムRealmの実装

// MyRealm.java - カスタム認証・認可の実装
package com.example;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Set;

public class MyRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(MyRealm.class);
    
    private UserService userService;
    
    public MyRealm() {
        setName("MyRealm");
        setCredentialsMatcher(new HashedCredentialsMatcher("SHA-256"));
    }
    
    // 認証処理
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }
        
        // データベースからユーザー情報を取得
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException("No account found for user [" + username + "]");
        }
        
        // アカウント状態チェック
        if (user.isLocked()) {
            throw new LockedAccountException("Account [" + username + "] is locked.");
        }
        
        if (!user.isActive()) {
            throw new DisabledAccountException("Account [" + username + "] is disabled.");
        }
        
        // パスワードハッシュ検証用の情報を返す
        return new SimpleAuthenticationInfo(
            user.getUsername(),
            user.getPasswordHash(),
            ByteSource.Util.bytes(user.getSalt()),
            getName()
        );
    }
    
    // 認可処理
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) getAvailablePrincipal(principals);
        
        User user = userService.findByUsername(username);
        if (user == null) {
            return null;
        }
        
        SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
        
        // ロール設定
        Set<String> roles = userService.getUserRoles(username);
        authzInfo.setRoles(roles);
        
        // 権限設定
        Set<String> permissions = userService.getUserPermissions(username);
        authzInfo.setStringPermissions(permissions);
        
        logger.debug("User [{}] has roles: {} and permissions: {}", 
                    username, roles, permissions);
        
        return authzInfo;
    }
    
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

// UserService.java - ユーザー管理サービス
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private RoleRepository roleRepository;
    
    public User findByUsername(String username) {
        return userRepository.findByUsername(username);
    }
    
    public Set<String> getUserRoles(String username) {
        User user = findByUsername(username);
        return user.getRoles().stream()
                  .map(Role::getName)
                  .collect(Collectors.toSet());
    }
    
    public Set<String> getUserPermissions(String username) {
        User user = findByUsername(username);
        return user.getRoles().stream()
                  .flatMap(role -> role.getPermissions().stream())
                  .map(Permission::getName)
                  .collect(Collectors.toSet());
    }
    
    public boolean createUser(String username, String password, Set<String> roles) {
        if (userRepository.existsByUsername(username)) {
            return false;
        }
        
        // パスワードのハッシュ化
        String salt = generateSalt();
        String hashedPassword = hashPassword(password, salt);
        
        User user = new User();
        user.setUsername(username);
        user.setPasswordHash(hashedPassword);
        user.setSalt(salt);
        user.setActive(true);
        user.setCreatedDate(new Date());
        
        // ロール設定
        Set<Role> userRoles = roles.stream()
                                  .map(roleRepository::findByName)
                                  .filter(Objects::nonNull)
                                  .collect(Collectors.toSet());
        user.setRoles(userRoles);
        
        userRepository.save(user);
        return true;
    }
    
    private String generateSalt() {
        return new SecureRandomNumberGenerator().nextBytes().toHex();
    }
    
    private String hashPassword(String password, String salt) {
        return new Sha256Hash(password, salt, 100000).toHex();
    }
}

Webアプリケーションでの実装

// ShiroConfig.java - Spring Boot設定
@Configuration
public class ShiroConfig {
    
    @Bean
    public MyRealm myRealm() {
        MyRealm realm = new MyRealm();
        realm.setUserService(userService());
        
        // 認証情報マッチャー設定
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("SHA-256");
        matcher.setHashIterations(100000);
        matcher.setStoredCredentialsHexEncoded(true);
        realm.setCredentialsMatcher(matcher);
        
        // キャッシュ設定
        realm.setCachingEnabled(true);
        realm.setAuthenticationCachingEnabled(true);
        realm.setAuthorizationCachingEnabled(true);
        
        return realm;
    }
    
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm());
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }
    
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        
        // セッション設定
        sessionManager.setGlobalSessionTimeout(30 * 60 * 1000); // 30分
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionValidationInterval(10 * 60 * 1000); // 10分間隔
        
        // Cookie設定
        SimpleCookie sessionIdCookie = new SimpleCookie("JSESSIONID");
        sessionIdCookie.setHttpOnly(true);
        sessionIdCookie.setSecure(true); // HTTPS環境でのみ
        sessionIdCookie.setMaxAge(-1); // ブラウザ終了時まで
        sessionManager.setSessionIdCookie(sessionIdCookie);
        
        return sessionManager;
    }
    
    @Bean
    public MemoryConstrainedCacheManager cacheManager() {
        return new MemoryConstrainedCacheManager();
    }
    
    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager());
        
        // ログインページ設定
        shiroFilter.setLoginUrl("/login");
        shiroFilter.setSuccessUrl("/dashboard");
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        
        // URL別アクセス制御
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/register", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/api/public/**", "anon");
        
        filterChainDefinitionMap.put("/admin/**", "authc, roles[admin]");
        filterChainDefinitionMap.put("/api/admin/**", "authc, roles[admin]");
        filterChainDefinitionMap.put("/user/**", "authc, roles[user,admin]");
        filterChainDefinitionMap.put("/api/user/**", "authc, roles[user,admin]");
        
        filterChainDefinitionMap.put("/**", "authc");
        
        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilter;
    }
    
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager());
        return advisor;
    }
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

// LoginController.java - 認証コントローラー
@RestController
@RequestMapping("/auth")
public class LoginController {
    
    @PostMapping("/login")
    public ResponseEntity<Map<String, Object>> login(
            @RequestBody LoginRequest loginRequest,
            HttpServletRequest request) {
        
        Subject currentUser = SecurityUtils.getSubject();
        Map<String, Object> response = new HashMap<>();
        
        if (currentUser.isAuthenticated()) {
            response.put("success", true);
            response.put("message", "Already logged in");
            response.put("user", getCurrentUserInfo(currentUser));
            return ResponseEntity.ok(response);
        }
        
        UsernamePasswordToken token = new UsernamePasswordToken(
            loginRequest.getUsername(), 
            loginRequest.getPassword()
        );
        token.setRememberMe(loginRequest.isRememberMe());
        
        try {
            currentUser.login(token);
            
            // ログイン成功後の処理
            response.put("success", true);
            response.put("message", "Login successful");
            response.put("user", getCurrentUserInfo(currentUser));
            response.put("sessionId", currentUser.getSession().getId());
            
            // ログイン履歴記録
            logLoginActivity(loginRequest.getUsername(), request.getRemoteAddr());
            
            return ResponseEntity.ok(response);
            
        } catch (UnknownAccountException uae) {
            response.put("success", false);
            response.put("message", "Invalid username or password");
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
            
        } catch (IncorrectCredentialsException ice) {
            response.put("success", false);
            response.put("message", "Invalid username or password");
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
            
        } catch (LockedAccountException lae) {
            response.put("success", false);
            response.put("message", "Account is locked");
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
            
        } catch (DisabledAccountException dae) {
            response.put("success", false);
            response.put("message", "Account is disabled");
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
            
        } catch (ExcessiveAttemptsException eae) {
            response.put("success", false);
            response.put("message", "Too many failed attempts");
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(response);
            
        } catch (AuthenticationException ae) {
            response.put("success", false);
            response.put("message", "Authentication failed");
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
        }
    }
    
    @PostMapping("/logout")
    public ResponseEntity<Map<String, Object>> logout() {
        Subject currentUser = SecurityUtils.getSubject();
        Map<String, Object> response = new HashMap<>();
        
        try {
            currentUser.logout();
            response.put("success", true);
            response.put("message", "Logout successful");
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            response.put("success", false);
            response.put("message", "Logout failed");
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }
    
    @GetMapping("/current")
    public ResponseEntity<Map<String, Object>> getCurrentUser() {
        Subject currentUser = SecurityUtils.getSubject();
        Map<String, Object> response = new HashMap<>();
        
        if (currentUser.isAuthenticated()) {
            response.put("authenticated", true);
            response.put("user", getCurrentUserInfo(currentUser));
            return ResponseEntity.ok(response);
        } else {
            response.put("authenticated", false);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
        }
    }
    
    private Map<String, Object> getCurrentUserInfo(Subject subject) {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("username", subject.getPrincipal());
        userInfo.put("roles", subject.hasRole("admin") ? Arrays.asList("admin") : Arrays.asList("user"));
        userInfo.put("sessionId", subject.getSession().getId());
        userInfo.put("sessionTimeout", subject.getSession().getTimeout());
        return userInfo;
    }
    
    private void logLoginActivity(String username, String ipAddress) {
        // ログイン履歴をデータベースに記録
        // 実装は省略
    }
}

アノテーションベースの認可制御

// UserController.java - アノテーション認可の例
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // 認証が必要
    @RequiresAuthentication
    @GetMapping("/profile")
    public ResponseEntity<User> getProfile() {
        Subject currentUser = SecurityUtils.getSubject();
        String username = (String) currentUser.getPrincipal();
        User user = userService.findByUsername(username);
        return ResponseEntity.ok(user);
    }
    
    // ユーザーロールが必要
    @RequiresRoles("user")
    @PutMapping("/profile")
    public ResponseEntity<Map<String, Object>> updateProfile(@RequestBody UserUpdateRequest request) {
        Subject currentUser = SecurityUtils.getSubject();
        String username = (String) currentUser.getPrincipal();
        
        boolean updated = userService.updateProfile(username, request);
        Map<String, Object> response = new HashMap<>();
        response.put("success", updated);
        
        return ResponseEntity.ok(response);
    }
    
    // 管理者ロールが必要
    @RequiresRoles("admin")
    @GetMapping("/all")
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.findAllUsers();
        return ResponseEntity.ok(users);
    }
    
    // 特定の権限が必要
    @RequiresPermissions("user:create")
    @PostMapping
    public ResponseEntity<Map<String, Object>> createUser(@RequestBody CreateUserRequest request) {
        boolean created = userService.createUser(
            request.getUsername(), 
            request.getPassword(), 
            request.getRoles()
        );
        
        Map<String, Object> response = new HashMap<>();
        response.put("success", created);
        
        return created ? 
            ResponseEntity.ok(response) : 
            ResponseEntity.badRequest().body(response);
    }
    
    // 複数権限の組み合わせ
    @RequiresPermissions({"user:read", "user:update"})
    @PutMapping("/{userId}")
    public ResponseEntity<Map<String, Object>> updateUser(
            @PathVariable Long userId, 
            @RequestBody UserUpdateRequest request) {
        
        boolean updated = userService.updateUser(userId, request);
        Map<String, Object> response = new HashMap<>();
        response.put("success", updated);
        
        return ResponseEntity.ok(response);
    }
    
    // 削除権限(危険な操作)
    @RequiresPermissions("user:delete")
    @DeleteMapping("/{userId}")
    public ResponseEntity<Map<String, Object>> deleteUser(@PathVariable Long userId) {
        boolean deleted = userService.deleteUser(userId);
        Map<String, Object> response = new HashMap<>();
        response.put("success", deleted);
        
        return ResponseEntity.ok(response);
    }
}

// AdminController.java - 管理者専用機能
@RestController
@RequestMapping("/api/admin")
@RequiresRoles("admin") // クラスレベルでの認可制御
public class AdminController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private AuditService auditService;
    
    @GetMapping("/dashboard")
    public ResponseEntity<Map<String, Object>> getDashboard() {
        Map<String, Object> dashboard = new HashMap<>();
        dashboard.put("totalUsers", userService.getTotalUserCount());
        dashboard.put("activeUsers", userService.getActiveUserCount());
        dashboard.put("recentLogins", auditService.getRecentLogins(10));
        dashboard.put("systemHealth", getSystemHealth());
        
        return ResponseEntity.ok(dashboard);
    }
    
    @RequiresPermissions("admin:system:read")
    @GetMapping("/system/info")
    public ResponseEntity<Map<String, Object>> getSystemInfo() {
        Map<String, Object> systemInfo = new HashMap<>();
        systemInfo.put("shiroVersion", getShiroVersion());
        systemInfo.put("activeSessions", getActiveSessionCount());
        systemInfo.put("cacheStatistics", getCacheStatistics());
        
        return ResponseEntity.ok(systemInfo);
    }
    
    @RequiresPermissions("admin:user:lock")
    @PostMapping("/users/{userId}/lock")
    public ResponseEntity<Map<String, Object>> lockUser(@PathVariable Long userId) {
        boolean locked = userService.lockUser(userId);
        Map<String, Object> response = new HashMap<>();
        response.put("success", locked);
        
        // 監査ログ記録
        Subject currentUser = SecurityUtils.getSubject();
        auditService.logAdminAction(
            (String) currentUser.getPrincipal(), 
            "LOCK_USER", 
            "User ID: " + userId
        );
        
        return ResponseEntity.ok(response);
    }
    
    private Map<String, Object> getSystemHealth() {
        // システムヘルス情報の実装
        return new HashMap<>();
    }
    
    private String getShiroVersion() {
        return "1.13.0"; // 実際のバージョン取得ロジック
    }
    
    private int getActiveSessionCount() {
        // アクティブセッション数の取得
        return 0;
    }
    
    private Map<String, Object> getCacheStatistics() {
        // キャッシュ統計情報の実装
        return new HashMap<>();
    }
}

セッション管理とセキュリティ強化

// SessionController.java - セッション管理
@RestController
@RequestMapping("/api/session")
@RequiresAuthentication
public class SessionController {
    
    @GetMapping("/info")
    public ResponseEntity<Map<String, Object>> getSessionInfo() {
        Subject currentUser = SecurityUtils.getSubject();
        Session session = currentUser.getSession();
        
        Map<String, Object> sessionInfo = new HashMap<>();
        sessionInfo.put("sessionId", session.getId());
        sessionInfo.put("startTimestamp", session.getStartTimestamp());
        sessionInfo.put("lastAccessTime", session.getLastAccessTime());
        sessionInfo.put("timeout", session.getTimeout());
        sessionInfo.put("host", session.getHost());
        sessionInfo.put("attributeKeys", session.getAttributeKeys());
        
        return ResponseEntity.ok(sessionInfo);
    }
    
    @PostMapping("/extend")
    public ResponseEntity<Map<String, Object>> extendSession() {
        Subject currentUser = SecurityUtils.getSubject();
        Session session = currentUser.getSession();
        
        // セッション延長(30分追加)
        session.setTimeout(30 * 60 * 1000);
        
        Map<String, Object> response = new HashMap<>();
        response.put("success", true);
        response.put("newTimeout", session.getTimeout());
        
        return ResponseEntity.ok(response);
    }
    
    @PostMapping("/invalidate")
    public ResponseEntity<Map<String, Object>> invalidateSession() {
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.logout(); // セッション無効化
        
        Map<String, Object> response = new HashMap<>();
        response.put("success", true);
        response.put("message", "Session invalidated");
        
        return ResponseEntity.ok(response);
    }
}

// SecurityUtils.java - セキュリティユーティリティ
public class SecurityUtilities {
    
    // 現在のユーザーが特定のロールを持つかチェック
    public static boolean hasRole(String role) {
        Subject currentUser = SecurityUtils.getSubject();
        return currentUser.hasRole(role);
    }
    
    // 現在のユーザーが特定の権限を持つかチェック
    public static boolean hasPermission(String permission) {
        Subject currentUser = SecurityUtils.getSubject();
        return currentUser.isPermitted(permission);
    }
    
    // 現在のユーザー名を取得
    public static String getCurrentUsername() {
        Subject currentUser = SecurityUtils.getSubject();
        return (String) currentUser.getPrincipal();
    }
    
    // セッション属性の設定
    public static void setSessionAttribute(String key, Object value) {
        Subject currentUser = SecurityUtils.getSubject();
        Session session = currentUser.getSession();
        session.setAttribute(key, value);
    }
    
    // セッション属性の取得
    public static Object getSessionAttribute(String key) {
        Subject currentUser = SecurityUtils.getSubject();
        Session session = currentUser.getSession(false);
        return session != null ? session.getAttribute(key) : null;
    }
    
    // 管理者権限のチェック
    public static void requireAdmin() {
        Subject currentUser = SecurityUtils.getSubject();
        if (!currentUser.hasRole("admin")) {
            throw new UnauthorizedException("Admin role required");
        }
    }
    
    // 自分自身または管理者のみアクセス可能
    public static void requireSelfOrAdmin(String targetUsername) {
        Subject currentUser = SecurityUtils.getSubject();
        String currentUsername = (String) currentUser.getPrincipal();
        
        if (!currentUsername.equals(targetUsername) && !currentUser.hasRole("admin")) {
            throw new UnauthorizedException("Access denied");
        }
    }
}

エラーハンドリングとテスト

// GlobalExceptionHandler.java - 統一例外処理
@ControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(UnauthenticatedException.class)
    public ResponseEntity<Map<String, Object>> handleUnauthenticated(UnauthenticatedException e) {
        Map<String, Object> response = new HashMap<>();
        response.put("error", "unauthenticated");
        response.put("message", "Authentication required");
        response.put("timestamp", System.currentTimeMillis());
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
    }
    
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseEntity<Map<String, Object>> handleUnauthorized(UnauthorizedException e) {
        Map<String, Object> response = new HashMap<>();
        response.put("error", "unauthorized");
        response.put("message", "Access denied");
        response.put("timestamp", System.currentTimeMillis());
        
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
    }
    
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<Map<String, Object>> handleAuthenticationException(AuthenticationException e) {
        logger.warn("Authentication failed: {}", e.getMessage());
        
        Map<String, Object> response = new HashMap<>();
        response.put("error", "authentication_failed");
        response.put("message", "Authentication failed");
        response.put("timestamp", System.currentTimeMillis());
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
    }
}

// ShiroTest.java - 単体テスト例
@ExtendWith(MockitoExtension.class)
class ShiroServiceTest {
    
    @Mock
    private UserService userService;
    
    @InjectMocks
    private MyRealm myRealm;
    
    @BeforeEach
    void setUp() {
        ThreadContext.bind(new DefaultSecurityManager(myRealm));
    }
    
    @AfterEach
    void tearDown() {
        ThreadContext.unbindSubject();
        ThreadContext.unbindSecurityManager();
    }
    
    @Test
    void testSuccessfulAuthentication() {
        // テストユーザーの準備
        User testUser = new User();
        testUser.setUsername("testuser");
        testUser.setPasswordHash("hashedpassword");
        testUser.setSalt("salt");
        testUser.setActive(true);
        
        when(userService.findByUsername("testuser")).thenReturn(testUser);
        
        // 認証テスト
        UsernamePasswordToken token = new UsernamePasswordToken("testuser", "password");
        Subject subject = SecurityUtils.getSubject();
        
        assertDoesNotThrow(() -> subject.login(token));
        assertTrue(subject.isAuthenticated());
        assertEquals("testuser", subject.getPrincipal());
    }
    
    @Test
    void testFailedAuthentication() {
        when(userService.findByUsername("invaliduser")).thenReturn(null);
        
        UsernamePasswordToken token = new UsernamePasswordToken("invaliduser", "password");
        Subject subject = SecurityUtils.getSubject();
        
        assertThrows(UnknownAccountException.class, () -> subject.login(token));
        assertFalse(subject.isAuthenticated());
    }
    
    @Test
    void testRoleBasedAuthorization() {
        // ロール設定済みのユーザーで認証
        Set<String> roles = Set.of("admin");
        when(userService.getUserRoles("admin")).thenReturn(roles);
        
        Subject subject = SecurityUtils.getSubject();
        // 認証済み状態をシミュレート
        
        assertTrue(subject.hasRole("admin"));
        assertFalse(subject.hasRole("user"));
    }
}