
이 시리즈에서는 JPA 예시 코드를 기반으로, 실제 운영 환경에서 각기 다른 도메인끼리의 통신을 가정한 RESTful API로 디벨롭한다.
* 기본 웹 프로젝트 세팅 완료된 상태에서 시작, 테스트는 POSTMAN 활용디벨롭 단계
- Interceptor 생성 및 적용
- API Key 검증 로직 구현
- JWT 발급, 인증/인가 로직 구현
- 서버용 JWT 발급 로직 구현
- 서버 인터셉터 내 검증 구현
- JWT의 형식 검증
- JWT의 서명(Signature) 검증
- 클레임(예: iss, exp, scope) 검증.
- 클라이언트 용 인증 로직 구현
- OAuth 2.0 기반 인증/인가 로직 구현
- 클라이언트 용 Access Token 발급 로직 구현
- 서버 인터셉터 내 검증 구현
- Access Token의 형식 검증
- 인증 서버에 검증 요청(옵션, 원격 검증)
- Access Token의 클레임(예: scope, exp) 검증
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration-ms}")
private long expirationMs;
public String generateToken(UserDetails user) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList());
return Jwts.builder()
.setIssuer("my-company") // issuer 클레임 설정, iss(발급자) 설정
.setSubject(user.getUsername())
.claim("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.claim("scope", "read write") // scope 클레임 설정, scope(권한 범위) 설정
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + expirationMs))
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()),
SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token, UserDetails user) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes())
.build()
.parseClaimsJws(token)
.getBody();
boolean subjectOk = claims.getSubject().equals(user.getUsername());
boolean notExpired = claims.getExpiration().after(new Date());
boolean issuerOk = "my-company".equals(claims.getIssuer()); // iss(발급자) 검증
boolean scopeOk = Arrays.stream(claims.get("scope", String.class)
.split(" "))
.collect(Collectors.toSet())
.containsAll(/* requiredScopes */); // scope(권한 범위) 검증
return subjectOk && notExpired && issuerOk && scopeOk;
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository; // JPA 리포 사용
public CustomUserDetailsService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public UserDetails loadUserByUsername(String username) {
Member member = memberRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(member.getUsername())
.password(member.getPassword())
.roles(member.getRoles().toArray(new String[0]))
.build();
}
}
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public AuthenticationInterceptor(JwtUtil jwtUtil,
UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized");
return false;
}
String token = header.substring(7);
String username = jwtUtil.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, user)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
return true;
}
}
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Invalid or expired token");
return false;
}
...
}
public class ApiClient {
private final RestTemplate rest;
private String jwt;
public ApiClient(RestTemplateBuilder b) {
this.rest = b.build();
}
public void login(String user, String pass) {
AuthRequest req = new AuthRequest(user, pass);
AuthResponse resp = rest.postForObject("/api/auth/login", req, AuthResponse.class);
this.jwt = resp.getToken();
}
public <T> T get(String url, Class<T> cls) {
HttpHeaders h = new HttpHeaders();
h.setBearerAuth(jwt);
HttpEntity<?> e = new HttpEntity<>(h);
return rest.exchange(url, GET, e, cls).getBody();
}
}