본문 바로가기
Back-end/Spring Boot + REST API

Spring Boot 시큐리티 + JWT 흐름 정리

by javapp 자바앱 2022. 11. 23.
728x90

시큐리티 로그인/회원가입/인증


로그인

    @PostMapping("/signup")
    public ResponseEntity<Object> login(@RequestBody MemberDto loginDto)
    {
        // add check for email exists in DB
        if(memberService.existsByEmail(loginDto.getEmail())) {
            return new ResponseEntity<>("이메일이 존재합니다.",HttpStatus.BAD_REQUEST);
        }

        User user = memberService.memberLogin(loginDto);
        if(user == null) return new ResponseEntity<>("회원가입 실패",HttpStatus.INTERNAL_SERVER_ERROR);

        return new ResponseEntity<>("success", HttpStatus.OK);
    }

User user = memberService.memberLogin(loginDto); // 회원가입 진행

 

 

    @Override
    public User memberLogin(MemberDto memberDto) {
        try{
            String rawPwd= memberDto.getPassword();
            String encPwd = bCryptPasswordEncoder.encode(rawPwd);
            String nickname = memberDto.getEmail().substring(0,memberDto.getEmail().indexOf("@"));
            User member = User.builder()
                            .nickname(nickname)
                            .email(memberDto.getEmail())
                            .password(encPwd)
                            .role(RoleEnum.USER.getKey())
                            .build();
            return memberRepository.save(member);
        }catch (StringIndexOutOfBoundsException s){
            return null;
        }
    }

 

 

JwtAuthenticationEntryPoint

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint
{
    // 예외시 호출, 허가 받지 않은 유저가 접근 할때
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

 

 


 

 

로그인

  • 로그인시 jwt 토큰 생성
  • 타입 : Bearer

 

    @PostMapping("/signin")
    public ResponseEntity<JWTAuthResponse> signin(@RequestBody MemberDto memberDto)
    {
        String token =memberService.userSignin(memberDto);

        return new ResponseEntity<>(new JWTAuthResponse(token),HttpStatus.OK);
    }

 

Console 테스트

doFilterInternal, null
2022-11-17 13:55:13.966  INFO 17568 --- [nio-8888-exec-6] p6spy                                    : #1668660913966 | took 1ms | statement | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email=?
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email='j2@naver.com';
서비스 처리
2022-11-17 13:55:13.972  INFO 17568 --- [nio-8888-exec-6] p6spy                                    : #1668660913972 | took 0ms | statement | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email=?
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email='j2@naver.com';
유저 체크 j2@naver.com
유저 존재
2022-11-17 13:55:13.973  INFO 17568 --- [nio-8888-exec-6] p6spy                                    : #1668660913973 | took 0ms | commit | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8

;
jwtSecret: jwtsecretkeyisthisissavingquestionandrandomdordergeneration, 604800000
  • doFilterInternal, null
    • SecurityConfig 의 securityFilterChain() 에 의해 필터 실행
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig
{

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(){
        return new JwtAuthenticationFilter();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
        return configuration.getAuthenticationManager();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        return httpSecurity
                .csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .exceptionHandling()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests()
                .antMatchers("/api/v1/member/**").permitAll()
                .antMatchers("/api/v1/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}
  • .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)

 

JwtAuthenticationFilter

    doFilterInternal() 실행

package com.javapp.qg.security;

public class JwtAuthenticationFilter extends OncePerRequestFilter
{
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Autowired
    private PrincipalDetailsService principalDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getJWTfromRequest(request);
        System.out.println("doFilterInternal, "+token);
        // 토큰 검증
        if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token))
        {
            System.out.println("jwt필터검증");

            Map<String, Object> userInfo = jwtTokenProvider.getUserFromJWT(token);

            UserDetails userDetails = principalDetailsService.loadUserByUsername((String)userInfo.get("email"));

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // set String Security
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }

    // Bearer <accessToken>
    private String getJWTfromRequest(HttpServletRequest request)
    {
        String bearerToken = request.getHeader("Authorization");
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }
}
  • 로그인시 token 이 null 이기 때문에 if 실행 X

 

 

ServiceImpl

@Override
    public String userSignin(MemberDto memberDto) {
        User user = memberRepository.findByEmail(memberDto.getEmail()).get();
        // null 에러 처리 추후 수정

        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
                memberDto.getEmail(), memberDto.getPassword()
        ));
        SecurityContextHolder.getContext().setAuthentication(authentication);

        String token = tokenProvider.generateToken(user);

        return token;
    }
  • authenticate에 의해 시큐리티 로그인
  • PrincipalDetailsService loadUserByUsername 실행

 

 

PrincipalDetailsService

  loadUserByUsername()

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService
{
    private final MemberRepository memberRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {// 닉네임

        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();

        Optional<User> user =  memberRepository.findByEmail(email);
        System.out.println("유저 체크 "+ user.get().getEmail());

        if(user.isPresent()) {
            System.out.println("유저 존재");

            User authenUser = user.get();
            grantedAuthorities.add(new SimpleGrantedAuthority(authenUser.getRole()));
            return new PrincipalDetails(authenUser, grantedAuthorities);
        }
        return null;
    }
}

Console

유저 체크 j2@naver.com
유저 존재
  • PrincipalDetails 으로 반환

 

 

PrincipalDetails

public class PrincipalDetails implements UserDetails
{
    private User user;

    private Set<GrantedAuthority> grantedAuthorities;

    public PrincipalDetails(User user) {
        this.user = user;
    }

    public PrincipalDetails(User user, Set<GrantedAuthority> grantedAuthorities) {
        this.user = user;
        this.grantedAuthorities = grantedAuthorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority>collection = grantedAuthorities;
//        collection.add(new GrantedAuthority() {
//            @Override
//            public String getAuthority() {
//                return user.getRole();
//            }
//        });
        return collection;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getEmail();
    }

    public String getNickname(){
        return user.getNickname();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

  • String token = tokenProvider.generateToken(user); // 로그인 성공시 토큰 생성
@Component
public class JwtTokenProvider
{
    @Value("${app.jwt-secret}")
    private String jwtSecret;

    @Value("${app.jwt-expiration-milliseconds}")
    private int jwtExpirationInMs;

    public String generateToken(User user)
    {
        System.out.println("jwtSecret: "+jwtSecret +", "+ jwtExpirationInMs);
        Key key = Keys.hmacShaKeyFor(jwtSecret.getBytes());

        Map<String,Object> payloads = new HashMap<>();
        payloads.put("nickname", user.getNickname());
        payloads.put("email",user.getEmail());

        Date currentDate = new Date();
        Date expireDate= new Date(currentDate.getTime() + jwtExpirationInMs);

        String token = Jwts.builder()
                .setSubject("user-info")
                .setClaims(payloads)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                .signWith(key,SignatureAlgorithm.HS256)
                .compact();
        return token;
    }

    // get username from the token
    public Map<String, Object> getUserFromJWT(String token) {
        Map<String, Object> claimMap = null;
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes())) // 시크릿 키 필요
                .build()
                .parseClaimsJws(token)
                .getBody();
        claimMap = claims;
        return claimMap;
    }

    public boolean validateToken(String token)
    {
        System.out.println("토큰 검증");

        try {
            Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
                    .build()
                    .parseClaimsJws(token);
            System.out.println("유효한 토큰");
            return true;
//        }catch(SignatureException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_REQUEST, "Invalid JWT signature");
//        }catch(MalformedJwtException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Invalid JWT token");
//        }catch(ExpiredJwtException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Expired JWT token");
//        }catch(UnsupportedJwtException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Unsupported JWT token");
//        }catch(IllegalArgumentException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "JWT claims string is empty");
//        }
        }catch (Exception e){
            System.out.println("유효하지 않은 토큰");
            return false;
        }
    }
}

 

 

 


 

 

 

인증

인증(로그인) 없이 접근시 접근 제한

.anyRequest().authenticated()
@PreAuthorize("hasRole('ROLE_USER')")
@GetMapping("/test")
public String test(){
    return "test";
}

 

 

그래서 해당 자원 접근시 Bearer 타입 토큰으로 인증

 

생성된 테스트 로그

doFilterInternal, eyJhbGciOiJIUzI1NiJ9.eyJuaWNrbmFtZSI6ImoyIiwiZW1haWwiOiJqMkBuYXZlci5jb20iLCJpYXQiOjE2Njg2NjI3NDIsImV4cCI6MTY2OTI2NzU0Mn0.funHk5zBeUm5XydoH8BgNQj2H80QcgaTrLlnagIubo4
토큰 검증
유효한 토큰
jwt필터검증
2022-11-17 14:25:49.615  INFO 5232 --- [nio-8888-exec-3] p6spy                                    : #1668662749615 | took 1ms | statement | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email=?
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email='j2@naver.com';
유저 체크 j2@naver.com
유저 존재
2022-11-17 14:25:49.618  INFO 5232 --- [nio-8888-exec-3] p6spy                                    : #1668662749618 | took 1ms | commit | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8

;

SecurityConfig 에 의해 JwtAuthenticationFilter 필터 클래스 실행

  • jwtTokenProvider.validateToken(token) 에서 토큰 검증, 유효한 토큰 검증
public boolean validateToken(String token)
    {
        System.out.println("토큰 검증");

        try {
            Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
                    .build()
                    .parseClaimsJws(token);
            System.out.println("유효한 토큰");
            return true;
//        }catch(SignatureException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_REQUEST, "Invalid JWT signature");
//        }catch(MalformedJwtException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Invalid JWT token");
//        }catch(ExpiredJwtException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Expired JWT token");
//        }catch(UnsupportedJwtException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Unsupported JWT token");
//        }catch(IllegalArgumentException ex) {
//            throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "JWT claims string is empty");
//        }
        }catch (Exception e){
            System.out.println("유효하지 않은 토큰");
            return false;
        }
    }

 

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getJWTfromRequest(request);
        System.out.println("doFilterInternal, "+token);
        // 토큰 검증
        if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token))
        {
            System.out.println("jwt필터검증");

            Map<String, Object> userInfo = jwtTokenProvider.getUserFromJWT(token);

            UserDetails userDetails = principalDetailsService.loadUserByUsername((String)userInfo.get("email"));

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // set String Security
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }

 // 토큰에서 email과 nickname 가져옴 

           Map<String, Object> userInfo = jwtTokenProvider.getUserFromJWT(token);

 

// 유저 인증
            UserDetails userDetails = principalDetailsService.loadUserByUsername((String)userInfo.get("email")); 
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // set String Security
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

 

 

 

테스트

 


 

JWT 토큰 생성시 키 설정 에러

 

io.jsonwebtoken.security.WeakKeyException

2022-11-17 11:52:37.638 ERROR 15548 --- [nio-8888-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 72 bits which is not secure enough for any JWT HMAC-SHA algorithm.  The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size).  Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm) method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.  See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.] with root cause

io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 72 bits which is not secure enough for any JWT HMAC-SHA algorithm.  The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size).  Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm) method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.

 

The specified key byte array is 72 bits

--> 현재 설정된 Key 의 bit 길이가 "jwtsecret" 으로 72 bits 임.

 

HMAC-SHA algorithms MUST have a size >= 256 bits

--> 이것을 256 bits 이상으로 설정해야함.

 

application.yml

app:
  jwt-secret: Q4NSl604sgyHJj1qwEkR3ycUeR4uUAt7WJraD7EN3O9DVM4yyYuHxMEbSF4XXyYJkal13eqgB0F7Bq4H

 


 

spring boot 2.7.0 이상 

jwt 토큰 키 설정 

 

jsonwebtoken 버전 0.10.0+ 에서 로그인 TokenProvider에러 · Discussion #48 · fsoftwareengineer/todo-application

jsonwebtoken 버전이 0.10.0인 경우 로그인시 아래와 같은 런타임 에러가 나는 것을 확인 할 수 있습니다. 2022-01-03 22:25:22.397 ERROR 8223 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for serv

github.com

 

 

 

 

댓글