Implement jwt base

This commit is contained in:
hou27
2022-06-10 18:22:19 +09:00
parent 26b0dad1ab
commit c51cf8b525
11 changed files with 193 additions and 74 deletions

View File

@@ -8,27 +8,27 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration //@Configuration
public class AppConfig { //public class AppConfig {
private final UserRepository userRepository; // private final UserRepository userRepository;
private final PasswordEncoder bCryptPasswordEncoder; // private final PasswordEncoder bCryptPasswordEncoder;
//
public AppConfig(UserRepository userRepository, PasswordEncoder bCryptPasswordEncoder) { // public AppConfig(UserRepository userRepository, PasswordEncoder bCryptPasswordEncoder) {
System.out.println("AppConfig"); // System.out.println("AppConfig");
System.out.println("userRepository = " + userRepository); // System.out.println("userRepository = " + userRepository);
this.userRepository = userRepository; // this.userRepository = userRepository;
this.bCryptPasswordEncoder = bCryptPasswordEncoder; // 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();
// } // }
} //
// @Bean
// public UserService userService() {
// System.out.println("userService");
// return new UserServiceImpl(userRepository, bCryptPasswordEncoder);
// }
//
//// @Bean
//// public BCryptPasswordEncoder passwordEncoder() {
//// System.out.println("passwordEncoder");
//// return new BCryptPasswordEncoder();
//// }
//}

View File

@@ -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<DefaultSecurityFilterChain, HttpSecurity> {
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);
}
}

View File

@@ -1,14 +1,14 @@
package demo.api.config; 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 lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; 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.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.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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
@@ -17,16 +17,22 @@ import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor @RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
@Bean // 추가된 jwt 관련 친구들을 security config에 추가
public UserDetailsService userDetailsService() { private final JwtTokenProvider jwtTokenProvider;
return new UserDetailsServiceImpl(); private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
} private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// @Bean
// public UserDetailsService userDetailsService() {
// return new UserDetailsServiceImpl();
// }
@Bean @Bean
public BCryptPasswordEncoder passwordEncoder() { public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//
http http
.csrf().disable() .csrf().disable()
.formLogin() .formLogin()
@@ -36,11 +42,33 @@ public class SecurityConfig {
.passwordParameter("password") .passwordParameter("password")
.defaultSuccessUrl("/") .defaultSuccessUrl("/")
.failureUrl("/user/signIn?fail=true"); .failureUrl("/user/signIn?fail=true");
//
http http
.authorizeRequests() .authorizeRequests()
.antMatchers("/", "/user/signUp", "/user/userList", "/user/signIn*").permitAll() .antMatchers(
"/",
"/user/signUp",
"/user/userList",
"/user/signIn*",
"/favicon.ico"
).permitAll()
.anyRequest().authenticated(); .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(); return http.build();
} }
} }

View File

@@ -12,9 +12,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Component
public class UserDetailsServiceImpl implements UserDetailsService { public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;

View File

@@ -1,4 +1,4 @@
package demo.api.jwt.exception; package demo.api.exception;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;

View File

@@ -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);
}
}

View File

@@ -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"); // 로그인 페이지로 리다이렉트되도록 하였다.
}
}

View File

@@ -1,6 +1,6 @@
package demo.api.jwt; package demo.api.jwt;
import demo.api.jwt.exception.CustomException; import demo.api.exception.CustomException;
import java.io.IOException; import java.io.IOException;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@@ -28,7 +28,7 @@ public class JwtTokenFilter extends OncePerRequestFilter {
try { try {
if (token != null && jwtTokenProvider.validateToken(token)) { if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token); Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth); SecurityContextHolder.getContext().setAuthentication(auth); // 정상 토큰이면 SecurityContext에 저장
} }
} catch (CustomException ex) { } catch (CustomException ex) {
//this is very important, since it guarantees the user is not authenticated at all //this is very important, since it guarantees the user is not authenticated at all

View File

@@ -1,31 +1,25 @@
package demo.api.jwt; package demo.api.jwt;
import demo.api.jwt.exception.CustomException; import demo.api.exception.CustomException;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Base64;
import java.util.Date; import java.util.Date;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
// 유저 정보로 JWT 토큰을 만들거나 토큰을 바탕으로 유저 정보를 가져옴 // 유저 정보로 JWT 토큰을 만들거나 토큰을 바탕으로 유저 정보를 가져옴
@Component @Component
public class JwtTokenProvider { public class JwtTokenProvider {
/** @Value("${jwt.token.secret-key}")
* 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}")
private String secret_key; private String secret_key;
@Value("${jwt.token.expire-length}") @Value("${jwt.token.expire-length}")
@@ -34,36 +28,44 @@ public class JwtTokenProvider {
@Autowired @Autowired
private UserDetailsService userDetailsService; 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<AppUserRole> appUserRoles) { Claims claims = Jwts.claims().setSubject(authentication.getName());
// claims.put("auth", appUserRoles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList()));
Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", appUserRoles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList()));
Date now = new Date(); Date now = new Date();
Date validity = new Date(now.getTime() + expire_time); Date expiresIn = new Date(now.getTime() + expire_time);
return Jwts.builder() return Jwts.builder()
.setClaims(claims) .setClaims(claims)
.setIssuedAt(now) .setIssuedAt(now)
.setExpiration(validity) .setExpiration(expiresIn)
.signWith(SignatureAlgorithm.HS256, secret_key) .signWith(SignatureAlgorithm.HS256, secret_key)
.compact(); .compact();
} }
/**
* 토큰으로부터 클레임을 만들고, 이를 통해 User 객체를 생성하여 Authentication 객체를 반환
* @param token
* @return
*/
public Authentication getAuthentication(String token) { 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()); 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) { public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization"); String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) { if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
@@ -72,6 +74,11 @@ public class JwtTokenProvider {
return null; return null;
} }
/**
* 토큰을 검증
* @param token
* @return
*/
public boolean validateToken(String token) { public boolean validateToken(String token) {
try { try {
Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token); Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token);

View File

@@ -42,14 +42,9 @@ public class UserController {
return "user/signIn"; return "user/signIn";
} }
// @Autowired
// private UserDetailsService userDetailsService;
@GetMapping("/profile") @GetMapping("/profile")
public String profile(Model model, @AuthenticationPrincipal UserDetails userDetails) { 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) { if (userDetails != null) {
User userDetail = userService.findByEmail(userDetails.getUsername()) User userDetail = userService.findByEmail(userDetails.getUsername())
.orElseThrow(() -> new UserNotFoundException()); .orElseThrow(() -> new UserNotFoundException());
@@ -60,10 +55,9 @@ public class UserController {
return "user/profile"; return "user/profile";
} }
@GetMapping("/user/userList") @GetMapping("/userList")
public String showUserList(Model model) { public String showUserList(Model model) {
List<User> userList = userService.findAll(); List<User> userList = userService.findAll();
model.addAttribute("userList", userList); model.addAttribute("userList", userList);
return "user/userList"; return "user/userList";

View File

@@ -8,8 +8,9 @@ spring:
jpa: jpa:
show-sql: true show-sql: true
hibernate: hibernate:
format_sql: true
ddl-auto: none ddl-auto: none
jwt: jwt:
token: token:
secret-key: open-secret-key secret-key: aG91Mjctc2ltcGxlLXNwcmluZy1ib290LWFwaS1qd3QK
expire-length: 300000 expire-length: 300000