1. Token 기반 인증
- 토큰 기반 인증 시스템은 인증받은 사용자들에게 토큰을 발급하고, 서버에 요청을 할 때 헤더에 토큰을 함께 보내도록 하여 유효성 검사를 한다.
- 이러한 시스템에서는 더 이상 사용자의 인증 정보를 서버나 세션에 유지하지 않고 클라이언트 측에서 들어오는 요청만으로 작업을 처리한다.
- 상태를 유지하지 않으므로 stateless 한 구조를 갖는다.
- 서버(세션) 기반 인증의 단점을 극복할 수 있다.
2. JWT (Json Web Token)
💡 JWT란 인증에 필요한 정보들을 Token에 담아 암호화시킨 토큰을 의미한다.
JWT 구조
JWT는 .을 구분자로 나누어지는 세 가지 문자열의 조합이다. JWT는 3가지 구조로 나누어진다.
Header
- alg : 암호화할 해싱 알고리즘, typ : 토큰의 타입
Payload
- Payload는 토큰에 담을 정보를 가지고 있다.
- 주로 클라이언트의 고유 ID 값 및 유효기간등이 포함되는 영역이다.
- Key-Value 형식으로 이루어진 한 쌍의 정보를 Claim이라고 부른다.
Signature
- Signature는 인코딩된 Header와 Payload를 더한 뒤 비밀키로 해싱하여 생성한다.
- Header와 Payload는 단순히 인코딩 된 값이기 때문에 제 3자가 복호화 및 조작할 수 있지만, Signature는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화할 수 없다.
- Signature는 토큰의 위변조 여부를 확인하는데 사용한다.
3. JWT 구현
application-jwt.yaml
jwt:
access-header : Access-Token
refresh-header : Refresh-Token
issuer: prgrms
client-secret:
access-token-expiry-seconds: 3600
refresh-token-expiry-seconds: 1209600
- access-header, refresh-header : HTTP header 이름
- issuer : 토큰 발급자
- client-secret : HS512 알고리즘으로 서명을 수행할 것이기 때문에 키 길이를 64바이트로 해야 한다.
- AccessToken 만료시간 : 1시간
- RefreshToken 만료시간 : 2주
JwtConfigure
@Component
@Getter @Setter
@ToString
@ConfigurationProperties(prefix = "jwt")
public class JwtConfigure {
private String accessHeader;
private String refreshHeader;
private int accessTokenExpirySeconds;
private int RefreshTokenExpirySeconds;
private String issuer;
private String clientSecret;
}
- @ConfigurationProperties(prefix = "jwt")
- application-jwt.yaml 에 있는 정보를 가져와서 필드에 넣어준다.
- @Component
- 해당 클래스를 Bean으로 등록한다.
Jwt
@ToString
public class Jwt {
private final String issuer;
private final String clientSecret;
private final int accessTokenExpirySeconds;
private final int refreshTokenExpirySeconds;
private final Algorithm algorithm;
private final JWTVerifier jwtVerifier;
public Jwt(String issuer, String clientSecret, int accessTokenExpirySeconds, int refreshTokenExpirySeconds) {
this.issuer = issuer;
this.clientSecret = clientSecret;
this.accessTokenExpirySeconds = accessTokenExpirySeconds;
this.refreshTokenExpirySeconds = refreshTokenExpirySeconds;
this.algorithm = Algorithm.HMAC512(clientSecret);
this.jwtVerifier = com.auth0.jwt.JWT.require(algorithm)
.withIssuer(issuer)
.build();
}
public String createAccessToken (Claims claims) {
Date now = new Date();
JWTCreator.Builder builder = com.auth0.jwt.JWT.create();
builder.withIssuer(issuer);
builder.withIssuedAt(now);
if (accessTokenExpirySeconds > 0) {
builder.withExpiresAt(new Date(now.getTime() + accessTokenExpirySeconds * 1_000L));
}
builder.withClaim("email", claims.email);
builder.withArrayClaim("roles", claims.roles);
return builder.sign(algorithm);
}
public String createRefreshToken (Claims claims) {
Date now = new Date();
JWTCreator.Builder builder = com.auth0.jwt.JWT.create();
builder.withIssuer(issuer);
builder.withIssuedAt(now);
if (refreshTokenExpirySeconds > 0) {
builder.withExpiresAt(new Date(now.getTime() + refreshTokenExpirySeconds * 1_000L));
}
builder.withClaim("email", claims.email);
builder.withArrayClaim("roles", claims.roles);
return builder.sign(algorithm);
}
public Claims verify(String token) {
return new Claims(jwtVerifier.verify(token));
}
public Boolean isExpiredToken(String accessToken) {
DecodedJWT decodedJWT = com.auth0.jwt.JWT.decode(accessToken);
return decodedJWT.getExpiresAt().after(new Date());
}
static public class Claims {
private String email;
private String[] roles;
private Date iat;
private Date exp;
private Claims() {}
Claims(DecodedJWT decodedJWT) {
Claim email = decodedJWT.getClaim("email");
if(!email.isNull()) {
this.email = email.asString();
}
Claim roles = decodedJWT.getClaim("roles");
if(!roles.isNull()) {
this.roles = roles.asArray(String.class);
}
this.iat = decodedJWT.getIssuedAt();
this.exp = decodedJWT.getExpiresAt();
}
public static Claims from(String email, String[] roles) {
Claims claims = new Claims();
claims.email = email;
claims.roles = roles;
return claims;
}
public Map<String, Object> asMap() {
Map<String, Object> map = new HashMap<>();
map.put("email", email);
map.put("roles", roles);
map.put("iat", iat());
map.put("exp", exp());
return map;
}
public long iat() {
return iat != null ? iat.getTime() : -1;
}
public long exp() {
return exp != null ? exp.getTime() : -1;
}
public String getEmail() {
return email;
}
public String[] getRoles() {
return roles;
}
public Date getIat() {
return iat;
}
public Date getExp() {
return exp;
}
}
}
- Jwt 클래스
- 해당 클래스는 토큰의 생성과 검증 역할과 책임이 있다.
- JWT 생성을 위한 sign 메서드
- JWT 검증을 위한 verify 메서드
- Claims Inner 클래스
- JWT의 payload 부분에 해당한다.
- email : 사용자 이메일
- roles : 사용자 권한
- iat : 토큰이 발급된 시간
- exp : 토큰 만료시간
WebSecurityConfigure
public class WebSecurityConfigure {
private final JwtConfigure jwtConfigure;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public Jwt jwt() {
return new Jwt(
jwtConfigure.getIssuer(),
jwtConfigure.getClientSecret(),
jwtConfigure.getAccessTokenExpirySeconds(),
jwtConfigure.getRefreshTokenExpirySeconds()
);
}
- PasswordEncoder, Jwt를 Bean으로 등록한다.
자세한 코드 위 링크에서 보실 수 있습니다.
Reference
https://tecoble.techcourse.co.kr/post/2021-05-22-cookie-session-jwt/
반응형
'프로젝트 > LinkBook' 카테고리의 다른 글
RefreshToken 도입 (0) | 2022.08.23 |
---|---|
AccessToken 로그인 로직 (feat. Spring Security) (0) | 2022.08.23 |
회원가입 이메일 인증 구현 (0) | 2022.08.21 |
Batch Fetch Size를 통해 페이징 문제 해결 (0) | 2022.08.20 |
계층형 Category 구현 (Enum으로의 전환) (0) | 2022.08.19 |
댓글