From d7fa6a070f72e2d6726f0ff73bb3408535440c7f Mon Sep 17 00:00:00 2001 From: junshock5 <61732452+junshock5@users.noreply.github.com> Date: Thu, 10 Dec 2020 18:31:15 +0900 Subject: [PATCH 1/4] =?UTF-8?q?#1=20=EC=9D=B8=EC=A6=9D=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20spring=20security,=20JWT=20-=20profiles=20add=20-?= =?UTF-8?q?=20project=20build,=20report=20Encoding=3DUTF8=20add=20-=20Swag?= =?UTF-8?q?ger=20UI=20add=20-=20h2databse,=20spring=20jdbc=20add=20-=20spr?= =?UTF-8?q?ing-security=20add=20-=20jpa=20,=20modelmapper=20add=20-=20jwt?= =?UTF-8?q?=20add=20-=20JWT=20=EC=9D=B8=EC=A6=9D=20=EC=9A=94=EC=95=BD=20(?= =?UTF-8?q?=ED=95=B5=EC=8B=AC=20=EC=BD=94=EB=93=9C)=20JwtTokenFilter=20(Jw?= =?UTF-8?q?tTokenFilter=ED=95=84=ED=84=B0=EB=8A=94=20=EA=B0=81=EA=B0=81?= =?UTF-8?q?=EC=9D=98=20API=20(=EC=9D=B8=EA=B0=80=EB=90=9C=EB=8B=A4=20/**?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=98=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=81=9D=EC=A0=90=EC=9D=98=20=EC=98=88=EC=99=B8=20(=ED=8F=AC?= =?UTF-8?q?=ED=95=A8)=20/users/signin)=EA=B3=BC=20=EB=81=9D=EC=A0=90=20sin?= =?UTF-8?q?gup=20(=20/users/signup).)=20JwtTokenFilterConfigurer=20(?= =?UTF-8?q?=EC=8A=A4=ED=94=84=EB=A7=81=20=EB=B6=80=ED=8A=B8=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20JwtTokenFilter=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4=20DefaultSecurityFilterChain)=20Jwt?= =?UTF-8?q?TokenProvider=20(=EC=95=A1=EC=84=B8=EC=8A=A4=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=98=20=EC=84=9C=EB=AA=85=20=ED=99=95=EC=9D=B8,?= =?UTF-8?q?=20=EC=95=A1=EC=84=B8=EC=8A=A4=20=ED=86=A0=ED=81=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20ID=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EC=97=AC=20=ED=81=B4=EB=A0=88=EC=9E=84=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EA=B3=A0=EC=9D=B4=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20UserContext=EB=A5=BC=20=EB=A7=8C?= =?UTF-8?q?=EB=93=AD=EB=8B=88=EB=8B=A4.,=20=EC=9E=98=EB=AA=BB=EB=90=98?= =?UTF-8?q?=EC=97=88=EA=B1=B0=EB=82=98=20=EB=A7=8C=EB=A3=8C=EB=90=98?= =?UTF-8?q?=EC=97=88=EA=B1=B0=EB=82=98=20=EC=98=88=EC=99=B8=EB=B0=9C?= =?UTF-8?q?=EC=83=9D)=20MyUserDetails=20(UserDetailsService=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A0=95=EC=9D=98=20loadUserbyUsername=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=A5=BC=20=EC=A0=95=EC=9D=98=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EA=B5=AC=ED=98=84,=20DaoAuthe?= =?UTF-8?q?nticationProvider=EC=9D=B8=EC=A6=9D=20=EC=A4=91=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=84=B8=EB=B6=80=20=EC=A0=95=EB=B3=B4=EB=A5=BC=EB=A1=9C?= =?UTF-8?q?=EB=93=9C)=20WebSecurityConfig=20(WebSecurityConfig=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20WebSecurityConfigurerAdapter=EB=8A=94=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A7=80=EC=A0=95=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20=EA=B5=AC=EC=84=B1=EC=9D=84=20=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=99=95=EC=9E=A5,=20JwtTokenFil?= =?UTF-8?q?ter,=20PasswordEncoder=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4?= =?UTF-8?q?=ED=99=94)=201.=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=8A=94=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EB=B6=80=EC=97=AC=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EC=97=90=20=EC=9E=90=EA=B2=A9=20=EC=A6=9D=EB=AA=85=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B3=B5=ED=95=98=EC=97=AC=20=EC=83=88=EB=A1=9C=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=EB=B0=8F=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=84=20=EC=96=BB=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=202.=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=8A=94=20?= =?UTF-8?q?=EB=B3=B4=ED=98=B8=20=EB=90=9C=20API=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EC=95=A1=EC=84=B8=EC=8A=A4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EA=B0=81=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EA=B3=BC=20=ED=95=A8=EA=BB=98=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=84=20=EB=B3=B4=EB=83=85=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=203.=20=EC=95=A1=EC=84=B8=EC=8A=A4=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=80=20=EC=84=9C=EB=AA=85=EB=90=98=EA=B3=A0=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20ID=20(=EC=98=88=20:=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20ID)=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EB=B6=80=EC=97=AC=20=ED=81=B4=EB=A0=88=EC=9E=84=EC=9D=84=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 68 ++++++++++++++ .../com/pay/billing/BillingApplication.java | 38 +++++++- .../billing/common/config/SwaggerConfig.java | 64 +++++++++++++ .../common/config/WebSecurityConfig.java | 80 ++++++++++++++++ .../common/exception/CustomException.java | 26 +++++ .../common/security/JwtTokenFilter.java | 41 ++++++++ .../security/JwtTokenFilterConfigurer.java | 22 +++++ .../common/security/JwtTokenProvider.java | 89 ++++++++++++++++++ .../common/security/MyUserDetails.java | 36 +++++++ .../billing/controller/UserController.java | 94 +++++++++++++++++++ .../com/pay/billing/domain/dto/UserDTO.java | 51 ++++++++++ .../billing/domain/dto/UserResponseDTO.java | 51 ++++++++++ .../com/pay/billing/domain/model/Role.java | 12 +++ .../com/pay/billing/domain/model/User.java | 67 +++++++++++++ .../domain/repository/UserRepository.java | 18 ++++ .../com/pay/billing/service/UserService.java | 71 ++++++++++++++ src/main/resources/application-dev.properties | 15 +++ .../resources/application-release.properties | 5 + src/main/resources/application.properties | 3 +- 19 files changed, 849 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/pay/billing/common/config/SwaggerConfig.java create mode 100644 src/main/java/com/pay/billing/common/config/WebSecurityConfig.java create mode 100644 src/main/java/com/pay/billing/common/exception/CustomException.java create mode 100644 src/main/java/com/pay/billing/common/security/JwtTokenFilter.java create mode 100644 src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java create mode 100644 src/main/java/com/pay/billing/common/security/JwtTokenProvider.java create mode 100644 src/main/java/com/pay/billing/common/security/MyUserDetails.java create mode 100644 src/main/java/com/pay/billing/controller/UserController.java create mode 100644 src/main/java/com/pay/billing/domain/dto/UserDTO.java create mode 100644 src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java create mode 100644 src/main/java/com/pay/billing/domain/model/Role.java create mode 100644 src/main/java/com/pay/billing/domain/model/User.java create mode 100644 src/main/java/com/pay/billing/domain/repository/UserRepository.java create mode 100644 src/main/java/com/pay/billing/service/UserService.java create mode 100644 src/main/resources/application-dev.properties create mode 100644 src/main/resources/application-release.properties diff --git a/pom.xml b/pom.xml index 8ea7bb8..59e013f 100644 --- a/pom.xml +++ b/pom.xml @@ -2,12 +2,14 @@ 4.0.0 + org.springframework.boot spring-boot-starter-parent 2.4.0 + com.pay billing 0.0.1-SNAPSHOT @@ -15,7 +17,10 @@ billing project for Spring Boot + UTF-8 + UTF-8 1.8 + com.pay.billing.BillingApplication @@ -29,11 +34,74 @@ lombok true + org.springframework.boot spring-boot-starter-test test + + + io.springfox + springfox-swagger2 + 2.9.2 + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.security + spring-security-test + test + + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.modelmapper + modelmapper + 2.3.5 + + + + javax.validation + validation-api + 2.0.1.Final + + diff --git a/src/main/java/com/pay/billing/BillingApplication.java b/src/main/java/com/pay/billing/BillingApplication.java index ce2ccf9..c864444 100644 --- a/src/main/java/com/pay/billing/BillingApplication.java +++ b/src/main/java/com/pay/billing/BillingApplication.java @@ -1,13 +1,49 @@ package com.pay.billing; +import com.pay.billing.domain.model.Role; +import com.pay.billing.domain.model.User; +import com.pay.billing.service.UserService; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.util.ArrayList; +import java.util.Arrays; @SpringBootApplication -public class BillingApplication { +public class BillingApplication implements CommandLineRunner { + + @Autowired + UserService userService; public static void main(String[] args) { SpringApplication.run(BillingApplication.class, args); } + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } + + @Override + public void run(String... params) throws Exception { + User admin = new User(); + admin.setUsername("admin"); + admin.setPassword("admin"); + admin.setEmail("admin@email.com"); + admin.setRoles(new ArrayList(Arrays.asList(Role.ROLE_ADMIN))); + + userService.signup(admin); + + User client = new User(); + client.setUsername("client"); + client.setPassword("client"); + client.setEmail("client@email.com"); + client.setRoles(new ArrayList(Arrays.asList(Role.ROLE_CLIENT))); + + userService.signup(client); + } } diff --git a/src/main/java/com/pay/billing/common/config/SwaggerConfig.java b/src/main/java/com/pay/billing/common/config/SwaggerConfig.java new file mode 100644 index 0000000..1540fa1 --- /dev/null +++ b/src/main/java/com/pay/billing/common/config/SwaggerConfig.java @@ -0,0 +1,64 @@ +package com.pay.billing.common.config; + +import com.google.common.base.Predicates; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.AuthorizationScope; +import springfox.documentation.service.SecurityReference; +import springfox.documentation.service.Tag; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + @Bean + public Docket swaggerApi() { + return new Docket(DocumentationType.SWAGGER_2)// + .select()// + .apis(RequestHandlerSelectors.any())// + .paths(Predicates.not(PathSelectors.regex("/error")))// + .build()// + .apiInfo(swaggerInfo()) + .useDefaultResponseMessages(false)// + .securitySchemes(Collections.singletonList(apiKey())) + .securityContexts(Collections.singletonList(securityContext())) + .tags(new Tag("users", "Operations about users"))// + .genericModelSubstitutes(Optional.class); + } + + private ApiInfo swaggerInfo() { + return new ApiInfoBuilder().title("Spring API Documentation") + .description("앱 개발시 사용되는 서버 API에 대한 연동 문서입니다").build(); + } + + private ApiKey apiKey() { + return new ApiKey("Authorization", "Authorization", "header"); + } + + private SecurityContext securityContext() { + return SecurityContext.builder() + .securityReferences(defaultAuth()) + .forPaths(PathSelectors.any()) + .build(); + } + + private List defaultAuth() { + AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + return Arrays.asList(new SecurityReference("Authorization", authorizationScopes)); + } +} \ No newline at end of file diff --git a/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java new file mode 100644 index 0000000..200ee9f --- /dev/null +++ b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java @@ -0,0 +1,80 @@ +package com.pay.billing.common.config; + +import com.pay.billing.common.security.JwtTokenFilterConfigurer; +import com.pay.billing.common.security.JwtTokenProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Override + protected void configure(HttpSecurity http) throws Exception { + + // Disable CSRF (cross site request forgery) + http.csrf().disable(); + + // No session will be created or used by spring security + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + // Entry points + http.authorizeRequests()// + .antMatchers("/users/signin").permitAll()// + .antMatchers("/users/signup").permitAll()// + .antMatchers("/h2-console/**/**").permitAll() + // Disallow everything else.. + .anyRequest().authenticated(); + + // If a user try to access a resource without having enough permissions + http.exceptionHandling().accessDeniedPage("/login"); + + // Apply JWT + http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); + + // Optional, if you want to test the API from a browser + // http.httpBasic(); + } + + @Override + public void configure(WebSecurity web) throws Exception { + // Allow swagger to be accessed without authentication + web.ignoring().antMatchers("/v2/api-docs")// + .antMatchers("/swagger-resources/**")// + .antMatchers("/swagger-ui.html")// + .antMatchers("/configuration/**")// + .antMatchers("/webjars/**")// + .antMatchers("/public") + + // Un-secure H2 Database (for testing purposes, H2 console shouldn't be unprotected in production) + .and() + .ignoring() + .antMatchers("/h2-console/**/**");; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } + + @Override + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + +} diff --git a/src/main/java/com/pay/billing/common/exception/CustomException.java b/src/main/java/com/pay/billing/common/exception/CustomException.java new file mode 100644 index 0000000..4433f3d --- /dev/null +++ b/src/main/java/com/pay/billing/common/exception/CustomException.java @@ -0,0 +1,26 @@ +package com.pay.billing.common.exception; + +import org.springframework.http.HttpStatus; + +public class CustomException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final String message; + private final HttpStatus httpStatus; + + public CustomException(String message, HttpStatus httpStatus) { + this.message = message; + this.httpStatus = httpStatus; + } + + @Override + public String getMessage() { + return message; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + +} diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java new file mode 100644 index 0000000..de31067 --- /dev/null +++ b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java @@ -0,0 +1,41 @@ +package com.pay.billing.common.security; + +import com.pay.billing.common.exception.CustomException; +import org.springframework.security.core.Authentication; +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; + +// We should use OncePerRequestFilter since we are doing a database call, there is no point in doing this more than once +public class JwtTokenFilter extends OncePerRequestFilter { + + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + String token = jwtTokenProvider.resolveToken(httpServletRequest); + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (CustomException ex) { + //this is very important, since it guarantees the user is not authenticated at all + SecurityContextHolder.clearContext(); + httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage()); + return; + } + + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + +} diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java b/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java new file mode 100644 index 0000000..dc34899 --- /dev/null +++ b/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java @@ -0,0 +1,22 @@ +package com.pay.billing.common.security; + +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter { + + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public void configure(HttpSecurity http) throws Exception { + JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } + +} diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java b/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java new file mode 100644 index 0000000..89e88cd --- /dev/null +++ b/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java @@ -0,0 +1,89 @@ +package com.pay.billing.common.security; + +import com.pay.billing.common.exception.CustomException; +import com.pay.billing.domain.model.Role; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Autowired; +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.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Component +public class JwtTokenProvider { + + /** + * THIS IS NOT A SECURE PRACTICE! For simplicity, we are storing a static key here. Ideally, in a + * microservices environment, this key would be kept on a config-server. + */ + @Value("${security.jwt.token.secret-key:secret-key}") + private String secretKey; + + @Value("${security.jwt.token.expire-length:3600000}") + private long validityInMilliseconds = 3600000; // 1h + + @Autowired + private MyUserDetails myUserDetails; + + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + public String createToken(String username, List roles) { + + Claims claims = Jwts.claims().setSubject(username); + claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); + + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder()// + .setClaims(claims)// + .setIssuedAt(now)// + .setExpiration(validity)// + .signWith(SignatureAlgorithm.HS256, secretKey)// + .compact(); + } + + public Authentication getAuthentication(String token) { + UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String getUsername(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + public String resolveToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/src/main/java/com/pay/billing/common/security/MyUserDetails.java b/src/main/java/com/pay/billing/common/security/MyUserDetails.java new file mode 100644 index 0000000..13a6ad8 --- /dev/null +++ b/src/main/java/com/pay/billing/common/security/MyUserDetails.java @@ -0,0 +1,36 @@ +package com.pay.billing.common.security; + +import com.pay.billing.domain.model.User; +import com.pay.billing.domain.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +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 +public class MyUserDetails implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final User user = userRepository.findByUsername(username); + + if (user == null) { + throw new UsernameNotFoundException("User '" + username + "' not found"); + } + + return org.springframework.security.core.userdetails.User// + .withUsername(username)// + .password(user.getPassword())// + .authorities(user.getRoles())// + .accountExpired(false)// + .accountLocked(false)// + .credentialsExpired(false)// + .disabled(false)// + .build(); + } + +} diff --git a/src/main/java/com/pay/billing/controller/UserController.java b/src/main/java/com/pay/billing/controller/UserController.java new file mode 100644 index 0000000..db6fb33 --- /dev/null +++ b/src/main/java/com/pay/billing/controller/UserController.java @@ -0,0 +1,94 @@ +package com.pay.billing.controller; + +import com.pay.billing.domain.dto.UserDTO; +import com.pay.billing.domain.dto.UserResponseDTO; +import com.pay.billing.domain.model.User; +import com.pay.billing.service.UserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.modelmapper.ModelMapper; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/users") +@Api(tags = "users") +public class UserController { + + @Autowired + private UserService userService; + + @Autowired + private ModelMapper modelMapper; + + @PostMapping("/signin") + @ApiOperation(value = "${UserController.signin}") + @ApiResponses(value = {// + @ApiResponse(code = 400, message = "Something went wrong"), // + @ApiResponse(code = 422, message = "Invalid username/password supplied")}) + public String login(// + @ApiParam("Username") @RequestParam String username, // + @ApiParam("Password") @RequestParam String password) { + return userService.signin(username, password); + } + + @PostMapping("/signup") + @ApiOperation(value = "${UserController.signup}") + @ApiResponses(value = {// + @ApiResponse(code = 400, message = "Something went wrong"), // + @ApiResponse(code = 403, message = "Access denied"), // + @ApiResponse(code = 422, message = "Username is already in use")}) + public String signup(@ApiParam("Signup User") @RequestBody UserDTO user) { + return userService.signup(modelMapper.map(user, User.class)); + } + + @DeleteMapping(value = "/{username}") + @PreAuthorize("hasRole('ROLE_ADMIN')") + @ApiOperation(value = "${UserController.delete}", authorizations = { @Authorization(value="apiKey") }) + @ApiResponses(value = {// + @ApiResponse(code = 400, message = "Something went wrong"), // + @ApiResponse(code = 403, message = "Access denied"), // + @ApiResponse(code = 404, message = "The user doesn't exist"), // + @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) + public String delete(@ApiParam("Username") @PathVariable String username) { + userService.delete(username); + return username; + } + + @GetMapping(value = "/{username}") + @PreAuthorize("hasRole('ROLE_ADMIN')") + @ApiOperation(value = "${UserController.search}", response = UserResponseDTO.class, authorizations = { @Authorization(value="apiKey") }) + @ApiResponses(value = {// + @ApiResponse(code = 400, message = "Something went wrong"), // + @ApiResponse(code = 403, message = "Access denied"), // + @ApiResponse(code = 404, message = "The user doesn't exist"), // + @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) + public UserResponseDTO search(@ApiParam("Username") @PathVariable String username) { + return modelMapper.map(userService.search(username), UserResponseDTO.class); + } + + @GetMapping(value = "/me") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + @ApiOperation(value = "${UserController.me}", response = UserResponseDTO.class, authorizations = { @Authorization(value="apiKey") }) + @ApiResponses(value = {// + @ApiResponse(code = 400, message = "Something went wrong"), // + @ApiResponse(code = 403, message = "Access denied"), // + @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) + public UserResponseDTO whoami(HttpServletRequest req) { + return modelMapper.map(userService.whoami(req), UserResponseDTO.class); + } + + @GetMapping("/refresh") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + public String refresh(HttpServletRequest req) { + return userService.refresh(req.getRemoteUser()); + } + +} diff --git a/src/main/java/com/pay/billing/domain/dto/UserDTO.java b/src/main/java/com/pay/billing/domain/dto/UserDTO.java new file mode 100644 index 0000000..02ab4fb --- /dev/null +++ b/src/main/java/com/pay/billing/domain/dto/UserDTO.java @@ -0,0 +1,51 @@ +package com.pay.billing.domain.dto; + +import com.pay.billing.domain.model.Role; +import io.swagger.annotations.ApiModelProperty; + +import java.util.List; + +public class UserDTO { + + @ApiModelProperty(position = 0) + private String username; + @ApiModelProperty(position = 1) + private String email; + @ApiModelProperty(position = 2) + private String password; + @ApiModelProperty(position = 3) + List roles; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} diff --git a/src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java b/src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java new file mode 100644 index 0000000..6fedf8d --- /dev/null +++ b/src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java @@ -0,0 +1,51 @@ +package com.pay.billing.domain.dto; + +import com.pay.billing.domain.model.Role; +import io.swagger.annotations.ApiModelProperty; + +import java.util.List; + +public class UserResponseDTO { + + @ApiModelProperty(position = 0) + private Integer id; + @ApiModelProperty(position = 1) + private String username; + @ApiModelProperty(position = 2) + private String email; + @ApiModelProperty(position = 3) + List roles; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} diff --git a/src/main/java/com/pay/billing/domain/model/Role.java b/src/main/java/com/pay/billing/domain/model/Role.java new file mode 100644 index 0000000..f31c24b --- /dev/null +++ b/src/main/java/com/pay/billing/domain/model/Role.java @@ -0,0 +1,12 @@ +package com.pay.billing.domain.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { + ROLE_ADMIN, ROLE_CLIENT; + + public String getAuthority() { + return name(); + } + +} diff --git a/src/main/java/com/pay/billing/domain/model/User.java b/src/main/java/com/pay/billing/domain/model/User.java new file mode 100644 index 0000000..fd2ff70 --- /dev/null +++ b/src/main/java/com/pay/billing/domain/model/User.java @@ -0,0 +1,67 @@ +package com.pay.billing.domain.model; + +import javax.persistence.*; +import javax.validation.constraints.Size; +import java.util.List; + +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Size(min = 4, max = 255, message = "Minimum username length: 4 characters") + @Column(unique = true, nullable = false) + private String username; + + @Column(unique = true, nullable = false) + private String email; + + @Size(min = 8, message = "Minimum password length: 8 characters") + private String password; + + @ElementCollection(fetch = FetchType.EAGER) + List roles; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} diff --git a/src/main/java/com/pay/billing/domain/repository/UserRepository.java b/src/main/java/com/pay/billing/domain/repository/UserRepository.java new file mode 100644 index 0000000..58efdf9 --- /dev/null +++ b/src/main/java/com/pay/billing/domain/repository/UserRepository.java @@ -0,0 +1,18 @@ +package com.pay.billing.domain.repository; + + +import com.pay.billing.domain.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import javax.transaction.Transactional; + +public interface UserRepository extends JpaRepository { + + boolean existsByUsername(String username); + + User findByUsername(String username); + + @Transactional + void deleteByUsername(String username); + +} diff --git a/src/main/java/com/pay/billing/service/UserService.java b/src/main/java/com/pay/billing/service/UserService.java new file mode 100644 index 0000000..fab2035 --- /dev/null +++ b/src/main/java/com/pay/billing/service/UserService.java @@ -0,0 +1,71 @@ +package com.pay.billing.service; + +import com.pay.billing.common.exception.CustomException; +import com.pay.billing.common.security.JwtTokenProvider; +import com.pay.billing.domain.model.User; +import com.pay.billing.domain.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private AuthenticationManager authenticationManager; + + public String signin(String username, String password) { + try { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + return jwtTokenProvider.createToken(username, userRepository.findByUsername(username).getRoles()); + } catch (AuthenticationException e) { + throw new CustomException("Invalid username/password supplied", HttpStatus.UNPROCESSABLE_ENTITY); + } + } + + public String signup(User user) { + if (!userRepository.existsByUsername(user.getUsername())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + userRepository.save(user); + return jwtTokenProvider.createToken(user.getUsername(), user.getRoles()); + } else { + throw new CustomException("Username is already in use", HttpStatus.UNPROCESSABLE_ENTITY); + } + } + + public void delete(String username) { + userRepository.deleteByUsername(username); + } + + public User search(String username) { + User user = userRepository.findByUsername(username); + if (user == null) { + throw new CustomException("The user doesn't exist", HttpStatus.NOT_FOUND); + } + return user; + } + + public User whoami(HttpServletRequest req) { + return userRepository.findByUsername(jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req))); + } + + public String refresh(String username) { + return jwtTokenProvider.createToken(username, userRepository.findByUsername(username).getRoles()); + } + +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties new file mode 100644 index 0000000..91f3907 --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -0,0 +1,15 @@ +# h2 DataSource +spring.h2.console.enabled: true +spring.datasource.url=jdbc:h2:~/billing +spring.datasource.username=sa +spring.datasource.password= + +# jwt security expire-length +jwt.secret=secret-key +jwt.expire-length=300000 # 5 minutes duration by default: 5 minutes * 60 seconds * 1000 miliseconds + +# jpa +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file diff --git a/src/main/resources/application-release.properties b/src/main/resources/application-release.properties new file mode 100644 index 0000000..506c107 --- /dev/null +++ b/src/main/resources/application-release.properties @@ -0,0 +1,5 @@ +# h2 +spring.h2.console.enabled: true +spring.datasource.url=jdbc:h2:~/billing +spring.datasource.username=sa +spring.datasource.password= \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..b848798 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ - +# profile name = local, dev, release +spring.profiles.active=dev \ No newline at end of file From f2b158fe5606a679b9ad3a716a81fee6e2f1eb54 Mon Sep 17 00:00:00 2001 From: junshock5 <61732452+junshock5@users.noreply.github.com> Date: Sat, 12 Dec 2020 00:41:48 +0900 Subject: [PATCH 2/4] =?UTF-8?q?#1=20=EC=9D=B8=EC=A6=9D=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20spring=20security,=20JWT=20-=20=EC=A3=BC=EC=9A=94?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=201.=20String=20token=20=3D=20jw?= =?UTF-8?q?tTokenProvider.resolveToken(httpServletRequest);=20//=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=ED=99=95=EC=9D=B8=202.=20Jwts.parser().se?= =?UTF-8?q?tSigningKey(secretKey).parseClaimsJws(token);=20//=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EB=A7=9E=EB=8A=94=EC=A7=80=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?3.=20org.springframework.security.core.userdetails.User=20//=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=ED=95=9C=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EC=9D=98=20=EC=A0=95=EB=B3=B4=20=EC=B0=BE=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JwtTokenFilter 1. Authorization 헤더에서 액세스 토큰을 확인한다. 2. 헤더에 액세스 토큰이있는 경우 인증을 위임한다. 3. JwtTokenProvider은 인증 프로세스의 결과에 따라 미충족시 인증 예외를 발생 시킵니다. - JwtTokenFilterConfigurer 1. 스프링 부트 security JwtTokenFilter을 추가합니다 - JwtTokenProvider 1. 액세스 토큰 서명 확인 2. 액세스 토큰 에서 ID 및 권한 부여 클레임(사용자가 누구인지 URI, 무엇에 접근 할 수 있는지, 토큰 만료 시간 확인) 을 추출하고 이를 사용하여 UserContext를 만듭니다. 3. 액세스 토큰의 형식이 잘못 되었거나 만료 되었거나 토큰이 적절한 서명 키로 서명되지 않은 경우 인증 예외가 발생시킵니다. - MyUserDetails 1. UserDetails 인터페이스는 사용자 관련 데이터를 검색하는데 사용한다. 2. 사용자 이름을 기반으로 사용자 엔터티를 찾는 loadUserByUsername() 이라는 하나의 메서드가 있으며 이를 재정 의하여 사용자를 찾는 프로세스를 사용자 지정 - WebSecurityConfig 1. WebSecurityConfigurerAdapter는 사용자 지정 보안 구성을 제공하도록 확장 됩니다. 2. 이 클래스에서 아래 Bean이 구성되고 인스턴스화됩니다. - JwtTokenFilter - PasswordEncoder --- pom.xml | 4 +- .../common/config/WebSecurityConfig.java | 19 ++-- .../common/security/JwtTokenFilter.java | 54 ++++++----- .../security/JwtTokenFilterConfigurer.java | 1 + .../common/security/JwtTokenProvider.java | 94 +++++++++---------- .../common/security/MyUserDetails.java | 5 + 6 files changed, 100 insertions(+), 77 deletions(-) diff --git a/pom.xml b/pom.xml index 59e013f..304d21f 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,9 @@ test - + io.jsonwebtoken jjwt diff --git a/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java index 200ee9f..5af1a73 100644 --- a/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java +++ b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java @@ -18,32 +18,37 @@ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) +/* +WebSecurityConfigurerAdapter는 사용자 지정 보안 구성을 제공하도록 확장 됩니다. +이 클래스에서 아래 Bean이 구성되고 인스턴스화됩니다. +- JwtTokenFilter +- PasswordEncoder +*/ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenProvider jwtTokenProvider; @Override + // 메서드 내에서 보호, 비보호 API 엔드 포인트를 정의하는 패턴을 구성합니다. + // 쿠키를 사용하지 않기 때문에 CSRF 보호를 비활성화했습니다. protected void configure(HttpSecurity http) throws Exception { - // Disable CSRF (cross site request forgery) http.csrf().disable(); - // No session will be created or used by spring security http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - // Entry points http.authorizeRequests()// .antMatchers("/users/signin").permitAll()// .antMatchers("/users/signup").permitAll()// .antMatchers("/h2-console/**/**").permitAll() - // Disallow everything else.. + // 다른것들은 모두 비활성화 .anyRequest().authenticated(); - // If a user try to access a resource without having enough permissions + // 요구사항을 만족하지 않는다면 예외(exceptionHandling)를 발생 시킨다. http.exceptionHandling().accessDeniedPage("/login"); - // Apply JWT + // Security에 JWT 적용 http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); // Optional, if you want to test the API from a browser @@ -52,7 +57,7 @@ protected void configure(HttpSecurity http) throws Exception { @Override public void configure(WebSecurity web) throws Exception { - // Allow swagger to be accessed without authentication + // 인증없이 swagger에는 사용하게 설정 web.ignoring().antMatchers("/v2/api-docs")// .antMatchers("/swagger-resources/**")// .antMatchers("/swagger-ui.html")// diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java index de31067..db97f75 100644 --- a/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java +++ b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java @@ -11,31 +11,41 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; -// We should use OncePerRequestFilter since we are doing a database call, there is no point in doing this more than once +/* +OncePerRequestFilter를 상속하여 구현한 경우 doFilter 대신 doFilterInternal 메서드를 구현 +이렇게 필터를 정의하였으면 bean 선언만 하면 spring boot를 사용하는 경우 자동으로 filter가 추가되게 된다. + +JwtTokenFilter 역할: JwtTokenFilter필터는 각각의 API에 인가된다 ex) /users/signin ,/users/signup. +1. Authorization 헤더에서 액세스 토큰을 확인한다. +2. 헤더에 액세스 토큰이있는 경우 인증을 위임한다. +3. JwtTokenProvider은 인증 프로세스의 결과에 따라 미충족시 인증 예외를 발생 시킵니다. +*/ public class JwtTokenFilter extends OncePerRequestFilter { - private JwtTokenProvider jwtTokenProvider; - - public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { - this.jwtTokenProvider = jwtTokenProvider; - } - - @Override - protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { - String token = jwtTokenProvider.resolveToken(httpServletRequest); - try { - if (token != null && jwtTokenProvider.validateToken(token)) { - Authentication auth = jwtTokenProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(auth); - } - } catch (CustomException ex) { - //this is very important, since it guarantees the user is not authenticated at all - SecurityContextHolder.clearContext(); - httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage()); - return; + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; } - filterChain.doFilter(httpServletRequest, httpServletResponse); - } + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + // 헤더의 req.getHeader("Authorization") 값을 가져와 유효 하다면 값을 가져온다. + String token = jwtTokenProvider.resolveToken(httpServletRequest); + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (CustomException ex) { + // 인증 실패시 호출 + SecurityContextHolder.clearContext(); + httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage()); + return; + } + + // 인증 성공시 호출 + filterChain.doFilter(httpServletRequest, httpServletResponse); + } } diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java b/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java index dc34899..22a2c9f 100644 --- a/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java +++ b/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java @@ -9,6 +9,7 @@ public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter roles) { + public String createToken(String username, List roles) { - Claims claims = Jwts.claims().setSubject(username); - claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); + Claims claims = Jwts.claims().setSubject(username); + claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); - Date now = new Date(); - Date validity = new Date(now.getTime() + validityInMilliseconds); + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); - return Jwts.builder()// - .setClaims(claims)// - .setIssuedAt(now)// - .setExpiration(validity)// - .signWith(SignatureAlgorithm.HS256, secretKey)// - .compact(); - } + return Jwts.builder()// + .setClaims(claims)// + .setIssuedAt(now)// + .setExpiration(validity)// + .signWith(SignatureAlgorithm.HS256, secretKey)// + .compact(); + } - public Authentication getAuthentication(String token) { - UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); - return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); - } + public Authentication getAuthentication(String token) { + UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } - public String getUsername(String token) { - return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); - } + public String getUsername(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } - public String resolveToken(HttpServletRequest req) { - String bearerToken = req.getHeader("Authorization"); - if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); + public String resolveToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; } - return null; - } - public boolean validateToken(String token) { - try { - Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); - return true; - } catch (JwtException | IllegalArgumentException e) { - throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR); + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR); + } } - } } diff --git a/src/main/java/com/pay/billing/common/security/MyUserDetails.java b/src/main/java/com/pay/billing/common/security/MyUserDetails.java index 13a6ad8..733c95c 100644 --- a/src/main/java/com/pay/billing/common/security/MyUserDetails.java +++ b/src/main/java/com/pay/billing/common/security/MyUserDetails.java @@ -9,6 +9,11 @@ import org.springframework.stereotype.Service; @Service +/* +1. UserDetails 인터페이스는 사용자 관련 데이터를 검색하는데 사용한다. +2. 사용자 이름을 기반으로 사용자 엔터티를 찾는 loadUserByUsername() 이라는 +하나의 메서드가 있으며 이를 재정 의하여 사용자를 찾는 프로세스를 사용자 지정 + */ public class MyUserDetails implements UserDetailsService { @Autowired From 66ff89b5f5b4666d24ca5dd9dc316dc3e6a6e842 Mon Sep 17 00:00:00 2001 From: junshock5 <61732452+junshock5@users.noreply.github.com> Date: Sat, 12 Dec 2020 02:16:22 +0900 Subject: [PATCH 3/4] =?UTF-8?q?#1=20=EC=9D=B8=EC=A6=9D=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20spring=20security,=20JWT=20-=20test=20User=20add=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20-=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80,=20=EC=9D=B8=EC=A6=9D=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=B2=B4=EC=A0=81=EC=9D=B4=EA=B2=8C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20=EC=84=A4=EB=AA=85=EC=9D=B4=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EB=93=A4=EC=97=AC=EC=93=B0=EA=B8=B0=20=ED=86=B5=EC=9D=BC=20-?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/pay/billing/BillingApplication.java | 46 ++---- .../billing/common/config/SwaggerConfig.java | 29 ++-- .../common/config/WebSecurityConfig.java | 91 ++++++------ .../common/exception/CustomException.java | 26 ---- .../token/validateTokenException.java | 26 ++++ .../exception/user/UserDeleteException.java | 9 ++ .../exception/user/UserInsertException.java | 13 ++ .../exception/user/UserLoginException.java | 14 ++ .../exception/user/UserNotFoundException.java | 15 ++ .../exception/user/UserUpdateException.java | 9 ++ .../common/security/JwtTokenFilter.java | 11 +- .../security/JwtTokenFilterConfigurer.java | 20 +-- .../common/security/JwtTokenProvider.java | 14 +- .../common/security/MyUserDetails.java | 36 ++--- .../billing/controller/UserController.java | 136 ++++++++++-------- .../com/pay/billing/domain/dto/UserDTO.java | 82 +++++------ .../billing/domain/dto/UserResponseDTO.java | 80 +++++------ .../com/pay/billing/domain/model/Role.java | 10 +- .../com/pay/billing/domain/model/User.java | 134 +++++++++-------- .../domain/repository/UserRepository.java | 12 +- .../com/pay/billing/service/UserService.java | 81 +++++------ src/main/resources/application-dev.properties | 2 +- .../resources/application-release.properties | 2 +- 23 files changed, 489 insertions(+), 409 deletions(-) delete mode 100644 src/main/java/com/pay/billing/common/exception/CustomException.java create mode 100644 src/main/java/com/pay/billing/common/exception/token/validateTokenException.java create mode 100644 src/main/java/com/pay/billing/common/exception/user/UserDeleteException.java create mode 100644 src/main/java/com/pay/billing/common/exception/user/UserInsertException.java create mode 100644 src/main/java/com/pay/billing/common/exception/user/UserLoginException.java create mode 100644 src/main/java/com/pay/billing/common/exception/user/UserNotFoundException.java create mode 100644 src/main/java/com/pay/billing/common/exception/user/UserUpdateException.java diff --git a/src/main/java/com/pay/billing/BillingApplication.java b/src/main/java/com/pay/billing/BillingApplication.java index c864444..4fc7d09 100644 --- a/src/main/java/com/pay/billing/BillingApplication.java +++ b/src/main/java/com/pay/billing/BillingApplication.java @@ -1,49 +1,19 @@ package com.pay.billing; -import com.pay.billing.domain.model.Role; -import com.pay.billing.domain.model.User; -import com.pay.billing.service.UserService; import org.modelmapper.ModelMapper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import java.util.ArrayList; -import java.util.Arrays; - @SpringBootApplication -public class BillingApplication implements CommandLineRunner { - - @Autowired - UserService userService; - - public static void main(String[] args) { - SpringApplication.run(BillingApplication.class, args); - } - - @Bean - public ModelMapper modelMapper() { - return new ModelMapper(); - } - - @Override - public void run(String... params) throws Exception { - User admin = new User(); - admin.setUsername("admin"); - admin.setPassword("admin"); - admin.setEmail("admin@email.com"); - admin.setRoles(new ArrayList(Arrays.asList(Role.ROLE_ADMIN))); - - userService.signup(admin); +public class BillingApplication { - User client = new User(); - client.setUsername("client"); - client.setPassword("client"); - client.setEmail("client@email.com"); - client.setRoles(new ArrayList(Arrays.asList(Role.ROLE_CLIENT))); + public static void main(String[] args) { + SpringApplication.run(BillingApplication.class, args); + } - userService.signup(client); - } + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } } diff --git a/src/main/java/com/pay/billing/common/config/SwaggerConfig.java b/src/main/java/com/pay/billing/common/config/SwaggerConfig.java index 1540fa1..938b704 100644 --- a/src/main/java/com/pay/billing/common/config/SwaggerConfig.java +++ b/src/main/java/com/pay/billing/common/config/SwaggerConfig.java @@ -26,28 +26,30 @@ public class SwaggerConfig { @Bean public Docket swaggerApi() { - return new Docket(DocumentationType.SWAGGER_2)// - .select()// - .apis(RequestHandlerSelectors.any())// - .paths(Predicates.not(PathSelectors.regex("/error")))// - .build()// - .apiInfo(swaggerInfo()) - .useDefaultResponseMessages(false)// - .securitySchemes(Collections.singletonList(apiKey())) - .securityContexts(Collections.singletonList(securityContext())) - .tags(new Tag("users", "Operations about users"))// - .genericModelSubstitutes(Optional.class); + return new Docket(DocumentationType.SWAGGER_2) // Swagger 설정의 핵심이 되는 Bean, API 자체에 대한 스펙은 컨트롤러에서 작성 + .select() // ApiSelectorBuilder를 생성 + .apis(RequestHandlerSelectors.any()) // GetMapping, PostMapping ... 이 선언된 API를 문서화 + .paths(Predicates.not(PathSelectors.regex("/error"))) // apis()로 선택되어진 API중 특정 path 조건에 맞는 API들을 다시 필터링하여 문서화 + .build() + .apiInfo(swaggerInfo()) // 제목, 설명 등 문서에 대한 정보들을 보여주기 위해 호출 + .useDefaultResponseMessages(false) // false로 설정하면, swagger에서 제공해주는 응답코드 ( 200,401,403,404 )에 대한 기본 메시지를 제거 + .securitySchemes(Collections.singletonList(apiKey())) // securitySchemesAPI가 지원하는 모든 보안 체계를 정의 하는 데 사용 security하고 전체 API 또는 개별 작업에 특정 체계를 적용 + .securityContexts(Collections.singletonList(securityContext())) // SecurityScheme 및 SecurityContext 지원을 사용하여 보안 API에 액세스하도록 Swagger를 구성 + .tags(new Tag("users", "Operations about users")) // 여러 개의 태그를 정의할 수도 있습니다. + .genericModelSubstitutes(Optional.class); // 각 제네릭 클래스를 직접 매개 변수화 된 유형으로 대체합니다. } private ApiInfo swaggerInfo() { - return new ApiInfoBuilder().title("Spring API Documentation") - .description("앱 개발시 사용되는 서버 API에 대한 연동 문서입니다").build(); + return new ApiInfoBuilder().title("빌링 시스템 개발") + .description("카드결제 / 결제취소 / 결제정보 조회 REST API 개발 문서입니다").build(); } + // 보안 체계(Authorization)를 jwt를 header에 삽입하여 사용 private ApiKey apiKey() { return new ApiKey("Authorization", "Authorization", "header"); } + // 보안 컨텍스트를 API에 적용 private SecurityContext securityContext() { return SecurityContext.builder() .securityReferences(defaultAuth()) @@ -55,6 +57,7 @@ private SecurityContext securityContext() { .build(); } + // 인증 범위 및 참조 이름 설정 private List defaultAuth() { AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; diff --git a/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java index 5af1a73..caeb05f 100644 --- a/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java +++ b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java @@ -26,60 +26,61 @@ */ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - @Autowired - private JwtTokenProvider jwtTokenProvider; + @Autowired + private JwtTokenProvider jwtTokenProvider; - @Override - // 메서드 내에서 보호, 비보호 API 엔드 포인트를 정의하는 패턴을 구성합니다. - // 쿠키를 사용하지 않기 때문에 CSRF 보호를 비활성화했습니다. - protected void configure(HttpSecurity http) throws Exception { + @Override + // 메서드 내에서 보호, 비보호 API 엔드 포인트를 정의하는 패턴을 구성합니다. + // 쿠키를 사용하지 않기 때문에 CSRF 보호를 비활성화했습니다. + protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable(); + http.csrf().disable(); - http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - http.authorizeRequests()// - .antMatchers("/users/signin").permitAll()// - .antMatchers("/users/signup").permitAll()// - .antMatchers("/h2-console/**/**").permitAll() - // 다른것들은 모두 비활성화 - .anyRequest().authenticated(); + http.authorizeRequests() + .antMatchers("/users/signin").permitAll() + .antMatchers("/users/signup").permitAll() + .antMatchers("/h2-console/**/**").permitAll() + // 다른것들은 모두 비활성화 + .anyRequest().authenticated(); - // 요구사항을 만족하지 않는다면 예외(exceptionHandling)를 발생 시킨다. - http.exceptionHandling().accessDeniedPage("/login"); + // 요구사항을 만족하지 않는다면 예외(exceptionHandling)를 발생 시킨다. + http.exceptionHandling().accessDeniedPage("/login"); - // Security에 JWT 적용 - http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); + // Security에 JWT 적용 + http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); - // Optional, if you want to test the API from a browser - // http.httpBasic(); - } + // Optional, if you want to test the API from a browser + // http.httpBasic(); + } - @Override - public void configure(WebSecurity web) throws Exception { - // 인증없이 swagger에는 사용하게 설정 - web.ignoring().antMatchers("/v2/api-docs")// - .antMatchers("/swagger-resources/**")// - .antMatchers("/swagger-ui.html")// - .antMatchers("/configuration/**")// - .antMatchers("/webjars/**")// - .antMatchers("/public") - - // Un-secure H2 Database (for testing purposes, H2 console shouldn't be unprotected in production) - .and() - .ignoring() - .antMatchers("/h2-console/**/**");; - } + @Override + public void configure(WebSecurity web) throws Exception { + // 인증없이 swagger에는 사용하게 설정 + web.ignoring().antMatchers("/v2/api-docs") + .antMatchers("/swagger-resources/**") + .antMatchers("/swagger-ui.html") + .antMatchers("/configuration/**") + .antMatchers("/webjars/**") + .antMatchers("/public") - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(12); - } + // Un-secure H2 Database (for testing purposes, H2 console shouldn't be unprotected in production) + .and() + .ignoring() + .antMatchers("/h2-console/**/**"); + ; + } - @Override - @Bean - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } + + @Override + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } } diff --git a/src/main/java/com/pay/billing/common/exception/CustomException.java b/src/main/java/com/pay/billing/common/exception/CustomException.java deleted file mode 100644 index 4433f3d..0000000 --- a/src/main/java/com/pay/billing/common/exception/CustomException.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.pay.billing.common.exception; - -import org.springframework.http.HttpStatus; - -public class CustomException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - private final String message; - private final HttpStatus httpStatus; - - public CustomException(String message, HttpStatus httpStatus) { - this.message = message; - this.httpStatus = httpStatus; - } - - @Override - public String getMessage() { - return message; - } - - public HttpStatus getHttpStatus() { - return httpStatus; - } - -} diff --git a/src/main/java/com/pay/billing/common/exception/token/validateTokenException.java b/src/main/java/com/pay/billing/common/exception/token/validateTokenException.java new file mode 100644 index 0000000..29cdc52 --- /dev/null +++ b/src/main/java/com/pay/billing/common/exception/token/validateTokenException.java @@ -0,0 +1,26 @@ +package com.pay.billing.common.exception.token; + +import org.springframework.http.HttpStatus; + +public class validateTokenException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final String message; + private final HttpStatus httpStatus; + + public validateTokenException(String message, HttpStatus httpStatus) { + this.message = message; + this.httpStatus = httpStatus; + } + + @Override + public String getMessage() { + return message; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + +} diff --git a/src/main/java/com/pay/billing/common/exception/user/UserDeleteException.java b/src/main/java/com/pay/billing/common/exception/user/UserDeleteException.java new file mode 100644 index 0000000..ea6d168 --- /dev/null +++ b/src/main/java/com/pay/billing/common/exception/user/UserDeleteException.java @@ -0,0 +1,9 @@ +package com.pay.billing.common.exception.user; + +public class UserDeleteException extends RuntimeException { + + public UserDeleteException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/com/pay/billing/common/exception/user/UserInsertException.java b/src/main/java/com/pay/billing/common/exception/user/UserInsertException.java new file mode 100644 index 0000000..3b2c270 --- /dev/null +++ b/src/main/java/com/pay/billing/common/exception/user/UserInsertException.java @@ -0,0 +1,13 @@ +package com.pay.billing.common.exception.user; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) +public class UserInsertException extends RuntimeException { + + public UserInsertException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/com/pay/billing/common/exception/user/UserLoginException.java b/src/main/java/com/pay/billing/common/exception/user/UserLoginException.java new file mode 100644 index 0000000..398710a --- /dev/null +++ b/src/main/java/com/pay/billing/common/exception/user/UserLoginException.java @@ -0,0 +1,14 @@ +package com.pay.billing.common.exception.user; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +// Unprocessable Entity +@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) +public class UserLoginException extends RuntimeException { + + public UserLoginException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/com/pay/billing/common/exception/user/UserNotFoundException.java b/src/main/java/com/pay/billing/common/exception/user/UserNotFoundException.java new file mode 100644 index 0000000..0cdf3a0 --- /dev/null +++ b/src/main/java/com/pay/billing/common/exception/user/UserNotFoundException.java @@ -0,0 +1,15 @@ +package com.pay.billing.common.exception.user; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Getter +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class UserNotFoundException extends RuntimeException { + private String memberId; + + public UserNotFoundException(String memberId) { + super("Member not found : " +memberId); + } +} diff --git a/src/main/java/com/pay/billing/common/exception/user/UserUpdateException.java b/src/main/java/com/pay/billing/common/exception/user/UserUpdateException.java new file mode 100644 index 0000000..5c7ee6a --- /dev/null +++ b/src/main/java/com/pay/billing/common/exception/user/UserUpdateException.java @@ -0,0 +1,9 @@ +package com.pay.billing.common.exception.user; + +public class UserUpdateException extends RuntimeException { + + public UserUpdateException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java index db97f75..f787d10 100644 --- a/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java +++ b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java @@ -1,6 +1,6 @@ package com.pay.billing.common.security; -import com.pay.billing.common.exception.CustomException; +import com.pay.billing.common.exception.token.validateTokenException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; @@ -37,8 +37,13 @@ protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServl Authentication auth = jwtTokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } - } catch (CustomException ex) { - // 인증 실패시 호출 + } catch (validateTokenException ex) { + /* + ThreadLocal 에 있는 SecurityContext 를 제거하는 역할 + SecurityContextHolder.clearContext() 가 실행되기 전에 어떤 시점에서 + SecurityContext 객체를 HttpSession 에 저장하는 처리를 먼저 하게 됩니다. + 참조: https://www.inflearn.com/questions/53657 + */ SecurityContextHolder.clearContext(); httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage()); return; diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java b/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java index 22a2c9f..c735c30 100644 --- a/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java +++ b/src/main/java/com/pay/billing/common/security/JwtTokenFilterConfigurer.java @@ -7,17 +7,17 @@ public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter { - private JwtTokenProvider jwtTokenProvider; + private JwtTokenProvider jwtTokenProvider; - // 스프링 부트 security JwtTokenFilter을 추가합니다 - public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) { - this.jwtTokenProvider = jwtTokenProvider; - } + // 스프링 부트 security JwtTokenFilter을 추가합니다 + public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } - @Override - public void configure(HttpSecurity http) throws Exception { - JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); - http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); - } + @Override + public void configure(HttpSecurity http) throws Exception { + JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } } diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java b/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java index 702e11c..98e5f0c 100644 --- a/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java +++ b/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java @@ -1,6 +1,6 @@ package com.pay.billing.common.security; -import com.pay.billing.common.exception.CustomException; +import com.pay.billing.common.exception.token.validateTokenException; import com.pay.billing.domain.model.Role; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; @@ -52,11 +52,11 @@ public String createToken(String username, List roles) { Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); - return Jwts.builder()// - .setClaims(claims)// - .setIssuedAt(now)// - .setExpiration(validity)// - .signWith(SignatureAlgorithm.HS256, secretKey)// + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } @@ -82,7 +82,7 @@ public boolean validateToken(String token) { Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { - throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR); + throw new validateTokenException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/src/main/java/com/pay/billing/common/security/MyUserDetails.java b/src/main/java/com/pay/billing/common/security/MyUserDetails.java index 733c95c..d8d467c 100644 --- a/src/main/java/com/pay/billing/common/security/MyUserDetails.java +++ b/src/main/java/com/pay/billing/common/security/MyUserDetails.java @@ -16,26 +16,26 @@ */ public class MyUserDetails implements UserDetailsService { - @Autowired - private UserRepository userRepository; + @Autowired + private UserRepository userRepository; - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - final User user = userRepository.findByUsername(username); + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final User user = userRepository.findByUsername(username); - if (user == null) { - throw new UsernameNotFoundException("User '" + username + "' not found"); - } + if (user == null) { + throw new UsernameNotFoundException("User '" + username + "' not found"); + } - return org.springframework.security.core.userdetails.User// - .withUsername(username)// - .password(user.getPassword())// - .authorities(user.getRoles())// - .accountExpired(false)// - .accountLocked(false)// - .credentialsExpired(false)// - .disabled(false)// - .build(); - } + return org.springframework.security.core.userdetails.User + .withUsername(username) + .password(user.getPassword()) + .authorities(user.getRoles()) + .accountExpired(false) + .accountLocked(false) + .credentialsExpired(false) + .disabled(false) + .build(); + } } diff --git a/src/main/java/com/pay/billing/controller/UserController.java b/src/main/java/com/pay/billing/controller/UserController.java index db6fb33..5918156 100644 --- a/src/main/java/com/pay/billing/controller/UserController.java +++ b/src/main/java/com/pay/billing/controller/UserController.java @@ -22,73 +22,87 @@ @Api(tags = "users") public class UserController { - @Autowired - private UserService userService; + @Autowired + private UserService userService; - @Autowired - private ModelMapper modelMapper; + /* + 배경: MSA(Micro Service Architecture)에서는 서로다른 도메인 간에 API를 이용해서 통신을 하게 된다. + API 콜을 하게 되면 데이터가 DTO에 담겨서 온다. 그런데 API가 제공하는 DTO를 그대로 사용하면 안된다. + 왜냐하면 API는 언제든지 변경될 수 있기 때문이다. 따라서 API DTO를 이름만 바꿔서(동일한 데이터 필드를 가짐) WrapDTO로 변환해서 사용한다. - @PostMapping("/signin") - @ApiOperation(value = "${UserController.signin}") - @ApiResponses(value = {// - @ApiResponse(code = 400, message = "Something went wrong"), // - @ApiResponse(code = 422, message = "Invalid username/password supplied")}) - public String login(// - @ApiParam("Username") @RequestParam String username, // - @ApiParam("Password") @RequestParam String password) { - return userService.signin(username, password); - } + DTO와 Entity의 필드명을 일치시켜주면 매핑을 수행해주는 라이브러리이다. + (매핑할 class에는 setter가 있어야하며 매핑될 class에는 getter가 있어야한다.) - @PostMapping("/signup") - @ApiOperation(value = "${UserController.signup}") - @ApiResponses(value = {// - @ApiResponse(code = 400, message = "Something went wrong"), // - @ApiResponse(code = 403, message = "Access denied"), // - @ApiResponse(code = 422, message = "Username is already in use")}) - public String signup(@ApiParam("Signup User") @RequestBody UserDTO user) { - return userService.signup(modelMapper.map(user, User.class)); - } + MatchingStrategies.STANDARD 지능적으로 매핑 해준다. (기본설정) + MatchingStrategies.STRICT 정확히 일치하는 필드만 매핑 해준다. + MatchingStrategies.LOOSE 느슨하게 매핑 해준다. - @DeleteMapping(value = "/{username}") - @PreAuthorize("hasRole('ROLE_ADMIN')") - @ApiOperation(value = "${UserController.delete}", authorizations = { @Authorization(value="apiKey") }) - @ApiResponses(value = {// - @ApiResponse(code = 400, message = "Something went wrong"), // - @ApiResponse(code = 403, message = "Access denied"), // - @ApiResponse(code = 404, message = "The user doesn't exist"), // - @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) - public String delete(@ApiParam("Username") @PathVariable String username) { - userService.delete(username); - return username; - } + 결론: 지겨운 get set 노가다를 벗어날 수 있다. + */ + @Autowired + private ModelMapper modelMapper; - @GetMapping(value = "/{username}") - @PreAuthorize("hasRole('ROLE_ADMIN')") - @ApiOperation(value = "${UserController.search}", response = UserResponseDTO.class, authorizations = { @Authorization(value="apiKey") }) - @ApiResponses(value = {// - @ApiResponse(code = 400, message = "Something went wrong"), // - @ApiResponse(code = 403, message = "Access denied"), // - @ApiResponse(code = 404, message = "The user doesn't exist"), // - @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) - public UserResponseDTO search(@ApiParam("Username") @PathVariable String username) { - return modelMapper.map(userService.search(username), UserResponseDTO.class); - } + @PostMapping("/signin") + @ApiOperation(value = "${UserController.signin}") // @ApiOperation 에 메서드에 대한 설명을 적을 수 있다. + @ApiResponses(value = { // @ApiResponses로 이미 여러 코드 값을 작성, 동일 코드 값을 반환 + @ApiResponse(code = 400, message = "Something went wrong"), + @ApiResponse(code = 422, message = "Invalid username/password supplied")}) + public String login( + @ApiParam("Username") @RequestParam String username, + @ApiParam("Password") @RequestParam String password) { + return userService.signin(username, password); + } - @GetMapping(value = "/me") - @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") - @ApiOperation(value = "${UserController.me}", response = UserResponseDTO.class, authorizations = { @Authorization(value="apiKey") }) - @ApiResponses(value = {// - @ApiResponse(code = 400, message = "Something went wrong"), // - @ApiResponse(code = 403, message = "Access denied"), // - @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) - public UserResponseDTO whoami(HttpServletRequest req) { - return modelMapper.map(userService.whoami(req), UserResponseDTO.class); - } + @PostMapping("/signup") + @ApiOperation(value = "${UserController.signup}") + @ApiResponses(value = { + @ApiResponse(code = 400, message = "Something went wrong"), + @ApiResponse(code = 403, message = "Access denied"), + @ApiResponse(code = 422, message = "Username is already in use")}) + public String signup(@ApiParam("Signup User") @RequestBody UserDTO user) { + return userService.signup(modelMapper.map(user, User.class)); + } - @GetMapping("/refresh") - @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") - public String refresh(HttpServletRequest req) { - return userService.refresh(req.getRemoteUser()); - } + @DeleteMapping(value = "/{username}") + @PreAuthorize("hasRole('ROLE_ADMIN')") // 권한 별로 접근을 통제한다. + @ApiOperation(value = "${UserController.delete}", authorizations = {@Authorization(value = "apiKey")}) + @ApiResponses(value = { + @ApiResponse(code = 400, message = "Something went wrong"), + @ApiResponse(code = 403, message = "Access denied"), + @ApiResponse(code = 404, message = "The user doesn't exist"), + @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) + public String delete(@ApiParam("Username") @PathVariable String username) { + userService.delete(username); + return username; + } + + @GetMapping(value = "/{username}") + @PreAuthorize("hasRole('ROLE_ADMIN')") + @ApiOperation(value = "${UserController.search}", response = UserResponseDTO.class, authorizations = {@Authorization(value = "apiKey")}) + @ApiResponses(value = { + @ApiResponse(code = 400, message = "Something went wrong"), + @ApiResponse(code = 403, message = "Access denied"), + @ApiResponse(code = 404, message = "The user doesn't exist"), + @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) + public UserResponseDTO search(@ApiParam("Username") @PathVariable String username) { + return modelMapper.map(userService.search(username), UserResponseDTO.class); + } + + @GetMapping(value = "/me") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + @ApiOperation(value = "${UserController.me}", response = UserResponseDTO.class, authorizations = {@Authorization(value = "apiKey")}) + @ApiResponses(value = { + @ApiResponse(code = 400, message = "Something went wrong"), + @ApiResponse(code = 403, message = "Access denied"), + @ApiResponse(code = 500, message = "Expired or invalid JWT token")}) + public UserResponseDTO whoami(HttpServletRequest req) { + return modelMapper.map(userService.whoami(req), UserResponseDTO.class); + } + + @GetMapping("/refresh") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + public String refresh(HttpServletRequest req) { + return userService.refresh(req.getRemoteUser()); + } } diff --git a/src/main/java/com/pay/billing/domain/dto/UserDTO.java b/src/main/java/com/pay/billing/domain/dto/UserDTO.java index 02ab4fb..970841a 100644 --- a/src/main/java/com/pay/billing/domain/dto/UserDTO.java +++ b/src/main/java/com/pay/billing/domain/dto/UserDTO.java @@ -6,46 +6,46 @@ import java.util.List; public class UserDTO { - - @ApiModelProperty(position = 0) - private String username; - @ApiModelProperty(position = 1) - private String email; - @ApiModelProperty(position = 2) - private String password; - @ApiModelProperty(position = 3) - List roles; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public List getRoles() { - return roles; - } - - public void setRoles(List roles) { - this.roles = roles; - } + // Swagger에 다른 메타 데이터를 추가 + @ApiModelProperty(position = 0) + private String username; + @ApiModelProperty(position = 1) + private String email; + @ApiModelProperty(position = 2) + private String password; + @ApiModelProperty(position = 3) + List roles; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } } diff --git a/src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java b/src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java index 6fedf8d..92ccc2f 100644 --- a/src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java +++ b/src/main/java/com/pay/billing/domain/dto/UserResponseDTO.java @@ -7,45 +7,45 @@ public class UserResponseDTO { - @ApiModelProperty(position = 0) - private Integer id; - @ApiModelProperty(position = 1) - private String username; - @ApiModelProperty(position = 2) - private String email; - @ApiModelProperty(position = 3) - List roles; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public List getRoles() { - return roles; - } - - public void setRoles(List roles) { - this.roles = roles; - } + @ApiModelProperty(position = 0) + private Integer id; + @ApiModelProperty(position = 1) + private String username; + @ApiModelProperty(position = 2) + private String email; + @ApiModelProperty(position = 3) + List roles; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } } diff --git a/src/main/java/com/pay/billing/domain/model/Role.java b/src/main/java/com/pay/billing/domain/model/Role.java index f31c24b..0fe8776 100644 --- a/src/main/java/com/pay/billing/domain/model/Role.java +++ b/src/main/java/com/pay/billing/domain/model/Role.java @@ -2,11 +2,11 @@ import org.springframework.security.core.GrantedAuthority; +// 액세스 권한을 부여, 제어하는 ​​권한을 얻는다. public enum Role implements GrantedAuthority { - ROLE_ADMIN, ROLE_CLIENT; - - public String getAuthority() { - return name(); - } + ROLE_ADMIN, ROLE_CLIENT; + public String getAuthority() { + return name(); + } } diff --git a/src/main/java/com/pay/billing/domain/model/User.java b/src/main/java/com/pay/billing/domain/model/User.java index fd2ff70..d0f667f 100644 --- a/src/main/java/com/pay/billing/domain/model/User.java +++ b/src/main/java/com/pay/billing/domain/model/User.java @@ -4,64 +4,86 @@ import javax.validation.constraints.Size; import java.util.List; +/* +JPA에서 사용할 엔티티 이름을 지정한다. 보통 기본값인 클래스 이름을 사용한다. +만약 다른 패키지에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다. + +- 방법 +1. 객체와 테이블 매핑 @Entity, @Table +2. 기본 키 매핑 @Id +3. 필드와 컬럼 매핑 @Column +4. 연관관계 매핑 @ManyToOne, @JoinColumn + +- 주의 사항 +1. 기본 생성자가 필수(파라미터가 없는 public 또는 protected 생성자) +2. final 클래스, enum, interface, inner 클래스에는 사용 불가 +3. 저장할 필드에 final 사용 불가 +*/ @Entity public class User { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - @Size(min = 4, max = 255, message = "Minimum username length: 4 characters") - @Column(unique = true, nullable = false) - private String username; - - @Column(unique = true, nullable = false) - private String email; - - @Size(min = 8, message = "Minimum password length: 8 characters") - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - List roles; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public List getRoles() { - return roles; - } - - public void setRoles(List roles) { - this.roles = roles; - } + @Id // @Id는 해당 프로퍼티가 테이블의 주키(primary key) 역할을 한다는 것을 나타낸다. + @GeneratedValue(strategy = GenerationType.IDENTITY) // @GeneratedValue는 주키의 값을 위한 자동 생성 전략을 명시하는데 사용한다. + private Integer id; + + @Size(min = 4, max = 255, message = "Minimum username length: 4 characters") // 필드의 크기 를 전달 + @Column(unique = true, nullable = false) // DDL 문을 제어하는 ​​데 사용 하는 JPA 주석 + private String username; + + @Column(unique = true, nullable = false) + private String email; + + @Size(min = 8, message = "Minimum password length: 8 characters") + private String password; + + /* + @ElementCollection을 통해 값을 추가할 경우, JPA 구현체는 해당 컬렉션의 모든 데이터를 삭제하고 다시 모든 데이터를 추가한다 + @ElementCollection의 fetch 속성을 EAGER로 설정했으면 Hibernate에서 제공하는 @Fetch로 긁어올 방식을 설정해 주어야 한다. + @ElementCollection으로 @Embeddable을 저장할 경우 관계 Key가 되는 값이 부모의 PK가 아닌 다른 컬럼이어도 작동은 한다. + 하지만 부모의 컬렉션을 로딩한 상태에서 자식들을 로딩하면 각 부모 객체가 동일한 관계 Key를 가지고 있을 경우 별다른 오류 메시지 없이 오류로 간주하고 rollback이 된다. + @ElementCollection의 관계 Key는 부모의 PK나 혹은 PK에 준하는 각 부모별로 동일한 값이 나올 수 없는 것으로 지정해야 한다. + */ + @ElementCollection(fetch = FetchType.EAGER) + List roles; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } } diff --git a/src/main/java/com/pay/billing/domain/repository/UserRepository.java b/src/main/java/com/pay/billing/domain/repository/UserRepository.java index 58efdf9..266b97a 100644 --- a/src/main/java/com/pay/billing/domain/repository/UserRepository.java +++ b/src/main/java/com/pay/billing/domain/repository/UserRepository.java @@ -8,11 +8,15 @@ public interface UserRepository extends JpaRepository { - boolean existsByUsername(String username); + boolean existsByUsername(String username); - User findByUsername(String username); + User findByUsername(String username); - @Transactional - void deleteByUsername(String username); + /* + 프록시 객체는 @Transactional이 포함된 메소드가 호출 될 경우, PlatformTransactionManager를 + 사용하여 트랜잭션을 시작하고, 정상 여부에 따라 Commit 또는 Rollback 한다. + */ + @Transactional + void deleteByUsername(String username); } diff --git a/src/main/java/com/pay/billing/service/UserService.java b/src/main/java/com/pay/billing/service/UserService.java index fab2035..1b5e6ff 100644 --- a/src/main/java/com/pay/billing/service/UserService.java +++ b/src/main/java/com/pay/billing/service/UserService.java @@ -1,11 +1,12 @@ package com.pay.billing.service; -import com.pay.billing.common.exception.CustomException; +import com.pay.billing.common.exception.user.UserInsertException; +import com.pay.billing.common.exception.user.UserLoginException; +import com.pay.billing.common.exception.user.UserNotFoundException; import com.pay.billing.common.security.JwtTokenProvider; import com.pay.billing.domain.model.User; import com.pay.billing.domain.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; @@ -17,55 +18,55 @@ @Service public class UserService { - @Autowired - private UserRepository userRepository; + @Autowired + private UserRepository userRepository; - @Autowired - private PasswordEncoder passwordEncoder; + @Autowired + private PasswordEncoder passwordEncoder; - @Autowired - private JwtTokenProvider jwtTokenProvider; + @Autowired + private JwtTokenProvider jwtTokenProvider; - @Autowired - private AuthenticationManager authenticationManager; + @Autowired + private AuthenticationManager authenticationManager; - public String signin(String username, String password) { - try { - authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); - return jwtTokenProvider.createToken(username, userRepository.findByUsername(username).getRoles()); - } catch (AuthenticationException e) { - throw new CustomException("Invalid username/password supplied", HttpStatus.UNPROCESSABLE_ENTITY); + public String signin(String username, String password) { + try { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + return jwtTokenProvider.createToken(username, userRepository.findByUsername(username).getRoles()); + } catch (AuthenticationException e) { + throw new UserLoginException("Invalid username/password supplied"); + } } - } - public String signup(User user) { - if (!userRepository.existsByUsername(user.getUsername())) { - user.setPassword(passwordEncoder.encode(user.getPassword())); - userRepository.save(user); - return jwtTokenProvider.createToken(user.getUsername(), user.getRoles()); - } else { - throw new CustomException("Username is already in use", HttpStatus.UNPROCESSABLE_ENTITY); + public String signup(User user) { + if (!userRepository.existsByUsername(user.getUsername())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + userRepository.save(user); + return jwtTokenProvider.createToken(user.getUsername(), user.getRoles()); + } else { + throw new UserInsertException("Username is already in use"); + } } - } - public void delete(String username) { - userRepository.deleteByUsername(username); - } + public void delete(String username) { + userRepository.deleteByUsername(username); + } - public User search(String username) { - User user = userRepository.findByUsername(username); - if (user == null) { - throw new CustomException("The user doesn't exist", HttpStatus.NOT_FOUND); + public User search(String username) { + User user = userRepository.findByUsername(username); + if (user == null) { + throw new UserNotFoundException(username); + } + return user; } - return user; - } - public User whoami(HttpServletRequest req) { - return userRepository.findByUsername(jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req))); - } + public User whoami(HttpServletRequest req) { + return userRepository.findByUsername(jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req))); + } - public String refresh(String username) { - return jwtTokenProvider.createToken(username, userRepository.findByUsername(username).getRoles()); - } + public String refresh(String username) { + return jwtTokenProvider.createToken(username, userRepository.findByUsername(username).getRoles()); + } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 91f3907..2c108c0 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,5 +1,5 @@ # h2 DataSource -spring.h2.console.enabled: true +spring.h2.console.enabled=true spring.datasource.url=jdbc:h2:~/billing spring.datasource.username=sa spring.datasource.password= diff --git a/src/main/resources/application-release.properties b/src/main/resources/application-release.properties index 506c107..6b325da 100644 --- a/src/main/resources/application-release.properties +++ b/src/main/resources/application-release.properties @@ -1,5 +1,5 @@ # h2 -spring.h2.console.enabled: true +spring.h2.console.enabled=true spring.datasource.url=jdbc:h2:~/billing spring.datasource.username=sa spring.datasource.password= \ No newline at end of file From ac8d8f6b4b4c71aa811e633f18473ed1e964acc9 Mon Sep 17 00:00:00 2001 From: junshock5 <61732452+junshock5@users.noreply.github.com> Date: Sun, 13 Dec 2020 19:02:30 +0900 Subject: [PATCH 4/4] =?UTF-8?q?#1=20=EC=9D=B8=EC=A6=9D=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20spring=20security,=20JWT=20-=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=84=9D=20=EB=B3=80=EA=B2=BD=20-=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EA=B5=AC=EB=8F=99=EC=8B=9C=20admin=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EC=B6=94=EA=B0=80=20-=20secretKey=20PostC?= =?UTF-8?q?onstruct=20=EC=82=AD=EC=A0=9C=20-=20formLogin()=20defaultSucces?= =?UTF-8?q?sUrl()=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20accessDeniedPage=20/l?= =?UTF-8?q?ogin=3Ferror=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 4 +--- .../com/pay/billing/BillingApplication.java | 24 ++++++++++++++++++- .../common/config/WebSecurityConfig.java | 11 +++++---- .../common/security/JwtTokenFilter.java | 1 - .../common/security/JwtTokenProvider.java | 7 ------ 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 304d21f..59e013f 100644 --- a/pom.xml +++ b/pom.xml @@ -76,9 +76,7 @@ test - + io.jsonwebtoken jjwt diff --git a/src/main/java/com/pay/billing/BillingApplication.java b/src/main/java/com/pay/billing/BillingApplication.java index 4fc7d09..1f86be3 100644 --- a/src/main/java/com/pay/billing/BillingApplication.java +++ b/src/main/java/com/pay/billing/BillingApplication.java @@ -1,12 +1,20 @@ package com.pay.billing; +import com.pay.billing.domain.model.Role; +import com.pay.billing.domain.model.User; +import com.pay.billing.service.UserService; import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import java.util.ArrayList; +import java.util.Arrays; + @SpringBootApplication -public class BillingApplication { +public class BillingApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(BillingApplication.class, args); @@ -16,4 +24,18 @@ public static void main(String[] args) { public ModelMapper modelMapper() { return new ModelMapper(); } + + @Autowired + UserService userService; + + @Override + public void run(String... params) throws Exception { + User admin = new User(); + admin.setUsername("admin"); + admin.setPassword("admin"); + admin.setEmail("admin@email.com"); + admin.setRoles(new ArrayList(Arrays.asList(Role.ROLE_ADMIN))); + + userService.signup(admin); + } } diff --git a/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java index caeb05f..886d680 100644 --- a/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java +++ b/src/main/java/com/pay/billing/common/config/WebSecurityConfig.java @@ -43,16 +43,17 @@ protected void configure(HttpSecurity http) throws Exception { .antMatchers("/users/signup").permitAll() .antMatchers("/h2-console/**/**").permitAll() // 다른것들은 모두 비활성화 - .anyRequest().authenticated(); + .anyRequest().authenticated() + .and() + .formLogin() + .defaultSuccessUrl("/swagger-ui.html"); - // 요구사항을 만족하지 않는다면 예외(exceptionHandling)를 발생 시킨다. - http.exceptionHandling().accessDeniedPage("/login"); + // 인증 오류 처리시 accessDenied.jsp 페이지 지정 + http.exceptionHandling().accessDeniedPage("/login?error"); // Security에 JWT 적용 http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); - // Optional, if you want to test the API from a browser - // http.httpBasic(); } @Override diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java index f787d10..23b1961 100644 --- a/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java +++ b/src/main/java/com/pay/billing/common/security/JwtTokenFilter.java @@ -42,7 +42,6 @@ protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServl ThreadLocal 에 있는 SecurityContext 를 제거하는 역할 SecurityContextHolder.clearContext() 가 실행되기 전에 어떤 시점에서 SecurityContext 객체를 HttpSession 에 저장하는 처리를 먼저 하게 됩니다. - 참조: https://www.inflearn.com/questions/53657 */ SecurityContextHolder.clearContext(); httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage()); diff --git a/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java b/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java index 98e5f0c..7652d69 100644 --- a/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java +++ b/src/main/java/com/pay/billing/common/security/JwtTokenProvider.java @@ -15,9 +15,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; -import java.util.Base64; import java.util.Date; import java.util.List; import java.util.Objects; @@ -39,11 +37,6 @@ public class JwtTokenProvider { @Autowired private MyUserDetails myUserDetails; - @PostConstruct - protected void init() { - secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); - } - public String createToken(String username, List roles) { Claims claims = Jwts.claims().setSubject(username);