From 19339b77c3e9aa6c392ee3f4a86acaed762165e5 Mon Sep 17 00:00:00 2001 From: PPakSang Date: Sun, 7 Aug 2022 22:06:02 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20spring=20security=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1,=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20spring=20security=20=EB=A5=BC=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=9D=B8=EC=A6=9D=EA=B3=BC=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=EB=A5=BC=20=EC=88=98=ED=96=89=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 + .../backend/config/SecurityConfigure.java | 43 ++++++++ .../mergedoc/backend/member/entity/Role.java | 7 ++ .../security/details/MemberDetails.java | 18 ++++ .../details/MemberDetailsService.java | 25 +++++ .../filter/JwtAuthenticationFilter.java | 52 +++++++++ .../security/provider/JwtProvider.java | 100 ++++++++++++++++++ .../backend/utils/cookie/CookieUtil.java | 39 +++++++ 8 files changed, 289 insertions(+) create mode 100644 src/main/java/com/mergedoc/backend/config/SecurityConfigure.java create mode 100644 src/main/java/com/mergedoc/backend/member/entity/Role.java create mode 100644 src/main/java/com/mergedoc/backend/security/details/MemberDetails.java create mode 100644 src/main/java/com/mergedoc/backend/security/details/MemberDetailsService.java create mode 100644 src/main/java/com/mergedoc/backend/security/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/mergedoc/backend/security/provider/JwtProvider.java create mode 100644 src/main/java/com/mergedoc/backend/utils/cookie/CookieUtil.java diff --git a/build.gradle b/build.gradle index a2667f8..a3b85bd 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0' + implementation group: 'com.auth0', name: 'java-jwt', version: '3.19.2' + + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-test' + runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/mergedoc/backend/config/SecurityConfigure.java b/src/main/java/com/mergedoc/backend/config/SecurityConfigure.java new file mode 100644 index 0000000..491037d --- /dev/null +++ b/src/main/java/com/mergedoc/backend/config/SecurityConfigure.java @@ -0,0 +1,43 @@ +package com.mergedoc.backend.config; + +import com.mergedoc.backend.security.filter.JwtAuthenticationFilter; +import com.mergedoc.backend.security.provider.JwtProvider; +import com.mergedoc.backend.utils.cookie.CookieUtil; +import org.springframework.context.annotation.Bean; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableWebSecurity +public class SecurityConfigure { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + public JwtAuthenticationFilter jwtAuthenticationFilter(JwtProvider provider, CookieUtil cookieUtil) { + return new JwtAuthenticationFilter(provider, cookieUtil); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, + JwtProvider jwtProvider, + CookieUtil cookieUtil) throws Exception{ + return http + .httpBasic().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/**").permitAll() + .and() + .addFilterBefore(jwtAuthenticationFilter(jwtProvider, cookieUtil), + UsernamePasswordAuthenticationFilter.class) + .build(); + } +} diff --git a/src/main/java/com/mergedoc/backend/member/entity/Role.java b/src/main/java/com/mergedoc/backend/member/entity/Role.java new file mode 100644 index 0000000..cfc764c --- /dev/null +++ b/src/main/java/com/mergedoc/backend/member/entity/Role.java @@ -0,0 +1,7 @@ +package com.mergedoc.backend.member.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum Role { + ROLE_NOT_ASSIGNED, ROLE_USER, ROLE,MANAGER +} diff --git a/src/main/java/com/mergedoc/backend/security/details/MemberDetails.java b/src/main/java/com/mergedoc/backend/security/details/MemberDetails.java new file mode 100644 index 0000000..18e0e98 --- /dev/null +++ b/src/main/java/com/mergedoc/backend/security/details/MemberDetails.java @@ -0,0 +1,18 @@ +package com.mergedoc.backend.security.details; + +import com.mergedoc.backend.member.entity.Member; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; + +public class MemberDetails extends User { + + public MemberDetails(Member member) { + super(member.getEmail(), + member.getPasswd(), + AuthorityUtils.createAuthorityList(member.getRole().toString())); + } + + public String getEmail() { + return this.getUsername(); + } +} diff --git a/src/main/java/com/mergedoc/backend/security/details/MemberDetailsService.java b/src/main/java/com/mergedoc/backend/security/details/MemberDetailsService.java new file mode 100644 index 0000000..eb45551 --- /dev/null +++ b/src/main/java/com/mergedoc/backend/security/details/MemberDetailsService.java @@ -0,0 +1,25 @@ +package com.mergedoc.backend.security.details; + +import com.mergedoc.backend.member.entity.Member; +import com.mergedoc.backend.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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.Service; + +@Service +@RequiredArgsConstructor +public class MemberDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("계정 정보가 올바르지 않습니다.")); + + return new MemberDetails(member); + } +} diff --git a/src/main/java/com/mergedoc/backend/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/mergedoc/backend/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..43b5fe6 --- /dev/null +++ b/src/main/java/com/mergedoc/backend/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,52 @@ +package com.mergedoc.backend.security.filter; + +import com.mergedoc.backend.security.provider.JwtProvider; +import com.mergedoc.backend.utils.cookie.CookieUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtProvider jwtProvider; + private final CookieUtil cookieUtil; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + String token = null; + Authentication authenticate; + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + Cookie memberTokenCookie = cookieUtil.getCookie(req, "member_token"); + + if (memberTokenCookie != null) { + token = memberTokenCookie.getValue(); + } + + if (token != null && !jwtProvider.isTokenExpired(token)) { + try { + String emailFromToken = jwtProvider.getEmailFromToken(token); + authenticate = jwtProvider. + authenticate(new UsernamePasswordAuthenticationToken(emailFromToken, "")); + SecurityContextHolder.getContext().setAuthentication(authenticate); + } catch (Exception e) { + } + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/mergedoc/backend/security/provider/JwtProvider.java b/src/main/java/com/mergedoc/backend/security/provider/JwtProvider.java new file mode 100644 index 0000000..8ea6ae8 --- /dev/null +++ b/src/main/java/com/mergedoc/backend/security/provider/JwtProvider.java @@ -0,0 +1,100 @@ +package com.mergedoc.backend.security.provider; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.mergedoc.backend.security.details.MemberDetails; +import com.mergedoc.backend.security.details.MemberDetailsService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +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.stereotype.Component; + +import java.util.Date; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class JwtProvider implements AuthenticationProvider { + + private final MemberDetailsService memberDetailsService; + + private static final long TOKEN_VALIDATION_SECOND = 1000L * 60 * 120; + private static final long REFRESH_TOKEN_VALIDATION_TIME = 1000L * 60 * 60 * 48; + + @Value("${spring.jwt.secret}") + private String SECRET_KEY; + + @Value("${group.name}") + private String ISSUER; + + private Algorithm getSigningKey(String secretKey) { + return Algorithm.HMAC256(secretKey); + } + + public String getEmailFromToken(String token) { + DecodedJWT verifiedToken = validateToken(token); + return verifiedToken.getClaim("email").asString(); + } + + private JWTVerifier getTokenValidator() { + return JWT.require(getSigningKey(SECRET_KEY)) + .withIssuer(ISSUER) + .build(); + } + + public String generateToken(Map payload) { + return doGenerateToken(TOKEN_VALIDATION_SECOND, payload); + } + + public String generateRefreshToken(Map payload) { + return doGenerateToken(REFRESH_TOKEN_VALIDATION_TIME, payload); + } + + private String doGenerateToken(long expireTime, Map payload) { + + return JWT.create() + .withIssuedAt(new Date(System.currentTimeMillis())) + .withExpiresAt(new Date(System.currentTimeMillis() + expireTime)) + .withPayload(payload) + .withIssuer(ISSUER) + .sign(getSigningKey(SECRET_KEY)); + } + + private DecodedJWT validateToken(String token) throws JWTVerificationException { + JWTVerifier validator = getTokenValidator(); + return validator.verify(token); + } + + public boolean isTokenExpired(String token) { + try { + DecodedJWT decodedJWT = validateToken(token); + return false; + } catch (JWTVerificationException e) { + return true; + } + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + MemberDetails userDetails = (MemberDetails) memberDetailsService + .loadUserByUsername((String) authentication.getPrincipal()); + + return new UsernamePasswordAuthenticationToken( + userDetails.getEmail(), + userDetails.getPassword(), + userDetails.getAuthorities() + ); + } + + @Override + public boolean supports(Class authentication) { + return false; + } +} diff --git a/src/main/java/com/mergedoc/backend/utils/cookie/CookieUtil.java b/src/main/java/com/mergedoc/backend/utils/cookie/CookieUtil.java new file mode 100644 index 0000000..a3725ae --- /dev/null +++ b/src/main/java/com/mergedoc/backend/utils/cookie/CookieUtil.java @@ -0,0 +1,39 @@ +package com.mergedoc.backend.utils.cookie; + +import org.springframework.stereotype.Component; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +@Component +public class CookieUtil { + + private final int COOKIE_VALIDATION_SECOND = 1000 * 60 * 60 * 48; + + public Cookie createCookie(String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(maxAge); + + return cookie; + } + + public Cookie createCookie(String name, String value) { + Cookie cookie = new Cookie(name, value); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(COOKIE_VALIDATION_SECOND); + + return cookie; + } + + public Cookie getCookie(HttpServletRequest req, String name) { + Cookie[] findCookies = req.getCookies(); + if (findCookies == null) return null; + for (Cookie cookie : findCookies) { + if (cookie.getName().equals(name)) return cookie; + } + return null; + } +} \ No newline at end of file