Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 사용자 인증 로직을 구현한다. #9

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.seong.shoutlink.domain.auth;

import com.seong.shoutlink.domain.auth.service.response.ClaimsResponse;
import com.seong.shoutlink.domain.auth.service.response.TokenResponse;
import com.seong.shoutlink.domain.member.MemberRole;

public interface JwtProvider {
TokenResponse createToken(Long memberId, MemberRole memberRole);

ClaimsResponse parseAccessToken(String accessToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public CreateMemberResponse createMember(CreateMemberCommand command) {
command.email(),
passwordEncoder.encode(command.password()),
command.nickname(),
MemberRole.USER);
MemberRole.ROLE_USER);
return new CreateMemberResponse(memberRepository.save(member));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.seong.shoutlink.domain.auth.service.response;

import com.seong.shoutlink.domain.member.MemberRole;
import java.util.List;

public record ClaimsResponse(Long memberId, MemberRole memberRole, List<String> authorities) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

@Getter
public enum MemberRole {
USER(Constants.ROLE_USER, List.of(Constants.ROLE_USER)),
ADMIN(Constants.ROLE_ADMIN, List.of(Constants.ROLE_USER, Constants.ROLE_ADMIN));
ROLE_USER(Constants.ROLE_USER, List.of(Constants.ROLE_USER)),
ROLE_ADMIN(Constants.ROLE_ADMIN, List.of(Constants.ROLE_USER, Constants.ROLE_ADMIN));

private final String value;
private final List<String> authorities;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.seong.shoutlink.global.auth.authentication;

import java.util.List;

public interface Authentication {

Long getPrincipal();

List<String> getAuthorities();

String getCredentials();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.seong.shoutlink.global.auth.authentication;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AuthenticationContext {

private final ThreadLocal<Authentication> context = new ThreadLocal<>();

public void setAuthentication(Authentication authentication) {
context.set(authentication);
log.debug("[Auth] 인증 컨텍스트 설정됨");
}

public Authentication getAuthentication() {
return context.get();
}

public void releaseContext() {
context.remove();
log.debug("[Auth] 인증 컨텍스트 소멸됨");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.seong.shoutlink.global.auth.authentication;

import com.seong.shoutlink.domain.member.MemberRole;
import java.util.List;

public record JwtAuthentication(
Long memberId,
MemberRole memberRole,
String accessToken) implements Authentication {

@Override
public Long getPrincipal() {
return memberId;
}

@Override
public List<String> getAuthorities() {
return memberRole.getAuthorities();
}

@Override
public String getCredentials() {
return accessToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.seong.shoutlink.global.auth.authentication;

import com.seong.shoutlink.global.exception.ErrorCode;
import com.seong.shoutlink.global.exception.ShoutLinkException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationInterceptor implements HandlerInterceptor {

private static final String HEADER = "Authorization";
private static final String BEARER = "Bearer ";

private final JwtAuthenticationProvider jwtAuthenticationProvider;
private final AuthenticationContext authenticationContext;

@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
log.debug("[Auth] JWT 인증 인터셉터 시작");
String bearerAccessToken = request.getHeader(HEADER);
if(Objects.nonNull(bearerAccessToken)) {
log.debug("[Auth] JWT 인증 프로세스 시작");
String accessToken = removeBearer(bearerAccessToken);
JwtAuthentication authentication = jwtAuthenticationProvider.authenticate(accessToken);
authenticationContext.setAuthentication(authentication);
log.debug("[Auth] JWT 인증 프로세스 종료. 사용자 인증됨. {}", authentication);
}
log.debug("[Auth] Jwt 인증 인터셉터 종료");
return true;
}

private String removeBearer(String bearerAccessToken) {
if(!bearerAccessToken.contains(BEARER)) {
throw new ShoutLinkException("올바르지 않은 액세스 토큰 형식입니다.", ErrorCode.INVALID_ACCESS_TOKEN);
}
return bearerAccessToken.replace(BEARER, "");
}

@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
authenticationContext.releaseContext();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.seong.shoutlink.global.auth.authentication;

import com.seong.shoutlink.domain.auth.JwtProvider;
import com.seong.shoutlink.domain.auth.service.response.ClaimsResponse;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class JwtAuthenticationProvider {

private final JwtProvider jwtProvider;

public JwtAuthentication authenticate(String accessToken) {
ClaimsResponse claims = jwtProvider.parseAccessToken(accessToken);
return new JwtAuthentication(claims.memberId(), claims.memberRole(), accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
package com.seong.shoutlink.global.auth.jwt;

import com.seong.shoutlink.domain.auth.JwtProvider;
import com.seong.shoutlink.domain.auth.service.response.ClaimsResponse;
import com.seong.shoutlink.domain.auth.service.response.TokenResponse;
import com.seong.shoutlink.domain.member.MemberRole;
import com.seong.shoutlink.global.exception.ErrorCode;
import com.seong.shoutlink.global.exception.ShoutLinkException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import javax.crypto.SecretKey;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JJwtProvider implements JwtProvider {

private static final String ROLE = "role";
Expand Down Expand Up @@ -73,4 +82,22 @@ private String createRefreshToken(Long userId, MemberRole memberRole) {
.signWith(refreshSecretKey)
.compact();
}

@Override
public ClaimsResponse parseAccessToken(String accessToken) {
try {
Claims claims = accessTokenParser.parseSignedClaims(accessToken).getPayload();
Long memberId = Long.valueOf(claims.getSubject());
MemberRole memberRole = MemberRole.valueOf(claims.get(ROLE, String.class));
List<String> authorities = memberRole.getAuthorities();
return new ClaimsResponse(memberId, memberRole, authorities);
} catch (ExpiredJwtException e) {
throw new ShoutLinkException("만료된 액세스 토큰입니다.", ErrorCode.EXPIRED_ACCESS_TOKEN);
} catch (JwtException e) {
log.debug("[Ex] {} 유효하지 않은 액세스 토큰입니다.", e.getClass().getSimpleName());
} catch (RuntimeException e) {
log.debug("[Ex] {} 유효하지 않은 인자가 전달되었습니다.", e.getClass().getSimpleName());
}
throw new ShoutLinkException("유요하지 않은 액세스 토큰입니다.", ErrorCode.INVALID_ACCESS_TOKEN);
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/seong/shoutlink/global/config/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.seong.shoutlink.domain.auth.JwtProvider;
import com.seong.shoutlink.domain.auth.PasswordEncoder;
import com.seong.shoutlink.domain.auth.SimplePasswordEncoder;
import com.seong.shoutlink.global.auth.authentication.AuthenticationContext;
import com.seong.shoutlink.global.auth.authentication.JwtAuthenticationProvider;
import com.seong.shoutlink.global.auth.jwt.JJwtProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
Expand All @@ -25,4 +27,14 @@ public JwtProvider jwtProvider(
public PasswordEncoder passwordEncoder() {
return new SimplePasswordEncoder();
}

@Bean
public JwtAuthenticationProvider jwtAuthenticationProvider(JwtProvider jwtProvider) {
return new JwtAuthenticationProvider(jwtProvider);
}

@Bean
public AuthenticationContext authenticationContext() {
return new AuthenticationContext();
}
}
25 changes: 25 additions & 0 deletions src/main/java/com/seong/shoutlink/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.seong.shoutlink.global.config;

import com.seong.shoutlink.global.auth.authentication.AuthenticationContext;
import com.seong.shoutlink.global.auth.authentication.JwtAuthenticationInterceptor;
import com.seong.shoutlink.global.auth.authentication.JwtAuthenticationProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final JwtAuthenticationProvider jwtAuthenticationProvider;
private final AuthenticationContext authenticationContext;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new JwtAuthenticationInterceptor(jwtAuthenticationProvider, authenticationContext))
.order(1)
.addPathPatterns("/api/**");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
@AllArgsConstructor
public enum ErrorCode {
ILLEGAL_ARGUMENT("SL001"),
UNAUTHENTICATED("SL401"),
UNAUTHENTICATED("SL101"),
INVALID_ACCESS_TOKEN("SL102"),
EXPIRED_ACCESS_TOKEN("SL103"),
DUPLICATE_EMAIL("SL901"),
DUPLICATE_NICKNAME("SL902");

Expand Down
28 changes: 27 additions & 1 deletion src/test/java/com/seong/shoutlink/base/BaseControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.seong.shoutlink.base.BaseControllerTest.BaseControllerConfig;
import com.seong.shoutlink.domain.auth.JwtProvider;
import com.seong.shoutlink.domain.auth.service.AuthService;
import com.seong.shoutlink.fixture.AuthFixture;
import com.seong.shoutlink.global.auth.authentication.AuthenticationContext;
import com.seong.shoutlink.global.auth.authentication.JwtAuthenticationProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
Expand All @@ -20,10 +27,29 @@
import org.springframework.web.filter.CharacterEncodingFilter;

@WebMvcTest
@Import({RestDocsConfig.class})
@Import({RestDocsConfig.class, BaseControllerConfig.class})
@ExtendWith(RestDocumentationExtension.class)
public class BaseControllerTest {

@TestConfiguration
static class BaseControllerConfig {

@Bean
public JwtProvider jwtProvider() {
return AuthFixture.jwtProvider();
}

@Bean
public JwtAuthenticationProvider jwtAuthenticationProvider(JwtProvider jwtProvider) {
return new JwtAuthenticationProvider(jwtProvider);
}

@Bean
public AuthenticationContext authenticationContext() {
return new AuthenticationContext();
}
}

protected MockMvc mockMvc;

@Autowired
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ void setUp() {
String email = "stub@stub.com";
String password = "stub123!";
String nickname = "stub";
MemberRole memberRole = MemberRole.USER;
MemberRole memberRole = MemberRole.ROLE_USER;
savedMember = new Member(email, password, nickname, memberRole);
memberRepository = new StubMemberRepository(savedMember);
authService = new AuthService(memberRepository, passwordEncoder, jwtProvider);
Expand Down Expand Up @@ -138,7 +138,7 @@ void setUp() {
savedMemberPassword = "stub123!";
String password = passwordEncoder.encode(savedMemberPassword);
String nickname = "stub";
MemberRole memberRole = MemberRole.USER;
MemberRole memberRole = MemberRole.ROLE_USER;
savedMember = new Member(1L, email, password, nickname, memberRole);
memberRepository = new StubMemberRepository(savedMember);
authService = new AuthService(memberRepository, passwordEncoder, jwtProvider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class NewMember {
private String email = "email@email.com";
private String password = "1234";
private String nickname = "nickname";
private MemberRole memberRole = MemberRole.USER;
private MemberRole memberRole = MemberRole.ROLE_USER;

@Test
@DisplayName("회원이 생성된다.")
Expand Down
16 changes: 16 additions & 0 deletions src/test/java/com/seong/shoutlink/fixture/AuthFixture.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.seong.shoutlink.fixture;

import com.seong.shoutlink.domain.auth.JwtProvider;
import com.seong.shoutlink.global.auth.jwt.JJwtProvider;

public final class AuthFixture {

public static JwtProvider jwtProvider() {
String issuer = "test";
int expirySeconds = 3600;
int refreshExpirySeconds = 18000;
String secret = "thisisjusttestaccesssecretsodontworry";
String refreshSecret = "thisisjusttestrefreshsecretsodontworry";
return new JJwtProvider(issuer, expirySeconds, refreshExpirySeconds, secret, refreshSecret);
}
}
Loading
Loading