From fcc8c0f3cbe1ebc50b71b5f09e5d38acd2bc19e6 Mon Sep 17 00:00:00 2001 From: jiyun Date: Mon, 27 May 2024 15:51:02 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20Security=20&=20JWT=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#61?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todolist/config/SecurityConfig.java | 15 +++- .../todolist/config/SwaggerConfig.java | 1 + .../todolist/customError/ErrorCode.java | 7 +- .../jwt/CustomAuthenticationProvider.java | 41 +++++++++++ .../jwt/CustomUserDetailsService.java | 14 +++- .../todolist/jwt/JwtAuthenticationFilter.java | 2 +- .../java/com/jiyunio/todolist/jwt/JwtDTO.java | 4 +- .../com/jiyunio/todolist/jwt/JwtProvider.java | 46 ++++++++++-- .../com/jiyunio/todolist/member/Member.java | 10 ++- .../todolist/member/MemberService.java | 73 +++++++++---------- .../src/main/resources/application.properties | 2 +- 11 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomAuthenticationProvider.java diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SecurityConfig.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SecurityConfig.java index b6ff60d..88ba798 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SecurityConfig.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SecurityConfig.java @@ -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; @@ -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() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) //접근 제어 @@ -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(){ + return new CustomAuthenticationProvider(userDetailsService, passwordEncoder()); + } } diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SwaggerConfig.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SwaggerConfig.java index 6799912..ba61069 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SwaggerConfig.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/config/SwaggerConfig.java @@ -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 { diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/customError/ErrorCode.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/customError/ErrorCode.java index 9f94f3a..e859cb4 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/customError/ErrorCode.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/customError/ErrorCode.java @@ -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 중복된 값 diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomAuthenticationProvider.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomAuthenticationProvider.java new file mode 100644 index 0000000..d2682b8 --- /dev/null +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomAuthenticationProvider.java @@ -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); + } +} diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomUserDetailsService.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomUserDetailsService.java index ce78c2c..d38284e 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomUserDetailsService.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/CustomUserDetailsService.java @@ -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) + .orElseThrow(()-> new CustomException(HttpStatus.UNAUTHORIZED, ErrorCode.NOT_EXIST_MEMBER)); + return User.builder() + .username(member.getUserId()) + .password(member.getUserPw()) + .authorities(member.getRole()) + .build(); } } diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtAuthenticationFilter.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtAuthenticationFilter.java index fbe9826..7092dc0 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtAuthenticationFilter.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtAuthenticationFilter.java @@ -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; diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtDTO.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtDTO.java index 3e6c6e2..82e6f78 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtDTO.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtDTO.java @@ -7,11 +7,13 @@ @Getter @Setter public class JwtDTO { + private String 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; } diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtProvider.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtProvider.java index 9990524..0406e7f 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtProvider.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/jwt/JwtProvider.java @@ -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; @@ -28,25 +36,37 @@ public SecretKey getSecretKey() { return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); } - public JwtDTO createToken(String userId) { + public JwtDTO createToken(Authentication authentication) { + log.info("createToken 메소드 들어옴"); Date now = new Date(); + 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()); } @@ -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 유효기간 검사 @@ -65,7 +89,19 @@ public boolean validationToken(String accessToken) { Jws 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(); } } } diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/Member.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/Member.java index 95f0495..a99a956 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/Member.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/Member.java @@ -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 @@ -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) { @@ -38,7 +42,11 @@ protected void updateUserPw(String userPw) { @Override public Collection getAuthorities() { - return null; + Collection collecter = new ArrayList<>(); + collecter.add(() -> { + return "ROLE_" + role; + }); + return collecter; } @Override diff --git a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/MemberService.java b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/MemberService.java index 102af24..5f57262 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/MemberService.java +++ b/contents/todoListAPI/jiyun/todolist/src/main/java/com/jiyunio/todolist/member/MemberService.java @@ -2,16 +2,17 @@ import com.jiyunio.todolist.customError.CustomException; import com.jiyunio.todolist.customError.ErrorCode; +import com.jiyunio.todolist.jwt.CustomAuthenticationProvider; import com.jiyunio.todolist.jwt.JwtDTO; import com.jiyunio.todolist.jwt.JwtProvider; import com.jiyunio.todolist.member.dto.ChangeUserPwDTO; import com.jiyunio.todolist.member.dto.SignInDTO; import com.jiyunio.todolist.member.dto.SignUpDTO; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; @@ -20,10 +21,12 @@ @Service @RequiredArgsConstructor +@Slf4j public class MemberService { private final MemberRepository memberRepository; private final BCryptPasswordEncoder passwordEncoder; private final AuthenticationManagerBuilder authenticationManagerBuilder; + private final CustomAuthenticationProvider customAuthenticationProvider; private final JwtProvider jwtProvider; public String signUp(@Valid SignUpDTO signUpDto) { @@ -45,6 +48,7 @@ public String signUp(@Valid SignUpDTO signUpDto) { .userEmail(signUpDto.getUserEmail()) .build(); memberRepository.save(member); + log.info(member.getPassword()); return member.getUserId(); } else { // 비밀번호 불일치 @@ -53,47 +57,38 @@ public String signUp(@Valid SignUpDTO signUpDto) { } public JwtDTO signIn(@Valid SignInDTO signInDto) { -// if (memberRepository.existsByUserId(signInDto.getUserId())) { -// Member member = memberRepository.findByUserId(signInDto.getUserId()).get(); -// if (passwordEncoder.matches(signInDto.getUserPw(), member.getUserPw())) { - // 로그인 성공 - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(signInDto.getUserId(), signInDto.getUserPw()); - Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); - - return jwtProvider.createToken(signInDto.getUserId()); -// -// } -// // 회원의 비밀번호와 불일치 -// throw new CustomException(HttpStatus.NOT_FOUND, ErrorCode.WRONG_USERID_PASSWORD); -// } -// // 아이디 없음 -// throw new CustomException(HttpStatus.NOT_FOUND, ErrorCode.WRONG_USERID_PASSWORD); + log.info("로그인 service 들어옴"); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(signInDto.getUserId(), signInDto.getUserPw()); + log.info("usernamepassword 토큰 만듦"); + Authentication authentication = customAuthenticationProvider.authenticate(authenticationToken); + log.info("authenticationmanager build 완료"); + return jwtProvider.createToken(authentication); } - public void updateUserPw(Long id, @Valid ChangeUserPwDTO changeUserPwDto) { - Member member = memberRepository.findById(id).get(); - if (member.getUserPw().equals(changeUserPwDto.getUserPw())) { // 회원 비밀번호 확인 - if (changeUserPwDto.getChangePw().equals(changeUserPwDto.getConfirmChangePw())) { - // 비밀번호 업데이트 성공 - member.updateUserPw(changeUserPwDto.getChangePw()); - memberRepository.save(member); + public void updateUserPw (Long id, @Valid ChangeUserPwDTO changeUserPwDto){ + Member member = memberRepository.findById(id).get(); + if (member.getUserPw().equals(changeUserPwDto.getUserPw())) { // 회원 비밀번호 확인 + if (changeUserPwDto.getChangePw().equals(changeUserPwDto.getConfirmChangePw())) { + // 비밀번호 업데이트 성공 + member.updateUserPw(changeUserPwDto.getChangePw()); + memberRepository.save(member); + } else { + // 변경 비밀번호 불일치 + throw new CustomException(HttpStatus.BAD_REQUEST, ErrorCode.NOT_SAME_CONFIRM_PASSWORD); + } } else { - // 변경 비밀번호 불일치 - throw new CustomException(HttpStatus.BAD_REQUEST, ErrorCode.NOT_SAME_CONFIRM_PASSWORD); + // 회원의 비밀번호와 불일치 + throw new CustomException(HttpStatus.NOT_FOUND, ErrorCode.WRONG_USERID_PASSWORD); } - } else { - // 회원의 비밀번호와 불일치 - throw new CustomException(HttpStatus.NOT_FOUND, ErrorCode.WRONG_USERID_PASSWORD); } - } - public void deleteMember(Long id, String userPw) { - Member member = memberRepository.findById(id).get(); - if (member.getUserPw().equals(userPw)) { - // 회원 탈퇴 성공 - memberRepository.deleteById(id); - } else { // 비밀번호 불일치 - throw new CustomException(HttpStatus.NOT_FOUND, ErrorCode.WRONG_USERID_PASSWORD); + public void deleteMember (Long id, String userPw){ + Member member = memberRepository.findById(id).get(); + if (member.getUserPw().equals(userPw)) { + // 회원 탈퇴 성공 + memberRepository.deleteById(id); + } else { // 비밀번호 불일치 + throw new CustomException(HttpStatus.NOT_FOUND, ErrorCode.WRONG_USERID_PASSWORD); + } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/contents/todoListAPI/jiyun/todolist/src/main/resources/application.properties b/contents/todoListAPI/jiyun/todolist/src/main/resources/application.properties index f49582b..7237191 100644 --- a/contents/todoListAPI/jiyun/todolist/src/main/resources/application.properties +++ b/contents/todoListAPI/jiyun/todolist/src/main/resources/application.properties @@ -22,4 +22,4 @@ springdoc.swagger-ui.display-request-duration=true springdoc.swagger-ui.operations-sorter=alpha #JWT -spring.jwt.secret=PPMehyLW7XNk4mGjmpBFsA== \ No newline at end of file +spring.jwt.secret=MDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAIjRbjEGFCVVq8HDB2EKHE3oI23CqtTFYtVUietD0OG7AgMBAAE=