From 8726a9dc24a8ee9c808677a17fb4906769a6b42a Mon Sep 17 00:00:00 2001 From: rkddnr Date: Wed, 30 Aug 2023 10:18:24 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20=EC=9E=90=EC=B2=B4=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EA=B0=80=EC=9E=85,=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20J?= =?UTF-8?q?wt=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issues #4 --- .idea/.gitignore | 3 + .idea/compiler.xml | 15 +++ .idea/gradle.xml | 16 +++ .idea/jarRepositories.xml | 20 +++ .idea/misc.xml | 7 + .idea/modules.xml | 9 ++ .idea/modules/008main_project.main.iml | 8 ++ .idea/seb45_main_008.iml | 9 ++ .idea/uiDesigner.xml | 124 ++++++++++++++++++ .idea/vcs.xml | 6 + server/008main_project/build.gradle | 9 ++ .../auth/erroesponse/ErrorResponse.java | 21 +++ .../auth/filter/JwtAuthenticationFilter.java | 85 ++++++++++++ .../auth/filter/JwtVerificationFilter.java | 58 ++++++++ .../MemberAuthenticationFailureHandler.java | 37 ++++++ .../MemberAuthenticationSuccessHandler.java | 30 +++++ .../main_project/auth/jwt/JwtTokenizer.java | 101 ++++++++++++++ .../main_project/auth/logindto/LoginDto.java | 10 ++ .../memberdetail/MemberDetailsService.java | 71 ++++++++++ .../auth/utils/CustomAuthorityUtils.java | 28 ++++ .../config/SecurityConfiguration.java | 92 +++++++++++++ .../member/controller/MemberController.java | 58 ++++++++ .../member/dto/MemberPatchDto.java | 13 ++ .../member/dto/MemberPostDto.java | 15 +++ .../member/dto/MemberResponseDto.java | 15 +++ .../main_project/member/entity/Member.java | 9 +- .../member/mapper/MemberMapper.java | 20 +++ .../member/repository/MemberRepository.java | 11 ++ .../member/service/MemberService.java | 66 ++++++++++ .../src/main/resources/application.yml | 7 +- 30 files changed, 966 insertions(+), 7 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/modules/008main_project.main.iml create mode 100644 .idea/seb45_main_008.iml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/erroesponse/ErrorResponse.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtAuthenticationFilter.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtVerificationFilter.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationFailureHandler.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationSuccessHandler.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/jwt/JwtTokenizer.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/logindto/LoginDto.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/memberdetail/MemberDetailsService.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/CustomAuthorityUtils.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/config/SecurityConfiguration.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/member/controller/MemberController.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPatchDto.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPostDto.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberResponseDto.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/member/mapper/MemberMapper.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/member/repository/MemberRepository.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/member/service/MemberService.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..ab2110df --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..2a89c9f4 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..fdc392fe --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..acea2b35 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..94f9a64b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/008main_project.main.iml b/.idea/modules/008main_project.main.iml new file mode 100644 index 00000000..5e995830 --- /dev/null +++ b/.idea/modules/008main_project.main.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/seb45_main_008.iml b/.idea/seb45_main_008.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/seb45_main_008.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 00000000..2b63946d --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/server/008main_project/build.gradle b/server/008main_project/build.gradle index c3d93d6d..44916b0a 100644 --- a/server/008main_project/build.gradle +++ b/server/008main_project/build.gradle @@ -33,6 +33,15 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // security 작업을 위한 의존성 + implementation 'com.google.code.gson:gson:2.8.8' // gson 의존성 추가 + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' +// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' } tasks.named('test') { diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/erroesponse/ErrorResponse.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/erroesponse/ErrorResponse.java new file mode 100644 index 00000000..66a0fc99 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/erroesponse/ErrorResponse.java @@ -0,0 +1,21 @@ +package com.stockholm.main_project.auth.erroesponse; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.http.HttpStatus; + +@Getter +@Setter +public class ErrorResponse { + private int status; + private String error; + private String message; + + public static ErrorResponse of(HttpStatus httpStatus) { + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.setStatus(httpStatus.value()); + errorResponse.setError(httpStatus.getReasonPhrase()); + errorResponse.setMessage("Authentication failed"); + return errorResponse; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtAuthenticationFilter.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..0b637778 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,85 @@ +package com.stockholm.main_project.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stockholm.main_project.auth.jwt.JwtTokenizer; +import com.stockholm.main_project.auth.logindto.LoginDto; +import com.stockholm.main_project.member.entity.Member; +import lombok.SneakyThrows; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + private final JwtTokenizer jwtTokenizer; + + public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenizer jwtTokenizer) { + this.authenticationManager = authenticationManager; + this.jwtTokenizer = jwtTokenizer; + } + + @SneakyThrows + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { + ObjectMapper objectMapper = new ObjectMapper(); + LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword()); + + return authenticationManager.authenticate(authenticationToken); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain, + Authentication authResult) throws ServletException, IOException { + Member member = (Member) authResult.getPrincipal(); + + String accessToken = delegateAccessToken(member); + String refreshToken = delegateRefreshToken(member); + + response.setHeader("Authorization", "Bearer " + accessToken); + response.setHeader("Refresh", refreshToken); + //response.setHeader("Access-Control-Expose-Headers", "Authorization"); + //response.addHeader("Access-Control-Expose-Headers", "MemberId"); + + this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult); + } + + + private String delegateAccessToken(Member member) { + Map claims = new HashMap<>(); + claims.put("memberEmail", member.getEmail()); + claims.put("roles", member.getRoles()); + + String subject = member.getEmail(); + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes()); + + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey); + + return accessToken; + } + + private String delegateRefreshToken(Member member) { + String subject = member.getEmail(); + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes()); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey); + + return refreshToken; + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtVerificationFilter.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtVerificationFilter.java new file mode 100644 index 00000000..b592cf4c --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtVerificationFilter.java @@ -0,0 +1,58 @@ +package com.stockholm.main_project.auth.filter; + +import com.stockholm.main_project.auth.jwt.JwtTokenizer; +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class JwtVerificationFilter extends OncePerRequestFilter { + private final JwtTokenizer jwtTokenizer; + private final CustomAuthorityUtils authorityUtils; + + // (2) + public JwtVerificationFilter(JwtTokenizer jwtTokenizer, + CustomAuthorityUtils authorityUtils) { + this.jwtTokenizer = jwtTokenizer; + this.authorityUtils = authorityUtils; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + Map claims = verifyJws(request); + setAuthenticationToContext(claims); + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String authorization = request.getHeader("Authorization"); + + return authorization == null || !authorization.startsWith("Bearer"); + } + + private Map verifyJws(HttpServletRequest request) { + String jws = request.getHeader("Authorization").replace("Bearer ", ""); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + Map claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody(); + + return claims; + } + + private void setAuthenticationToContext(Map claims) { + String memberEmail = (String) claims.get("email"); + List authorities = authorityUtils.createAuthorities((List)claims.get("roles")); + Authentication authentication = new UsernamePasswordAuthenticationToken(memberEmail, null, authorities); + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationFailureHandler.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationFailureHandler.java new file mode 100644 index 00000000..601b8b67 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationFailureHandler.java @@ -0,0 +1,37 @@ +package com.stockholm.main_project.auth.handler; + +import com.google.gson.Gson; +import com.stockholm.main_project.auth.erroesponse.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class MemberAuthenticationFailureHandler implements AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + log.error("# Authentication failed: {}", exception.getMessage()); + + sendErrorResponse(response); + } + + private void sendErrorResponse(HttpServletResponse response) throws IOException { + Gson gson = new Gson(); + ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class)); + } +} + + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationSuccessHandler.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationSuccessHandler.java new file mode 100644 index 00000000..71e4e9ac --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package com.stockholm.main_project.auth.handler; + +import com.google.gson.Gson; +import com.stockholm.main_project.auth.erroesponse.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class MemberAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + log.info("# Authenticated successfully!"); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/jwt/JwtTokenizer.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/jwt/JwtTokenizer.java new file mode 100644 index 00000000..d9e75045 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/jwt/JwtTokenizer.java @@ -0,0 +1,101 @@ +package com.stockholm.main_project.auth.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; + + +@Component +public class JwtTokenizer { + + @Getter + @Value("abcdefgabcdefgabcdefgabcdefgabcdefg") //임시 키 + private String secretKey; + + @Getter + @Value("${jwt.access-token-expiration-minutes}") //Access Token에 대한 만료 시간 정보 + private int accessTokenExpirationMinutes; + + @Getter + @Value("${jwt.refresh-token-expiration-minutes}") //Refresh Token에 대한 만료 시간 정보 + private int refreshTokenExpirationMinutes; + + public String encodeBase64SecretKey(String secretKey){ + + return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + public String generateAccessToken(Map claims, + String subject, + Date expiration, + String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(Calendar.getInstance().getTime()) + .setExpiration(expiration) + .signWith(key) + .compact(); + } + + public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(Calendar.getInstance().getTime()) + .setExpiration(expiration) + .signWith(key) + .compact(); + } + + public Jws getClaims(String jws, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + Jws claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jws); + return claims; + } + + public void verifySignature(String jws, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jws); + + } + + public Date getTokenExpiration(int expirationMinutes) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, expirationMinutes); + Date expiration = calendar.getTime(); + + return expiration; + } + + private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) { + byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); + Key key = Keys.hmacShaKeyFor(keyBytes); + + return key; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/logindto/LoginDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/logindto/LoginDto.java new file mode 100644 index 00000000..de46e8d0 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/logindto/LoginDto.java @@ -0,0 +1,10 @@ +package com.stockholm.main_project.auth.logindto; + +import lombok.Getter; + +@Getter +public class LoginDto { + private String email; + private String password; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/memberdetail/MemberDetailsService.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/memberdetail/MemberDetailsService.java new file mode 100644 index 00000000..8c777677 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/memberdetail/MemberDetailsService.java @@ -0,0 +1,71 @@ +package com.stockholm.main_project.auth.memberdetail; + +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.repository.MemberRepository; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Optional; + +@Component +public class MemberDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + private final CustomAuthorityUtils authorityUtils; + + public MemberDetailsService(MemberRepository memberRepository, CustomAuthorityUtils authorityUtils) { + this.memberRepository = memberRepository; + this.authorityUtils = authorityUtils; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Optional optionalMember = memberRepository.findByEmail(email); + Member findMember = optionalMember.orElseThrow(() -> new UsernameNotFoundException("User not found")); + + return new MemberDetails(findMember); + } + + private final class MemberDetails extends Member implements UserDetails { + MemberDetails(Member member) { + setMemberId(member.getMemberId()); + setEmail(member.getEmail()); + setPassword(member.getPassword()); + setRoles(member.getRoles()); + } + + @Override + public Collection getAuthorities() { + return authorityUtils.createAuthorities(this.getRoles()); + } + + @Override + public String getUsername() { + return getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/CustomAuthorityUtils.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/CustomAuthorityUtils.java new file mode 100644 index 00000000..542cc7bd --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/CustomAuthorityUtils.java @@ -0,0 +1,28 @@ +package com.stockholm.main_project.auth.utils; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class CustomAuthorityUtils { + + private final List USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER"); + private final List USER_ROLES_STRING = List.of("USER"); + + public List createRoles(String email) { + + return USER_ROLES_STRING; + } + + public List createAuthorities(List roles) { + List authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .collect(Collectors.toList()); + return authorities; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/config/SecurityConfiguration.java b/server/008main_project/src/main/java/com/stockholm/main_project/config/SecurityConfiguration.java new file mode 100644 index 00000000..d5945704 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/config/SecurityConfiguration.java @@ -0,0 +1,92 @@ +package com.stockholm.main_project.config; + +import com.stockholm.main_project.auth.filter.JwtAuthenticationFilter; +import com.stockholm.main_project.auth.filter.JwtVerificationFilter; +import com.stockholm.main_project.auth.handler.MemberAuthenticationFailureHandler; +import com.stockholm.main_project.auth.handler.MemberAuthenticationSuccessHandler; +import com.stockholm.main_project.auth.jwt.JwtTokenizer; +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + private final JwtTokenizer jwtTokenizer; + private final CustomAuthorityUtils authorityUtils; + + public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) { + this.jwtTokenizer = jwtTokenizer; + this.authorityUtils = authorityUtils; + } + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .headers().frameOptions().sameOrigin() //H2 웹 콘솔에 정상적으로 접근 가능하도록 설정 + .and() + .csrf().disable() //CSRF 공격에 대한 설정 + .cors(withDefaults()) // CORS 설정을 추가 + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)를 통해서 세션을 생성하지 않도록 설정 + .and() + .formLogin().disable() // JSON 포맷 전달 방식 사용을 위해 비활성화 + .httpBasic().disable() // request 전송마다 로그인 정보를 받지 않을 것임으로 비활성화 + .apply(new CustomFilterConfigurer()) + .and() + .authorizeHttpRequests(authorize -> authorize + .antMatchers("members/login").permitAll() + .anyRequest().permitAll() + ); + + return httpSecurity.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + @Bean + CorsConfigurationSource corsConfigurationSource(){ + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } + public class CustomFilterConfigurer extends AbstractHttpConfigurer { + @Override + public void configure(HttpSecurity builder) throws Exception { + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + + JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer); + jwtAuthenticationFilter.setFilterProcessesUrl("/members/login"); + jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler()); + jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()); + builder.addFilter(jwtAuthenticationFilter); + + JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils); + + builder + .addFilter(jwtAuthenticationFilter) + .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); + } + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/controller/MemberController.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/controller/MemberController.java new file mode 100644 index 00000000..05c371c9 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/controller/MemberController.java @@ -0,0 +1,58 @@ +package com.stockholm.main_project.member.controller; + +import com.stockholm.main_project.member.dto.MemberPatchDto; +import com.stockholm.main_project.member.dto.MemberPostDto; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.mapper.MemberMapper; +import com.stockholm.main_project.member.repository.MemberRepository; +import com.stockholm.main_project.member.service.MemberService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/members") +public class MemberController { + private final MemberService memberService; + private final MemberMapper mapper; + + + public MemberController(MemberService memberService, MemberMapper mapper) { + this.memberService = memberService; + this.mapper = mapper; + } + + @PostMapping + public ResponseEntity postUser(@RequestBody MemberPostDto memberPostDto){ + Member member = mapper.memberPostToMember(memberPostDto); + + Member response = memberService.createMember(member); + + return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), + HttpStatus.CREATED); + } + + @PatchMapping("{userId}") + private ResponseEntity patchUser(@PathVariable long memberId, @RequestBody MemberPatchDto memberPatchDto){ + memberPatchDto.setMemberId(memberId); + + Member response = memberService.updateMember(mapper.memberPatchToMember(memberPatchDto)); + + return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), + HttpStatus.OK); + } + + @GetMapping("{userId}") + private ResponseEntity getUser(@PathVariable long memberId){ + Member response = memberService.findMember(memberId); + + return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK); + } + + @DeleteMapping("{userId}") + private ResponseEntity deleteUser(@PathVariable long memberId){ + memberService.deleteMember(memberId); + + return new ResponseEntity(HttpStatus.NO_CONTENT); + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPatchDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPatchDto.java new file mode 100644 index 00000000..231b1d06 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPatchDto.java @@ -0,0 +1,13 @@ +package com.stockholm.main_project.member.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MemberPatchDto { + private long memberId; + private String email; + private String name; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPostDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPostDto.java new file mode 100644 index 00000000..ccc6a3ce --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPostDto.java @@ -0,0 +1,15 @@ +package com.stockholm.main_project.member.dto; + +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; + +@Getter +@Setter +public class MemberPostDto { + private String email; + private String name; + private String password; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberResponseDto.java new file mode 100644 index 00000000..38e48d45 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberResponseDto.java @@ -0,0 +1,15 @@ +package com.stockholm.main_project.member.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class MemberResponseDto { + public long memberId; + public String email; + private String name; + private LocalDateTime createdAt; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/entity/Member.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/entity/Member.java index 1a51ebb6..2d318bc1 100644 --- a/server/008main_project/src/main/java/com/stockholm/main_project/member/entity/Member.java +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/entity/Member.java @@ -9,22 +9,21 @@ import java.util.ArrayList; import java.util.List; -@Entity +@Entity(name = "members") @Getter @Setter -@NoArgsConstructor public class Member extends Auditable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long memberId; - @Column + @Column(length = 30, nullable = false) private String email; - @Column + @Column(length = 10, nullable = false) private String name; - @Column + @Column(length = 255, nullable = false) private String password; @Enumerated(value = EnumType.STRING) diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/mapper/MemberMapper.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/mapper/MemberMapper.java new file mode 100644 index 00000000..4644056b --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/mapper/MemberMapper.java @@ -0,0 +1,20 @@ +package com.stockholm.main_project.member.mapper; + +import com.stockholm.main_project.member.dto.MemberPatchDto; +import com.stockholm.main_project.member.dto.MemberPostDto; +import com.stockholm.main_project.member.dto.MemberResponseDto; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.repository.MemberRepository; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface MemberMapper { + + Member memberPostToMember(MemberPostDto requestBody); + + Member memberPatchToMember(MemberPatchDto requestBody); + + MemberResponseDto memberToMemberResponseDto(Member member); + + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/repository/MemberRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/repository/MemberRepository.java new file mode 100644 index 00000000..64679bbd --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/repository/MemberRepository.java @@ -0,0 +1,11 @@ +package com.stockholm.main_project.member.repository; + +import com.stockholm.main_project.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/service/MemberService.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/service/MemberService.java new file mode 100644 index 00000000..965b1ba2 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/service/MemberService.java @@ -0,0 +1,66 @@ +package com.stockholm.main_project.member.service; + +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.repository.MemberRepository; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class MemberService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final CustomAuthorityUtils authorityUtils; + + public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder, CustomAuthorityUtils authorityUtils) { + this.memberRepository = memberRepository; + this.passwordEncoder = passwordEncoder; + this.authorityUtils = authorityUtils; + } + + public Member createMember(Member member) { + String encryptedPassword = passwordEncoder.encode(member.getPassword()); + member.setPassword(encryptedPassword); + + List roles = authorityUtils.createRoles(member.getEmail()); + member.setRoles(roles); + + member.setMemberStatus(Member.MemberStatus.MEMBER_ACTIVE); + Member saveMember = memberRepository.save(member); + System.out.println("# Create Member in DB"); + + return saveMember; + } + + public Member updateMember(Member member) { + Optional optionalMember = memberRepository.findById(member.getMemberId()); + + if (optionalMember.isPresent()) { + Member foundMember = optionalMember.get(); + + if (member.getName() != null) { + foundMember.setName(member.getName()); + } + memberRepository.save(foundMember); + + return foundMember; + } + + return null; + } + + public Member findMember(long memberId) { + + return memberRepository.findById(memberId).orElse(null); + } + + public void deleteMember(long memberId) { + + memberRepository.deleteById(memberId); + } +} + diff --git a/server/008main_project/src/main/resources/application.yml b/server/008main_project/src/main/resources/application.yml index c5ede541..b8ee4aad 100644 --- a/server/008main_project/src/main/resources/application.yml +++ b/server/008main_project/src/main/resources/application.yml @@ -11,7 +11,6 @@ spring: hibernate: ddl-auto: create-drop show-sql: true - springdoc: swagger-ui: path: /swagger-ui.html @@ -25,4 +24,8 @@ springdoc: default-consumes-media-type: application/json default-produces-media-type: application/json paths-to-match: - - /stockholm/** \ No newline at end of file + - /stockholm/** +jwt: + #key: ${JWT_SECRET_KEY} + access-token-expiration-minutes: 40 + refresh-token-expiration-minutes: 420 \ No newline at end of file From a1a772e6bdbb2d0e364efb2975be689f2a8b8d2a Mon Sep 17 00:00:00 2001 From: rkddnr Date: Thu, 31 Aug 2023 16:36:26 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issues #4 --- .idea/compiler.xml | 3 +- .idea/gradle.xml | 1 + server/008main_project/build.gradle | 10 ++- .../auth/mail/AccountController.java | 39 +++++++++ .../auth/mail/Dto/ConfirmPostDto.java | 11 +++ .../auth/mail/Dto/SendEmailPostDto.java | 19 +++++ .../main_project/auth/mail/EmailConfig.java | 55 ++++++++++++ .../main_project/auth/mail/EmailService.java | 5 ++ .../auth/mail/EmailServiceImpl.java | 85 +++++++++++++++++++ .../member/controller/MemberController.java | 5 +- .../member/dto/MemberPostDto.java | 21 ++++- .../main_project/member/entity/Member.java | 5 +- .../member/mapper/MemberMapper.java | 6 +- .../member/service/MemberService.java | 16 ++++ .../src/main/resources/application.yml | 11 +++ .../main/resources/stockholmMail.properties | 10 +++ 16 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/AccountController.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/ConfirmPostDto.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/SendEmailPostDto.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailConfig.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailService.java create mode 100644 server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailServiceImpl.java create mode 100644 server/008main_project/src/main/resources/stockholmMail.properties diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ab2110df..ab5befe4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,11 +2,12 @@ + - + diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 2a89c9f4..e1895728 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,5 +1,6 @@ +