Spring 숙련 230304 #6 인증 / JWT 실습

김춘복·2023년 3월 4일
0

Spring 공부

목록 보기
12/14

인증 실습

인증 / 인가만 구현

  • Spring Boot 2.7부터 H2가 업데이트 되어 USER가 예악어로 등록.
    @Entity(name = "users")로 테이블 이름을 USER와 안겹치게 설정해주어야 한다.

  • userRole은 Enum으로 생성해서 Entity에 넣음

package com.sparta.myselectshop.entity;

public enum UserRoleEnum {
    USER,  // 사용자 권한
    ADMIN  // 관리자 권한
}
  • User에 컬럼으로 위의 enum을 받은 role을 만들어 넣음
    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;
  • userRole에 명시해둔 admin 관리자 권한은 미리 키값을 service에 private static final로 등록해 서비스에서 검증함. 이 키값은 관리자가 미리 확보해둬야. (실제로는 이렇게 엉성하게는 안함)
    현업에서는 관리자 권한을 부여할 수 있는 관리자페이지를 구현해서 승인 결재 과정을 거쳐 관리자 권한을 부여한다.

JWT 구현 (예시일뿐 그대로 따라하는건 x)

  • JWT dependency 추가 (build.gradle의 dependencies에 추가 후 코끼리 누르기 )
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
  • application.properties에 아래 추가(예시)
    어떤 문자열을 Base64로 인코딩 한 값. 너무 짧아도 안되고 어느정도 길이 맞춰야
jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=
  • 토큰 생성에 필요한 값
		// Header KEY 값
	public static final String AUTHORIZATION_HEADER = "Authorization";
		// 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";
		// Token 식별자
    private static final String BEARER_PREFIX = "Bearer ";
		// 토큰 만료시간    밀리세컨드 기준이라 60 * 60 * 1000L면 한시간
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

	@Value("${jwt.secret.key}") //application.properties의 key값을 @value로 가져옴
    private String secretKey;
    private Key key;		
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    // enum 알고리즘안에 여러 암호화 알고리즘이 있어 골라서 쓰면 된다.

    @PostConstruct	// 처음 이 객체가 생성될 때 초기화하는 함수
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey); // byte[]로 디코딩
        key = Keys.hmacShaKeyFor(bytes);		// 디코딩한 값을 키객체에 넣어줌
    }
  • @PostConstruct : 의존성 주입이 이루어진 후 초기화를 수행하는 메서드
    @PostConstruct를 사용하면, bean이 초기화 됨과 동시에 의존성을 확인할 수 있다.

  • Header에서 Token 가져오기

		// header 토큰을 가져오기
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER); 
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7); //"BEARER "까지 7글자 떼냄
        }
        return null;
    }
  • JWT 생성 (JWT를 만드는 메서드)
		// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
  Date date = new Date();

  return BEARER_PREFIX +		//위의 토큰에서 "BEARER " 7글자 떼내서 다시 붙임
        Jwts.builder()
        .setSubject(username)	// 특정 공간에 유저이름 넣고
        .claim(AUTHORIZATION_KEY, role)	// claim공간엔 사용자 권한넣고
        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 유효기간 지정(지금기준에서+유효기간)
        .setIssuedAt(date)  // 토큰 생성 시간 넣어줌. 필수는 아님
        .signWith(key, signatureAlgorithm) // 위에서 만든 key객체와 어떤 알고리즘으로 암호화할건지 지정
        .compact();
        
        // String 형식의 JWT토큰으로 반환.
}
  • JWT 검증
		// 토큰 검증
    public boolean validateToken(String token) {
        try { // parserBuilder()로 검증. set~에 토큰 만들때 쓴 key 넣어주고, parse~에 검증할 토큰 넣는다.
           Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }
  • JWT에서 사용자 정보 가져오기
		// 위의 검증과 거의 비슷한 키지만 마지막에 .getBody()로 안의 정보를 가져옴
        // 위의 검증에서 이미 유효성 검사를 했다는 가정하라서 try-catch가 없다.
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
  • JWT를 사용해서 로그인한 회원만 어떤 로직을 수행할 수 있게 하려면 아래처럼
    HttpServletRequest request 이 파라미터로 추가된다. (request의 header에 JWT가 있으니까)
@PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, HttpServletRequest request) {
  • Service 부분의 메서드 예시 코드 (로그인시 내 관심 상품 조회)
@Transactional(readOnly = true)
    public List<ProductResponseDto> getProducts(HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request); // 토큰을 가져온다
        Claims claims;      //JWT안에 들어있는 정보들을 담을 수 있는 객체

        // 토큰이 있는 경우에만 관심상품 조회 가능
        if(token != null) {
            // Token 검증 .validateToken()은 검증이 잘되면 true
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져와서 claims에 넣음. 문제가 생기면 예외처리
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회 / claims.getSubject()를 하면 username 가져온다
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            // 사용자 권한 가져와서 ADMIN 이면 전체 조회, USER 면 본인이 추가한 부분 조회
            UserRoleEnum userRoleEnum = user.getRole();
            System.out.println("role = " + userRoleEnum);

            List<ProductResponseDto> list = new ArrayList<>();
            List<Product> productList;

            if (userRoleEnum == UserRoleEnum.USER) {
                // 사용자 권한이 USER일 경우 USER것만 가져오고 관리자면 다 가져옴
                productList = productRepository.findAllByUserId(user.getId());
            } else {
                productList = productRepository.findAll();
            }

//            그렇게 가져온 productlist를 DTO에 담아서 반환
            for (Product product : productList) {
                list.add(new ProductResponseDto(product));
            }

            return list;

        }else {
            return null;
        }
    }
profile
Backend Dev / Data Engineer

0개의 댓글