Skip to content

Commit

Permalink
Merge pull request #153 from woowacourse-teams/feature/#148
Browse files Browse the repository at this point in the history
JWT 관련 설정 및 로그인 API 기능 구현
  • Loading branch information
ksk0605 authored Jul 31, 2024
2 parents c85a227 + c2b1ff1 commit 4c96b02
Show file tree
Hide file tree
Showing 14 changed files with 294 additions and 4 deletions.
4 changes: 4 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// jwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'

// database
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package mouda.backend.auth.controller;

import lombok.RequiredArgsConstructor;
import mouda.backend.auth.dto.LoginRequest;
import mouda.backend.auth.dto.LoginResponse;
import mouda.backend.auth.service.AuthService;
import mouda.backend.common.RestResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/login")
public ResponseEntity<RestResponse<LoginResponse>> login(
@RequestBody LoginRequest loginRequest) {
LoginResponse response = authService.login(loginRequest);

return ResponseEntity.ok().body(new RestResponse<>(response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package mouda.backend.auth.dto;

public record LoginRequest(String nickname) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package mouda.backend.auth.dto;

public record LoginResponse(String accessToken) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package mouda.backend.auth.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum AuthErrorMessage {

UNAUTHORIZED("모임이 존재하지 않습니다.");

private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package mouda.backend.auth.exception;

import mouda.backend.exception.MoudaException;
import org.springframework.http.HttpStatus;

public class AuthException extends MoudaException {

public AuthException(HttpStatus httpStatus, AuthErrorMessage authErrorMessage) {
super(httpStatus, authErrorMessage.getMessage());
}
}
36 changes: 36 additions & 0 deletions backend/src/main/java/mouda/backend/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package mouda.backend.auth.service;

import mouda.backend.auth.dto.LoginRequest;
import mouda.backend.auth.dto.LoginResponse;
import mouda.backend.member.domain.Member;
import mouda.backend.member.repository.MemberRepository;
import mouda.backend.security.JwtProvider;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

private final JwtProvider jwtProvider;

private final MemberRepository memberRepository;


public AuthService(JwtProvider jwtProvider, MemberRepository memberRepository) {
this.jwtProvider = jwtProvider;
this.memberRepository = memberRepository;
}

public LoginResponse login(LoginRequest loginRequest) {
return memberRepository.findByNickname(loginRequest.nickname())
.map(member -> {
String token = jwtProvider.createToken(member);
return new LoginResponse(token);
})
.orElseGet(() -> {
Member newMember = new Member(loginRequest.nickname());
memberRepository.save(newMember);
String token = jwtProvider.createToken(newMember);
return new LoginResponse(token);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package mouda.backend.member.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import mouda.backend.member.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {

List<Member> findAllByMoimId(long moimId);
List<Member> findAllByMoimId(long moimId);

Optional<Member> findByNickname(String nickname);
}
75 changes: 75 additions & 0 deletions backend/src/main/java/mouda/backend/security/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package mouda.backend.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import mouda.backend.auth.exception.AuthErrorMessage;
import mouda.backend.auth.exception.AuthException;
import mouda.backend.member.domain.Member;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Component
public class JwtProvider {

@Value("${security.jwt.token.secret-key}")
private String secretKey;

@Value("${security.jwt.token.expire-length}")
private long validityInMilliseconds;

public JwtProvider(
@Value("${security.jwt.token.secret-key}") String secretKey,
@Value("${security.jwt.token.expire-length}") long validityInMilliseconds
) {
this.secretKey = secretKey;
this.validityInMilliseconds = validityInMilliseconds;
}

public String createToken(Member member) {
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);

return Jwts.builder()
.claim("id", member.getId())
.claim("nickname", member.getNickname())
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

public long extractMemberId(String token) {
Claims claims = getPayload(token);
return claims.get("id", Long.class);
}

public String extractNickname(String token) {
Claims claims = getPayload(token);
return claims.get("nickname", String.class);
}

public Claims getPayload(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();

validateExpiration(claims);
return claims;
} catch (JwtException | IllegalArgumentException e) {
throw new AuthException(HttpStatus.UNAUTHORIZED, AuthErrorMessage.UNAUTHORIZED);
}
}

public void validateExpiration(Claims claims) {
if (claims.getExpiration().before(new Date())) {
throw new AuthException(HttpStatus.UNAUTHORIZED, AuthErrorMessage.UNAUTHORIZED);
}
}
}

6 changes: 6 additions & 0 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ spring:
console:
enabled: true
path: /h2-console

security:
jwt:
token:
secret-key: kksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabo
expire-length: 3600000
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package mouda.backend.auth.controller;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import mouda.backend.auth.dto.LoginRequest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class AuthControllerTest {

@Autowired
AuthController authController;

@DisplayName("로그인 하기")
@Test
void login() {
LoginRequest request = new LoginRequest("테바");

RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(request)
.when().post("v1/auth/login")
.then().log().all()
.statusCode(200);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mouda.backend.auth.service;

import mouda.backend.auth.dto.LoginRequest;
import mouda.backend.auth.dto.LoginResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class AuthServiceTest {

@Autowired
AuthService authService;

@DisplayName("로그인을 시도한다.")
@Test
void login() {
LoginRequest request = new LoginRequest("테바");

LoginResponse response = authService.login(request);

System.out.println(response.accessToken());
}
}
45 changes: 45 additions & 0 deletions backend/src/test/java/mouda/backend/security/JwtProviderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package mouda.backend.security;

import static org.assertj.core.api.Assertions.assertThat;

import mouda.backend.config.DatabaseCleaner;
import mouda.backend.member.domain.Member;
import mouda.backend.member.repository.MemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class JwtProviderTest {

@Autowired
JwtProvider jwtProvider;

@Autowired
MemberRepository memberRepository;

@Autowired
private DatabaseCleaner databaseCleaner;

@AfterEach
void cleanUp() {
databaseCleaner.cleanUp();
}

@DisplayName("토큰을 발급한다.")
@Test
void createToken() {
String nickname = "테바";
Member member = Member.builder()
.nickname(nickname)
.build();
Member savedMember = memberRepository.save(member);
String token = jwtProvider.createToken(savedMember);

String savedNickname = jwtProvider.extractNickname(token);

assertThat(savedNickname).isEqualTo(nickname);
}
}
6 changes: 6 additions & 0 deletions backend/src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ spring:
sql:
init:
data-locations: classpath:test-data.sql

security:
jwt:
token:
secret-key: kksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabo
expire-length: 3600000

0 comments on commit 4c96b02

Please sign in to comment.