Skip to content

Commit

Permalink
feat: 사용자 인증 로직을 구현한다.
Browse files Browse the repository at this point in the history
feat: 사용자 인증 로직을 구현한다.
  • Loading branch information
hseong3243 authored Jan 23, 2024
2 parents fa76c2f + ce1b010 commit 982ebcf
Show file tree
Hide file tree
Showing 21 changed files with 605 additions and 13 deletions.
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

0 comments on commit 982ebcf

Please sign in to comment.