OAuth2 Login with JWT - 2 [User & OAuth2UserInfo & Token]

조건웅·2023년 10월 11일

OAuth2 Login

목록 보기
2/8
post-thumbnail

User

가장 기본으로 우리가 벡엔드 서버에서 사용자 entity를 제어하기 위해 정의해야 한다.

아래와 같이 코드를 작성하였다.

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "TB_USER")
public class SiteUser {
    @JsonIgnore
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;

    @Column(name = "USER_ID", length = 64, unique = true)
    @NotNull
    @Size(max = 64)
    private String userId;

    @Column(name = "USERNAME", length = 100)
    @NotNull
    @Size(max = 100)
    private String username;

    @JsonIgnore
    @Column(name = "PASSWORD", length = 128)
    @NotNull
    @Size(max = 128)
    private String password;

    @Column(name = "IS_VERIFIED_EMAIL")
    @NotNull
    private boolean isVerifiedEmail;

    @Column(name = "PROVIDER_TYPE", length = 20)
    @Enumerated(EnumType.STRING)
    @NotNull
    private ProviderType providerType;

    @Column(name = "ROLE_TYPE", length = 20)
    @Enumerated(EnumType.STRING)
    @NotNull
    private RoleType roleType;

    @Column(name = "CREATED_AT")
    @NotNull
    private LocalDateTime createdAt;

    @Column(name = "MODIFIED_AT")
    @NotNull
    private LocalDateTime modifiedAt;

    public SiteUser(String userId, String username, boolean isVerifiedEmail, ProviderType providerType, RoleType roleType, LocalDateTime createdAt, LocalDateTime modifiedAt) {
        this.userId = userId;
        this.username = username;
        this.isVerifiedEmail = isVerifiedEmail;
        this.providerType = providerType;
        this.roleType = roleType;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
    }
}
@Getter
@AllArgsConstructor
public enum RoleType {
    ADMIN("ROLE_ADMIN","관리자 권한"),
    USER("ROLE_USER", "일반 사용자 권한"),
    GUEST("ROLE_GUEST", "게스트 권한");

    private final String code;
    private final String description;

    private static final Map<String, RoleType> roleTypeMap = Collections.unmodifiableMap(
            Stream.of(values()).collect(Collectors.toMap(RoleType::getCode, Function.identity()))
    );

    public static RoleType find(String code) {
        return Optional.ofNullable(roleTypeMap.get(code)).orElse(GUEST);
    }
}
@Getter
public enum ProviderType {
    LOCAL,
    GOOGLE;
}

OAuth2UserInfo

OAuth2 로그인을 성공적으로 진행했을 때, AccessToken을 통해 Resource Server에 유저에 대한 정보를 해당 클래스를 통해 정보를 담고 사용할 것이다. 수정이 필요없기 때문에 get함수만 선언해서 값만 가져올 수 있도록 변경 가능성을 최소화한다.

public class OAuth2UserInfoFactory {
    public static OAuth2UserInfo getOAuth2UserInfo(ProviderType providerType, Map<String, Object> attributes) {
        switch (providerType) {
            case GOOGLE: return new GoogleOAuth2UserInfo(attributes);
            default: throw new IllegalArgumentException("Invalid Provider Type.");
        }
    }
}
public abstract class OAuth2UserInfo {
    protected Map<String, Object> attributes;

    public OAuth2UserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    public Map<String, Object> getAttributes() {
        return attributes;
    }

    public abstract String getId();
    public abstract String getName();
    public abstract String getEmail();
}
public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
    public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
        super(attributes);
    }

    @Override
    public String getId() {
        return (String) attributes.get("sub");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }
}

임시로 Google에 대한 UserInfo만 생성하였다. 필요하다면 다른 플랫폼에 대한 UserInfo를 생성하길 바란다.

추가하는 방법은 별거 없다. 코딩을 하면서 Resource Server에 유저에 관한 정보를 받을 때 어떠한 key값으로 받는지 확인하고 위의 구글과 같이 추가하면 된다.

Token

우선 우리가 알고있는 JWT는 JWS(Json Web Signature)를 의미하는 경우이다. 즉, 어떠한 JWT를 만들 때는 signature가 필수라는 뜻이다.

또한, 인증시 Refresh Token을 사용했다. Refresh Token을 사용한 이유는 JWT가 만료 기간이 지났을 경우 Refresh Token을 메인으로 사용하고 보조 토큰을 해당 Refresh Token을 만들게 되면 비용 절감이 되기 때문이다.

이러한 이유로 처음 JWT를 만들 때 Refresh Token을 같이 만든다.

아래와 같이 토큰을 생성하는 코드를 작성하였다.

@Slf4j
@RequiredArgsConstructor
public class AuthToken {

    @Getter
    private final String token;
    private final Key key;

    private static final String AUTHORITIES_KEY = "role";

    AuthToken(String id, Date expiry, Key key) {
        this.key = key;
        this.token = createAuthToken(id, expiry);
    }

    AuthToken(String id, String role, Date expiry, Key key) {
        this.key = key;
        this.token = createAuthToken(id, role, expiry);
    }

    private String createAuthToken(String id, Date expiry) {
        return Jwts.builder()
                .setSubject(id)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(expiry)
                .compact();
    }

    private String createAuthToken(String id, String role, Date expiry) {
        return Jwts.builder()
                .setSubject(id)
                .claim(AUTHORITIES_KEY, role)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(expiry)
                .compact();
    }

    public boolean validate() {
        return this.getTokenClaims() != null;
    }

    public Claims getTokenClaims() {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (SignatureException e) {
            log.error("Invalid JWT signature.");
        } catch (MalformedJwtException e) {
            log.error("Invalid JWT token.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token.");
        } catch (IllegalArgumentException e) {
            log.error("JWT token compact of handler are invalid.");
        }
        return null;
    }

    public Claims getExpiredTokenClaims() {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token.");
            return e.getClaims();
        }
        return null;
    }
}
@Slf4j
public class AuthTokenProvider {
    private final Key key;
    private static final String AUTHORITIES_KEY = "role";

    public AuthTokenProvider(String secret) {
        this.key = Keys.hmacShaKeyFor(secret.getBytes());
    }

    public AuthToken createAuthToken(String id, Date expiry) {
        return new AuthToken(id, expiry, key);
    }

    public AuthToken createAuthToken(String id, String role, Date expiry) {
        return new AuthToken(id, role, expiry, key);
    }

    public AuthToken convertAuthToken(String token) {
        return new AuthToken(token, key);
    }

    public Authentication getAuthentication(AuthToken authToken) {

        if(authToken.validate()) {
            Claims claims = authToken.getTokenClaims();
            Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(new String[]{claims.get(AUTHORITIES_KEY).toString()})
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());

            log.debug("claims subject := [{}]", claims.getSubject());
            User principal = new User(claims.getSubject(), "", authorities);

            return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);
        } else {
            throw new TokenValidFailedException();
        }
    }
}

위의 코드처럼 AuthToken 클래스 안에 토큰과 키 값이 있는데 키 값을 signature로 지정해서 토큰을 생성하였다.

아래와 같이 Refresh Token를 코딩하였다.

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER_REFRESH_TOKEN")
public class UserRefreshToken {
    @JsonIgnore
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;

    @Column(name = "USER_ID", length = 64, unique = true)
    @NotNull
    @Size(max = 64)
    private String userId;

    @Column(name = "REFRESH_TOKEN", length = 256)
    @NotNull
    @Size(max = 256)
    private String refreshToken;

    public UserRefreshToken(String userId, String refreshToken) {
        this.userId = userId;
        this.refreshToken = refreshToken;
    }
}

전체 코드 Github

https://github.com/gwj0421/OAuth2Login/tree/main

profile
내게 남은 소중한 자식은 누군지 아나? 쑨양이다!

0개의 댓글