[java] Apache Shiro와 JWT 토큰 기반 인증

이 글에서는 Apache Shiro와 JWT(JSON Web Token)를 사용하여 토큰 기반 인증 시스템을 구축하는 방법을 살펴보겠습니다.

Apache Shiro란 무엇인가요?

Apache Shiro는 Java 애플리케이션을 위한 강력한 보안 프레임워크입니다. Shiro는 다양한 인증 방식, 세션 관리, 권한 부여 및 암호화와 같은 고급 보안 기능을 제공합니다. Shiro는 애플리케이션의 보안 요구사항을 간편하고 유연하게 처리할 수 있도록 도와줍니다.

JWT(JSON Web Token)란 무엇인가요?

JWT는 인증 및 정보 교환을 위한 안전한 방법을 제공하는 토큰 기반 인증 시스템입니다. JWT는 토큰에 필요한 정보를 JSON 형식으로 저장하며, 이를 서명하여 인증 및 데이터 무결성을 보장합니다. JWT 토큰은 클라이언트와 서버 간의 통신에 사용되며, 서버는 토큰을 확인하여 사용자를 인증하고 권한을 부여합니다.

Shiro와 JWT를 함께 사용하기

  1. Apache Shiro와 JWT를 Maven 또는 Gradle을 통해 프로젝트에 추가합니다.

  2. JWT의 토큰 생성 및 검증을 담당할 JWTUtil 클래스를 작성합니다. 이 클래스는 토큰 생성, 토큰에서 정보 추출, 토큰 검증 등의 기능을 제공해야 합니다.

public class JWTUtil {
    private static final String SECRET_KEY = "your_secret_key";

    public static String generateToken(String subject) {
        long currentTime = System.currentTimeMillis();
        Date issuedAt = new Date(currentTime);
        Date expiresAt = new Date(currentTime + 3600000); // 토큰의 만료 시간은 1시간 후로 설정

        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(issuedAt)
                .setExpiration(expiresAt)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public static String getSubjectFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public static boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}
  1. Shiro의 인증 및 권한 부여 설정을 구성합니다. 이 설정에서 JWT를 사용하여 토큰 기반 인증을 처리해야 합니다.
public class ShiroConfig {
    @Bean
    public Realm realm() {
        JWTRealm jwtRealm = new JWTRealm();
        jwtRealm.setCredentialsMatcher(matcher());
        return jwtRealm;
    }

    @Bean
    public HashedCredentialsMatcher matcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        matcher.setStoredCredentialsHexEncoded(false);
        return matcher;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        // 인증에 필요한 URL 패턴 설정
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/login", "anon"); // 로그인은 인증 없이 허용
        filterChainDefinitionMap.put("/**", "authc"); // 나머지 URL은 인증 필요
        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilter;
    }
}
  1. JWTRealm 클래스를 작성하여 Shiro와 JWT를 통합합니다. 이 클래스에서는 JWT 토큰을 검증하고 사용자 인증 및 권한 부여를 처리해야 합니다.
public class JWTRealm extends AuthorizingRealm {
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String jwtToken = (String) token.getCredentials();

        if (!JWTUtil.validateToken(jwtToken)) {
            throw new AuthenticationException("Invalid token");
        }

        String username = JWTUtil.getSubjectFromToken(jwtToken);

        // 사용자 정보를 DB에서 가져와서 SimpleAuthenticationInfo 객체를 생성하여 반환
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException("User not found");
        }

        return new SimpleAuthenticationInfo(username, user.getPassword(), getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();

        // 사용자에게 할당된 권한 정보를 DB에서 가져와서 SimpleAuthorizationInfo 객체를 생성하여 반환
        Set<String> roles = userService.findRolesByUsername(username);
        Set<String> permissions = userService.findPermissionsByUsername(username);

        return new SimpleAuthorizationInfo(roles);
    }
}
  1. Controller에서 로그인 작업을 수행하여 JWT 토큰을 생성하고 응답으로 반환합니다.
@RestController
public class AuthController {
    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
        // 사용자 인증 로직 수행

        // 로그인 성공 시 JWT 토큰 생성
        String token = JWTUtil.generateToken(username);

        return ResponseEntity.ok(token);
    }
}

이제 Apache Shiro와 JWT를 함께 사용하여 토큰 기반 인증 시스템을 구축할 준비가 되었습니다. 이 방법을 사용하면 간편한 인증과 권한 부여를 제공하는 안전한 애플리케이션을 개발할 수 있습니다.

참고 자료