jwt를 생성하고 jwt를 통해 인증을 하는 과정에 대한 정리.
Json Web Token
의존성 추가
처음 프로젝트 진행할때는 0.9.1버전을 사용하였지만 JDK 9 이상에서는 사용할 수 없어서 최신버전 사용( deprecated된 메소드가 있음)
//implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
토큰 생성 및 관리
토큰을 생성하고 관리하는 JwtProvider 클래스를 생성해 준다.
JwtProvider 클래스는 Spring Security에서 인증된 사용자의 정보를 기반으로 JWT 토큰을 생성하고, 토큰에서 인증 정보를 추출하여 사용자를 인증하는 역할을 한다.
토큰 생성 과정
토큰의 생성과정은 다음과 같이 이루어진다.
1. 로그인 정보를 통한 사용자 인증
프론트에서 전달받은 username과 password를 AuthService의 authenticate로 전달해주어 사용자인증을 진행하고 인증정보를 기준으로 token을 생성해준다.
AuthService.java의 전체코드는 아래와 같다.
@Component
@RequiredArgsConstructor
public class AuthService {
private final JwtProvider jwtProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public String authenticate(String username, String password) throws BadCredentialsException {
// 받아온 유저네임과 패스워드를 이용해 UsernamePasswordAuthenticationToken 객체 생성
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// authenticationToken 객체를 통해 Authentication 객체 생성
// 이 과정에서 재정의한 loadUserByUsername 메서드 호출
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 인증 정보를 기준으로 jwt access 토큰 생성
String accessToken = jwtProvider.createToken(authentication);
return accessToken;
}
}
2. loadUserByUsername을 통한 사용자 인증 로직 작성
Spring Security의 UserDetailsService를 상속받아 인터페이스에 정의된 메소드 loadUserByUsername을 재정의 하여 애플리케이션의 요구사항에 맞는 사용자 인증 로직을 작성한다.
loadUserByUsername는 주어진 사용자 이름(username)에 따라 사용자의 인증 정보를 조회하고, 이를 기반으로 UserDetails 객체를 반환해주는 역할을 한다.
해당 부분에 대한 코드는 아래와 같다.
@Service
public class TestService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws RuntimeException {
//username과 일치하는 User정보를 찾아오는 로직
Firestore firestore = FirestoreClient.getFirestore();
CollectionReference collectionRef = firestore.collection("User");
ApiFuture<QuerySnapshot> querySnapshotFuture = collectionRef.whereEqualTo("id", username).get();
QuerySnapshot querySnapshot = null;
try {
querySnapshot = (QuerySnapshot)querySnapshotFuture.get();
DocumentSnapshot documentSnapshot = (DocumentSnapshot)querySnapshot.getDocuments().get(0);
Test user = (Test) documentSnapshot.toObject(Test.class);
//여기까지 User정보 찾아오는 로직( firebase를 사용하지 않을때는 해당부분을 맞게 고쳐서 사용)
return User
.builder()
.username(user.getId())
.password(user.getPw())
.authorities(String.valueOf(Collections.singletonList("ROLE_USER")))//권한부여
.build();//유저 정보
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
FireStore Database에서 일치하는 id를 찾아 해당 유저의 정보를 반환해준다.
3. 토큰생성
JwtProvider.java
// 토큰 생성
public String createToken(Authentication authentication) {
// 1. 사용자 권한 정보 추출
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
// 2. 현재 시간과 유효 기간 설정
long now = (new Date()).getTime();
Date validity = new Date(now + tokenValidTime);
// 3. JWT 토큰 생성
return Jwts.builder()
.setSubject(authentication.getName())
.claim("Authorities", authorities)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, AUTHORITIES_KEY)
.compact();
}
사용자 권한 정보를 추출하고 토큰의 유효기간을 설정하여 이 내용을 기반으로 토큰을 생성한다.
이렇게 생성된 토큰을 프론트로 반환해주면 된다.
토큰을 생성 후 프론트로 토큰을 반환해주는 컨트롤러 코드(로그인 시 사용하는 api)
Controller.java
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody Map<String,String> user) {
String token = authService.authenticate(user.get("username"),user.get("password"));//토큰 생성
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization","Bearer " + token);//헤더 세팅
return ResponseEntity.ok().headers(httpHeaders).body(token);
}
jwt 인증과정
1. Request의 Header에서 token 추출
프론트에서는 api를 요청할때 헤더에 "Authorization" : "Bearer TOKEN값" 형식으로 값을 담아 요청을 보내온다.
그러면 백에서는 Authorization 값에서 "Bearer " 접두사를 제거하여 토큰 값을 추출하면 된다.
JwtProvider.java
// Request의 Header에서 token 값을 가져옵니다. "Authorization" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
2. 토큰에서 인증정보 조회
JwtProvider.java
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(AUTHORITIES_KEY).parseClaimsJws(token).getBody().getSubject();
}
JWT 토큰에서 사용자 정보를 추출하고, UserDetailsService를 사용하여 토큰에 저장된 사용자명으로 사용자 정보를 조회한다.
3. 토큰의 유효성 확인
JwtProvider.java
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(AUTHORITIES_KEY).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
토큰의 서명을 확인하고, 만료 일자를 검사하여 유효한 토큰인지 확인한다.
JwtProvider 전체코드
// 토큰을 생성하고 검증하는 클래스
// 해당 컴포넌트는 필터클래스에서 사전 검증을 거친다.
@RequiredArgsConstructor
@Component
public class JwtProvider {
private String AUTHORITIES_KEY = "anythingyouwant_djWjrnwjWJrnthiffkthiffk";
// 토큰 유효시간 300분
private long tokenValidTime = 300 * 60 * 1000L;
private final UserDetailsService userDetailsService;
protected Key key;
@PostConstruct
protected void init() {
AUTHORITIES_KEY = Base64.getEncoder().encodeToString(AUTHORITIES_KEY.getBytes());
}
// 토큰 생성
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date validity = new Date(now + tokenValidTime);
return Jwts.builder()
.setSubject(authentication.getName())
.claim("Authorities", authorities)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, AUTHORITIES_KEY)
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(AUTHORITIES_KEY).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져옵니다. "Authorization" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(AUTHORITIES_KEY).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
이후 JWT를 사용한 인증 필터 작성 후 SpringConfig.java를 해당 필터를 거치도록 수정해야한다.
JwtAuthenticationFilter.java
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends GenericFilter {
@Autowired
private final JwtProvider jwtProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 헤더에서 JWT 를 받아옵니다.
String token = jwtProvider.resolveToken((HttpServletRequest) request);
// 유효한 토큰인지 확인합니다.
if (token != null && jwtProvider.validateToken(token)) {
// 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
Authentication authentication = jwtProvider.getAuthentication(token);
// SecurityContext 에 Authentication 객체를 저장합니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
SpringConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(c->c.disable())
.headers(f->f.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.sessionManagement(s->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((request) -> request
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**","/test/login").permitAll()//해당경로 url은 인증없이 사용가능
.anyRequest().authenticated())
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider),
UsernamePasswordAuthenticationFilter.class)//토큰으로 검사
.logout(Customizer.withDefaults())
;
return http.build();
}
Spring Security 설정을 통해 API 요청 시 헤더에 유효한 토큰이 있는지 검사하고, 직접 만든 로그인 화면을 사용하여 로그인 인증을 처리한다. 이를 위해 "/login" 경로는 인증 없이 접근할 수 있도록 설정해준다.
결과확인
swagger를 이용하여 테스트를 진행하였다.
인증을 위한 설정은 다음 글을 보고 참고하면 된다.
[SpringBoot] - [Springboot] swagger 사용하기- 의존성 추가 및 jwt 인증 추가하기
1. 로그인 컨트롤러를 통해 토큰 생성
api를 호출하면

'Encoded password does not look like BCrypt' 라는 경고가 뜨는데 이것은 데이터베이스에 저장되어있는 pw가 암호화가 안되어 있기 때문이다.
나중에 사용자 회원가입 서비스를 만들때 데이터베이스에 암호화해서 저장해주면된다.
하지만 일단 테스트를 위해 임시로 암호화를 진행했다.
>> BCryptPasswordEncoder 빈을 추가해주고
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
PasswordEncoder passwordEncoder = passwordEncoder();
System.out.println(passwordEncoder.encode("123123"));

콘솔창에 암호를 찍어서 나온 암호를 데이터베이스에 저장해준다.


토큰이 성공적으로 반환되는것을 확인할 수 있다.
2. api 요청시 토큰 검증
- 헤더에 토큰이 없는경우 403에러 발생

- 헤더에 유효한 토큰이 있는 경우

토큰 입력

응답이 성공적으로 이루어진다.
'SpringBoot' 카테고리의 다른 글
🎯 Spring Security + GitHub OAuth 연동 및 API 호출하기 (0) | 2025.05.12 |
---|---|
[Springboot] #1 웹소켓과 STOMP로 실시간 채팅 구현하기 - spring boot에 웹소켓 연결 (0) | 2024.08.08 |
[Springboot] #1 spring security와 jwt로 인증구현하기 - spring security 사용하기 (0) | 2024.05.13 |
[Springboot] swagger 사용하기- 의존성 추가 및 jwt 인증 추가하기 (0) | 2024.05.13 |
[Springboot] api 컨트롤러에서 json형식의 데이터와 MultipartFile 동시에 받기 (0) | 2024.05.13 |