[스프링 부트 핵심가이드] 서비스의 인증과 권한부여 👀

FeelingXD·2023년 4월 28일
0

북스터디

목록 보기
11/13
post-thumbnail

인증과 인가

인증(authentication)

인증은 사용자가 누구인지 확인하는 단계를 의미. (Who are you)
인증의 대표적인 에시로는 로그인이 있으며 사용자가 입력한 아이디 ,비밀번호(그외 사용자를 식별할수있는 토큰) 을 서버내의 정보와 비교하여 일치 여부를 확인합니다. 로그인에 성공하면( 사용자를 식별했다면) 서버는 사용자를 식별하는 토큰을 전달하며 사용자는 이토큰을 바탕으로 리소스에 접근할수 있습니다.

인가(Authoriztion)

sholud you do it ?
인가는 위의 인증과정을 거친 사용자가 애플리케이션의 특정리소스를 접근할떄 사용자가 해당 리소스에 접근할 권리가 있는지 확인화는 과정(단계) 입니다.
예를 들어 같은 어플리케이션을 사용하더라도 관리자, 사용자 의 역할, 사용자중에서도 등급에따라 리소스에 접근할수있는지 여부를 확인이 필요할수있습니다. 이에따라 접근을 허가하거나 거부하는것이 대표적인 인가의 예시입니다.

스프링 시큐리티

스프링 시큐리티는 애플리케이션의 인증, 인가 보안 기능을 제공하는 스프링의 하위 프로젝트(모듈) 중 하나입니다. 보안과 관련된 많은 기능을 제공하기에 스프링 시큐리티를 통해 편리하게 원하는 기능을 설계할수 있게합니다.

스프링 시큐리티의 필터

스프링 시큐리티의 필터는 기존 서블릿 필터의 사이에 DelegatingFilterProxy 라는 스프링 시큐리티에서 사용하는 필터를 만들어 기존 서블릿 필터사이에 끼워넣어 사용합니다.

개발자는 DelegatingFilterProxy에서 SecurityFilterChain을 등록하여 요청 흐름을 제어합니다.

(서블릿 필터 (n-1). -> DelegatingFilterProxy(security Filter) -> 서블릿 필터(n) )

스프링 시큐리티의 필터체인은 여러가지 필터를 제공하지만 이번 포스트에서는 자세하게 다루지 않습니다.

JWT를 이용한 로그인

JWT (Json Web token) 👀
JWT는 일반적으로 웹 애플리케이션의 인증 및 권한 부여에 사용됩니다. 사용자가 로그인하면 서버는 JWT를 생성하여 클라이언트로 보냅니다. 클라이언트는 서버에 대한 후속 요청에 JWT를 포함하여 사용자가 특정 리소스에 액세스할 수 있도록 인증되고 권한이 있음을 증명 하는 방법입니다.

jwtTokenProvider( jwt token 생성주체)

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

    private final UserDetailsService userDetailsService; // Spring Security 에서 제공하는 서비스 레이어

    @Value("${springboot.jwt.secret}") // property에 선언되어잇는 암호화 키 
    private String secretKey = "secretKey";
    private final long tokenValidMillisecond = 1000L * 60 * 60; // 1시간 토큰 유효 1000ms * 60 * 60 => 1h

   
    @PostConstruct
    protected void init() {
        System.out.println(secretKey);
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
        System.out.println(secretKey);
    }

    // JWT 토큰 생성
    public String createToken(String userUid, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(userUid);
        claims.put("roles", roles);

        Date now = new Date();
        String token = Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(new Date(now.getTime() + tokenValidMillisecond))
            .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘, secret 값 세팅
            .compact();

        return token;
    }

    // JWT 토큰으로 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUsername(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "",
            userDetails.getAuthorities());
    }

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

      // JWT 토큰 확인하기

    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN"); //->X-AUTH-TOKEN 헤더값에 대한 값을 반환 해당헤더로 토큰 전달
    }


    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

JwtAuthenticationFilter Class (jwt 인증 필터)

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest servletRequest,
        HttpServletResponse servletResponse,
        FilterChain filterChain) throws ServletException, IOException {
        String token = jwtTokenProvider.resolveToken(servletRequest);

        if (token != null && jwtTokenProvider.validateToken(token)) {
        //token 이 존재하며 token이 검증된경우
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(servletRequest, servletResponse);
        //-> 다음 필터 작업 수행
    }
}

Security Configuration (스프링 시큐리티 설정 클래스)

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
    //->WebSecurityConfigurerAdapter 상속받아 설정 overide
    private final JwtTokenProvider provider;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.httpBasic().disable() // REST API는 UI를 사용하지 않으므로 기본설정을 비활성화

                .csrf().disable() // REST API는 csrf 보안이 필요 없으므로 비활성화

                .sessionManagement()
                .sessionCreationPolicy(
                        SessionCreationPolicy.STATELESS) // JWT Token 인증방식으로 세션은 필요 없으므로 비활성화

                .and()
                .authorizeRequests() // 리퀘스트에 대한 사용권한 체크
                .antMatchers("/sign-api/sign-in", "/sign-api/sign-up",
                        "/sign-api/exception").permitAll() // 가입 및 로그인 주소는 허용
                .antMatchers(HttpMethod.GET, "/product/**").permitAll() // product로 시작하는 Get 요청은 허용

                .antMatchers("**exception**").permitAll()

                .anyRequest().hasRole("ADMIN") // 나머지 요청은 인증된 ADMIN만 접근 가능

                .and()
                .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())

                .and()
                .addFilterBefore(new JwtAuthenticationFilter(provider),
                        UsernamePasswordAuthenticationFilter.class); // JWT Token 필터를 id/password 인증 필터 이전에 추가
    }

  
    @Override
    public void configure(WebSecurity webSecurity) {
        webSecurity.ignoring().antMatchers("/v2/api-docs", "/swagger-resources/**",
                "/swagger-ui.html", "/webjars/**", "/swagger/**", "/sign-api/exception");// -> 해당경로 파일은 검증하지않음
    }

}

Login Controller

 @PostMapping(value = "/sign-in")
    public SignInResultDto signIn(
        throws RuntimeException {
        SignInResultDto signInResultDto = signService.signIn(id, password);

        if (signInResultDto.getCode() == 0) {
                signInResultDto.getToken());
        }
        return signInResultDto;
    }

로그인 시나리오

  1. 사용자가 로그인 요청을 보냄 (아이디, 비밀번호)
  2. 서버에서 사용자가 보낸 정보들을 내부정보와 비교하며 사용자를 식별 (인증과정)
  3. 서버에 사용자에대한 정보가있으면 이에따른 jwt token을 생성후 반환 (jwt 토큰에는 비밀번호 와같은 민감한정보를 포함하면 안됨)
  4. 사용자는 서버로부터 받은 jwt token 을 이용해 서비스를 이용함( 인가 , 위의 예제코드에서는 'x-auth-header' 를 통해 토큰을 서버로 전송함)

작성중


참고

jwt : https://brunch.co.kr/@jinyoungchoi95/1
security :https://mangkyu.tistory.com/76

profile
tistory로 이사갑니다. :) https://feelingxd.tistory.com/

0개의 댓글