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

[Docs] week7 & [Feat] Security & JWT 구현 #64

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@

package com.jiyunio.todolist.config;
import com.jiyunio.todolist.jwt.CustomAuthenticationProvider;
import com.jiyunio.todolist.jwt.CustomUserDetailsService;
import com.jiyunio.todolist.jwt.JwtAuthenticationFilter;
import com.jiyunio.todolist.jwt.JwtProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
Expand All @@ -27,16 +31,17 @@
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService userDetailsService;
private final JwtProvider jwtProvider;
@Bean
public static BCryptPasswordEncoder bCryptPasswordEncoder() {
public static BCryptPasswordEncoder passwordEncoder() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 static class로 빈을 등록한 이유는?
그럴거면 따로 클래스를 만들어도 되지 않아?

return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)

//접근 제어
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

접근 제어를 다루는 부분이 100개면 100개의 requestMatchers를 적어줘야 할까?

Expand All @@ -46,12 +51,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.requestMatchers("/v3/**", "/swagger-ui/**").permitAll()
.anyRequest().authenticated()) //외의 접근은 인증 필수!

.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}


@Bean
public AuthenticationProvider authenticationProvider(){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 언제 쓰는 거야?
이것도 따로 클래스로 빼도 괜찮지 않을까?

return new CustomAuthenticationProvider(userDetailsService, passwordEncoder());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.context.SecurityContext;

@Configuration
public class SwaggerConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ public enum ErrorCode {
WRONG_USERID_PASSWORD("400_Bad_Request", "아이디 및 비밀번호가 맞지 않습니다."),

//401 Unauthorized
NO_AUTHENTICATION_MEMBER("401_Unauthorized", "인증된 회원이 아닙니다."),
NOT_EXIST_MEMBER("401_Unauthorized", "회원이 존재하지 않습니다."),

//403 Forbidden
NO_AUTHORIZED_MEMBER("403_Forbidden", "인가된 회원이 아닙니다."),
//403 Forbidden <- 인증 자체는 성공함
NOT_AUTHORIZATION("403_Forbidden", "접근 권한이 없습니다."),

//404 Not Found
NOT_EXIST_MEMBER("404_Not_Found", "회원이 존재하지 않습니다."),
NOT_EXIST_TODO("404_Not_Found", "TODO가 존재하지 않습니다."),

//409 Conflict 중복된 값
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 클래스는 네 코드에서 어떻게 동작하는거야?

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.jiyunio.todolist.jwt;


import com.jiyunio.todolist.customError.CustomException;
import com.jiyunio.todolist.customError.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Component
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService userDetailsService;
private final BCryptPasswordEncoder passwordEncoder;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userId = authentication.getName();
String userPw = (String) authentication.getCredentials();

UserDetails user = userDetailsService.loadUserByUsername(userId);
if (user == null || !this.passwordEncoder.matches(userPw, user.getPassword())) {
throw new CustomException(HttpStatus.UNAUTHORIZED, ErrorCode.NOT_EXIST_MEMBER);
}
return new UsernamePasswordAuthenticationToken(userId, userPw);
}

@Override
public boolean supports(Class<?> authentication) {
return CustomAuthenticationProvider.class.isAssignableFrom(authentication);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@

import com.jiyunio.todolist.customError.CustomException;
import com.jiyunio.todolist.customError.ErrorCode;
import com.jiyunio.todolist.member.Member;
import com.jiyunio.todolist.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;

@Override
public UserDetails loadUserByUsername(String userId) throws CustomException {
return memberRepository.findByUserId(userId)
.orElseThrow(()-> new CustomException(HttpStatus.UNAUTHORIZED, ErrorCode.NO_AUTHENTICATION_MEMBER));
Member member = memberRepository.findByUserId(userId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드는 결국 member의 정보를 가져오는 건데 리턴을 할 때는 왜 user.builder로 인스턴스화 해서 보내주는거야?

.orElseThrow(()-> new CustomException(HttpStatus.UNAUTHORIZED, ErrorCode.NOT_EXIST_MEMBER));
return User.builder()
.username(member.getUserId())
.password(member.getUserPw())
.authorities(member.getRole())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import java.io.IOException;

public class JwtAuthenticationFilter extends GenericFilterBean {
private JwtProvider jwtProvider;
private final JwtProvider jwtProvider;

public JwtAuthenticationFilter(JwtProvider jwtProvider) {
this.jwtProvider = jwtProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
@Getter
@Setter
public class JwtDTO {
private String grantType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서의 grantType이 의미하는 바가 뭐야?

private String userId;
private String accessToken;

@Builder
protected JwtDTO(String userId, String accessToken){
protected JwtDTO(String grantType, String userId, String accessToken){
this.grantType = grantType;
this.userId = userId;
this.accessToken = accessToken;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package com.jiyunio.todolist.jwt;

import com.jiyunio.todolist.customError.CustomException;
import com.jiyunio.todolist.customError.ErrorCode;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Component
@Slf4j
public class JwtProvider {
@Value("${spring.jwt.secret}")
private String secretKey;
Expand All @@ -28,25 +36,37 @@ public SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
}

public JwtDTO createToken(String userId) {
public JwtDTO createToken(Authentication authentication) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토큰을 만들 때 authentication 객체를 담는 이유가 뭐야?

log.info("createToken 메소드 들어옴");
Date now = new Date();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date 타입은 Java 8부터 Deprecated 됐는데 해당 타입을 사용한 이유가 있을까?

String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
System.out.println(authorities);
String accessToken = Jwts.builder()
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.setSubject(userId)
.claim("auth", authorities)
.setSubject(authentication.getName())
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidTime))
.signWith(getSecretKey())
.compact();

return JwtDTO.builder()
.userId(userId)
.grantType("Bearer")
.userId(authentication.getName())
.accessToken(accessToken)
.build();
}

//인증 (Authentication) 객체 반환
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
if (claims.get("auth") == null) {
throw new CustomException(HttpStatus.UNAUTHORIZED, ErrorCode.NOT_EXIST_MEMBER);
}

UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserId(accessToken));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
Expand All @@ -56,7 +76,11 @@ public String getUserId(String accessToken) {
}

public String resolveToken(HttpServletRequest request) {
return request.getHeader("AUTH-TOKEN");
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer")) {
return token.substring(7);
}
return null;
}

//token 유효기간 검사
Expand All @@ -65,7 +89,19 @@ public boolean validationToken(String accessToken) {
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(accessToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
throw new CustomException(HttpStatus.UNAUTHORIZED, ErrorCode.NOT_EXIST_MEMBER);
}
}

private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJwt(accessToken)
.getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

@Getter
Expand All @@ -25,11 +26,14 @@ public class Member implements UserDetails {

private String userEmail;

private String role;

@Builder
protected Member(String userId, String userPw, String userEmail) {
this.userId = userId;
this.userPw = userPw;
this.userEmail = userEmail;
this.role = "USER";
}

protected void updateUserPw(String userPw) {
Expand All @@ -38,7 +42,11 @@ protected void updateUserPw(String userPw) {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
Collection<GrantedAuthority> collecter = new ArrayList<>();
collecter.add(() -> {
return "ROLE_" + role;
});
return collecter;
}

@Override
Expand Down
Loading