Spring Security + JWT

김성인·2023년 6월 27일
1

🍃 SpringBoot

목록 보기
1/18
post-thumbnail

🟢SecurityConfig

서버로 들어오는 접근을 처리하여 접근에 대한 권한을 보고 나누는 역할

@EnableWebSecurity // 0
public class Security Config extends WebSecurityConfigurerAdapter{
	@Override
    protected void configure(HttpSecurity http) throws Exception{
    	http.csrf.disable() // 1
        .and()
        .authroizeRequests() // 2
        .antMatchers("/").permitAll() // 3
        .antMatchers("api/v1/**").hasRole(Role.USER.name()) 
        .anyRequest().authenticated() // 4
        .and()
        .logout.logoutSuccessUrl("/") // 5
      

0. @EnableWebSecurity

  • 스프링 웹 세큐리티를 지원하고 제공하기위한 어노테이션

1. http.csrf.disable()

2. authroizeRequests()

  • URL별 권한 관리를 설정하는 옵션의 시작점.
  • authorizeRequests가 선언되어야만 antMatchers 옵션을 사용할 수 있음.

3. antMatchers()

  • 권한 관래 대상을 지정하는 옵션. URL, HTTP 메소드별로 관리가 가능함.
  • "/" 등 지정된 URL을 permitAll()옵션을 통해 전체 열람 권한 부여.
  • "/api/v1/**" 주소를 가진 API는 USER 권한을 가진 사람만 가능하도록함.

4. anyRequest().authenticated()

  • anyRequest : 설정된 값들 이외 나머지 URL을 나타냄.
  • autehnticated: 인증된 사용자들에게만 허용함. (로그인한 사용자)

5. logout.logoutSuccessUrl("/")

  • 로그아웃 기능에 대한 여러 설정의 진입점.
  • 로그아웃 시 / 주소로 이동

🟢Spring Security + JWT !!

[JWT 로그인 스크랩 블로그]
출처 : 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


<인증과정>
1. Http Request가 서버로 넘어온다.
2. 가장먼저 AuthenticationFilter가 요청을 낚아챈다.
3. AuthenticationFilter에서 Request의 Username, password를 이용하여 UsernamePasswordAuthenticationToken을 생성한다.
4. 토큰을 AuthenticationManager가 받는다.
5. AuthenticationManager는 토큰을 AuthenticationProvider에게 토큰을 넘겨준다.
6. AuthenticationProvider는 UserDetailsService로 토큰의 사용자 아이디(username)을 전달하여 DB에 존재하는지 확인한다. 이 때, UserDetailsService는 DB의 회원정보를 UserDetails 라는 객체로 반환한다.
7. AuthenticationProvider는 반환받은 UserDetails 객체와 실제 사용자의 입력정보를 비교한다.
8. 비교가 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행한다.(실패시 AuthenticationFailureHandler를 실행한다.)

Security 클래스 구조

1- WebSecurityConfig

Security, Filter 관련 설정

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class
WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    //https://velog.io/@cminmins/Spring-Status-401-error
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/jat/users/test").authenticated()
                .anyRequest().permitAll()
                .and()
                .addFilterBefore(new JwtAuthenticateFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class);

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

2- JwtAuthenticateFilter

config 파일에서 설정한 필터 클래스, 요청을 받아서 토큰이 유효한지 처리한다.

@RequiredArgsConstructor
public class JwtAuthenticateFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //헤더에서 JWT를 받아옴
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);

        // 기간이 유효한 토큰인지 확인
        if (token != null && jwtTokenProvider.validateToken(token)){
            //토큰이 유효하면 토큰으로부터 유저정보 받아옴.
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext에 Authentication 객체를 저장함.
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}

3- JwtTokenProvider

요청에 들어오는 유저의 PK (Long) 타입을 이용하여 JWT 생성 및 확인, 발급

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {

    private final UserService userService;
    // 23.06.27 나중에 토큰 유효시간 바꿀것
    private long tokenValidTime = 1*(1000*60*60*24*365);

    /*
    //https://superbono-2020.tistory.com/186
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }*/

    // 23.06.27 나중에 구매자, 판매자에 따른 Role 설정 해줘야함
    public String createJwt(int userIdx){
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam("type", "jwt")
                .claim("userIdx", userIdx) // jwt 토큰 내의 payload 정보 (key, value)
                .setIssuedAt(now) //토큰 발행 시간 정보
                .setExpiration(new Date(System.currentTimeMillis()+ 1*tokenValidTime)) // 토큰 유효기간
                .signWith(SignatureAlgorithm.HS256, Secret.JWT_SECRET_KEY) // 사용할 암호화 알고리즘, signature에 들어갈 secret값 세팅
                .compact();
    }

    // JWT 토큰에서 인증정보 조회
    public Authentication getAuthentication(String token){
        UserDetails userDetails = userService.loadUserByUserIdx(this.getUserId(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    //토큰에서 회원정보 추출
    public Long getUserId(String token){
        return new Long(Jwts.parser().setSigningKey(Secret.JWT_SECRET_KEY).parseClaimsJws(token).getBody().get("userIdx", Integer.class));
    }

    // Request의 Header에서 token 값을 가져옴 "X-ACCESS-TOKEN" : "TOKEN 값"
    public String resolveToken(HttpServletRequest request){
        return request.getHeader("X-ACCESS-TOKEN");
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(Secret.JWT_SECRET_KEY).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

4- UserAuthentication

UserDetails 인터페이스 상속

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserAuthentication implements UserDetails {

    private Long sellerIdx;
    private String uid;
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles = new ArrayList<>();


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return uid;
    }

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

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

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

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

🟢 API 테스트

  1. 로그인 테스트

컨트롤러에서 의도한데로 jwt토큰이 발행되는것을 확인할 수 있다.

  1. 헤더에 JWT를 추가하여 인가된 접근 시도

    WebSecurityConfig에서 설정한 "/jat/users/tests" URI는 인가된 접근이 필요

올바르게 요청에 대한 응답이 오는것을 확인할 수 있다.

헤더에 유효하지않은 JWT를 넣는다면??
(예외처리를 하지 않아서 코드가 500으로 뜨지만, 오류가 나는것을 확인)

🟢 JWT 검증

https://jwt.io/ 서버에서 응답으로 온 JWT를 복호화하였더니 올바른 Idx를 확인 가능.

🟢 유저별 권한부여

ROLE 적용하여 인가된 접근 시도 (23.07.03 추가)

Merchandisers 유저에게 ROLE_SELLER라는 권한을 부여하였음.
DB 테이블에도 role 칼럼을 만들어서 회원가입시 권한을 저장하게됨.

1. <수정된 클래스 코드>

  • WebSecurityConfig 클래스

  • DB User테이블에는 role 칼럼에 값이 "ROLE_SELLER"로 들어가 있음

  • JwtTokenProvider 클래스

  • SellerService

  • SellerDao

  • SellerController

2. 권한을 부여한 API 테스트

  • 2-1) 올바른 권한부여를 하지않고 API 요청

  • 2-2) 올바른 권한 부여 후 API 요청 (잘 동작하는 모습)

profile
개발자가 꿈인 25살 대학생입니다.

0개의 댓글