diff --git a/src/main/java/demo/api/AppConfig.java b/src/main/java/demo/api/AppConfig.java index c514271..143ce85 100644 --- a/src/main/java/demo/api/AppConfig.java +++ b/src/main/java/demo/api/AppConfig.java @@ -8,27 +8,27 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.password.PasswordEncoder; -@Configuration -public class AppConfig { - private final UserRepository userRepository; - private final PasswordEncoder bCryptPasswordEncoder; - - public AppConfig(UserRepository userRepository, PasswordEncoder bCryptPasswordEncoder) { - System.out.println("AppConfig"); - System.out.println("userRepository = " + userRepository); - this.userRepository = userRepository; - this.bCryptPasswordEncoder = bCryptPasswordEncoder; - } - - @Bean - public UserService userService() { - System.out.println("userService"); - return new UserServiceImpl(userRepository, bCryptPasswordEncoder); - } - -// @Bean -// public BCryptPasswordEncoder passwordEncoder() { -// System.out.println("passwordEncoder"); -// return new BCryptPasswordEncoder(); +//@Configuration +//public class AppConfig { +// private final UserRepository userRepository; +// private final PasswordEncoder bCryptPasswordEncoder; +// +// public AppConfig(UserRepository userRepository, PasswordEncoder bCryptPasswordEncoder) { +// System.out.println("AppConfig"); +// System.out.println("userRepository = " + userRepository); +// this.userRepository = userRepository; +// this.bCryptPasswordEncoder = bCryptPasswordEncoder; // } -} +// +// @Bean +// public UserService userService() { +// System.out.println("userService"); +// return new UserServiceImpl(userRepository, bCryptPasswordEncoder); +// } +// +//// @Bean +//// public BCryptPasswordEncoder passwordEncoder() { +//// System.out.println("passwordEncoder"); +//// return new BCryptPasswordEncoder(); +//// } +//} diff --git a/src/main/java/demo/api/config/JwtSecurityConfig.java b/src/main/java/demo/api/config/JwtSecurityConfig.java new file mode 100644 index 0000000..50cdf81 --- /dev/null +++ b/src/main/java/demo/api/config/JwtSecurityConfig.java @@ -0,0 +1,30 @@ +package demo.api.config; + +import demo.api.jwt.JwtTokenFilter; +import demo.api.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +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; + +/** + * SecurityConfigurerAdapter를 확장. + * JwtTokenProvider를 주입받음. + * JwtFilter를 통해 Security filterchain에 filter를 추가 등록 + */ +@RequiredArgsConstructor +public class JwtSecurityConfig extends SecurityConfigurerAdapter { + private final JwtTokenProvider jwtTokenProvider; + +// public JwtSecurityConfig(JwtTokenProvider jwtTokenProvider) { +// this.jwtTokenProvider = jwtTokenProvider; +// } + + @Override + public void configure(HttpSecurity http) throws Exception { + JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } + +} \ No newline at end of file diff --git a/src/main/java/demo/api/config/SecurityConfig.java b/src/main/java/demo/api/config/SecurityConfig.java index 6ec14a2..5b01272 100644 --- a/src/main/java/demo/api/config/SecurityConfig.java +++ b/src/main/java/demo/api/config/SecurityConfig.java @@ -1,14 +1,14 @@ package demo.api.config; -import demo.api.user.repository.UserRepository; +import demo.api.jwt.JwtAccessDeniedHandler; +import demo.api.jwt.JwtAuthenticationEntryPoint; +import demo.api.jwt.JwtTokenFilter; +import demo.api.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -17,16 +17,22 @@ import org.springframework.security.web.SecurityFilterChain; @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - @Bean - public UserDetailsService userDetailsService() { - return new UserDetailsServiceImpl(); - } + // 추가된 jwt 관련 친구들을 security config에 추가 + private final JwtTokenProvider jwtTokenProvider; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + +// @Bean +// public UserDetailsService userDetailsService() { +// return new UserDetailsServiceImpl(); +// } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // http .csrf().disable() .formLogin() @@ -36,11 +42,33 @@ public class SecurityConfig { .passwordParameter("password") .defaultSuccessUrl("/") .failureUrl("/user/signIn?fail=true"); + + // http .authorizeRequests() - .antMatchers("/", "/user/signUp", "/user/userList", "/user/signIn*").permitAll() + .antMatchers( + "/", + "/user/signUp", + "/user/userList", + "/user/signIn*", + "/favicon.ico" + ).permitAll() .anyRequest().authenticated(); + // No session will be created or used by spring security + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + // exception handling for jwt + http + .exceptionHandling() + .accessDeniedHandler(jwtAccessDeniedHandler) + .authenticationEntryPoint(jwtAuthenticationEntryPoint); + + // Apply JWT + http.apply(new JwtSecurityConfig(jwtTokenProvider)); + return http.build(); } } diff --git a/src/main/java/demo/api/config/UserDetailsServiceImpl.java b/src/main/java/demo/api/config/UserDetailsServiceImpl.java index 6b36707..8ff2c30 100644 --- a/src/main/java/demo/api/config/UserDetailsServiceImpl.java +++ b/src/main/java/demo/api/config/UserDetailsServiceImpl.java @@ -12,9 +12,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; - +@Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; diff --git a/src/main/java/demo/api/jwt/exception/CustomException.java b/src/main/java/demo/api/exception/CustomException.java similarity index 89% rename from src/main/java/demo/api/jwt/exception/CustomException.java rename to src/main/java/demo/api/exception/CustomException.java index 9aa49d6..9dc6a9a 100644 --- a/src/main/java/demo/api/jwt/exception/CustomException.java +++ b/src/main/java/demo/api/exception/CustomException.java @@ -1,4 +1,4 @@ -package demo.api.jwt.exception; +package demo.api.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/demo/api/jwt/JwtAccessDeniedHandler.java b/src/main/java/demo/api/jwt/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..7f15433 --- /dev/null +++ b/src/main/java/demo/api/jwt/JwtAccessDeniedHandler.java @@ -0,0 +1,24 @@ +package demo.api.jwt; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +/** + * AccessDeniedHandler + * + * AuthenticationEntryPoint와 달리 AccessDeniedHandler는 + * 유저 정보는 있으나, 엑세스 권한이 없는 경우 동작하는 친구이다. + */ +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } +} diff --git a/src/main/java/demo/api/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/demo/api/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..24a4ad5 --- /dev/null +++ b/src/main/java/demo/api/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,34 @@ +package demo.api.jwt; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +/** + * AuthenticationEntryPoint + * + * 인증 과정에서 실패하거나 인증을 위한 헤더정보를 보내지 않은 경우 + * 401(UnAuthorized) 에러가 발생하게 된다. + * + * Spring Security에서 인증되지 않은 사용자에 대한 접근 처리는 AuthenticationEntryPoint가 담당하는데, + * commence 메소드가 실행되어 처리된다. + */ + +@Slf4j +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e + ) throws IOException { + System.out.println(request.getRequestURI()); + log.error("UnAuthorized -- message : " + e.getMessage()); // 로그를 남기고 + response.sendRedirect("/user/signIn"); // 로그인 페이지로 리다이렉트되도록 하였다. + } +} \ No newline at end of file diff --git a/src/main/java/demo/api/jwt/JwtTokenFilter.java b/src/main/java/demo/api/jwt/JwtTokenFilter.java index b319159..56bee40 100644 --- a/src/main/java/demo/api/jwt/JwtTokenFilter.java +++ b/src/main/java/demo/api/jwt/JwtTokenFilter.java @@ -1,6 +1,6 @@ package demo.api.jwt; -import demo.api.jwt.exception.CustomException; +import demo.api.exception.CustomException; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -28,7 +28,7 @@ public class JwtTokenFilter extends OncePerRequestFilter { try { if (token != null && jwtTokenProvider.validateToken(token)) { Authentication auth = jwtTokenProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(auth); + SecurityContextHolder.getContext().setAuthentication(auth); // 정상 토큰이면 SecurityContext에 저장 } } catch (CustomException ex) { //this is very important, since it guarantees the user is not authenticated at all diff --git a/src/main/java/demo/api/jwt/JwtTokenProvider.java b/src/main/java/demo/api/jwt/JwtTokenProvider.java index 590d064..4ec9649 100644 --- a/src/main/java/demo/api/jwt/JwtTokenProvider.java +++ b/src/main/java/demo/api/jwt/JwtTokenProvider.java @@ -1,31 +1,25 @@ package demo.api.jwt; -import demo.api.jwt.exception.CustomException; +import demo.api.exception.CustomException; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import java.util.Base64; +import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import javax.servlet.http.HttpServletRequest; 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.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; // 유저 정보로 JWT 토큰을 만들거나 토큰을 바탕으로 유저 정보를 가져옴 @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("${jwt.token.open-secret-key}") + @Value("${jwt.token.secret-key}") private String secret_key; @Value("${jwt.token.expire-length}") @@ -34,36 +28,44 @@ public class JwtTokenProvider { @Autowired private UserDetailsService userDetailsService; - @PostConstruct // 의존성 주입이 이루어진 후 초기화를 수행 - protected void init() { - secret_key = Base64.getEncoder().encodeToString(secret_key.getBytes()); - } + /** + * 적절한 설정을 통해 토큰을 생성하여 반환 + * @param authentication + * @return + */ + public String generateToken(Authentication authentication) { - public String createToken(String username, List appUserRoles) { - - Claims claims = Jwts.claims().setSubject(username); - claims.put("auth", appUserRoles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); + Claims claims = Jwts.claims().setSubject(authentication.getName()); +// claims.put("auth", appUserRoles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); Date now = new Date(); - Date validity = new Date(now.getTime() + expire_time); + Date expiresIn = new Date(now.getTime() + expire_time); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) - .setExpiration(validity) + .setExpiration(expiresIn) .signWith(SignatureAlgorithm.HS256, secret_key) .compact(); } + /** + * 토큰으로부터 클레임을 만들고, 이를 통해 User 객체를 생성하여 Authentication 객체를 반환 + * @param token + * @return + */ public Authentication getAuthentication(String token) { - UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token)); + String username = Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token).getBody().getSubject(); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } - public String getUsername(String token) { - return Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token).getBody().getSubject(); - } - + /** + * http 헤더로부터 bearer 토큰을 가져옴. + * @param req + * @return + */ public String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { @@ -72,6 +74,11 @@ public class JwtTokenProvider { return null; } + /** + * 토큰을 검증 + * @param token + * @return + */ public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token); diff --git a/src/main/java/demo/api/user/UserController.java b/src/main/java/demo/api/user/UserController.java index 8c8b2d1..2037218 100644 --- a/src/main/java/demo/api/user/UserController.java +++ b/src/main/java/demo/api/user/UserController.java @@ -42,14 +42,9 @@ public class UserController { return "user/signIn"; } -// @Autowired -// private UserDetailsService userDetailsService; + @GetMapping("/profile") public String profile(Model model, @AuthenticationPrincipal UserDetails userDetails) { -// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); -// System.out.println("principal : " + authentication.getPrincipal()); -// System.out.println("Implementing class of UserDetails: " + authentication.getPrincipal().getClass()); -// System.out.println("Implementing class of UserDetailsService: " + userDetailsService.getClass()); if (userDetails != null) { User userDetail = userService.findByEmail(userDetails.getUsername()) .orElseThrow(() -> new UserNotFoundException()); @@ -60,10 +55,9 @@ public class UserController { return "user/profile"; } - @GetMapping("/user/userList") + @GetMapping("/userList") public String showUserList(Model model) { List userList = userService.findAll(); - model.addAttribute("userList", userList); return "user/userList"; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 49c5873..e65df0b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,8 +8,9 @@ spring: jpa: show-sql: true hibernate: + format_sql: true ddl-auto: none - jwt: - token: - secret-key: open-secret-key - expire-length: 300000 \ No newline at end of file +jwt: + token: + secret-key: aG91Mjctc2ltcGxlLXNwcmluZy1ib290LWFwaS1qd3QK + expire-length: 300000 \ No newline at end of file