본문 바로가기

Programming/Spring Boot

[Spring Boot] Spring Security JWT Token

JWT(Json Web Token)

Json 객체를 통해 안전하게 정보를 전송할 수 있는 웹표준

Json 객체를 암호화하여 만든 String 값

기본적으로 암호화가 되어 있어 변조하기 어려움

 

 

 

JWT를 이용하여 로그인 구현

1. gradle dependency 추가

     - build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
    	implementation 'io.jsonwebtoken:jjwt:0.9.1'
}

 

 

2. 비밀키 설정

     - application.properties

jwt.secret=thisiskey

 

 

3. JwtTokenProvider 생성

Jwt 생성하고, 유효성을 검증하는 컴포넌트

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
    @Value("${jwt.secret}")
    private String secretKey;
    
    // 토큰 유효 시간(30분)
    private long defaultTokenValidTime = 30 * 60 * 1000L;
    
    private final UserDetailsService userDetailsService;

    // 객체 초기화
    // secretKey를 Base64로 인코딩
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userPk) {
        Claims claims = Jwts.claims().setSubject(userPk);
        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + defaultTokenValidTime))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // JWT 토큰 생성(유효시간 지정)
    public String createToken(String userPk, long tokenValidTime) {
        Claims claims = Jwts.claims().setSubject(userPk);
        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + tokenValidTime))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));

        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 header에서 token 값을 가져옴
    // "X-AUTH-TOKEN" : "token값"
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN");
    }

    // token 유효성, 만료 일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

 

 

4. JwtAuthenticationFilter 생성

Jwt가 유효한 token인지 인증하기 위한 필터

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
    private  final  JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // header에서 JWT를 받아옴
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);

        // 유효한 token인지 확인
        if(token != null && jwtTokenProvider.validateToken(token)) {
            // token이 유효하면 token으로부터 유저 정보 받아옴
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext에 Authentication 객체를 저장함
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        chain.doFilter(request, response);
    }
}

GenericFilterBean

기존 Filter에서 얻어올 수 없는 정보였던 Spring의 설정 정보를 가져올 수 있게 확장된 추상 클래스

 

 

5. 로그인 성공 시, Jwt Token 생성 후 return

@Service
public class MemberService {
    private MemberRepository memberRepository;
    private BCryptPasswordEncoder passwordEncoder;
    private JwtTokenProvider jwtTokenProvider;
    private ApiResponseService apiResponseService;

    public MemberService(MemberRepository memberRepository, BCryptPasswordEncoder passwordEncoder,
                         JwtTokenProvider jwtTokenProvider, ApiResponseService apiResponseService) {
        super();
        this.memberRepository = memberRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtTokenProvider = jwtTokenProvider;
        this.apiResponseService = apiResponseService;
    }

    // 로그인
    public String login(MemberLoginDto loginInfo) {
        ApiResponseModel apiResponse = null;
        MemberEntity member = null;

        try {
            // 전달 받은 Id로 회원 조회
            Optional<MemberEntity> memberEntity = memberRepository.findById(loginInfo.getUserId());

            // 일치하는 Id가 없을 때
            if(memberEntity.isEmpty()) {        
                apiResponse = apiResponseService.getApiResponse("fail", "가입되지 않은 Id 입니다.", "");
            }
            // 일치하는 Id가 있을 때
            else {
                member = memberEntity.get();
                // 비밀번호가 틀렸을 때
                if (!passwordEncoder.matches(loginInfo.getPassword(), member.getPassword())) {          
                    apiResponse = apiResponseService.getApiResponse("fail", "잘못된 비밀번호입니다.", "");
                } 
                // 비밀번호가 일치했을 때
                else {
                    // 로그인 성공 시 token 발행 후 결과로 전송할 TokenInfoDto에 저장
                    TokenInfoModel jwtToken = TokenInfoModel.builder()
                                    .userId(member.getUserId())
                                    .token(jwtTokenProvider.createToken(member.getUserId()))
                                    .build();
                    apiResponse = apiResponseService.getApiResponse("success", "", jwtToken);
                }
            }
        } catch (Exception e) {
            apiResponse = apiResponseService.getApiResponse("fail", "로그인에 실패하였습니다.", "");
        }

        return (new Gson()).toJson(apiResponse);
    }
}

jwtTokenProvider.createToken(member.getUserId())

 

회원 ID(userId)를 넣어 JwtToken 생성

 

 

6. Token을 이용하여 접근 권한 체크

@RequiredArgsConstructor
@RestController
public class TestRestApiController {
    private final JwtTokenProvider jwtTokenProvider;
    private final MemberService memberService;
    private final ApiResponseService apiResponseService;

    @ApiOperation(value="내 정보 보기", notes="jwt token을 받아 권한이 있는 회원만 본인의 정보 열람 가능")
    @ApiImplicitParams({
            @ApiImplicitParam(name="X-AUTH-TOKEN", value="로그인 성공 후 발급 받은 token", dataType="String", paramType="header", required=true)

    })
    @PostMapping("/test/me")
    public String me(HttpServletRequest request) {
        String jwtToken = jwtTokenProvider.resolveToken(request);
        ApiResponseModel apiResponse = null;

        // token 확인 후 유효한 token일 때
        if(jwtTokenProvider.validateToken(jwtToken)) {
            String userId = jwtTokenProvider.getUserPk(jwtToken);
            MemberDto member = memberService.getMemberInfo(userId);

            if(member == null) apiResponse = apiResponseService.getApiResponse("fail", "회원 정보 조회에 실패하였습니다.", "");
            else apiResponse = apiResponseService.getApiResponse("success", "", member);
        }
        // token이 유효하지 않을 때
        else {
            apiResponse = apiResponseService.getApiResponse("fail", "token이 유효하지 않습니다.", "");
        }

        return (new Gson()).toJson(apiResponse);
    }
}

X-AUTH-TOKEN

header에 X-AUTH-TOKEN라는 이름으로 담아 전송

 

jwtTokenProvider.resolveToken(request)

header에 X-AUTH-TOKEN라는 이름으로 전송받은 Jwt Token 추출

 

jwtTokenProvider.validateToken(jwtToken)

Jwt Token 유효성 체크

 

jwtTokenProvider.getUserPk(jwtToken)

Jwt Token에 담긴 정보 가져오기

 

 

 

 

 

reference

     -  SpringBoot2로 Rest api 만들기(8) –  SpringSecurity 를 이용한 인증 및 권한부여

        https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization

     -  SPRING SECURITY + JWT 회원가입, 로그인 기능 구현

        https://webfirewood.tistory.com/115