Skip to content

Commit

Permalink
feat: 사용자는 로그인을 할 수 있다.
Browse files Browse the repository at this point in the history
feat: 사용자는 로그인을 할 수 있다.
  • Loading branch information
hseong3243 authored Jan 22, 2024
2 parents 3d82962 + 943374d commit fa76c2f
Show file tree
Hide file tree
Showing 21 changed files with 417 additions and 9 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.seong.shoutlink.domain.auth;

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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
public interface PasswordEncoder {

String encode(String rawPassword);

boolean isMatches(String rawPassword, String encodedPassword);

boolean isNotMatches(String rawPassword, String encodedPassword);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package com.seong.shoutlink.domain.auth;

import org.springframework.stereotype.Service;

@Service
public class SimplePasswordEncoder implements PasswordEncoder {

@Override
public String encode(String rawPassword) {
return new StringBuilder(rawPassword).reverse().toString();
}

@Override
public boolean isMatches(String rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}

@Override
public boolean isNotMatches(String rawPassword, String encodedPassword) {
return !isMatches(rawPassword, encodedPassword);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.seong.shoutlink.domain.auth.controller;

import com.seong.shoutlink.domain.auth.controller.request.CreateMemberRequest;
import com.seong.shoutlink.domain.auth.controller.request.LoginRequest;
import com.seong.shoutlink.domain.auth.controller.response.LoginApiResponse;
import com.seong.shoutlink.domain.auth.service.AuthService;
import com.seong.shoutlink.domain.auth.service.request.CreateMemberCommand;
import com.seong.shoutlink.domain.auth.service.request.LoginCommand;
import com.seong.shoutlink.domain.auth.service.response.CreateMemberResponse;
import com.seong.shoutlink.domain.auth.service.response.LoginResponse;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -30,4 +36,33 @@ public ResponseEntity<CreateMemberResponse> createMember(
));
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@PostMapping("/login")
public ResponseEntity<LoginApiResponse> login(
@Valid @RequestBody LoginRequest request,
HttpServletResponse res) {
LoginResponse response = authService.login(new LoginCommand(
request.email(),
request.password()
));
addRefreshTokenCookie(res, response);
return ResponseEntity.ok(new LoginApiResponse(
response.memberId(),
response.accessToken()
));
}

private void addRefreshTokenCookie(
HttpServletResponse response,
LoginResponse loginResponse) {
ResponseCookie refreshTokenCookie = ResponseCookie
.from("refreshToken", loginResponse.refreshToken())
.path("/api")
.httpOnly(true)
.secure(true)
.sameSite("None")
.domain(".shoutlink.me")
.build();
response.addHeader("Set-Cookie", refreshTokenCookie.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.seong.shoutlink.domain.auth.controller.request;

import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
@NotBlank(message = "이메일은 공백일 수 없습니다.")
String email,
@NotBlank(message = "비밀번호는 공백일 수 없습니다.")
String password
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.seong.shoutlink.domain.auth.controller.response;

public record LoginApiResponse(Long memberId, String accessToken) {

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.seong.shoutlink.domain.auth.service;

import com.seong.shoutlink.domain.auth.JwtProvider;
import com.seong.shoutlink.domain.auth.PasswordEncoder;
import com.seong.shoutlink.domain.auth.service.request.CreateMemberCommand;
import com.seong.shoutlink.domain.auth.service.request.LoginCommand;
import com.seong.shoutlink.domain.auth.service.response.CreateMemberResponse;
import com.seong.shoutlink.domain.auth.service.response.LoginResponse;
import com.seong.shoutlink.domain.auth.service.response.TokenResponse;
import com.seong.shoutlink.domain.member.Member;
import com.seong.shoutlink.domain.member.MemberRole;
import com.seong.shoutlink.domain.member.service.MemberRepository;
Expand All @@ -21,6 +25,7 @@ public class AuthService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;

private void validatePassword(CreateMemberCommand command) {
if(!PASSWORD_PATTEN.matcher(command.password()).matches()) {
Expand All @@ -46,4 +51,20 @@ public CreateMemberResponse createMember(CreateMemberCommand command) {
MemberRole.USER);
return new CreateMemberResponse(memberRepository.save(member));
}

public LoginResponse login(LoginCommand command) {
Member member = memberRepository.findByEmail(command.email())
.orElseThrow(() -> new ShoutLinkException(
"이메일/비밀번호가 일치하지 않습니다.", ErrorCode.UNAUTHENTICATED));
if(passwordEncoder.isNotMatches(command.password(), member.getPassword())) {
throw new ShoutLinkException("이메일/비밀번호가 일치하지 않습니다.", ErrorCode.UNAUTHENTICATED);
}
TokenResponse tokenResponse = jwtProvider.createToken(
member.getMemberId(),
member.getMemberRole());
return new LoginResponse(
member.getMemberId(),
tokenResponse.accessToken(),
tokenResponse.refreshToken());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.seong.shoutlink.domain.auth.service.request;

public record LoginCommand(String email, String password) {

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

public record LoginResponse(Long memberId, String accessToken, String refreshToken) {

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

public record TokenResponse(String accessToken, String refreshToken) {

}
21 changes: 20 additions & 1 deletion src/main/java/com/seong/shoutlink/domain/member/MemberRole.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
package com.seong.shoutlink.domain.member;

import java.util.List;
import lombok.Getter;

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

private final String value;
private final List<String> authorities;

MemberRole(String value, List<String> authorities) {
this.value = value;
this.authorities = authorities;
}

private static class Constants {

private static final String ROLE_USER = "ROLE_USER";
private static final String ROLE_ADMIN = "ROLE_ADMIN";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.seong.shoutlink.global.auth.jwt;

import com.seong.shoutlink.domain.auth.JwtProvider;
import com.seong.shoutlink.domain.auth.service.response.TokenResponse;
import com.seong.shoutlink.domain.member.MemberRole;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;

public class JJwtProvider implements JwtProvider {

private static final String ROLE = "role";

private final String issuer;
private final int expirySeconds;
private final int refreshExpirySeconds;
private final SecretKey secretKey;
private final SecretKey refreshSecretKey;
private final JwtParser accessTokenParser;
private final JwtParser refreshTokenParser;

public JJwtProvider(
String issuer,
int expirySeconds,
int refreshExpirySeconds,
String secret,
String refreshSecret) {
this.issuer = issuer;
this.expirySeconds = expirySeconds;
this.refreshExpirySeconds = refreshExpirySeconds;
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.refreshSecretKey = Keys.hmacShaKeyFor(refreshSecret.getBytes(StandardCharsets.UTF_8));
this.accessTokenParser = Jwts.parser()
.verifyWith(secretKey)
.build();
this.refreshTokenParser = Jwts.parser()
.verifyWith(refreshSecretKey)
.build();
}

@Override
public TokenResponse createToken(Long memberId, MemberRole memberRole) {
String accessToken = createAccessToken(memberId, memberRole);
String refreshToken = createRefreshToken(memberId, memberRole);
return new TokenResponse(accessToken, refreshToken);
}

private String createAccessToken(Long userId, MemberRole memberRole) {
Date now = new Date();
Date expiresAt = new Date(now.getTime() + expirySeconds * 1000L);
return Jwts.builder()
.issuer(issuer)
.issuedAt(now)
.subject(userId.toString())
.expiration(expiresAt)
.claim(ROLE, memberRole.getValue())
.signWith(secretKey)
.compact();
}

private String createRefreshToken(Long userId, MemberRole memberRole) {
Date now = new Date();
Date expiresAt = new Date(now.getTime() + refreshExpirySeconds * 1000L);
return Jwts.builder()
.issuer(issuer)
.issuedAt(now)
.subject(userId.toString())
.expiration(expiresAt)
.claim(ROLE, memberRole.getValue())
.signWith(refreshSecretKey)
.compact();
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/seong/shoutlink/global/config/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.seong.shoutlink.global.config;

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.jwt.JJwtProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AuthConfig {

@Bean
public JwtProvider jwtProvider(
@Value("${jwt.issuer}") String issuer,
@Value("${jwt.expiry-seconds}") int expirySeconds,
@Value("${jwt.refresh-expiry-seconds}") int refreshExpirySeconds,
@Value("${jwt.secret}") String secret,
@Value("${jwt.refresh-secret}") String refreshSecret) {
return new JJwtProvider(issuer, expirySeconds, refreshExpirySeconds, secret, refreshSecret);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new SimplePasswordEncoder();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@AllArgsConstructor
public enum ErrorCode {
ILLEGAL_ARGUMENT("SL001"),
UNAUTHENTICATED("SL401"),
DUPLICATE_EMAIL("SL901"),
DUPLICATE_NICKNAME("SL902");

Expand Down
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

6 changes: 6 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
jwt:
issuer: test
expiry-seconds: 3600
secret: bAM5hBSuhrVhI6aVvNPk1r9rR3n8Ar4Atest
refresh-expiry-seconds: 18000
refresh-secret: cjOYckwZQhEqZOCZCCjCIG4W0HFPfSjMtest
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,14 @@ public class FakePasswordEncoder implements PasswordEncoder{
public String encode(String rawPassword) {
return new StringBuilder(rawPassword).reverse().toString();
}

@Override
public boolean isMatches(String rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}

@Override
public boolean isNotMatches(String rawPassword, String encodedPassword) {
return !isMatches(rawPassword, encodedPassword);
}
}
Loading

0 comments on commit fa76c2f

Please sign in to comment.