JWT는 이전 포스트에서 다뤘기 때문에 간단하게 소개만 하겠다.
사용자 인증 정보를 JSON형태로 담아 서명한 토큰
세션을 사용하여 로그인 할 때
로그인 정보가 서버(세션)에 저장되어 있다.
JWT로그인을 사용하여 인증할 때
로그인 정보가 클라이언트(브라우저)에 저장되어있다.
그래서 서버 입장에서 요청을 보내는 사람이 누구인지 알 수 없다.
클라이언트가 스스로 누구인지 증명을 해야하는데 이 JWT를 이용해서 증명한다.
보통 4가지의 파일을 가지고 가야된다.
- JwtUtil.java ( 토큰 생성 및 검증 도구 클래스)
- JwtAuthFilter.java (요청마다 토큰을 꺼내서 검증하는 필터)
- JwtLoginController.java (로그인 시 토큰 발급하는 컨트롤러)
- SecurityConfig.java (필터 등록 및 시큐리티 설정 수정)
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
젤 처음에는 pom.xml에 해당 의존성을 주입시켜준다.
package com.example.security.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtil {
// 256비트(32바이트) 이상의 키를 필요
// Base64는 문자 1개가 6비트로 처리 (3바이트 == Base64 문자 4개)
// 문자 4개당 3바이트를 표현하므로 32바이트를 넘기려면 최소 43자 이상 필요
// (40개 == 30바이트, 43자 == 32.25바이트, 44자 == 33바이트)
private final String SECRET = "1234567890123456789012345678901234567890123";
private final long EXPIRATION = 1000 * 60 * 60; // 1시간
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
public String createToken(String username) { // JWT 생성
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) { // 정보 추출
return Jwts.parserBuilder()
.setSigningKey(getSigningKey()).build()
.parseClaimsJws(token)
.getBody().getSubject();
}
public boolean validate(String token) { // 유효성 검사
try {
Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
}
이 코드는 JWT를 생성 정보 추출 유효성 검사하는 도구이다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException{
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (jwtUtil.validate(token)) {
String username = jwtUtil.extractUsername(token);
//이부분은 아무나 인증을 해주고, DB를 연결안하는 코드이다.
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, List.of());
// UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// UsernamePasswordAuthenticationToken authentication =
// new UsernamePasswordAuthenticationToken(
// userDetails, null, List.of());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
요청할때 마다 JWT를 서버로 전달하는 역할을 하는 코드이다.
그리고 전달받은 코드가 정말로 유효한 값인지 유효성 검사까지 한다.
왜냐하면 JWT는 토큰 유효기간이 있기때문이다.
SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder에서 로그인 한 것처럼 만들어준다.(이 필터의 핵심코드이다.)
.addFilterBefore(new JwtAuthFilter(jwtUtil, userDetailsService),
UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> csrf.disable())
SecurityConfig파일에 해당 addFilterBefore메서드에 넣어주고 테스트를 진행해보면 된다.
그럼 이제 POST방식으로 login을 해보겠다.
다음과 같이 토큰이 발행이 된 것을 볼 수 있다.
그럼 이 값으로 인증절차까지 해보겠다.
인증은 GET방식으로 해야되는데 이유는 이미 인증된 사용자의 토큰을 가지고 URL에 접근하는 것이니까
Header에 인증받은 토큰값만 넘겨주면 되는 것이기 때문
Header에 Authorization과 Value에
무조건 Bearer + 발급받은 Token값으로 해주면
위와 같이 JWT인증된 사용자만 접근 가능한 영역입니다. 라고 뜨게된다.