Sring Security 를 이용하여 JWT를 인증하는 방법이다.
기본적으로 DB연결이 설정 되어 있다는 것과 일반적인 Spring Security Form Login을 알고 있다는 가정하에 시작한다. 되어 있지 않다면 아래의 글을 참고한다.
2021/01/19 - [Develop/Spring Boot] - Spring Boot Security form login
Spring Boot Security form login
Spring Boot 에서 Security Form Login 적용 방법이다. 먼저, 간단한 DB, 의존성 세팅과 DB연결 후 Security를 적용한다. 1. 기본 세팅 1.1 DB설정 로그인에 사용할 DB테이블을 먼저 생성한다. CREATE TABLE `user..
wky.kr
1. SecurityConfig 생성
SecurityConfig.java 파일을 생성하여 WebSecurityConfigurerAdapter를 상속받고, 아래 3개의 메소드를 Override한다.
1. configure(HttpSecurity http) 메소드에서는 Jwt 인증이 실패할 경우 처리할 EntryPoint 설정과 JWT인증에는 기본적으로 세션을 사용하지 않기 때문에 Session을 STATELESS해준다. 또한, JWT인증을 처리할 Filter를 SpringSecurity의 기본적인 필터인 UsernamePasswordAuthenticationFilter 앞에 넣어준다.
2. configure(AuthenticationManagerBuilder auth) 메소드는 인증방법을 의미하며,
Spring에서 xml로 Security 설정시 <authentication-manager> <authentication-provider>를 설정하는 것과 같다. 예를 들면 DB에서 사용자 정보를 불러와 현재 입력된 정보와 비교 후 UserDetails 을 반환하여 인증하겠다 라는 의미이다. 현재는 CustomUserDetailSerivce 객체에 인증 방법이 있으며, 패스워드 인코더를 설정해 두었다.
3. PasswordEncoder는 로그인시 DB에 BCrpyt로 저장되어있는 패스워드를 로그인할 때 입력한 비밀번호와 비교하기 위해 사용되며, 이는 인증할 때 사용되는 configure(AuthenticationManagerBuilder auth) 메소드에 사용될 것이다.
4. AuthenticationManger는 이 글의 뒤에서 나중에 진짜로 인증을 하는 곳에서 사용하기 위해 Bean으로 등록한다.
CustomUserDetailService는 이전 글인 Spring Boot Security form login 에서 볼 수 있다.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private CustomUserDetailService userDetailsService;
@Autowired
private JwtEntryPoint jwtPoint;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2. JWT 인증
Filter 생성
요청이 들어올 때 마다 JWT를 인증할 Filter를 생성한다. 요청이 들어올 때마다 하기 위해 OncePerRequestFilter를 상속받는다. OncePerRequestFilter가 아니더라도 일반적인 GenericFilterBean을 상속받아도 무관하다. (OncePerRequestFilter가 이미 GenericFilterBean를 상속받고 있기 때문.)
Filter 작동 방식은 간단하다. Bearer로 토큰을 받으면 토큰을 추출하여 올바른 토큰인지 체크를 한다.
토큰이 올바르다면, 토큰에 있는 정보(username, role 등등) 을 가져와 인증을 시킨다.
토큰이 없거나, 올바르지않다면 그냥 다음으로 넘어간다.
CustomUserDetailService는 이전 글인 Spring Boot Security form login 에서 볼 수 있다.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtProvider jwtProvider;
@Autowired
private CustomUserDetailService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getToken(request);
if (token != null && jwtProvider.validateJwtToken(token)) {
String username = jwtProvider.getUserNameFromJwtToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String getToken(HttpServletRequest request){
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
3. JWT Provider 생성
토큰을 인증하거나 생성할 Provider를 생성한다.
generateJwtToken 메소드로 로그인 후 인증에 성공시 토큰을 생성한다.
validateJwtToken 메소드로 토큰이 올바른지 검사한다. 필터에서 이 메소드를 통해 토큰 인증을 진행하고 인증이 성공하면 다음단계로 넘어가고 인증실패할 경우 에러를 호출한다.
getUserNameFromJwtToken 으로 토큰에 포함되어 있는 Body를 추출할 수 있다.
@Component
public class JwtProvider {
private static String jwtSecret ="www.wky.krSecretKey";
private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class);
private static int jwtExpirationMs =86400000;
public String generateJwtToken(Authentication authentication) {
String name = authentication.getName();
return Jwts.builder()
.setSubject(name)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}
4. EntryPoint 생성
SecurityConfig에서 등록했던 인증에 실패할 경우 진행될 EntryPoint를 생성한다.
AuthenticationEntryPoint를 implements하여 commence를 작성한다 commence에는 인증이 실패할 경우 넘어갈 URL을 호출하거나 에러메세지를 호출한다.
@Component
public class JwtEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// TODO Auto-generated method stub
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
5. Login 처리
Login을 직접 처리할 Controller를 생성한다.
LoginVO에는 userId, userPw, token 변수가 선언되어 있다.
프론트에서 userId와 userPw를 JSON으로 전달받아 SecurityConfig에서 Bean으로 등록했던
authenticationManager를 통해 인증을 시도한다. 여기서 인증이 성공되면 다음 코드로 넘어가고
실패하면 SecurityConfig에서 등록한 EntryPoint가 실행된다.
성공 후 인증한 객체정보를 가지고 토큰을 생성하고 Token을 반환한다.
반환 객체는 필요한 데이터에 맞게 알맞게 생성하여 반환하면 Token과 함께 반환만 하면 된다.
@PostMapping("/login")
@ResponseBody
public ResponseEntity<LoginVO> login(HttpServletResponse response, @RequestBody LoginVO loginVO) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginVO.getUserId(), loginVO.getUserPw()));
// SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtProvider.generateJwtToken(authentication);
loginVO.setUserPw("");
loginVO.setToken(jwt);
return ResponseEntity.ok(loginVO);
}
이러한 작동방식으로 Front에선 Token을 받아 LocalStroage에 Bearer + Token 형태로 저장 후
백엔드로 데이터를 요청할때 LoacalStorage에서 저장한 Token을 꺼내어 Authorization 헤더에 저장한 후 요청한다.
Header : Authorizaion Bearer token
번 외
LocalStorage에 저장하여 Header 에 계속 Token을 세팅하기 불편하고 Cookie로 하고싶을 땐 아래와 같이 사용하면 된다.
번외 1. Login처리
위에서 사용한 Login처리 Controller에서 Token 생성후 아래와 같이 Cookie에 적용한다.
@PostMapping("/login")
@ResponseBody
public ResponseEntity<LoginVO> login(HttpServletResponse response, @RequestBody LoginVO loginVO) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginVO.getUserId(), loginVO.getUserPw()));
//SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtProvider.generateJwtToken(authentication);
Cookie cookie = new Cookie("token", "Bearer " + jwt);
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 7);
response.addCookie(cookie);
loginVO.setUserPw("");
loginVO.setToken(jwt);
return ResponseEntity.ok(loginVO);
}
번외 2. JWT 인증 Filter 생성
위에서 생성한 Filter에 아래와 같이 Cookie에 있는 정보를 가져오는 코드를 작성하여 Cookie에 있는 token을 가져온다.
가져온 뒤의 절차는 위와 동일하다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String token = Arrays.stream(request.getCookies())
.filter(c -> c.getName().equals("token"))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
String jwt = getToken(request);
if (jwt != null && jwtProvider.validateJwtToken(jwt)) {
String username = jwtProvider.getUserNameFromJwtToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String getToken(String token){
String headerAuth = token;
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
이러한 방식으로 JWT인증을 LocalStorage 뿐만아니라 Cookie를 이용하여 작성할 수 있다.
참고 : bezkoder.com/spring-boot-jwt-authentication/
Spring Boot Token based Authentication with Spring Security & JWT - BezKoder
Spring Boot JWT Authentication example with MySQL/PostgreSQL and Spring Security - Spring Boot 2 Application with Spring Security and JWT Authentication
bezkoder.com