Authentication ( 인증 ) : 제공된 자격 증명을 기반으로 사용자의 시원을 확인합니다. 웹 사이트에 접근할 때 사용되는 아이디, 비밀번호를 말합니다.
Authorization ( 인가 ) : 사용자가 성공적으로 인증되었다는 가정 하에 특정 작업을 수행하거나 특정 데이터를 읽을 수 있는 권한이 있는지 확인합니다.
내용을 정리하자면 로그인 요청이 들어오면 내용으로 인증 조회 절차를 밟은 후
만약 결과가 맞으면 Authentication 객체를 SecurityContext에 저장합니다.
@Transactional(readOnly = true)
public TokenResponseDto signIn(String userId, String pw) {
// userId 확인
UserDetails userDetails = myUserDetailsService.loadUserByUsername(userId);
// pw 확인
if (!passwordEncoder.matches(pw, userDetails.getPassword())) {
throw new BadCredentialsException(userDetails.getUsername() + "Invalid password");
}
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()
);
String refreshToken = jwtTokenProvider.createRefreshToken(authentication);
RefreshRedisToken token = RefreshRedisToken.createToken(userId, refreshToken);
// 기존 토큰이 있으면 수정, 없으면 생성
refreshRedisRepository.save(token);
// accessToken과 refreshToken 리턴
return TokenResponseDto.builder()
accessToken("Bearer-" + jwtTokenProvider.createAccessToken(authentication))
.accessToken("Bearer-" + refreshToken)
.build();
}
서비스 로직을 보면 위에 있는 내용을 더 쉽게 이해할 수 있다.
UserDetails userDetails = myUserDetailsService.loadUserByUsername(userId);
첫번째 코드는 스프링에서 지원하는 UserDetailsService Interface
를 통해 userId가 유효한지 확인하는 과정이다. 만약 userId가 존재한다면 UserDetails
객체에 담아둔다.
if (!passwordEncoder.matches(pw, userDetails.getPassword())) {
throw new BadCredentialsException(userDetails.getUsername() + "Invalid password");
}
두번째 코드는 비밀번호가 유효한지 검증하는 단계이다 이 떄 회원가입 시 비밀번호가 암호화 되어있어 passwordEncoder.matches
메서드를 확인하여 비밀번호를 검증한다.
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()
);
세번째 코드는 검증이 마치고 Authentication
객체에 아이디, 패스워드, 인가정보를 넣어 저장한다.
String refreshToken = jwtTokenProvider.createRefreshToken(authentication);
RefreshRedisToken token = RefreshRedisToken.createToken(userId, refreshToken);
// 기존 토큰이 있으면 수정, 없으면 생성
refreshRedisRepository.save(token);
// accessToken과 refreshToken 리턴
return TokenResponseDto.builder()
.accessToken("Bearer-" + jwtTokenProvider.createAccessToken(authentication))
.accessToken("Bearer-" + refreshToken)
.build();
}
네번째 코드는 JWT관련 코드이다. 검증이 완료된 상태의 정보로 토큰을 생성하고 Redis DB
에
저장하면 자동으로 없으면 생성하고, 기존에 있으면 수정하게 된다.
Refresh 토큰을 사용하는 목적은 실제 인증 목적으로 쓰이는 Access-Token을 발행하는 용도로 쓰인다. 그래서 Access-Token의 기간을 1시간 Refresh-Token의 기간을 훨씬 길게 사용한다. 이로인해서 Access-Token만 노출시켜 취약한 보안을 해결할 수 있다.
우리가 사용하는 어플들은 대부분 Access-Token을 재발행할 때 새로운 로그인을 하지 않고 연장되는 모습을 볼 수 있다. 이것을 위해서 클라이언트 서버가 접근을 Intercept하여 토큰의 기간을 연장하는 과정을 갖는다. 아래 코드는 그 과정에서 조회되는 Api이다.
@Transactional(readOnly = true)
public TokenResponseDto reissueAccessToken(String token) {
// token 앞에 "Bearer-" 제거
String resolveToken = resolveToken(token);
// 토큰 검증 메서드
// 실패시 jwtTokenProvider.validateToken(resolveToken)에서 exception을 리턴
JwtTokenProvider.validateToken(resolveToken);
Authentication authentication = jwtTokenProvider.getAuthentication(resolveToken);
// 디비에서 확인
RefreshRedisToken refreshRedisToken = refreshRedisRepository.findById(authentication.getName()).get();
// 토큰이 같은지 확인
if (!resolveToken.equals(refreshRedisToken.getToken())) {
throw new RuntimeException("not equals refresh token");
}
// 재발행해서 저장
String newToken = jwtTokenProvider.createRefreshToken(authentication);
RefreshRedisToken newRedisToken = RefreshRedisToken.createToken(authentication.getName(), newToken);
refreshRedisRepository.save(newRedisToken);
// accessToken과 refreshToken 모두 재발행
return TokenResponseDto.builder()
.accessToken("Bearer-" + jwtTokenProvider.createAccessToken(authentication))
.refreshToken("Bearer-" + newToken)
.build();
}