종우의 삶 (전체 공개)

Spring security - 7 // 프로젝트 설명 - 로그인 본문

개발/Spring

Spring security - 7 // 프로젝트 설명 - 로그인

jonggae 2024. 2. 1. 15:28

대상 Repository :

https://github.com/Jonggae/security

 

코드에 대한 코멘트는 commentary 브랜치에서도 확인할 수 있습니다.

https://github.com/Jonggae/security/tree/feature/6/commentary

 

참고자료:

인프런 Jwt 관련 영상 (github 코드 포함)

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt


 

회원가입을 완료하였으므로 로그인을 진행해보자. 가장 어려웠던 부분이었으므로 자세히 정리해본다.


■ Spring Security에서 JWT를 이용한 로그인 ■

1. 로그인 - JWT 기능 준비하기

 

기본적으로 Spring Security 에서는 form login 기능을 제공하고있다. 처음에는 이 방법을 이용하여 로그인 과정을 진행하려했지만 JWT를 경험해본 적도 있고하니 JWT 이용하여 로그인 과정을 구현해보기로 했다.

 

JWT를 사용하게되면 form login과 관련된 부분들은 사용하지 않는다. form login은 세션을 사용하므로 세션도 사용하지 않게 변경하고, JWT와 관련된 설정을 더 하면 된다. 

 

SecurityConfig 클래스의

 ㄴSecurityFilterChain 에 다음과 같이 추가해준다.

   /*jwt를 사용하므로 세션은 사용하지 않음으로 설정*/
  http.sessionManagement(sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

 

대신 JWT 관련 설정을 만들어 필터로 추가해주면 되겠다.

http.with(new JwtSecurityConfig(tokenProvider), customizer -> {
        });

 

이번 프로젝트에서 tokenProvider, JwtSecurityConfig 등 JWT 와 관련된 내용들을 구현할 것이다.


 

JWT란 JSON Web Token의 약자로 인증에 필요한 정보를 암호화 시킨 토큰을 의미한다. 쿠키 / 세션 방식과 유사하게 JWT access Token을 HTTP 헤더에 실어보내 서버가 클라이언트를 식별할 수 있도록 한다.

 

JWT 에는

1. Header : JWT의 타입과 사용하는 해시 알고리즘등의 메타 정보가 포함된다.

2. Payload : 실제로 전송할 정보가 포함되며, Claim 이라고 부르는 특정한 정보를 포함할 수 있다.

3. Signature : Header와 Payload를 합친 후, Secret Key를 사용하여 서명된 부분으로 토큰이 변조되지 않음을 검증한다.

 

이 세가지 요소가 들어가며, Payload에 들어가는 Claim에 보통 권한 정보가 함께 들어간다. 

 

SecretKey는 암호화된 상태로 존재하며, 보안문제로 인해 서버측에서 안전하게 관리해야한다.

 

이번 개발에서는 application.yml파일에 변수로 추가해 넣었다. gitignore를 통해 게시되지 않도록 지정한다. (이 방법도 권장되지 않음, 환경변수를 추천)

 

application.yml에 추가

 

헤더의 이름(Authorization)과 검증에 필요한 key, 만료시간을 정해준다.

 

사전설정이 끝났고, 실제로 필요한 과정들을 살펴보자.

 

1. Controller로 들어오는 사용자의 요청을 받을 수 있는 api 엔드포인트 

-> @PostMapping으로 ("/authenticate") 따위의 엔드포인트를 설정한다.

로그인 시 입력한 정보를 LoginRequestDto로 받아와 @RequestBody로 전달한다.

 

2. 1. 에서 들어온 요청이 DB에 담겨있는 정보와 일치하는지 검증

-> UsernamePasswordAuthenticationToken 객체를 만들어 LoginRequestDto의 정보에서 username, password를 추출한다. 

 

3. Authetication 객체를 생성한다. 이 객체에는 authenticationManagerBuilder를 사용하여 앞서 만든 UsernamePasswordAuthenticationToken 객체를 인증한 내용이 들어있다. 

-> 사용자가 제공한 아이디와 비밀번호를 기반으로 실제로 사용자를 확인, 인증한다.

 

4. JWT를 생성한다.

-> 3에서 인증이 확인된 사용자의 정보와 추가 정보들을 포함하여 JWT 를 만든다 tokenProvider가 작동한다.

 

5. HTTP헤더로 클라이언트에게 보내준다.

-> 일반적으로 "Bearer " 스키마로 Authroization 헤더에 넣어 전송됨.

 

6. HTTP응답을 반환한다. (성공 / 실패)

 

이 과정들을 코드로 살펴보자. 리팩토링이 필요하지만 우선 완성된 로직을 구현해본다.

 


Controller

Postman으로 테스트를 진행하였고, RestController로 생성했다.

 

@PostMapping("/authenticate")
    public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginRequestDto loginRequestDto) {
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginRequestDto.getUsername(), loginRequestDto.getPassword());

        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

		String jwt = tokenProvider.createToken(authentication);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);

        return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);

 

성공적으로 토큰이 발급되면 200 ok 응답을 내어준다. 앞서 말한 과정들이 들어가있다.

 

(전체 코드)

더보기
package com.example.securitydemo.user.controller;

import com.example.securitydemo.common.dto.TokenDto;
import com.example.securitydemo.common.jwt.JwtFilter;
import com.example.securitydemo.common.jwt.TokenProvider;
import com.example.securitydemo.user.dto.LoginRequestDto;
import com.example.securitydemo.user.dto.UserDto;
import com.example.securitydemo.user.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

/*
 * view 페이지가 없으므로 postmans 같은 테스트 어플을 이용하기 위한 컨트롤러
 * api 엔드포인트에 따라 접근 로직이 다르다.
 * */

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ApiTestController {

    /*의존성 주입*/
    private final UserService userService;
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    /*회원가입 api 접근
    UserDto 를 받아와서 userService.register 메서드로 전달하여 저장 진행.
     */
    @PostMapping("/register")
    public ResponseEntity<UserDto> register(@Valid @RequestBody UserDto requestDto) {
        return ResponseEntity.ok(userService.register(requestDto));
    }

   
    @PostMapping("/authenticate")
    public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginRequestDto loginRequestDto) {
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginRequestDto.getUsername(), loginRequestDto.getPassword());

        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
//        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = tokenProvider.createToken(authentication);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);

        return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
    }

    /*
    * 인증 여부에 따른 접근 제한
    * todo : private -> user 로 변경
    * USER 는 자신의, ADMIN 은 특정 유저 개인 정보는 확인할 수 있음.
    */
    @GetMapping("/private")
    @PreAuthorize("hasAnyRole('USER','ADMIN')")
    public ResponseEntity<UserDto> getMyUserInfo(HttpServletRequest request) {
        return ResponseEntity.ok(userService.getMyUserWithAuthorities());
    }

    @GetMapping("/user/{username}")
    @PreAuthorize("hasAnyRole('ADMIN')")
    public ResponseEntity<UserDto> getUserInfo(@PathVariable String username) {
        return ResponseEntity.ok(userService.getUserWithAuthorities(username));
    }
}

 

TokenProvider

JWT를 생성하는 tokenProvider를 만든다.

 

@Component로 설정하여 Bean으로 등록한다.

Secret Key와 만료시간을 설정하고 tokenProvider 빈이 초기화될때 설정된 secertkey값을 JWT 서명에 사용하도록 미리 설정을 한다.

 

public TokenProvider(
            @Value("${jwt.secret.key}") String secret,
            @Value("${jwt.expiration_time}") long tokenExpirationTime) {
        this.secret = secret;
        this.tokenExpirationTime = tokenExpirationTime * 1000;
    }
    
@Override
public void afterPropertiesSet() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

 

createToken 메서드를 만들어 실제로 토큰을 생성한다.

 

public String createToken(Authentication authentication) {
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.tokenExpirationTime);

        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(validity)
                .compact();
    }

 

변수로 들어오는 Authentication 객체는 springSecurity에서 사용자의 인증 정보를 나타낸다.

사용자의 식별 정보 (username, email, password 따위), 자격증명, 권한등이 포함된다.

 

일반적으로 username, password 로 로그인 하는 경우 Authentication 객체는 사용자의 아이디를
Principal 로, 사용자에게 부여된 권한 정보를 Authorities 로 갖고있음.

 

Principal
사용자를 식별하는 정보, UserDetails 객체가 주로 이 역할을 한다. 따라서 UserDetails도 따로 구현해주어야 한다.
Authorities
사용자에게 부여된 권한 정보. GrantedAuthority 인터페이스를 구현한 객체들의 컬렉션으로 표현

Controller에서 언급한 UsernamePasswordAuthenticationToken을 생성하는 getAuthentication 메서드도 추가해준다. 이후 필터를 통과하여 Security에서 사용될때 필요한 로직이다.

 

 public Authentication getAuthentication(String token) {
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

JWT 내의 claim을 토큰을 파싱하여 얻어온 후 Collection으로 권한을 저장하고

모든 정보를 User객체를 생성하여 principal에 저장한다.

최종적으로 로그인을 하려는 사용자의 정보가 들어있는 UsernamePasswordAuthenticationToken 객체를 반환한다.

 

토큰의 유효성을 검증하는 메서드도 추가해준다.

public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            logger.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            logger.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            logger.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            logger.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }

 

(전체코드)

더보기
package com.example.securitydemo.common.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
/*
* JWT 를 생성하는 주된 로직들
* 로그 작성을 위한 Logger
* 권한 정보를 추출할 때 사용하는 AUTHORITIES_KEY = "auth"
* JWT 가 변조되지 않았음을 검증하는 secret (key 값)
* 토큰 만료시간 tokenExpirationTime 등 변수 설정
* */
@Component
public class TokenProvider implements InitializingBean {
    private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
    private static final String AUTHORITIES_KEY = "auth";
    private final String secret;
    private final long tokenExpirationTime;
    private Key key;

    /*
    * jwt key 같은 정보는 유출되지 않도록 다른 파일에 저장하여 사용
    */
    public TokenProvider(
            @Value("${jwt.secret.key}") String secret,
            @Value("${jwt.expiration_time}") long tokenExpirationTime) {
        this.secret = secret;
        this.tokenExpirationTime = tokenExpirationTime * 1000;
    }
    /*
    * tokenProvider 빈이 초기화 될때, 설정된 secret 값을 디코딩하여
    * 해당 값을 JWT 서명에 사용할 수 있도록 디코딩한다.
    * HMAC-SHA (Hash-based Message Authentication Code with Secure Hash Algorithm)
    * 빈이 사용될 때 설정하는 역할
    */
    @Override
    public void afterPropertiesSet() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    /*
    * 실제로 jwt 를 생성하는 메서드
    * 변수로 받는 Authentication 객체는 SpringSecurity 에서 사용자의 인증정보를 나타낸다.
    * 사용자의 식별 정보(username, password 따위), 자격 증명, 권한이 포함된다
    * 일반적으로 Principal, Authorities를 갖고있음.
    */

    /*
    * Principal
    * 사용자를 식별하는 정보, UserDetails 객체가 주로 이 역할을 한다.
    *
    * Authorities
    * 사용자에게 부여된 권한 정보. GrantedAuthority 인터페이스를 구현한 객체들의 컬렉션으로 표현
    * 일반적으로 username, password 로 로그인 하는 경우 Authentication 객체는 사용자의 아이디를
    * Principal 로, 사용자에게 부여된 권한 정보를 Authorities 로 갖고있음.
    * */

    /*
    * createToken 메서드를 통해 이러한 정보들을 가져온다.
    * 유저 정보와 권한, 생성 일시, 필요한 정보들을 넣고 JWT 객체를 생성함
    * builder 로 작성된 부분이
    * JWT 의 주제 (주로 username, 아이디 따위)
    * 사용자의 권한 정보 claim(Au~)
    * JWT 서명 (signWith~)
    * 만료 시간
    * 문자열로 압축 을 뜻한다.
    * */
    public String createToken(Authentication authentication) {
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.tokenExpirationTime);

        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(validity)
                .compact();
    }

    /*
    * JWT 내의 클레임(정보)를 이용하는 메서드
    * 토큰을 파싱하여 클레임을 얻어온 후
    * 클레임 내의 권한 정보를 가져와 각 권한을 SimpleGrantedAuthority 로 변환하여 컬레션으로 만듬
    * 이때 AUTHORITIES_KEY 는 권한 정보를 추출할 때 사용
    *
    * JWT 의 사용자 정보로 User 객체를 생성함
    * 최종적으로 UsernamePasswordAuthenticationToken 을 반환한다. */
    public Authentication getAuthentication(String token) {
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    /*
    * JwtFilter 클래스 내의  doFilter 메서드에서 넘어옴
    * T, F 값을 리턴하며 유효성을 검증한다
    * JWT 를 서명하는데 사용되는 key 를 설정하고
    * 서명이 유효한 경우 Claim 을 반환함  -> True 로 넘겨줌
    * */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            logger.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            logger.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            logger.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            logger.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
}

 

토큰을 생성하였으므로 JWT 인증을 처리하는 필터를 생성한다. 처음에 SecurityConfig에 추가된 이 부분을 구현하게 된다.

    http.with(new JwtSecurityConfig(tokenProvider), customizer -> {
        });

 

JwtSecurityConfig

Jwt 관련 설정을 담아놓은 클래스이고 tokenProvider와 jwtFilter를 담고있다.

아직 filter를 구현하지 않았으므로 바로 이어서 filter를 만들어본다.

 

package com.example.securitydemo.common.jwt;

import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final TokenProvider tokenProvider;
    public JwtSecurityConfig(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public void configure(HttpSecurity http) {
        http.addFilterBefore(
            new JwtFilter(tokenProvider),
            UsernamePasswordAuthenticationFilter.class
        );
    }
}

 

JwtFilter

jwt 인증을 처리하는 필터이다.

유효한 인증인 경우, securityContextHolder에 인증 정보를 저장한다.

 

Jwt 필터로 재정의 하는 것이기때문에 doFilter 메서드를 override 한다.

 @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String jwt = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

 

Bearer 스키마로 전달되는 토큰을 추출하기 위한 메서드 resolveToken을 추가한다.

substring을 이용하여 Bearer 문자 이후의 토큰만 가져온다.

 private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }

        return null;
    }

 

SecurityConfig를 완성하기 전에 

 

권한 설정에 필요한 UserDetailsService와 사용자의 정보를 포함하는 UserDetails를 만들어준다. 

둘다 인터페이스이므로 implement로 구현한다.

 

UserDetailsImpl (UserDetails 구현)

package com.example.securitydemo.common.security;

import com.example.securitydemo.user.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class UserDetailsImpl implements UserDetails {

    private final User user;

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

    public User getUser() {
        return user;
    }

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {


        return null;
    }

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

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

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

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

GrantAuthority 부분은 ServiceImpl에서 구현하므로 생략하여도 될듯 (추후 리팩토링)

UserDetails는 형식이 거의 정해져있으므로 잘 참고해서 진행하자. 

 

UserDetailsServiceImpl (UserDetailsService 구현)

package com.example.securitydemo.common.security;

import com.example.securitydemo.user.domain.User;
import com.example.securitydemo.user.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

/*
* 사용자 정보를 로드하는 인터페이스 UserDetailsService 를 구현하여 사용*/

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    /*
    * loadUserByUsername 메서드는 DB의 사용자 정보를 조회하여 반환한다.
    * 이 정보를 기반으로 Security 에서 사용자를 인증하고 권한을 부여한다.
    *  */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findOneWithAuthoritiesByUsername(username)
                .map(user -> createUser(username, user))
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
    }

    private org.springframework.security.core.userdetails.User createUser(String username, User user) {
        List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
                .collect(Collectors.toList());
        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                user.getPassword(),
                grantedAuthorities);
    }
}

 

두 클래스를 완성하면 사용자의 정보를 가져올 수 있다.

 

개발 도중 GrantedAuthority를 고려하지 않은 순간이 있었는데, 역시나 권한 부분이 요청에서 넘어오지 않았었다. 

시큐리티는 디버깅을 많이 해보아야 한다. 어디서 무언가가 들어오고, 어떤 메서드를 사용하는지 잘 파악하는 것이 중요한듯.

 

이후 SecurityConfig를 알맞게 설정해준다.

SecurityConfig

설명은 적당히 코드 내에 적어두었다. 

더보기
package com.example.securitydemo.common.config;

import com.example.securitydemo.common.jwt.JwtAccessDeniedHandler;
import com.example.securitydemo.common.jwt.JwtAuthenticationEntryPoint;
import com.example.securitydemo.common.jwt.JwtSecurityConfig;
import com.example.securitydemo.common.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

    /*
     * jwt 생성용 TokenProvider
     * CORS 설정을 위한 CorsFilter
     * 유효하지 않은 인증을 진행하면 401 에러를 반환하는 JwtAuthenticationEntryPoint
     * 필요한 권한이 없이 접근할 때 JwtAccessDeniedHandler
     */
    private final TokenProvider tokenProvider;
    private final CorsFilter corsFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    /*
     * 회원 가입시 작성한 비밀번호 암호화, 복호화를 위한 PasswordEncoder
     * @Bean 을 이용하여 빈으로 등록하여 사용하여야 함
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /*
     * 다양한 filter 들을 관리하는 SecurityFilterChain
     * 여러 설정들을 이 빈에서 진행한다.
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        /*
         * CSRF 비활성화
         * Cross-Site Request Forgery 는 악의적인 웹사이트 공격으로 사용자의 인증정보를 사용하여
         * 해당 사용자의 권한으로 요청을 보내는 공격이다. -> 해킹이네
         * 단순한 웹 페이지에서는 CSRF 비활성화하는 편*/
        http.csrf(AbstractHttpConfigurer::disable);

        /*
         * addFilterBefore 를 이용하여 특정한 필터를 다른 필터보다 먼저 실행되도록 등록함
         * 발생할 수 있는 예외도 등록하여 처리함 ( jwtAccessDeniedHandler, jwtAuthenticationEntryPoint)
         */
        http.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .accessDeniedHandler(jwtAccessDeniedHandler)
                        .authenticationEntryPoint(jwtAuthenticationEntryPoint));

        /*h2 console 디스플레이 설정*/
        http.headers(headers ->
                headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));

        /*
         * 인증이 필요한 엔드포인트들을 설정하는 부분
         * 회원가입, 로그인 페이지들은 인증없이 접근이 가능해야하기 때문에 permitAll 로 열어준다.
         * 나머지 페이지들은 인증 과정이 필요하기에 그 외 요청은 authenticated() 를 걸어준다.
         * */
        http.authorizeHttpRequests((authorizeHttpRequest ->
                authorizeHttpRequest
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/authenticate", "/api/register").permitAll()
                        .requestMatchers(PathRequest.toH2Console()).permitAll()
                        .anyRequest().authenticated())
        );

        /*jwt를 사용하므로 세션은 사용하지 않음으로 설정*/
        http.sessionManagement(sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        /*jwt 인증 과정 추가 */
        http.with(new JwtSecurityConfig(tokenProvider), customizer -> {
        });


        return http.build();
    }
}

 

전체 코드를 올리고 싶지만 내용이 많이 길어지고 있기에,

누락된 코드나 자세한 내용은 github를 참고바란다.

 

 

 

Comments