diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6415ed5..b633b0f 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
index 5bf1db7..6674b96 100644
--- a/.idea/dataSources.xml
+++ b/.idea/dataSources.xml
@@ -1,11 +1,11 @@
-
+
mysql.8
true
com.mysql.cj.jdbc.Driver
- jdbc:mysql://localhost:3306/user?serverTimezone=UTC&characterEncoding=UTF-8
+ jdbc:mysql://localhost:3305?play_ground
$ProjectFileDir$
diff --git a/README.md b/README.md
index 25feba4..d276ddb 100644
--- a/README.md
+++ b/README.md
@@ -31,4 +31,12 @@
- https://console.cloud.google.com/ 개발자 센터 접속
- 프로젝트 생성 후 OAuth 2.0 클라이언트 Id 생성
- 승인된 리디렉션 URI: http://localhost:8080/login/oauth2/code/google path 고정
- - 클라이언트ID, 보안 비밀번호 yml 설정 추가
\ No newline at end of file
+ - 클라이언트ID, 보안 비밀번호 yml 설정 추가
+
+### JWT
+- JSON 객체를 사용해서 토큰 자체에 정보들을 저장하고 있는 Web Token
+- 기존 세션 방식의 처리는 서버가 메모리 혹은 별도의 세션을 저장할 공간이 필요했음(하드디스크, DB)
+ - 서버의 부담
+ - auto scale이 되어 서버 인스턴스가 늘어났을 때, 메모리를 서로 공유하지 못하기 때문에 세션을 공유할 수 없음.
+ - 중간에 서버가 죽으면? 메모리에 있던 정보들 다 날라감. 별도 저장소를 구성하기엔 비용이 듦
+ - 이러한 이유 등으로 인해 jwt 웹 표준(RFC 7519) 등장.
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index bf895e1..97e1240 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,15 +24,20 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
- implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
- implementation 'org.springframework.boot:spring-boot-starter-mustache'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
- testImplementation 'org.springframework.security:spring-security-test'
- runtimeOnly 'com.mysql:mysql-connector-j:8.0.33'
+
+ runtimeOnly 'com.h2database:h2'
+// runtimeOnly 'com.mysql:mysql-connector-j:8.0.33'
+
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
tasks.named('test') {
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..630bf30
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,24 @@
+## compose 파일 버전
+#version: '3'
+#services:
+# # 서비스 명
+# db:
+# # 사용할 이미지
+# image: mysql
+# # 컨테이너 실행 시 재시작
+# restart: always
+# # 컨테이너명 설정
+# container_name: mysql-container-by-compose
+# # 접근 포트 설정(컨테이너 외부:컨테이너 내부)
+# ports:
+#
+# - 3307:3306
+# # 환경 변수 설정
+# environment:
+# MYSQL_ROOT_PASSWORD: root
+# # 명령어 설정
+# command:
+# - --character-set-server=utf8mb4
+# - --collation-server=utf8mb4_unicode_ci
+## volumes:
+## -
\ No newline at end of file
diff --git a/src/main/java/com/example/security2/auth/PrincipalDetails.java b/src/main/java/com/example/security2/auth/PrincipalDetails.java
deleted file mode 100644
index 4a6caad..0000000
--- a/src/main/java/com/example/security2/auth/PrincipalDetails.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.example.security2.auth;
-
-import com.example.security2.entity.User;
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.oauth2.core.user.OAuth2User;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
- * 로그인이 진행이 완료가 되면 시큐리티 session을 만들어준다.(SecurityContextHolder)
- * 오브젝트 => Authentication 타입의 객체
- * Authentication 안에 User 정보가 있어야 됨.
- * User 객체는 UserDetails 타입 객체(Security)
- *
- * Security Session => Authentication => UserDetails(PrincipalDetails)
- */
-@Getter
-@Setter
-public class PrincipalDetails implements UserDetails, OAuth2User {
-
- // composition
- private final User user;
- private Map attributes;
-
- // 일반 로그인
- public PrincipalDetails(User user) {
- this.user = user;
- }
-
- // OAuth 로그인
- public PrincipalDetails(User user, Map attributes) {
- this(user);
- this.attributes = attributes;
- }
-
- @Override
- public Map getAttributes() {
- return attributes;
- }
-
- // 해당 User의 권한을 리턴하는 곳!
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
- return List.of((GrantedAuthority) user::getRole);
- }
-
- @Override
- public String getPassword() {
- return user.getPassword();
- }
-
- @Override
- public String getUsername() {
- return user.getEmail();
- }
-
- // 계정이 만료되지 않은지(true: 만료되지 않음 / false: 만료됨)
- @Override
- public boolean isAccountNonExpired() {
- return true;
- }
-
- // 계정이 잠겨있지 않은지(true: 잠기지 않음 / false: 잠김)
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
-
- // 비밀번호가 만료되지 않은지(true: 만료안됨 / false: 만료)
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
-
- // 계정이 활성화되어 있는지(true: 활성화 / false: 비활)
- @Override
- public boolean isEnabled() {
- /**
- * TODO
- * 만일 우리 사이트의 정책이 1년동안 로그인하지 않은 회원을 휴면처리 하기로 했다면?
- * 현재 서버시간 - 마지막 로그인 시간 => 1년을 초과하면 return false;
- * System.currentTimeMillis() - user.getLastLoginDate();
- */
-
- return true;
- }
-
- @Override
- public String getName() {
- return (String) attributes.get("sub");
- }
-}
diff --git a/src/main/java/com/example/security2/auth/PrincipalDetailsService.java b/src/main/java/com/example/security2/auth/PrincipalDetailsService.java
deleted file mode 100644
index aa1278f..0000000
--- a/src/main/java/com/example/security2/auth/PrincipalDetailsService.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.example.security2.auth;
-
-import com.example.security2.entity.User;
-import com.example.security2.repository.UserRepository;
-import lombok.RequiredArgsConstructor;
-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;
-
-// 시큐리티 설정에서 .loginProcessingUrl("/login");
-// /login 요청이 오면 자동으로 UserDetailsService 타입으로 IOC 되어있는 loadUserByUsername 메서드가 실행
-@Service
-@RequiredArgsConstructor
-public class PrincipalDetailsService implements UserDetailsService {
-
- private final UserRepository userRepository;
-
- // 시큐리티 session(내부 Authentication(내부 UserDetails))
- // 함수 종료 시 @AuthenticationPrincipal 어노테이션이 만들어진다.
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- User foundUser = userRepository.findByEmail(username)
- .stream()
- .findFirst()
- .orElseThrow(() -> new UsernameNotFoundException("존재하지 유저"));
-
- return new PrincipalDetails(foundUser);
- }
-}
diff --git a/src/main/java/com/example/security2/config/CorsConfig.java b/src/main/java/com/example/security2/config/CorsConfig.java
new file mode 100644
index 0000000..2bc5680
--- /dev/null
+++ b/src/main/java/com/example/security2/config/CorsConfig.java
@@ -0,0 +1,25 @@
+package com.example.security2.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+@Configuration
+public class CorsConfig {
+
+ @Bean
+ public CorsFilter corsFilter() {
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.addAllowedOriginPattern("*");
+ config.addAllowedHeader("*");
+ config.addAllowedMethod("*");
+
+ source.registerCorsConfiguration("/rest/api/**", config);
+ return new CorsFilter(source);
+ }
+}
diff --git a/src/main/java/com/example/security2/config/JwtSecurityConfig.java b/src/main/java/com/example/security2/config/JwtSecurityConfig.java
new file mode 100644
index 0000000..541882c
--- /dev/null
+++ b/src/main/java/com/example/security2/config/JwtSecurityConfig.java
@@ -0,0 +1,25 @@
+package com.example.security2.config;
+
+import com.example.security2.jwt.JwtFilter;
+import com.example.security2.jwt.TokenProvider;
+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 JwtSecurityConfig extends SecurityConfigurerAdapter {
+
+ private final TokenProvider tokenProvider;
+
+ public JwtSecurityConfig(TokenProvider tokenProvider) {
+ this.tokenProvider = tokenProvider;
+ }
+
+ @Override
+ public void configure(HttpSecurity http) throws Exception {
+ // security 로직에 JwtFilter 등록
+ http.addFilterBefore(
+ new JwtFilter(tokenProvider),
+ UsernamePasswordAuthenticationFilter.class);
+ }
+}
diff --git a/src/main/java/com/example/security2/config/SecurityConfig.java b/src/main/java/com/example/security2/config/SecurityConfig.java
index 1a40adb..a96af67 100644
--- a/src/main/java/com/example/security2/config/SecurityConfig.java
+++ b/src/main/java/com/example/security2/config/SecurityConfig.java
@@ -1,59 +1,75 @@
package com.example.security2.config;
-import com.example.security2.oauth.PrincipalOauth2UserService;
+import com.example.security2.jwt.JwtAccessDeniedHandler;
+import com.example.security2.jwt.JwtAuthenticationEntryPoint;
+import com.example.security2.jwt.TokenProvider;
+import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.web.filter.CorsFilter;
@Configuration
-@EnableWebSecurity // 스프링 필터체인에 등록
-@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) // 권한 처리, Secured 어노테이션 활성화. preAuthorize, postAuthorize 어노테이션 활성화
+@EnableWebSecurity
+@EnableMethodSecurity
public class SecurityConfig {
- private final PrincipalOauth2UserService principalOauth2UserService;
+ private final CorsFilter corsFilter;
+ private final TokenProvider tokenProvider;
+ private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
+ private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
- public SecurityConfig(PrincipalOauth2UserService principalOauth2UserService) {
- this.principalOauth2UserService = principalOauth2UserService;
+ public SecurityConfig(CorsFilter corsFilter, TokenProvider tokenProvider, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtAccessDeniedHandler jwtAccessDeniedHandler) {
+ this.corsFilter = corsFilter;
+ this.tokenProvider = tokenProvider;
+ this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
+ this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
}
- // SecurityConfig <-> PrincipalOauth2UserService 순환참조로 인해 분리(PasswordConfig)
- /*@Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }*/
-
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// csrf, cors 일단 비활성화
- http.csrf(AbstractHttpConfigurer::disable);
- http.cors(AbstractHttpConfigurer::disable);
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+// .cors(AbstractHttpConfigurer::disable)
- http.authorizeHttpRequests((requests) -> requests
- .requestMatchers("/user/**").authenticated()
- .requestMatchers("/manager/**").access(
- new WebExpressionAuthorizationManager("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
+ .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
+ // jwt 예외핸들러 등록
+ .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
+ httpSecurityExceptionHandlingConfigurer
+ .authenticationEntryPoint(jwtAuthenticationEntryPoint)
+ .accessDeniedHandler(jwtAccessDeniedHandler)
)
- .requestMatchers("/admin/**").access(
- new WebExpressionAuthorizationManager("hasRole('ROLE_ADMIN')")
+
+ // 세션을 사용하지 않기 때문에 "STATELESS"로 설정
+ .sessionManagement(sessionManagementConfigurer ->
+ sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
- .anyRequest().permitAll())
- .formLogin(form -> form
- .loginPage("/loginForm")
- .usernameParameter("email")
- .loginProcessingUrl("/login")
- .defaultSuccessUrl("/")
+
+ .authorizeHttpRequests((requests) -> requests
+ .requestMatchers(PathRequest.toH2Console()).permitAll()
+ .requestMatchers(
+ new AntPathRequestMatcher("/api/v1/auth"),
+ new AntPathRequestMatcher("/api/v1/user/hello"),
+ new AntPathRequestMatcher("/api/v1/user/signup")
+ ).permitAll()
+ .anyRequest().authenticated()
)
- .oauth2Login(oauth2 -> oauth2
- .loginPage("/loginForm")
- .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
- .userService(principalOauth2UserService)
- )
- );
+ // enable h2 console
+ .headers(header -> header
+ .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
+ )
+
+ // JwtFilter 적용
+ .apply(new JwtSecurityConfig(tokenProvider));
return http.build();
}
diff --git a/src/main/java/com/example/security2/config/WebMvcConfig.java b/src/main/java/com/example/security2/config/WebMvcConfig.java
deleted file mode 100644
index 2684341..0000000
--- a/src/main/java/com/example/security2/config/WebMvcConfig.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.example.security2.config;
-
-import org.springframework.boot.web.servlet.view.MustacheViewResolver;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
-@Configuration
-public class WebMvcConfig implements WebMvcConfigurer {
-
- @Override
- public void configureViewResolvers(ViewResolverRegistry registry) {
- // 머스테치를 html로 재설정.
- MustacheViewResolver viewResolver = new MustacheViewResolver();
- viewResolver.setCharset("UTF-8");
- viewResolver.setContentType("text/html;charset=UTF-8");
- viewResolver.setPrefix("classpath:/templates/");
- viewResolver.setSuffix(".html");
-
- registry.viewResolver(viewResolver);
- }
-}
diff --git a/src/main/java/com/example/security2/controller/AuthController.java b/src/main/java/com/example/security2/controller/AuthController.java
new file mode 100644
index 0000000..6ed0086
--- /dev/null
+++ b/src/main/java/com/example/security2/controller/AuthController.java
@@ -0,0 +1,46 @@
+package com.example.security2.controller;
+
+import com.example.security2.dto.LoginDto;
+import com.example.security2.dto.TokenDto;
+import com.example.security2.jwt.JwtFilter;
+import com.example.security2.jwt.TokenProvider;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/auth")
+public class AuthController {
+
+ private final TokenProvider tokenProvider;
+ private final AuthenticationManagerBuilder authenticationManagerBuilder;
+
+ public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
+ this.tokenProvider = tokenProvider;
+ this.authenticationManagerBuilder = authenticationManagerBuilder;
+ }
+
+ @PostMapping
+ public ResponseEntity authenticate(@Valid @RequestBody LoginDto loginDto) {
+ UsernamePasswordAuthenticationToken authenticationToken =
+ new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
+
+ Authentication authenticate = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
+ SecurityContextHolder.getContext().setAuthentication(authenticate);
+ String jwt = tokenProvider.createToken(authenticate);
+
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);
+
+ return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
+ }
+}
diff --git a/src/main/java/com/example/security2/controller/HealthController.java b/src/main/java/com/example/security2/controller/HealthController.java
deleted file mode 100644
index 89126aa..0000000
--- a/src/main/java/com/example/security2/controller/HealthController.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.example.security2.controller;
-
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/health")
-public class HealthController {
-
- @GetMapping
- public String health() {
- return "health";
- }
-}
diff --git a/src/main/java/com/example/security2/controller/UserController.java b/src/main/java/com/example/security2/controller/UserController.java
new file mode 100644
index 0000000..72a2158
--- /dev/null
+++ b/src/main/java/com/example/security2/controller/UserController.java
@@ -0,0 +1,39 @@
+package com.example.security2.controller;
+
+import com.example.security2.dto.UserDto;
+import com.example.security2.service.UserService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/user")
+@RequiredArgsConstructor
+public class UserController {
+
+ private final UserService userService;
+
+ @GetMapping("/hello")
+ public ResponseEntity hello() {
+ return ResponseEntity.ok("hello");
+ }
+
+ @PostMapping("/signup")
+ public ResponseEntity signUp(@RequestBody UserDto userDto) {
+ return ResponseEntity.ok(userService.signUp(userDto));
+ }
+
+ @GetMapping
+ @PreAuthorize("hasAnyRole('USER','ADMIN')")
+ public ResponseEntity getMyUserInfo(HttpServletRequest request) {
+ return ResponseEntity.ok(userService.getMyUserWithAuthorities());
+ }
+
+ @GetMapping("/{username}")
+ @PreAuthorize("hasAnyRole('ADMIN')")
+ public ResponseEntity getUserInfo(@PathVariable String username) {
+ return ResponseEntity.ok(userService.getUserWithAuthorities(username));
+ }
+}
diff --git a/src/main/java/com/example/security2/controller/ViewController.java b/src/main/java/com/example/security2/controller/ViewController.java
deleted file mode 100644
index c35f0e5..0000000
--- a/src/main/java/com/example/security2/controller/ViewController.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.example.security2.controller;
-
-import com.example.security2.auth.PrincipalDetails;
-import com.example.security2.entity.User;
-import com.example.security2.enums.AuthType;
-import com.example.security2.repository.UserRepository;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.security.access.annotation.Secured;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.security.core.annotation.AuthenticationPrincipal;
-import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
-
-/**
- * View
- */
-@Controller
-@Slf4j
-@RequiredArgsConstructor
-public class ViewController {
-
- // 간편구현을 위해 레포지토리만 생성
- private final UserRepository userRepository;
- private final PasswordEncoder passwordEncoder;
-
- @GetMapping({"", "/"})
- public String index() {
- return "index";
- }
-
- // @AuthenticationPrincipal 어노테이션으로 인증객체 내 User정보 확인 가능
- @GetMapping("/user")
- public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
- log.info("principalDetails: {}", principalDetails.getUser());
- return "user";
- }
-
- @GetMapping("/admin")
- public @ResponseBody String admin() {
- return "admin";
- }
-
- @GetMapping("/manager")
- public @ResponseBody String manager() {
- return "manager";
- }
-
- @GetMapping("/loginForm")
- public String loginForm() {
- return "loginForm";
- }
-
- @GetMapping("/joinForm")
- public String joinForm() {
- return "joinForm";
- }
-
- @PostMapping("/join")
- public String join(User user) {
- user.setRole(AuthType.ROLE_USER.name());
- user.setPassword(passwordEncoder.encode(user.getPassword()));
-
- userRepository.save(user);
- return "redirect:/loginForm";
- }
-
- @Secured("ROLE_ADMIN")
- @GetMapping("/admin-only")
- public @ResponseBody String accessOnlyAdmin() {
- return "어드민 권한만 접근 가능";
- }
-
- /**
- * 권한 하나로만 걸고 싶으면 @Secured로 처리하기 쉽고, 여러개면 @PreAuthorize로 가능
- *
- * @PreAuthorize 함수 실행 전 실행되는 어노테이션
- * @PostAuthorize 함수 실행 후 실행되는 어노테이션 -> 후처리는 잘 사용하지 않음
- */
- @PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
- @GetMapping("/manager-and-admin")
- public @ResponseBody String accessManagerAndAdmin() {
- return "매니저, 관리자 권한만 접근 허용";
- }
-}
diff --git a/src/main/java/com/example/security2/dto/AuthorityDto.java b/src/main/java/com/example/security2/dto/AuthorityDto.java
new file mode 100644
index 0000000..42b5beb
--- /dev/null
+++ b/src/main/java/com/example/security2/dto/AuthorityDto.java
@@ -0,0 +1,4 @@
+package com.example.security2.dto;
+
+public record AuthorityDto(String authorityName) {
+}
diff --git a/src/main/java/com/example/security2/dto/ErrorDto.java b/src/main/java/com/example/security2/dto/ErrorDto.java
new file mode 100644
index 0000000..271faf1
--- /dev/null
+++ b/src/main/java/com/example/security2/dto/ErrorDto.java
@@ -0,0 +1,18 @@
+package com.example.security2.dto;
+
+import org.springframework.validation.FieldError;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public record ErrorDto(int status, String message, List fieldErrors) {
+
+ public ErrorDto(int status, String message) {
+ this(status, message, new ArrayList<>());
+ }
+
+ public void addFieldError(String objectName, String path, String message) {
+ FieldError error = new FieldError(objectName, path, message);
+ fieldErrors.add(error);
+ }
+}
diff --git a/src/main/java/com/example/security2/dto/LoginDto.java b/src/main/java/com/example/security2/dto/LoginDto.java
new file mode 100644
index 0000000..ce6a047
--- /dev/null
+++ b/src/main/java/com/example/security2/dto/LoginDto.java
@@ -0,0 +1,21 @@
+package com.example.security2.dto;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.*;
+
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class LoginDto {
+
+ @NotNull
+ @Size(min = 3, max = 50)
+ private String username;
+
+ @NotNull
+ @Size(min = 3, max = 100)
+ private String password;
+}
diff --git a/src/main/java/com/example/security2/dto/TokenDto.java b/src/main/java/com/example/security2/dto/TokenDto.java
new file mode 100644
index 0000000..f3bebaa
--- /dev/null
+++ b/src/main/java/com/example/security2/dto/TokenDto.java
@@ -0,0 +1,4 @@
+package com.example.security2.dto;
+
+public record TokenDto(String token) {
+}
diff --git a/src/main/java/com/example/security2/dto/UserDto.java b/src/main/java/com/example/security2/dto/UserDto.java
new file mode 100644
index 0000000..1359523
--- /dev/null
+++ b/src/main/java/com/example/security2/dto/UserDto.java
@@ -0,0 +1,47 @@
+package com.example.security2.dto;
+
+import com.example.security2.entity.Authority;
+import com.example.security2.entity.User;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.*;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserDto {
+
+ @NotNull
+ @Size(min = 3, max = 50)
+ private String username;
+
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ @NotNull
+ @Size(min = 3, max = 100)
+ private String password;
+
+ @NotNull
+ @Size(min = 3, max = 50)
+ private String nickname;
+
+ private Set authorityDtoSet;
+
+ public static UserDto from(User user) {
+ if (user == null)
+ return null;
+
+ return UserDto.builder()
+ .username(user.getUsername())
+ .nickname(user.getNickname())
+ .authorityDtoSet(user.getAuthorities().stream()
+ .map(authority -> new AuthorityDto(authority.getAuthorityName()))
+ .collect(Collectors.toSet()))
+ .build();
+ }
+}
diff --git a/src/main/java/com/example/security2/entity/Authority.java b/src/main/java/com/example/security2/entity/Authority.java
new file mode 100644
index 0000000..f80574e
--- /dev/null
+++ b/src/main/java/com/example/security2/entity/Authority.java
@@ -0,0 +1,22 @@
+package com.example.security2.entity;
+
+import lombok.*;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "authority")
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class Authority {
+
+ @Id
+ @Column(name = "authority_name", length = 50)
+ private String authorityName;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/security2/entity/User.java b/src/main/java/com/example/security2/entity/User.java
index 3e65117..22fe5ba 100644
--- a/src/main/java/com/example/security2/entity/User.java
+++ b/src/main/java/com/example/security2/entity/User.java
@@ -3,48 +3,46 @@
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
+import java.util.Set;
@Entity
@Getter
@Setter
-@Table(name = "users")
+@Table(name = "`user`")
+@Builder
+@AllArgsConstructor
@NoArgsConstructor
-@ToString
public class User {
@Id
+ @Column(name = "user_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+ private Long userId;
- @Column(nullable = false, unique = true)
- private String email;
+ @Column(length = 50, unique = true)
+ private String username;
- @Column(nullable = false)
+ @Column(length = 100)
private String password;
- private String name;
+ @Column(length = 50)
+ private String nickname;
- private Integer age;
-
- private String role;
-
- private String provider;
- private String providerId;
+ private boolean activated;
@CreationTimestamp
private LocalDateTime createDate;
-// private LocalDateTime lastLoginDate;
-
-
- @Builder
- public User(String email, String password, String name, String role, String provider, String providerId) {
- this.email = email;
- this.password = password;
- this.name = name;
- this.role = role;
- this.provider = provider;
- this.providerId = providerId;
- }
+
+ @UpdateTimestamp
+ private LocalDateTime updateDate;
+
+ @ManyToMany
+ @JoinTable(
+ name = "user_authority",
+ joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")},
+ inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
+ private Set authorities;
}
diff --git a/src/main/java/com/example/security2/enums/AuthType.java b/src/main/java/com/example/security2/enums/AuthType.java
deleted file mode 100644
index f29fe93..0000000
--- a/src/main/java/com/example/security2/enums/AuthType.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.example.security2.enums;
-
-public enum AuthType {
-
- ROLE_ADMIN,
- ROLE_MANAGER,
- ROLE_USER;
-}
diff --git a/src/main/java/com/example/security2/exception/DuplicateMemberException.java b/src/main/java/com/example/security2/exception/DuplicateMemberException.java
new file mode 100644
index 0000000..27154cc
--- /dev/null
+++ b/src/main/java/com/example/security2/exception/DuplicateMemberException.java
@@ -0,0 +1,20 @@
+package com.example.security2.exception;
+
+public class DuplicateMemberException extends RuntimeException {
+
+ public DuplicateMemberException() {
+ super();
+ }
+
+ public DuplicateMemberException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public DuplicateMemberException(String message) {
+ super(message);
+ }
+
+ public DuplicateMemberException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/example/security2/exception/NotFoundMemberException.java b/src/main/java/com/example/security2/exception/NotFoundMemberException.java
new file mode 100644
index 0000000..a1106a5
--- /dev/null
+++ b/src/main/java/com/example/security2/exception/NotFoundMemberException.java
@@ -0,0 +1,20 @@
+package com.example.security2.exception;
+
+public class NotFoundMemberException extends RuntimeException {
+
+ public NotFoundMemberException() {
+ super();
+ }
+
+ public NotFoundMemberException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public NotFoundMemberException(String message) {
+ super(message);
+ }
+
+ public NotFoundMemberException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/example/security2/handler/MethodArgumentNotValidExceptionHandler.java b/src/main/java/com/example/security2/handler/MethodArgumentNotValidExceptionHandler.java
new file mode 100644
index 0000000..c5a2fde
--- /dev/null
+++ b/src/main/java/com/example/security2/handler/MethodArgumentNotValidExceptionHandler.java
@@ -0,0 +1,38 @@
+package com.example.security2.handler;
+
+import com.example.security2.dto.ErrorDto;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.util.List;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@ControllerAdvice
+public class MethodArgumentNotValidExceptionHandler {
+
+ @ResponseStatus(BAD_REQUEST)
+ @ResponseBody
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ErrorDto methodArgumentNotValidException(MethodArgumentNotValidException ex) {
+ BindingResult result = ex.getBindingResult();
+ List fieldErrors = result.getFieldErrors();
+ return processFieldErrors(fieldErrors);
+ }
+
+ private ErrorDto processFieldErrors(List fieldErrors) {
+ ErrorDto errorDto = new ErrorDto(BAD_REQUEST.value(), "@Valid Error");
+ for (FieldError fieldError : fieldErrors) {
+ errorDto.addFieldError(fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
+ }
+ return errorDto;
+ }
+}
diff --git a/src/main/java/com/example/security2/handler/RestResponseExceptionHandler.java b/src/main/java/com/example/security2/handler/RestResponseExceptionHandler.java
new file mode 100644
index 0000000..14385f3
--- /dev/null
+++ b/src/main/java/com/example/security2/handler/RestResponseExceptionHandler.java
@@ -0,0 +1,29 @@
+package com.example.security2.handler;
+
+import com.example.security2.dto.ErrorDto;
+import com.example.security2.exception.DuplicateMemberException;
+import com.example.security2.exception.NotFoundMemberException;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+import java.nio.file.AccessDeniedException;
+
+import static org.springframework.http.HttpStatus.CONFLICT;
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+
+@RestControllerAdvice
+public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler {
+
+ @ResponseStatus(CONFLICT)
+ @ExceptionHandler(value = { DuplicateMemberException.class })
+ protected ErrorDto conflict(RuntimeException ex, WebRequest request) {
+ return new ErrorDto(CONFLICT.value(), ex.getMessage());
+ }
+
+ @ResponseStatus(FORBIDDEN)
+ @ExceptionHandler(value = { NotFoundMemberException.class, AccessDeniedException.class })
+ protected ErrorDto forbidden(RuntimeException ex, WebRequest request) {
+ return new ErrorDto(FORBIDDEN.value(), ex.getMessage());
+ }
+}
diff --git a/src/main/java/com/example/security2/jwt/JwtAccessDeniedHandler.java b/src/main/java/com/example/security2/jwt/JwtAccessDeniedHandler.java
new file mode 100644
index 0000000..393d827
--- /dev/null
+++ b/src/main/java/com/example/security2/jwt/JwtAccessDeniedHandler.java
@@ -0,0 +1,22 @@
+package com.example.security2.jwt;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+public class JwtAccessDeniedHandler implements AccessDeniedHandler {
+ @Override
+ public void handle(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AccessDeniedException accessDeniedException
+ ) throws IOException {
+ // 필요한 권한이 없이 접근하려 할때 403
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
+}
diff --git a/src/main/java/com/example/security2/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/security2/jwt/JwtAuthenticationEntryPoint.java
new file mode 100644
index 0000000..c03f5c4
--- /dev/null
+++ b/src/main/java/com/example/security2/jwt/JwtAuthenticationEntryPoint.java
@@ -0,0 +1,23 @@
+package com.example.security2.jwt;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+ @Override
+ public void commence(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException authException
+ ) throws IOException {
+ // 유효한 자격증명을 제공하지 않고 접근하려 할 때 401 권한에러
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+}
diff --git a/src/main/java/com/example/security2/jwt/JwtFilter.java b/src/main/java/com/example/security2/jwt/JwtFilter.java
new file mode 100644
index 0000000..cf5b3ff
--- /dev/null
+++ b/src/main/java/com/example/security2/jwt/JwtFilter.java
@@ -0,0 +1,55 @@
+package com.example.security2.jwt;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class JwtFilter extends GenericFilterBean {
+
+ private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
+ public static final String AUTHORIZATION_HEADER = "Authorization";
+ public static final String BEARER_AUTH = "Bearer ";
+ private final TokenProvider tokenProvider;
+
+ public JwtFilter(TokenProvider tokenProvider) {
+ this.tokenProvider = tokenProvider;
+ }
+
+ // 실제 필터링 로직
+ // 토큰의 인증정보를 SecurityContext에 저장하는 역할 수행
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ String jwt = resolveToken(httpServletRequest);
+ String requestURI = httpServletRequest.getRequestURI();
+
+ if (StringUtils.hasText(jwt)) {
+ Authentication authentication = tokenProvider.getAuthentication(jwt);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다. uri: {}", authentication, requestURI);
+ } else {
+ logger.debug("유효한 JWT 토큰이 없습니다. uri: {}", requestURI);
+ }
+
+ chain.doFilter(request, response);
+ }
+
+ private String resolveToken(HttpServletRequest request) {
+ String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
+ return isBearerToken(bearerToken) ? bearerToken.substring(7) : null;
+ }
+
+ private boolean isBearerToken(String token) {
+ return (StringUtils.hasText(token) && token.startsWith(BEARER_AUTH));
+ }
+}
diff --git a/src/main/java/com/example/security2/jwt/TokenProvider.java b/src/main/java/com/example/security2/jwt/TokenProvider.java
new file mode 100644
index 0000000..46fd206
--- /dev/null
+++ b/src/main/java/com/example/security2/jwt/TokenProvider.java
@@ -0,0 +1,92 @@
+package com.example.security2.jwt;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import io.jsonwebtoken.security.SecurityException;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.stereotype.Component;
+
+import java.security.Key;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.stream.Collectors;
+
+/**
+ * JWT를 생성하고 검증
+ */
+@Component
+@Slf4j
+public class TokenProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
+ private static final String AUTHORITIES_KEY = "secret";
+ private final long tokenValidityInMilliseconds;
+ private final Key key;
+
+ public TokenProvider(
+ @Value("${jwt.secret}") String secret,
+ @Value("${jwt.token-validity-in-seconds}") long tokenValidityInMilliseconds) {
+ this.tokenValidityInMilliseconds = tokenValidityInMilliseconds * 1000;
+ this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
+ }
+
+ public String createToken(Authentication authentication) {
+ String authorities = authentication.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.joining(","));
+
+ // 토큰의 expire 시간을 설정
+ long now = new Date().getTime();
+ Date validity = new Date(now + this.tokenValidityInMilliseconds);
+
+ return Jwts.builder()
+ .setSubject(authentication.getName())
+ .claim(AUTHORITIES_KEY, authorities)
+ .signWith(key, SignatureAlgorithm.HS512)
+ .setExpiration(validity)
+ .compact();
+ }
+
+ // 토큰으로 클레임을 만들고 이를 이용해 유저 객체를 만들어서 최종적으로 authentication 객체를 리턴
+ public Authentication getAuthentication(String token) {
+ Claims claims = getTokenClaims(token);
+
+ Collection extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
+ .map(SimpleGrantedAuthority::new)
+ .toList();
+
+ User principal = new User(claims.getSubject(), "", authorities);
+
+ return new UsernamePasswordAuthenticationToken(principal, token, authorities);
+ }
+
+ // jwt 토큰 리턴
+ private Claims getTokenClaims(String token) {
+ try {
+ return Jwts.parserBuilder()
+ .setSigningKey(key)
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ } catch (SecurityException | MalformedJwtException e) {
+ throw new JwtException("잘못된 JWT 서명입니다.");
+ } catch (ExpiredJwtException e) {
+ throw new JwtException("만료된 JWT 토큰입니다.");
+ } catch (UnsupportedJwtException e) {
+ throw new JwtException("지원하지않는 JWT 토큰입니다.");
+ } catch (IllegalArgumentException e) {
+ throw new JwtException("JWT 토큰이 잘못되없습니다.");
+ }
+ }
+
+}
diff --git a/src/main/java/com/example/security2/oauth/PrincipalOauth2UserService.java b/src/main/java/com/example/security2/oauth/PrincipalOauth2UserService.java
deleted file mode 100644
index 6e28197..0000000
--- a/src/main/java/com/example/security2/oauth/PrincipalOauth2UserService.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.example.security2.oauth;
-
-import com.example.security2.auth.PrincipalDetails;
-import com.example.security2.entity.User;
-import com.example.security2.enums.AuthType;
-import com.example.security2.repository.UserRepository;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
-import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
-import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
-import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.stereotype.Service;
-
-import java.util.Map;
-import java.util.Optional;
-import java.util.Random;
-
-@Service
-@Slf4j
-public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
-
- private final PasswordEncoder passwordEncoder;
- private final UserRepository userRepository;
-
- public PrincipalOauth2UserService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
- this.passwordEncoder = passwordEncoder;
- this.userRepository = userRepository;
- }
-
- // 함수 종료 시 @AuthenticationPrincipal 어노테이션이 만들어진다.
- // 구글로 부터 받은 userRequest 데이터에 대한 후처리되는 함수.
- @Override
- public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
- OAuth2User oAuth2User = super.loadUser(userRequest);
- Map attributes = oAuth2User.getAttributes();
- log.info("attr: {}", attributes.toString());
-
- String email = oAuth2User.getAttribute("email");
- Optional optionalUser = userRepository.findByEmail(email);
-
- User userEntity;
- if (optionalUser.isEmpty()) {
- userEntity = createUserEntityByOAuth2User(userRequest, oAuth2User);
- userRepository.save(userEntity);
- } else {
- userEntity = optionalUser.get();
- }
-
- return new PrincipalDetails(userEntity, oAuth2User.getAttributes());
- }
-
- private User createUserEntityByOAuth2User(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
- String provider = userRequest.getClientRegistration().getRegistrationId(); // google
- String providerId = oAuth2User.getAttribute("sub");
- String userName = String.format("%s_%s", provider, providerId);
- String password = passwordEncoder.encode(randomPassword());
- String email = oAuth2User.getAttribute("email");
- String role = AuthType.ROLE_USER.name();
-
- return User.builder()
- .email(email)
- .name(userName)
- .password(password)
- .role(role)
- .provider(provider)
- .providerId(providerId)
- .build();
- }
-
- private String randomPassword() {
- // 랜덤 비밀번호(0 ~ z까지 20자리 랜덤 비밀번호 생성)
- Random random = new Random();
- int leftLimit = 97; // letter '0'
- int rightLimit = 122; // letter 'z'
- int targetStringLength = 20;
-
- return random.ints(leftLimit, rightLimit + 1)
- .limit(targetStringLength)
- .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
- .toString();
- }
-}
diff --git a/src/main/java/com/example/security2/repository/AuthorityRepository.java b/src/main/java/com/example/security2/repository/AuthorityRepository.java
new file mode 100644
index 0000000..00e3176
--- /dev/null
+++ b/src/main/java/com/example/security2/repository/AuthorityRepository.java
@@ -0,0 +1,7 @@
+package com.example.security2.repository;
+
+import com.example.security2.entity.Authority;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface AuthorityRepository extends JpaRepository {
+}
diff --git a/src/main/java/com/example/security2/repository/UserRepository.java b/src/main/java/com/example/security2/repository/UserRepository.java
index 862c00e..e845ec7 100644
--- a/src/main/java/com/example/security2/repository/UserRepository.java
+++ b/src/main/java/com/example/security2/repository/UserRepository.java
@@ -1,10 +1,13 @@
package com.example.security2.repository;
import com.example.security2.entity.User;
+import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository {
- Optional findByEmail(String email);
+
+ @EntityGraph(attributePaths = "authorities")
+ Optional findByUsername(String username);
}
diff --git a/src/main/java/com/example/security2/service/CustomUserDetailService.java b/src/main/java/com/example/security2/service/CustomUserDetailService.java
new file mode 100644
index 0000000..af984e9
--- /dev/null
+++ b/src/main/java/com/example/security2/service/CustomUserDetailService.java
@@ -0,0 +1,46 @@
+package com.example.security2.service;
+
+import com.example.security2.entity.User;
+import com.example.security2.repository.UserRepository;
+import jakarta.transaction.Transactional;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+
+@Component("userDetailsService")
+public class CustomUserDetailService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ public CustomUserDetailService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ @Override
+ @Transactional
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return userRepository.findByUsername(username)
+ .map(user -> createUser(username, user))
+ .orElseThrow(() -> new UsernameNotFoundException(username + " > DB에서 찾을 수 없음."));
+ }
+
+ private org.springframework.security.core.userdetails.User createUser(String username, User user) {
+ if (!user.isActivated())
+ throw new RuntimeException(username + " > 활성화된 유저가 아닙니다.");
+
+ Collection extends GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
+ .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
+ .toList();
+
+ return new org.springframework.security.core.userdetails.User(
+ user.getUsername(),
+ user.getPassword(),
+ grantedAuthorities
+ );
+ }
+}
diff --git a/src/main/java/com/example/security2/service/UserService.java b/src/main/java/com/example/security2/service/UserService.java
new file mode 100644
index 0000000..0e52243
--- /dev/null
+++ b/src/main/java/com/example/security2/service/UserService.java
@@ -0,0 +1,62 @@
+package com.example.security2.service;
+
+import com.example.security2.dto.UserDto;
+import com.example.security2.entity.Authority;
+import com.example.security2.entity.User;
+import com.example.security2.exception.DuplicateMemberException;
+import com.example.security2.exception.NotFoundMemberException;
+import com.example.security2.repository.UserRepository;
+import com.example.security2.util.SecurityUtil;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+
+@Service
+public class UserService {
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
+ this.userRepository = userRepository;
+ this.passwordEncoder = passwordEncoder;
+ }
+
+ @Transactional
+ public UserDto signUp(UserDto userDto) {
+ if (userRepository.findByUsername(userDto.getUsername()).orElse(null) != null)
+ throw new DuplicateMemberException("이미 가입되어 있는 유저");
+
+ Authority authority = Authority.builder()
+ .authorityName("ROLE_USER")
+ .build();
+
+ User user = User.builder()
+ .username(userDto.getUsername())
+ .password(passwordEncoder.encode(userDto.getPassword()))
+ .nickname(userDto.getNickname())
+ .authorities(Collections.singleton(authority))
+ .activated(true)
+ .build();
+
+ return UserDto.from(userRepository.save(user));
+ }
+
+ @Transactional(readOnly = true)
+ public UserDto getMyUserWithAuthorities() {
+ /*String findUsername = SecurityUtil.getCurrentUsername().stream().findFirst().orElseThrow(() -> new UsernameNotFoundException("Member not found"));
+ User user = userRepository.findByUsername(findUsername).orElseThrow(() -> new NotFoundMemberException("Member not found"));*/
+
+ return UserDto.from(
+ SecurityUtil.getCurrentUsername()
+ .flatMap(userRepository::findByUsername)
+ .orElseThrow(() -> new NotFoundMemberException("Member not found"))
+ );
+ }
+
+ @Transactional(readOnly = true)
+ public UserDto getUserWithAuthorities(String username) {
+ return UserDto.from(userRepository.findByUsername(username).orElse(null));
+ }
+}
diff --git a/src/main/java/com/example/security2/util/SecurityUtil.java b/src/main/java/com/example/security2/util/SecurityUtil.java
new file mode 100644
index 0000000..ee3abd8
--- /dev/null
+++ b/src/main/java/com/example/security2/util/SecurityUtil.java
@@ -0,0 +1,32 @@
+package com.example.security2.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Optional;
+
+@Slf4j
+public class SecurityUtil {
+
+ private SecurityUtil() {}
+
+ public static Optional getCurrentUsername() {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+ if (authentication == null) {
+ log.debug("Security Context에 인증 정보가 없습니다.");
+ return Optional.empty();
+ }
+
+ String username;
+ if (authentication.getPrincipal() instanceof UserDetails springSecurityUser) {
+ username = springSecurityUser.getUsername();
+ } else {
+ username = (String) authentication.getPrincipal();
+ }
+
+ return Optional.ofNullable(username);
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 069b94e..3cfe1e4 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,34 +1,33 @@
spring:
- security:
- oauth2:
- client:
- registration:
- google:
- client-id: 574898903244-qrjp11vrbl3dh7alaneqman8odgao4fb.apps.googleusercontent.com
- client-secret: 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
- scope:
- - email
- - profile
-# - openid
-# facebook:
+
+ h2:
+ console:
+ enabled: true
datasource:
- driver-class-name: com.mysql.cj.jdbc.Driver
- username: root
- password: root
- url: jdbc:mysql://localhost:3306/user?serverTimezone=UTC&characterEncoding=UTF-8
+ url: jdbc:h2:~/test
+# url: jdbc:h2:mem:testdb
+ driver-class-name: org.h2.Driver
+ username: sa
+ password:
jpa:
- open-in-view: true
+ database-platform: org.hibernate.dialect.H2Dialect
hibernate:
- ddl-auto: update
- naming:
- physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
- show-sql: true
+ ddl-auto: create-drop
properties:
hibernate:
format_sql: true
+ show_sql: true
+ defer-datasource-initialization: true
+
+jwt:
+ header: Authorization
+ #HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
+ #echo 'jvmoverloads-spring-boot-jwt-tutorial-secret-jvmoverloads-spring-boot-jwt-tutorial-secret'| base64
+ secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
+ token-validity-in-seconds: 86400
logging:
level:
- org.hibernate.sql: debug
\ No newline at end of file
+ com.example: DEBUG
\ No newline at end of file
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
new file mode 100644
index 0000000..fd138b2
--- /dev/null
+++ b/src/main/resources/data.sql
@@ -0,0 +1,9 @@
+insert into "user" (username, password, nickname, activated, create_date, update_date) values ('admin', '$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO/Vxd5DOi', 'admin', 1, current_time, null);
+insert into "user" (username, password, nickname, activated, create_date, update_date) values ('user', '$2a$08$UkVvwpULis18S19S5pZFn.YHPZt3oaqHZnDwqbCW9pft6uFtkXKDC', 'user', 1, current_time, null);
+
+insert into authority (authority_name) values ('ROLE_USER');
+insert into authority (authority_name) values ('ROLE_ADMIN');
+
+insert into user_authority (user_id, authority_name) values (1, 'ROLE_USER');
+insert into user_authority (user_id, authority_name) values (1, 'ROLE_ADMIN');
+insert into user_authority (user_id, authority_name) values (2, 'ROLE_USER');
\ No newline at end of file
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
deleted file mode 100644
index 5a2cc2e..0000000
--- a/src/main/resources/templates/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- 인덱스 페이지
-
-
- 인덱스 페이지입니다.
-
-
\ No newline at end of file
diff --git a/src/main/resources/templates/joinForm.html b/src/main/resources/templates/joinForm.html
deleted file mode 100644
index f06b897..0000000
--- a/src/main/resources/templates/joinForm.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
- 회원가입 페이지
-
-
-회원가입 페이지
-
-
-
-
\ No newline at end of file
diff --git a/src/main/resources/templates/loginForm.html b/src/main/resources/templates/loginForm.html
deleted file mode 100644
index 4e18845..0000000
--- a/src/main/resources/templates/loginForm.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
- 로그인 페이지
-
-
-로그인 페이지
-
-
-
-구글 로그인
-회원가입
-
-
-
\ No newline at end of file