Skip to content

Commit

Permalink
Merge pull request #15 from RecruitUs/feat/#13-signIn
Browse files Browse the repository at this point in the history
[FEAT]#13 JWT access-token, 스프링 시큐리티 적용 로그인 기능 구현
  • Loading branch information
LEEJaeHyeok97 authored Aug 27, 2023
2 parents 4202154 + 4a6a1f2 commit b49ef5d
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 14 deletions.
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,18 @@ dependencies {
implementation 'io.springfox:springfox-boot-starter:3.0.0'

// Security, Authentication
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'
implementation(group: 'io.jsonwebtoken', name: 'jjwt', version: '0.11.5')
implementation('io.jsonwebtoken:jjwt:0.9.1')

//java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter 에러 날 때 추가해야 하는 라이브러리
implementation('javax.xml.bind:jaxb-api:2.3.0')


// https://mvnrepository.com/artifact/com.auth0/java-jwt
implementation group: 'com.auth0', name: 'java-jwt', version: '4.3.0'



implementation('org.springframework.boot:spring-boot-starter')
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/example/rcp1/WantedApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication
public class WantedApplication {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@

import com.example.rcp1.domain.user.domain.User;
import com.example.rcp1.domain.user.domain.repository.UserRepository;
import com.example.rcp1.domain.user.dto.SignInReq;
import com.example.rcp1.domain.user.dto.SignUpReq;
import com.example.rcp1.global.BaseResponse;
import com.example.rcp1.global.SuccessCode;
import com.example.rcp1.global.CustomAuthenticationException;
import com.example.rcp1.global.config.security.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;


@Service
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;

@Value("${SECRET_KEY}")
private String secret_key;

// 1시간
private Long expiredMs = 1000 * 60 * 60L;

// 회원가입
@Transactional
Expand All @@ -39,4 +48,33 @@ public User signUp(SignUpReq signUpReq) {

return user;
}


public String signIn(SignInReq signInReq) {
// 인증 과정
// Optional<User> byEmail = userRepository.findByEmail(signInReq.getEmail());
// System.out.println("byEmail = " + byEmail);
// if (!BCrypt.checkpw(password, user.getPassword())) {
//
// return null;
// }

// 이메일을 통해 사용자 정보 조회
Optional<User> byEmail = userRepository.findByEmail(signInReq.getEmail());

if (byEmail.isPresent()) {
User user = byEmail.get();
if (BCrypt.checkpw(signInReq.getPassword(), user.getPassword())) {
return JwtUtil.createJwt(signInReq.getEmail(), secret_key, expiredMs);
} else {
throw new CustomAuthenticationException("비밀번호가 일치하지 않습니다.");
}
}

// return JwtUtil.createJwt(signInReq.getEmail(), secret_key, expiredMs);

throw new CustomAuthenticationException("사용자를 찾을 수 없습니다.");
}


}
18 changes: 11 additions & 7 deletions src/main/java/com/example/rcp1/domain/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import com.example.rcp1.domain.common.BaseEntity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Entity
@Getter
@Setter
@Data
@NoArgsConstructor
@Table(name = "USER")
public class User extends BaseEntity {
Expand Down Expand Up @@ -58,7 +61,6 @@ public class User extends BaseEntity {


@Builder

public User(Long id, String email, String password, String name, String phoneNumber, String specializedField, Long career, String position, String school, String job, LocalDateTime created, LocalDateTime updated, String status) {
this.id = id;
this.email = email;
Expand All @@ -74,4 +76,6 @@ public User(Long id, String email, String password, String name, String phoneNum
this.updated = updated;
this.status = status;
}


}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.example.rcp1.domain.user.domain.repository;

import com.example.rcp1.domain.user.domain.User;
import com.example.rcp1.domain.user.dto.SignInReq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {


Optional<User> findByEmail(String email);
}
15 changes: 15 additions & 0 deletions src/main/java/com/example/rcp1/domain/user/dto/SignInReq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.rcp1.domain.user.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class SignInReq {
@NotBlank
@Email
private String email;

@NotBlank
private String password;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@

import com.example.rcp1.domain.user.application.UserService;
import com.example.rcp1.domain.user.domain.User;
import com.example.rcp1.domain.user.dto.SignInReq;
import com.example.rcp1.domain.user.dto.SignUpReq;
import com.example.rcp1.global.BaseResponse;
import com.example.rcp1.global.CustomAuthenticationException;
import com.example.rcp1.global.ErrorCode;
import com.example.rcp1.global.SuccessCode;
import io.swagger.models.Response;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;


@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
Expand All @@ -31,11 +36,34 @@ public ResponseEntity<BaseResponse<User>> signUp(@Valid @RequestBody SignUpReq s
}
}

@GetMapping("/hello")
public ResponseEntity<String> hello() {
return ResponseEntity.ok("Hello, Postman!");

// 로그인 - access-token 발급 성공
@PostMapping("/signIn")
public ResponseEntity<BaseResponse<String>> signIn(@Valid @RequestBody SignInReq signInReq) {
try {
String token = userService.signIn(signInReq);
if (token != null) {
return ResponseEntity.ok(BaseResponse.success(SuccessCode.SIGNIN_SUCCESS, token));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(BaseResponse.error(ErrorCode.EXPIRED_TOKEN, "로그인에 실패했습니다."));
}
} catch (CustomAuthenticationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(BaseResponse.error(ErrorCode.REQUEST_VALIDATION_EXCEPTION, e.getMessage()));
}

// return ResponseEntity.ok(BaseResponse.success(SuccessCode.SIGNIN_SUCCESS, userService.login("이재혁", "")));
}

// 테스트용 인가 글쓰기 (삭제 예정)
@PostMapping("/write")
public ResponseEntity<String> writeReview(Authentication authentication) {
return ResponseEntity.ok().body(authentication.getName() + "님의 글작성이 완료되었습니다.");
}





}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.rcp1.global;

import lombok.RequiredArgsConstructor;

public class CustomAuthenticationException extends RuntimeException {
public CustomAuthenticationException(String message) {
super(message);
}

}
4 changes: 2 additions & 2 deletions src/main/java/com/example/rcp1/global/SuccessCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public enum SuccessCode {
// api 만들고 수정하기
// CUSTOM_SUCCESS(OK, "~ 조회에 성공했습니다."),
// CUSTOM_CREATED_SUCCESS(CREATED, "~ 생성에 성공했습니다.");
SIGNUP_SUCCESS(OK, "회원가입에 성공했습니다.");

SIGNUP_SUCCESS(OK, "회원가입에 성공했습니다."),
SIGNIN_SUCCESS(OK, "로그인에 성공했습니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.rcp1.global.config.security;

import com.example.rcp1.domain.user.application.UserService;
import com.example.rcp1.global.config.security.util.JwtUtil;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {


private final UserService userService;
@Value("${SECRET_KEY}")
private final String secretKey;

// 필터 관문
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("authorization : {}", authorization);

// 토큰 안보내면 블락
if (authorization == null || !authorization.startsWith("Bearer ")) {
// log.error("authentication을 잘못 보냈습니다.");
filterChain.doFilter(request, response);
return;
}

// 토큰 꺼내기
String token = authorization.split(" ")[1];

// 토큰 만료 여부 확인
if (JwtUtil.isExpired(token, secretKey)) {
log.error("토큰이 만료되었습니다.");
filterChain.doFilter(request, response);
return;
}

// UserName Token에서 꺼내기
String email = JwtUtil.getUserEmail(token, secretKey);
log.info("email:{}", email);

// 권한 부여
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(email, null, List.of(new SimpleGrantedAuthority("USER")));

// Detail
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.example.rcp1.global.config.security;

import com.example.rcp1.domain.user.application.UserService;
import com.example.rcp1.domain.user.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final UserService userService;

@Value("${SECRET_KEY}")
private String secretKey;


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeHttpRequests()
.requestMatchers("/user/signUp", "/user/signIn").permitAll()
.requestMatchers(HttpMethod.POST, "/user/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
.build();
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.rcp1.global.config.security.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtUtil {

public static String getUserEmail(String token, String secretkey) {
return Jwts.parser().setSigningKey(secretkey).parseClaimsJws(token)
.getBody().get("email", String.class);
}

public static boolean isExpired(String token, String secretkey) {
System.out.println("token = " + token);
return Jwts.parser().setSigningKey(secretkey).parseClaimsJws(token).getBody().getExpiration().before(new Date());
}

public static String createJwt(String email, String secretKey, Long expiredMs) {
Claims claims = Jwts.claims();
claims.put("email", email);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}

0 comments on commit b49ef5d

Please sign in to comment.