sm 기술 블로그

[스프링부트] JWT(Json Web Token) - 코드 본문

스프링부트

[스프링부트] JWT(Json Web Token) - 코드

sm_hope 2022. 9. 6. 19:18

https://smhope.tistory.com/517

 

[스프링 부트] JWT(Json Web Token)란?

JWT(Json Web Token) : 두 개체에서 JSON객체를 사용하여 정보를 안정성 있게 전달해주는 인증 방식이다. jwt는 다음과 같은 구성을 지니고 있다. 실제로 다음과 같은 형식을 지니고, https://jwt.io/ 다음 사

smhope.tistory.com

 

Dependency

jwt와 security가 필요하다.

// == gradle ==
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

DB

진행하고 있는 채팅 프로젝트에 사용되는 user 테이블을 가지고 왔다.

DROP DATABASE chatting;
CREATE DATABASE chatting;
USE chatting;

CREATE TABLE `user`(
   user_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
   user_reg_date DATE NOT NULL,
   user_update_date DATE NOT NULL,
   user_email VARCHAR(50) NOT NULL,
   user_password VARCHAR(35) NOT NULL,
   user_role VARCHAR(10) NOT NULL DEFAULT "USER"
);

ALTER TABLE `user` MODIFY COLUMN user_role VARCHAR(10) NOT NULL DEFAULT "USER";
ALTER TABLE `user` ADD user_name VARCHAR(30) NOT NULL;
ALTER TABLE `user` MODIFY COLUMN user_name VARCHAR(30) NOT NULL AFTER user_email;

INSERT INTO `user` SET
   user_reg_date = NOW(),
   user_update_date = NOW(),
   user_email = "admin@test.com",
   user_password = "admin";
   
UPDATE `user` SET user_role = "ROLE_ADMIN" WHERE user_email = "admin@test.com"; 
UPDATE `user` SET user_name = "관리자" WHERE user_role = "ROLE_ADMIN";
  
SELECT * FROM `user`;

※ 권한을 줄때(user_role)는 반드시 "ROLE_ADMIN" 혹은 "ROLE_USER" 와 같이 "ROLE_"를 붙여야한다.

 

ENTITY

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Entity
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long userId;
    private LocalDateTime userRegDate;
    private LocalDateTime userUpdateDate;
    private String userEmail;
    private String userName;
    private String userPassword;
    private String userRole;

    public List<String> getRoleList() {
        if(this.userRole.length() > 0) {
            return Arrays.asList(this.userRole.split(","));
        }
        return new ArrayList<>();
    }
}

DB내용과 동일하게 작성한다.

 

Repository

import com.sbb.sm_chatting.Entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUserEmail(String userEmail);

    Optional<User> findByUserName(String username);

}

리포지터리를 생성하고 findByUserEmail과 findByUserName을 선언 한다. (나중에 사용하기 위함이다.)

 

SecurityConfig

import com.sbb.sm_chatting.Config.JWT.JwtAuthenticationFilter;
import com.sbb.sm_chatting.Config.JWT.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
                 http
                         .csrf().disable()
                         .httpBasic().disable()
                         .cors()
                         .and()
                         .sessionManagement()
                         .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 사용하지 않는다.
                         .and()
                         .authorizeRequests()
                         .antMatchers("/admin/**").hasRole("ADMIN")
                         .antMatchers("/user/**").hasAnyRole("USER","ADMIN")
                         .antMatchers("/**").permitAll()
                         .and()
                         .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                                 UsernamePasswordAuthenticationFilter.class);


        return http.build();
    }
}
  • csrf() : 사이트간 요청 위조 공격
  • httpBasic() : Header에 username, password를 실어 보내어 브라우저 혹은 서버가 그 값을 읽어 인증하는 방식
  • cors() : 교차 출처 리소스 공유 (실행 중인 웹 애플리케이션이 다른 출처의 선탁한 자원에 접근할 수 있는 권한을 부여)
  • sessionMangement() : 세션 관리함
  • sessionCreationPolicy() : 스프링 시큐리티 세션 정책

  • authorizeRequests() : 권한 설정 하겠다
  • antMatchers( url ) : 해당 url에 권한 설정
  • hasRole(" ") : " "에게 접속 권한 부여(단수)
  • hasAnyRole(" ", " ") :" "와 " "에게 접속 권한 부여(복수) 
  • permitAll() : 전체에게 권한 허용
  • addFilterBefore() : 필터 추가

JwtTokenProvider(토큰 생성)

package com.sbb.sm_chatting.Config.JWT;

import com.sbb.sm_chatting.Repository.UserRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;
import java.util.List;

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {

    @Autowired
    private UserRepository userRepository;
    private String secretKey = "smhopeisrich";

    // 토큰 유효시간 30분
    private long tokenValidTime = 30 * 60 * 1000L;

    private final UserDetailsService userDetailsService;

    // 객체 초기화, secretKey를 Base64로 인코딩한다.
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userEmail,String userPk, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
        claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
        claims.put("email",userEmail); // 이메일을 추가
        Date now = new Date(); // 시간 정보 객체 생성

        // 진짜로 토큰 생성
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        // getUserPk에서 뽑은 정보(유저 이름)로 userDetails 를 생성한다.
        // 인증이 완료되면 인증된 생성자 Authentiation 객체를 생성한다.
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));//!!
        UsernamePasswordAuthenticationToken tmpToken = new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
        return tmpToken;
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        // subject 에 유저 이름을 넣었다. return 값은 이름이 나올것이다.
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

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

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

 

PrincipalDetails (유저 정보를 저장)

import com.sbb.sm_chatting.Entity.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

@Data
public class PrincipalDetails implements UserDetails {
    private User user;

    public PrincipalDetails(User user) {
        this.user = user;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        user.getRoleList().forEach(r -> {
            authorities.add(() ->  r);
        });
        return authorities;
    }

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

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

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

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

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

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

getUsername을 사용하며, getUsername에 return 값을 원하는 것으로 사용할 수 있다. (이메일,이름 등)

 

PrincipalDetailService (유저 정보를 저장)

import com.sbb.sm_chatting.Entity.User;
import com.sbb.sm_chatting.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
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.Service;

@RequiredArgsConstructor
@Service
public class PrincipalDetailService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUserName(userName).orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));;
        return new PrincipalDetails(userEntity);
    }
}

유저 이름으로 유저를 찾고, 만약 없다면 사용자가 없음을 알린다.

PrincipalDetails에서 찾은 유저의 정보를 뽑아서 저장한다. (이름, 비밀번호 등)

 

JwtAuthenticationFilter (토큰 인증 필터)

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter 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);
    }
}

 


여기까지 기본셋팅은 끝났다.

이제 로그인을 통해 토큰이 잘 생성되는지, 접근제어는 잘 되는지 확인하기 위한 코드를 작성해 보자.

UserController (로그인)

import com.sbb.sm_chatting.Config.JWT.JwtTokenProvider;
import com.sbb.sm_chatting.Entity.User;
import com.sbb.sm_chatting.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Controller
@RequiredArgsConstructor
public class UserController {

    private final UserRepository userRepository;
    private final JwtTokenProvider jwtTokenProvider;

    @PostMapping("/login")
    @ResponseBody
    public String login(@RequestBody Map<String, String> user){
        // 유저 이메일로 유저를 찾는다.
        User _user = userRepository.findByUserEmail(user.get("userEmail")).get();
        // 유저 역할은 리스트로 선언했다.
        List<String> userRole = new ArrayList<>();
        userRole.add(_user.getUserRole());
        // 토큰을 생성함 -> 이메일, 이름, 역할 추가
        String Token = jwtTokenProvider.createToken(_user.getUserEmail(),_user.getUserName(), userRole);

        return Token;
    }
 }

MainController(홈페이지)

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
public class MainController {
    
    @PostMapping("/user/test")
    @ResponseBody
    public Map userResponseTest() {
        Map<String, String> result = new HashMap<>();
        result.put("result","user ok");
        return result;
    }

    @PostMapping("/admin/test")
    @ResponseBody
    public Map adminResponseTest() {
        Map<String, String> result = new HashMap<>();
        result.put("result","admin ok");
        return result;
    }
}

다음과 같이 user와 admin 홈페이지를 만들었다.

 

다음과 같이 로그인을 진행하고, 토큰을 발급받았다.

이 토큰을 디코더 해보면, 

이름, 권한, 이메일이 정상적으로 담겨있는것을 알 수 있다. (https://jwt.io/)

 

이제 user 홈페이지에 정상적으로 들어가지는지 확인해보자.

발급된 토큰을 헤더에 집적 넣어서 확인해 보면, 유저 페이지에 관리자가 정상적으로 접근가능하다.

만약 X-AUTH-TOKEN에 값이 없다면,

다음과 같이 접근 제한이 걸린다.

 

위 코드는 깃허브에서도 확인 가능하다. 

https://github.com/denist2322/smChatting

 

GitHub - denist2322/smChatting

Contribute to denist2322/smChatting development by creating an account on GitHub.

github.com

 

Comments