[Spring] Spring-Security, Oauth2 & JWT를 통한 인증·인가 -2

윤성철·2024년 7월 5일

Back-End

목록 보기
9/22
post-thumbnail

서론

이전 포스트에 이어서 필터, 핸들러, 엔티티를 구현해보겠습니다.

해당 게시글은 다음 강의를 참고하여 작성했습니다.

https://www.youtube.com/watch?v=xsmKOo-sJ3c&list=PLJkjrxxiBSFALedMwcqDw_BPaJ3qqbWeB&ab_channel=%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9C%A0%EB%AF%B8

구현

OAuth2 상위 계층 Interface를 만들어 하위 계층인 Naver, Google에서 상속받아 구현

OAuth2Response

public interface OAuth2Response {

    String getProvider();

    String getProviderId();

    String getEmail();

    String getName();

    String getProfileImage();
}

GoogleResponse, NaverResponse

package dailymissionproject.demo.domain.auth.dto;

import java.util.Map;

public class GoogleResponse implements OAuth2Response {

    private final Map<String, Object> attribute;
    public GoogleResponse(Map<String, Object> attribute) {
        this.attribute = attribute;
    }

    @Override
    public String getProvider() {
        return "google";
    }

    @Override
    public String getProviderId() {
        return attribute.get("sub").toString();
    }

    @Override
    public String getEmail() {
        return attribute.get("email").toString();
    }

    @Override
    public String getName() {
        return attribute.get("name").toString();
    }

    @Override
    public String getProfileImage() {
        return attribute.get("picture").toString();
    }
}

public class NaverResponse implements OAuth2Response{

    private final Map<String, Object> attribute;

    public NaverResponse(Map<String, Object> attribute) {
        this.attribute = (Map<String, Object>) attribute.get("response");
    }

    @Override
    public String getProvider() {
        return "naver";
    }

    @Override
    public String getProviderId() {
        return attribute.get("id").toString();
    }

    @Override
    public String getEmail() {
        return attribute.get("email").toString();
    }

    @Override
    public String getName() {
        return attribute.get("name").toString();
    }

    @Override
    public String getProfileImage() {
        return attribute.get("profile_image").toString();
    }

}

User 엔티티

@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String name, String email, String picture, Role role){
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

    public User update(String name, String picture){
        this.name = name;
        this.picture = picture;
        return this;
    }
    public String getRoleKey(){
        return this.role.getKey();
    }
}

인가를 위한 Role 객체

@Getter
@RequiredArgsConstructor
public enum Role {

    GUEST("ROLE_GUEST" , "손님"),
    USER("ROLE_USER" , "일반 사용자");

    private final String key;
    private final String title;
}

Spring Security에서는 SecurityContextHolder라는 저장공간에 로그인한 사용자 정보를 Authentication 객체의 principal안에 담아둔다.

Autentication객체에 담기 위한 OAuth2User class

@RequiredArgsConstructor
public class CustomOAuth2User implements OAuth2User {

    private final UserDto userDto;

    @Override
    public Map<String, Object> getAttributes() {
        return null;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userDto.getRole().toString();
            }
        });
        return collection;
    }

    @Override
    public String getName() {
        return userDto.getName();
    }

    public String getUsername(){
        return userDto.getUsername();
    }
}

이제 jwt 토큰 발행과, jwt 토큰 검증하는 필터를 구현하겠습니다.

JwtUtil

@Component
public class JWTUtil {

    private SecretKey secretKey;

    public JWTUtil(@Value("${spring.jwt.secret}")String secret){
        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    public String createJwt(String username, Role role, Long expireMs){

        return Jwts.builder()
                .claim("username", username)
                .claim("role" , role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expireMs))
                .signWith(secretKey)
                .compact();
    }
    public String getUsername(String token){
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
    }

    public String getRole(String token){
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
    }

    public Boolean isExpired(String token){
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }
}

createJwt 메서드에서 username(naver + id)와 role)을 인자로 application.properties에 등록한 secretKey를 통해 jwt 토큰을 발행하도록 구현했습니다.
추가로, isExpired를 통해 jwtFilter에서 만료시간을 검증하도록 구현했습니다. 💡

로그인 성공 시, successHandler

@Component
@RequiredArgsConstructor
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JWTUtil jwtUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
        throws IOException, ServletException {

        CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();
        String username = customUserDetails.getUsername();

        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority auth = iterator.next();
        Role role = Role.valueOf(auth.getAuthority());

        String token = jwtUtil.createJwt(username, role,3600*60*60L);

        response.addCookie(createCookie("Authorization", token));
        response.sendRedirect("http://localhost:3000/");
    }

    private Cookie createCookie(String key, String value){

        Cookie cookie =  new Cookie(key, value);
        cookie.setMaxAge(60*60*60);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        return cookie;
    }
}

jwt를 발행하고 cookie에 담아 응답하도록 구현했습니다.

마지막으로, api 요청할 때, 쿠키의 jwt토큰을 검증하는 로직을 구현하겠습니다.

JWTFilter

@RequiredArgsConstructor
@Slf4j
public class JWTFilter extends OncePerRequestFilter {

    private final JWTUtil jwtUtil;

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

        String requestUri = request.getRequestURI();
        if(requestUri.matches("^\\/login(?:\\/.*)?$")){

            filterChain.doFilter(request, response);
            return;
        }

        if(requestUri.matches("^\\/oauth2(?:\\/.*)?$")){
            filterChain.doFilter(request, response);
            return;
        }

        String authorization = null;
        Cookie[] cookies = request.getCookies();
        log.info("{}", cookies);
        for(Cookie cookie : cookies){

            if(cookie.getName().equals("Authorization")){

                authorization = cookie.getValue();
            }
        }

        if(authorization == null){
            log.info("token is null");
            filterChain.doFilter(request, response);

            return;
        }

        String token = authorization;

        if(jwtUtil.isExpired(token)){
            log.info("token is expired");
            filterChain.doFilter(request, response);
            return;
        }

        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        UserDto userDto = new UserDto();
        userDto.setUsername(username);
        userDto.setRole(Role.valueOf(role));

        //인증 객체 담기
        CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDto);

        //시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}

cookie key값 authorization에 해당하는 value를 검증합니다. 만료시간이 지났다면, fiterchian메서드에서 jwtExpired 에러메세지를 응답합니다.
토큰이 유효하다면, securityContextHolder에 유저 정보를 담도록 구현했습니다.

테스트

Rest API로만 개발되었기 때문에, Spring Security에서 제공하는 로그인 화면에서 oauth2 로그인을 수행하고 jwt토큰을 응답받아, postman에서 해당 토큰을 쿠키에 담아 api를 요청하는 순서로 진행했습니다.



유저 정보를 조회하는 api를 요청 이후, 조회 쿼리가 정상적으로 나가는 것을 확인했습니다.

성공!

profile
내 기억보단 내가 작성한 기록을 보자..

0개의 댓글