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

[BE] 김예찬 로그인 #3

Open
wants to merge 23 commits into
base: yechan-kim
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9afaee8
chore: 의존성 추가 및 db 설정 추가
yechan-kim May 15, 2024
1c41a88
feat: 유저 엔티티 추가
yechan-kim May 15, 2024
8695519
feat: 유저 DTO 추가
yechan-kim May 15, 2024
ffab0ab
feat: 유저 repostiory 추가
yechan-kim May 15, 2024
c85f6f3
feat: DB에서 사용자를 검색하는 서비스 로직 구현
yechan-kim May 15, 2024
5b9a0d3
feat: 출석 엔티티 추가
yechan-kim May 15, 2024
f4a2ec8
feat: 요일 enum 추가
yechan-kim May 15, 2024
3dc7cd8
feat: 요일 repository 추가
yechan-kim May 15, 2024
1b5abec
feat: 토큰 DTO 추가
yechan-kim May 15, 2024
c549aef
feat: 토큰 생성 기능 추가
yechan-kim May 15, 2024
56007a4
feat: JWT 인증 필터 추가
yechan-kim May 15, 2024
f3dd33a
feat: 보안 설정 추가
yechan-kim May 15, 2024
86e14a5
feat: 출석 기능 구현
yechan-kim May 15, 2024
e3be414
feat: 로그인 및 회원 가입 기능 구현
yechan-kim May 15, 2024
c93e518
refactor: JAVA 버전 변경
yechan-kim May 15, 2024
8e5b642
refactor: 보안 설정 변경
yechan-kim May 16, 2024
3f32da2
refactor: 출석 엔티티 코드 정리
yechan-kim May 16, 2024
aabe08a
refactor: 요일 enum 코드 정리
yechan-kim May 16, 2024
866d19b
fix: register 메서드 500 err 해결
yechan-kim May 16, 2024
daf812e
fix: 출석률 비즈니스 로직 수정
yechan-kim May 16, 2024
a165f28
chore: DB H2 -> MySQL
yechan-kim May 16, 2024
6ec060e
refactor: 디렉터리 구조 변경
yechan-kim May 16, 2024
ddfb25c
test: 테스트 코드 작성
yechan-kim May 16, 2024
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
20 changes: 19 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,34 @@ group = 'leets'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '17'
sourceCompatibility = "17"
}

repositories {
mavenCentral()
}

dependencies {
//Spring Boot
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// MySQL
runtimeOnly 'mysql:mysql-connector-java:8.2.0'
runtimeOnly 'com.mysql:mysql-connector-j'
}

tasks.named('test') {
Expand Down
54 changes: 54 additions & 0 deletions http/test.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
### 1. 회원가입
POST localhost:8080/users/register
Authorization: Bearer {{access_token}}
Content-Type: application/json

{
"username": "test",
"password": "test",
"confirmPassword": "test",
"name": "test",
"partName": "test"
}

### 2. 로그인
POST localhost:8080/users/login
Authorization: Bearer {{access_token}}
Content-Type: application/json

{
"username": "test",
"password": "test",
"confirmPassword": "test",
"name": "test",
"partName": "test"
}

### 3. 중복 아이디 확인
GET localhost:8080/users/check-duplicate-id
Authorization: Bearer {{access_token}}
Content-Type: application/json

{
"username": "test",
"password": "test",
"confirmPassword": "test",
"name": "test",
"partName": "test"
}


### 4. 출석( 결석 )
PATCH localhost:8080/attendances
Authorization: Bearer {{access_token}}
Content-Type: application/json

### 5. 출석 정보 모두 조회
GET localhost:8080/attendances
Authorization: Bearer {{access_token}}
Content-Type: application/json

### 6. 출석률 조회
GET localhost:8080/attendances/rates
Authorization: Bearer {{access_token}}
Content-Type: application/json
63 changes: 63 additions & 0 deletions src/main/java/leets/attendance/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package leets.attendance.config;

import leets.attendance.filter.JwtFilter;
import leets.attendance.domain.TokenProvider;
import lombok.RequiredArgsConstructor;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenProvider tokenProvider;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// CSRF 설정 Disable
http
.csrf(AbstractHttpConfigurer::disable)

//CORS 설정 Disable
.cors(AbstractHttpConfigurer::disable)

//From 로그인 방식 disable
.formLogin(AbstractHttpConfigurer::disable)

//http basic 인증 방식 disable
.httpBasic(AbstractHttpConfigurer::disable)

// 시큐리티는 기본적으로 세션을 사용
// JWT는 세션을 사용하지 않기 때문에 세션 설정을 Stateless 로 설정

.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// 로그인, 회원가입 API 는 토큰이 없는 상태에서 요청이 들어오기 때문에 permitAll 설정
//경로별 인가 작업

.authorizeHttpRequests((auth) -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/users/**", "/attendances/**").permitAll()
.anyRequest().permitAll()
)


// JwtFilter 를 addFilterBefore 로 등록했던 JwtSecurityConfig 클래스를 적용
.addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package leets.attendance.controller;

import leets.attendance.domain.Attendances;
import leets.attendance.service.AttendanceService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.util.List;

@RestController
@RequiredArgsConstructor
public class AttendanceController {

private final AttendanceService attendanceService;

@PatchMapping("/attendances")
public ResponseEntity<String> updateAttendances() {
String userid = SecurityContextHolder.getContext().getAuthentication().getName();
attendanceService.makeAttendance(userid, LocalDate.now());
return ResponseEntity.ok("Attendance updated");
}

@GetMapping("/attendances")
public ResponseEntity<List<Attendances>> getAllAttendances(){
String userid = SecurityContextHolder.getContext().getAuthentication().getName();
List<Attendances> allAttendanceRecord = attendanceService.getAllAttendanceRecord(userid);
return ResponseEntity.ok(allAttendanceRecord);
}

@GetMapping("/attendances/rates")
public ResponseEntity<Double> getAttendances() {
String userid = SecurityContextHolder.getContext().getAuthentication().getName();
double attendanceRate = attendanceService.getAttendanceRate(userid, LocalDate.now());
return ResponseEntity.ok(attendanceRate);
}
}
31 changes: 31 additions & 0 deletions src/main/java/leets/attendance/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package leets.attendance.controller;

import jakarta.validation.Valid;
import leets.attendance.dto.UserRequestDTO;
import leets.attendance.dto.UserResponseDTO;
import leets.attendance.dto.TokenDTO;
import leets.attendance.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;

@PostMapping("/users/register")
public ResponseEntity<UserResponseDTO> signup(@RequestBody @Valid UserRequestDTO userRequestDTO) {
return ResponseEntity.ok(authService.signup(userRequestDTO));
}

@PostMapping("/users/login")
public ResponseEntity<TokenDTO> login(@RequestBody @Valid UserRequestDTO userRequestDTO) {
return ResponseEntity.ok(authService.login(userRequestDTO));
}

@GetMapping("/users/check-duplicate-id")
public ResponseEntity<Boolean> checkDuplicateId(@RequestBody @Valid UserRequestDTO userRequestDTO) {
return ResponseEntity.ok(authService.checkDuplicateId(userRequestDTO));
}
}
25 changes: 25 additions & 0 deletions src/main/java/leets/attendance/domain/Attendances.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package leets.attendance.domain;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDate;

@Getter
@Setter
@AllArgsConstructor
@RequiredArgsConstructor
@Builder
@Entity
public class Attendances {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "attendance_id")
private Long id;

@ManyToOne
private User user;
private LocalDate date;
private Boolean attendance;
}
109 changes: 109 additions & 0 deletions src/main/java/leets/attendance/domain/TokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package leets.attendance.domain;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import leets.attendance.dto.TokenDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Slf4j
@Component
public class TokenProvider {

private static final String AUTHORITIES_KEY = "auth";
private static final String BEARER_TYPE = "Bearer";
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; // 30분

private final Key key;

public TokenProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}

public TokenDTO generateTokenDTO(Authentication authentication) {
// 권한들 가져오기
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));

long now = (new Date()).getTime();

// Access Token 생성
Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
String accessToken = Jwts.builder()
.setSubject(authentication.getName()) // payload "sub": "name"
.claim(AUTHORITIES_KEY, authorities) // payload "auth": "ROLE_USER"
.setExpiration(accessTokenExpiresIn) // payload "exp": 1516239022 (예시)
.signWith(key, SignatureAlgorithm.HS512) // header "alg": "HS512"
.compact();

return TokenDTO.builder()
.grantType(BEARER_TYPE)
.accessToken(accessToken)
.accessTokenExpiresIn(accessTokenExpiresIn.getTime())
.build();
}

public Authentication getAuthentication(String accessToken) {
// 토큰 복호화
Claims claims = parseClaims(accessToken);

if (claims.get(AUTHORITIES_KEY) == null) {
log.error("권한 정보가 없는 토큰입니다.");
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
}

// 클레임에서 권한 정보 가져오기
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());

// UserDetails 객체를 만들어서 Authentication 리턴
UserDetails principal = new User(claims.getSubject(), "", authorities);

return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}

public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}

private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
Loading