저번 글에서는 스프링 시큐리티를 이용해 세션 기반 인증을 하였습니다. 세션 기반 인증을 사용해 사용자마다 사용자의 정보를 담은 세션을 생성하고 저장해서 인증을 합니다. 토큰 기반 인증은 서버가 토큰을 생성해서 클라이언트에게 제공하면, 클라이언트는 이 토큰을 갖고 있다가 여러 요청을 이 토큰과 함께 신청합니다. 그럼 서버는 토큰만 보고 유효한 사용자인지 검증합니다!!
토큰을 전달하고 인증받는 과정
- 클라이언트가 아이디어와 비밀번호를 서버에게 전달하면서 인증을 요청합니다.
- 서버는 아이디어와 비밀번호를 확인해 유효한 사용자인지 검증합니다. 유효한 사용자면 토큰을 생성해서 응답합니다,
- 클라이언트는 서버에서 준 토큰을 저장합니다.
- 이후 인증이 필요한 API를 사용할 때 토큰을 함께 보냅니다.
- 서버는 토큰이 유효한지 검사합니다.
- 토큰이 유효하다면 클라이언트가 요청한 내용을 처리합니다.
토큰 기반 인증의 특징
- 상태를 유지하지 않음: 토큰 기반 인증은 상태를 유지하지 않는(Stateless) 인증 방식입니다. 서버는 클라이언트의 상태를 저장하지 않고, 각각의 요청이 독립적으로 인증되도록 합니다. 이는 서버의 확장성과 부하 분산을 향상하는 장점을 제공합니다.
- 확장성과 분산 환경에 용이: 토큰 기반 인증은 각각의 요청이 독립적으로 인증되므로, 여러 서버에 대한 부하 분산이나 서비스의 확장성에 용이합니다. 토큰은 클라이언트에게 전달되므로, 다중 서버 환경에서도 인증을 처리할 수 있습니다.
- 보안과 권한 부여: 토큰은 암호화되어 있으며, 서명(signature)이 포함되어 있어 위조를 방지합니다. 이를 통해 토큰의 유효성을 검증하고 인증된 사용자의 권한을 확인할 수 있습니다. 토큰에는 클라이언트의 정보와 해당 사용자의 권한 정보 등이 포함될 수 있습니다. (HMAC 방식)
- 범용성: 토큰 기반 인증은 HTTP를 기반으로 하며, RESTful API와 같은 웹 서비스 환경에서 많이 사용됩니다. 토큰은 일반적으로 HTTP 헤더나 쿼리 매개변수로 전달되며, HTTP 기반의 다양한 클라이언트 (웹 애플리케이션, 모바일 앱 등)와 상호 운용이 가능합니다.
토큰 기반 인증을 할 때 사용하는 것이 JWT입니다.
Spring Boot와 JWT를 이용한 회원 가입(1)
지금 부터 혼자서 하는 토이 프로젝트를 해볼려고 한다. 쇼핑몰을 만들어 볼 생각이다. 첫번째 순서는 JWT를 이용해서 회원 가입을 하는 것이다. JWT란? 지금부터 JWT에 대한 내용은 JWT 이곳을 참고
lympsw12.tistory.com
위는 예전에 JWT에대해 공부한 내용입니다. JWT 내용에는 토큰과 관련된 정보를 담습니다. 내용의 한 덩어리를 클레임이라고 부르며, 클레임은 키값의 한 쌍으로 이루어져 있습니다. 클레임은 등록된 클레임, 공개 클레임, 비공개 클레임이 존재합니다. 서명은 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도입니다. 헤더의 인코딩 값과 내용의 인코딩값을 합친 후에 주어진 비밀키를 사용해 해시값을 생성합니다.
리프레시 토큰이 필요한 이유
토큰이 노출될 경우 서버는 토큰과 함께 들어온 요청이 토큰을 탈취한 사람의 요청인지 확인할 수 없습니다. 그러므로 토큰의 유효기간이 짧아야 합니다. 하지만 짧으면 사용자 입장에서는 받은 토큰을 너무 짧게 사용해 불편합니다. 이런 단점을 보완하기 위해 리프레시 토큰을 사용합니다.
위에 그림에서 보여주는 순서대로 리프레시 토큰이 생성됩니다.
JWT 토큰 구현하기
JWT 토큰의 이슈 발급자, 비밀키 설정
저는 이 프로젝트를 할 때, 서브 모듈을 사용했기 때문에 submodule에
jwt:
issuer: lympsw12@naver.com
secret_key : ```
를 추가해줬습니다. 그 후 서브모듈의 recursive 했습니다.
JwtProperties
@Setter
@Getter
@Component
@PropertySource("classpath:application.yml")
public class JwtProperties {
@Value("${jwt.issuer}")
private String issuer;
@Value("${jwt.secret_key}")
private String secretKey;
}
저만의 방식대로 만들었습니다.
TokenProvider
//JWT 토큰 생성 메서드
private String makeToken(Date expiry, Member member){
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 tpy : JWT
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now) // 내용 iat : 현재 시간
.setExpiration(expiry) // 내용 exp : expiry 멤버 변숫값
.setSubject(member.getEmail()) // 내용 sub : 유저의 이메일
.claim("id", member.getId()) // 클레임 id : 유저 ID
// 서명 : 비밀값과 함께 해시값을 HS256 방식으로 암호화
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
public String generateToken(Member member, Duration expiredAt){
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), member);
}
위 코드는 JWT 토큰을 만드는 코드입니다. 인자는 만료시간과 유저 정보입니다. 각 내용은 주석에 있습니다. 클레임에는 유저 ID가 들어가고 토큰을 만들 때는 위에 설정해 둔 SecretKey와 함께 HS256 방식으로 암호화합니다. 이제 이것을 기반으로 generateToken을 통해 token을 생성합니다.
//JWT 토큰 유효성 검증 메서드
public boolean validToken(String token) {
try{
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey()) // 비밀값으로 복호화
.parseClaimsJws(token);
return true;
} catch (Exception e){ // 복호화 과정에서 에러가 나면 유효하지 않은 토큰
return false;
}
}
토큰이 유효한지 검사합니다. SecretKey를 통해 복호화를 진행합니다.
//토큰 기반으로 인증 정보를 가져오는 메서드
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject(), "", authorities), token, authorities);
}
private Claims getClaims(String token){
return Jwts.parser()// 클레임 조회
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
//토큰 기반으로 유저 ID를 가져오는 메서드
public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
토큰을 받아 인증 정보를 담은 객체 Authentication을 반환하는 메서드입니다. getClaims는 비밀 값으로 토큰을 복호화한 뒤 클레임을 가져옵니다. 클레임 정보를 반환받아 사용자 이메일이 들어 있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성합니다. 이때 UsernamePasswordAuthenticationToken을 반환합니다.
이렇게 JWT 토큰을 생성하고 유효한지 검사 할 수 있습니다!!!
리프레시 토큰 구현하기
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "user_id", nullable = false, unique = true)
private Long userId;
@Column(name = "refresh_token", nullable = false)
private String refreshToken;
public RefreshToken(Long userId, String refreshToken){
this.userId = userId;
this.refreshToken = refreshToken;
}
public RefreshToken update(String refreshToken){
this.refreshToken = refreshToken;
return this;
}
}
DB에 저장해야 하므로 Entity를 만듭니다.
토큰 필터 구현하기
필터는 실제로 각종 요청이 요청을 처리하기 위한 로직으로 전달되기 전후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공합니다. 유효 토큰이면 security context holder에 인증 정보를 저장합니다.
security context는 인증 객체가 저장되는 보관소로 인증 정보가 필요할 때 언제든지 인증 객체를 꺼내 사용할 수 있습니다. 또 이 클래스는 스레드마다 공간을 할당하는 스레드 로컬에 저장되므로 코드의 아무 곳에서나 참조할 수 있습니다.
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 요청 헤더의 Authorization 키의 값 조회
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
// 가져온 값에서 접두사 제거
String token = getAccessToken(authorizationHeader);
//가져온 토큰이 유효한지 확인하고, 유호한 때는 인증 정보를 설정
if(tokenProvider.validToken(token)){
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getAccessToken(String authorizationHeader){
if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
return authorizationHeader.substring(TOKEN_PREFIX.length());
}
return null;
}
}
헤더값을 가져온 뒤 액세스 토큰이 유효하다면 인증 정보를 설정합니다. 즉, 헤더에서 Authorization인 필드의 값을 가져온 다음 토큰의 접두사 Bearer를 제외 한 값을 얻습니다. 이 토큰이 유효한지 검사하고 유효하다면 SecurityContextHolder에 인증 정보를 저장합니다.
리프레시 토큰 서비스
@RequiredArgsConstructor
@Service
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
public RefreshToken findByRefreshToken(String refreshToken) {
return refreshTokenRepository.findByRefreshToken(refreshToken)
.orElseThrow(() -> new IllegalArgumentException("Unexpected token"));
}
}
리프레시 토큰을 가져옵니다.
@RequiredArgsConstructor
@Service
public class TokenService {
private final TokenProvider tokenProvider;
private final RefreshTokenService refreshTokenService;
private final MemberService memberService;
public String createNewAccessToken(String refreshToken) throws IllegalAccessException {
// 토큰 유효성 검사에 실패하면 예외 발생
if(!tokenProvider.validToken(refreshToken)){
throw new IllegalAccessException("Unexpected token");
}
Long memberId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
Member member = memberService.findById(memberId);
return tokenProvider.generateToken(member, Duration.ofHours(2));
}
}
가져온 리프레시 토큰을 유효성검사를 해 유효 하다면 다시 accessToken을 생성합니다.
리프레시 토큰 컨트롤러
@RequiredArgsConstructor
@RestController
public class TokenApiController {
public final TokenService tokenService;
@PostMapping("/api/token")
public ResponseEntity<CreatedAccessTokenResponse> createNewAccessToken(@RequestBody CreatedAccessTokenRequest request) throws IllegalAccessException {
String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());
return ResponseEntity.status(HttpStatus.CREATED)
.body(new CreatedAccessTokenResponse(newAccessToken));
}
}
리프레시 토큰을 받아 accessToken을 만들어 return 하는 Controller를 만듭니다.
지금 까지 토큰 기반 인증 방법에 대해 공부했습니다. AccessToken과 RefreshToken을 사용해 토큰 기반 인증을 구현해 봤습니다. 다음에는 이제 JWT를 Oauth2.0과 합쳐 소셜 로그인을 구현해 볼 것입니다.
https://product.kyobobook.co.kr/detail/S000201766024
스프링 부트 3 백엔드 개발자 되기: 자바 편 | 신선영 - 교보문고
스프링 부트 3 백엔드 개발자 되기: 자바 편 | ★ 자바 백엔드 개발자가 되고 싶다면★ 자바 언어 입문 그다음에 꼭 보세요실력을 갖춘 개발자로 성장하려면 시작이 중요합니다. 그래서 이 책은
product.kyobobook.co.kr
'혼자하는 프로젝트 > 나만의 프로젝트' 카테고리의 다른 글
스프링 부트로 채팅 서버 구현하기 (0) | 2023.07.15 |
---|---|
스프링 시큐리티로 회원가입, 로그인 구현하기 (0) | 2023.06.27 |
테스트 코드 공부 (0) | 2023.06.21 |
Docker로 CI/CD 구축하기 (0) | 2023.06.20 |
데이터베이스 정규화 하기 (2) | 2023.04.29 |