BackEnd

[Backend] SpringSecurity + JWT + 소셜로그인 + 회원가입 (1)

Seok_IN 2022. 7. 24. 02:35

Spring Security

- Spring 기반의 Application의 보안을 담당하는 스프링 하위 프레임워크로 '인증' 및 '권한'에 대한 부분을 Filter 흐름에 따라 처리한다. 

- 보안과 관련해서 여러가지 기능들을 쉽게 사용할 수 있도록 제공해줘서 개발자에게 편하다는 장점이 있다.

 

📑 프로젝트 구현 내용

이번 프로젝트를 통해서 내가 구현한 로그인 시스템은 아래와 같다.

  • Member와 MemberInfo로 분리되어 유저의 세부정보는 같이 입력받지만 다른 테이블에 저장함.
  • 이메일로 회원가입 및 로그인 시에 JWT Access Token과 RefreshToken 반환
  • 소셜로그인의 경우 최초 로그인이면 추가적인 정보 기입을 위해 세션에 소셜계정 정보 저장하여 추가정보 기입 페이지로 이동시켜줌.
  • 최초로그인이 아니면 바로 JWT 발급
  • 소셜로그인시 이미 사용중인 이메일이라면 이메일 혹은 다른 소셜로그인을 통해 로그인 하도록 함.

 

📘 코드구조

< 정리해서 올릴 예정>

 

📘 코드설명

JwtProvider.java

@Component
@RequiredArgsConstructor
public class JwtProvider {
    private final String secretKey="c88d74ba-1554-48a4-b549-b926f5d77c9e";
    private final long accessExpireTime = 60 * 60 * 1000L;
    private final long refreshExpireTime = 60 * 360 * 1000L;
    private MemberRepository memberRepository;


    public String createAccessToken(Long memberId){

        Date expiration = new Date();
        expiration.setTime(expiration.getTime() + accessExpireTime);

        Map<String, Object> headers = new HashMap<>();
        headers.put("type", "token");

        Map<String, Object> payloads = new HashMap<>();
        payloads.put("memberId", memberId);

        String jwt = Jwts
                .builder()
                .setHeader(headers)
                .setClaims(payloads)
                .setIssuedAt(new Date())
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        return jwt;
    }

    public Map<String, String> createRefreshToken(Long memberId){
        Date expiration = new Date();
        expiration.setTime(expiration.getTime() + refreshExpireTime);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
        String refreshTokenExpirationAt = simpleDateFormat.format(expiration);

        Map<String, Object> headers = new HashMap<>();
        headers.put("type", "token");

        Map<String, Object> payloads = new HashMap<>();
        payloads.put("memberId", memberId);

        String jwt = Jwts
                .builder()
                .setHeader(headers)
                .setClaims(payloads)
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        Map<String, String> result = new HashMap<>();
        result.put("refreshToken", jwt);
        result.put("refreshTokenExpirationAt",refreshTokenExpirationAt);
        return result;
    }

    // 토큰 유효성 검사
    public Authentication getAuthentication(String token) {
        Optional<Member> member = memberRepository.findById(this.getMemberId(token));
        UserDetails userDetails = new PrincipalDetails(member.get());
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    public Long getMemberId(String token) {
        return (Long)Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("memberId");
    }

    public String resolveToken(HttpServletRequest request){
        return request.getHeader("token");
    }

    public boolean validateJwtToken(ServletRequest request, String authToken) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
            return true;
        } catch (MalformedJwtException e) {
            request.setAttribute("exception", "MalformedJwtException");
        } catch (ExpiredJwtException e) {
            request.setAttribute("exception", "ExpiredJwtException");
        } catch (UnsupportedJwtException e) {
            request.setAttribute("exception", "UnsupportedJwtException");
        } catch (IllegalArgumentException e) {
            request.setAttribute("exception", "IllegalArgumentException");
        }
        return false;
    }
}
  • createAccessToken(Long memberId) & createRefreshToken(Long memberId) : AccessToken과 RefreshToken을 각각 생성하여 넣어주는 로직
  • getAuthentication(String token) : 토큰이 유효한지 검사해주는 로직으로 토큰을 복호화하여 ID값으로 유저가 있는지 없는지 검사 후 있으면 검증된 유저임을 리턴해준다.
  • getMemberId(String token) : 토큰을 복호화하여 ID값 리턴해주는 로직
  • resolveToken(request) : request 앞에 Header에 있는 토큰을 가지고오는 로직
  • validateJwtToken : token의 값이 유효한지 기간이 지나지는 않았는지 등을 판단하는 로직

 

JwtAuthenticationFilter.Java

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
    private final JwtProvider jwtProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        String token = jwtProvider.resolveToken((HttpServletRequest) request);

        if(token != null && jwtProvider.validateJwtToken(request, token)){
            Authentication authentication = jwtProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

- doFilter : 사실상 jwt 실사용시 검증을 해주는 로직으로 토큰이 올바르면 권한이 있다고 설정해주는 필터.

 

MemberController.Java

@RestController

public class MemberController {
    @Autowired
    MemberService memberService;

    @PostMapping("/signup")
    @ResponseBody
    public ApiResponse signUp(@RequestBody SignUpRequestDTO signUpRequestDTO) throws ParseException {
        return memberService.memberInsert(signUpRequestDTO);
    }

    @PostMapping("/login")
    @ResponseBody
    public ApiResponse login(@RequestBody @Validated LoginRequestDTO loginRequestDTO){
        return memberService.login(loginRequestDTO);
    }

    @PostMapping("/newtoken")
    @ResponseBody
    public ApiResponse newAccessToken(@RequestBody @Validated NewTokenRequestDTO newTokenRequestDTO, HttpServletRequest request) {
        return memberService.newAccessToken(newTokenRequestDTO, request);
    }
}

 

MemberService.Java

@RequiredArgsConstructor
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    private final MemberInfoRepository memberInfoRepository;
    private final RefreshTokenRepository refreshTokenRepository;
    private final AuthenticationManager authenticationManager;
    private final JwtProvider jwtProvider;

    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public ApiResponse memberInsert(SignUpRequestDTO signUpRequestDTO) throws ParseException {
        ResponseMap result = new ResponseMap();
        String encPassword = bCryptPasswordEncoder.encode(signUpRequestDTO.getPassword());
        Member member = memberRepository.save(Member.builder()
                    .email(signUpRequestDTO.getEmail())
                    .nickName(signUpRequestDTO.getNickName())
                    .role(Role.MEMBER)
                    .pwd(encPassword)
                    .status(Status.YES)
                    .providerType(ProviderType.EMAIL)
                    .build());

        memberInfoRepository.save(MemberInfo.builder()
                .member(member)
                .gender(signUpRequestDTO.getGender())
                .name(signUpRequestDTO.getName())
                .birth(signUpRequestDTO.getBirth())
                .point(0)
                .imageLink(signUpRequestDTO.getImageLink())
                .score(0)
                .build());
        Map createToken = createTokenReturn(member.getId());
        result.setMessage("MEMBER INSERT SUCCESS");
        result.setResponseData("accessToken", createToken.get("accessToken"));
        result.setResponseData("refreshToken", createToken.get("refreshToken"));
        return result;
    }

    public ApiResponse login(LoginRequestDTO loginRequestDTO) {
        ResponseMap result = new ResponseMap();

        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginRequestDTO.getEmail(), loginRequestDTO.getPassword())
            );

            Member member = memberRepository.findByEmail(loginRequestDTO.getEmail());
            Map createToken = createTokenReturn(member.getId());
            result.setMessage("LOGIN SUCCESS");
            result.setResponseData("accessToken", createToken.get("accessToken"));
            result.setResponseData("refreshToken", createToken.get("refreshToken"));
        } catch (Exception e) {
            e.printStackTrace();
            throw new AuthenticationException(ErrorCode.UsernameOrPasswordNotFoundException);
        }

        return result;
    }

    public ApiResponse newAccessToken(NewTokenRequestDTO newTokenRequestDTO, HttpServletRequest request){
        ResponseMap result = new ResponseMap();
        String refreshToken = newTokenRequestDTO.getToken();

        // AccessToken은 만료되었지만 RefreshToken은 만료되지 않은 경우
        if(jwtProvider.validateJwtToken(request, refreshToken)){
            Long memberId = jwtProvider.getMemberId(refreshToken);

            Map createToken = null;
            try {
                createToken = createTokenReturn(memberId);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            result.setResponseData("accessToken", createToken.get("accessToken"));
            result.setResponseData("refreshToken", createToken.get("refreshToken"));
        }else{
            // RefreshToken 또한 만료된 경우는 로그인을 다시 진행해야 한다.
            result.setResponseData("code", ErrorCode.ReLogin.getCode());
            result.setResponseData("message", ErrorCode.ReLogin.getMessage());
            result.setResponseData("HttpStatus", ErrorCode.ReLogin.getStatus());
        }
        return result;
    }

    private Map<String, String> createTokenReturn(Long memberId) throws ParseException {
        Map result = new HashMap();

        String accessToken = jwtProvider.createAccessToken(memberId);
        String refreshToken = jwtProvider.createRefreshToken(memberId).get("refreshToken");
        SimpleDateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date expiredAt = fm.parse(jwtProvider.createRefreshToken(memberId).get("refreshTokenExpirationAt"));
        RefreshToken insertRefreshToken = RefreshToken.builder()
                .token(refreshToken)
                .expiredAt(expiredAt)
                .build();

        refreshTokenRepository.save(insertRefreshToken);

        result.put("accessToken", accessToken);
        result.put("refreshToken", insertRefreshToken.getToken());
        return result;
    }
}

- memberInsert : 회원가입 로직으로 Member와 MemberInfo 둘다에 데이터를 넣어주며 Access, Refresh 토큰을 반환한다. 비밀번호 설정 시에 BcryptPasswordEncoder를 사용하여 암호화하여 DB에 저장한다.

- login : 로그인 로직으로 로그인 성공시 Access, Refresh 토큰을 반환한다. 이 때 authenticationManger가 이메일과 비밀번호를 통해 알맞은 로그인인지 검증해준다.

- newAccessToken: accessToken이 만료되고 RefreshToken은 만료되지 않은 경우 둘다 갱신해준다.

- createTokenReturn : Access, Refresh를 둘 다 실직적으로 만들어주는 로직.


📖 참고

- https://deeplify.dev/back-end/spring/oauth2-social-login#buildgradle-%EC%9D%98%EC%A1%B4%EC%84%B1

 

[Spring Boot] OAuth2 소셜 로그인 가이드 (구글, 페이스북, 네이버, 카카오)

스프링부트를 이용하여 구글, 페이스북, 네이버, 카카오 OAuth2 로그인 구현하는 방법에 대해서 소개합니다.

deeplify.dev

- https://jojoldu.tistory.com/170

 

Spring Security & 구글 OAuth & 테스트코드로 진행하는 계정 권한 관리 - 2

1-4. 로그인 세션 관리 OAuth2를 사용한다고해서 기존과 다른 마법같은 일이 펼쳐지는것은 아닙니다. OAuth2는 사용자 인증 및 허가된 정보를 가져오는 것외에는 사용하지 않습니다. 인증된 정보를

jojoldu.tistory.com

- https://www.youtube.com/watch?v=VDyY0RZAYtE&list=PL93mKxaRDidERCyMaobSLkvSPzYtIk0Ah&index=27 

- https://velog.io/@jkijki12/Spirng-Security-Jwt-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

 

Spirng Security + Jwt 로그인 적용하기

프로젝트를 진행하면서 Spring Security + Jwt를 이용한 로그인을 구현하게 되었다. 목차 Spring Security JWT Spring SEcurity + JWT Spring Security > 가장먼저 스프링 시큐리티에 대해서 알아보자. Sprin

velog.io

- https://mangkyu.tistory.com/76

 

[SpringBoot] Spring Security란?

대부분의 시스템에서는 회원의 관리를 하고 있고, 그에 따른 인증(Authentication)과 인가(Authorization)에 대한 처리를 해주어야 한다. Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능

mangkyu.tistory.com