본문 바로가기

Springboot

Springboot security (5) - jwt 적용

이번 포스팅은 Spring security에 jwt를 적용시키는 방법입니다.

https://woolbro.tistory.com/45 를 참고했습니다.

 

1. Application structure

2. pom.xml 추가

<!-- jwt -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

<!--  jdk error handling -->
<dependency>
  <groupId>javax.xml.bind</groupId>
  <artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-core</artifactId>
<version>2.3.0.1</version>
</dependency>
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-impl</artifactId>
  <version>2.3.1</version>
</dependency>

jdk error handling부분은

해당 에러들이 떠서 넣어주게되었습니다.

jdk 버전이 9이상이경우 나는 에러라고 하는데 jdk버전을 다운그레이 해줘도 계속 에러가 나기때문에 넣어두었습니다.

해당에러가 나지 않을경우 안넣어주셔도 됩니다.

 

3. application.properties 추가

## jwt
app.jwtSecret=JWTSuperSecretKey
app.jwtExpirationMin=1

jwtSecret의 경우 https://www.youtube.com/watch?v=67UwxR3ts2E

이 동영상에서 설명해주는 salt암호에 대한 설정부분입니다.

 

4. JwtTokenProvider 생성

import java.util.Date;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

@Component
public class JwtTokenProvider {
	private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

	@Value("${app.jwtSecret}")
	private String jwtSecret;

	@Value("${app.jwtExpirationMin}")
	private int jwtExpirationMin;

	// token 생성
	public String generateToken(Authentication authentication) {
		final JwtBuilder builder = Jwts.builder();
		// JWT Token = Header + Payload + Signagure
		builder.setHeaderParam("typ", "JWT");// 토큰의 타입으로 고정 값
		// Payload 설정 - claim 정보 포함
		builder.setSubject("login-token")// 토큰 제목 설정
		.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * jwtExpirationMin))// 유효기간
		.claim("data", authentication);
		// signature - secret key를 이용한 암호화
		builder.signWith(SignatureAlgorithm.HS256, jwtSecret);
		// 마지막 직렬화 처리
		final String token = builder.compact();
		logger.info("login-token 발행: " + token);
		return token;
	}

	// token 조회
	public Map<String, Object> getTokeninfo(String token) {
		Jws<Claims> claims = null;
        try {
            claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
        } catch (final Exception e) {
            throw new RuntimeException();
        }
        logger.info("claims : " + claims);
        return claims.getBody();
	}

	// token 유효성 검사
	public boolean validateToken(String authToken) {
		try {
			Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
			return true;
		} catch (SignatureException e) {
			logger.error("Invalid JWT signature");
		} catch (MalformedJwtException e) {
			logger.error("Invalid JWT token");
		} catch (ExpiredJwtException e) {
			logger.error("Expired JWT token");
		} catch (UnsupportedJwtException e) {
			logger.error("Unsupported JWT token");
		} catch (IllegalArgumentException e) {
			logger.error("JWT claims string is empty");
		}

		return false;
	}

}

 jwt토큰을 생성 / 조회 / 검사하는 클래스입니다.

properties에서 선언한 secretkey를 이용하여 암호화를 진행하게 됩니다.

 

5. JwtAuthenticationFilter 생성

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

	@Autowired
	private JwtTokenProvider tokenProvider;

	private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		HttpSession session = request.getSession();
		String validateToken = (String) session.getAttribute("Authorization");

		// 토큰이 발급되어 있을 경우
		if(validateToken != null) {
			// 토큰기간이 유효
			if(tokenProvider.validateToken(validateToken)) {
				Map<String, Object> resultMap = new HashMap<>();
				try {
					resultMap.putAll(tokenProvider.getTokeninfo(validateToken));
				} catch (RuntimeException e) {
					logger.error("정보조회 실패", e.getMessage());
				}
			}
			// 토큰기간이 만료
			else {
				// 필터를 거치면서 로그인 정보를 null로 만들어버려서 로그인 안한상태로 판단하도록
				SecurityContextHolder.getContext().setAuthentication(null);
				// 세션으로 토큰검사하니까 세션도 지우기
				session.removeAttribute("Authorization");
			}
		}
		// 토큰이 발급되어있지 않은 경우
		else {
			Authentication loginuser_info = SecurityContextHolder.getContext().getAuthentication();

			if (loginuser_info != null) {
				try {
					// 토큰발급
					String jwt = tokenProvider.generateToken(loginuser_info);
					// 토큰정보를 세션에 저장
			        session.setAttribute("Authorization", jwt);
				} catch (Exception e) {
					logger.error("Could not set user authentication in security context", e);
				}
			}
		}

		filterChain.doFilter(request, response);
	}
}

      1) 토큰이 발급되어있지 않은경우

             SecurityContextHolder.getContext().getAuthentication()를 통해 현재 접속한 유저의 정보를 가져옵니다.

            접속한 유저의 정보가 있는경우(로그인시 입력한 정보의 유저가 있을경우) 토큰을 발급하고 세션에 저장합니다.

 

      2) 토큰이 발급되어 있는경우

             토큰기간이 유효한경우 토큰안에 있는 정보를 조회합니다.

             토큰기간이 유효하지 않은경우 현재 로그인한 정보를 null로 만들어 로그인하지 않은상태로 만들고 세션도 제거합니다.

 

6. SecurityConfig 추가

import javax.servlet.Filter;

import org.springframework.beans.factory.annotation.Autowired;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.security_blog.yg1110.filter.OauthFilter;
import com.security_blog.yg1110.filter.jwt.JwtAuthenticationFilter;
import com.security_blog.yg1110.servicer.UserService;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private UserService userService;
	
	@Autowired
	private OauthFilter filter;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
			.antMatchers("/user/admin/**").access("hasAuthority('ROLE_ADMIN')")
			.antMatchers("/user/myinfo").access("hasAuthority('ROLE_USER')") // 페이지 권한 설정
			.antMatchers("/", "/user/signup", "/user/denied", "/user/logout/result").permitAll()
			.anyRequest().authenticated()
			.and()
			.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class) // 소셜로그인 설정
			.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class) // jwt 필터 설정
			.formLogin().loginPage("/user/loginPage")
			.loginProcessingUrl("/login")
			.defaultSuccessUrl("/user/login/result")
			.permitAll() // 로그인 설정
			.and()
			.logout().logoutRequestMatcher(new AntPathRequestMatcher("/user/logout")) // 로그아웃 설정
			.logoutSuccessUrl("/user/logout/result").invalidateHttpSession(true)
			.and()
			.exceptionHandling().accessDeniedPage("/user/denied") // 403 예외처리 핸들링
			.and()
			.csrf().disable();
	}
	
	@Bean
    public Filter ssoFilter(){
        return filter.ssoFilter();
    }
	
	@Bean
	public JwtAuthenticationFilter jwtFilter() {
		return new JwtAuthenticationFilter();
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userService).passwordEncoder(userService.passwordEncoder());
	}
}

위에서 만든 JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter.class의 경우에 적용시킵니다.

UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter클래스를 통해 로그인 URL인지 확인하고 로그이 요청이 확인될 시 아이디 패스워드를 가져와서 인증을 위한 객체를 생성합니다.

 

참고 소스 : https://woolbro.tistory.com/45