JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
JWT는 당사자 간의 정보를 JSON 객체로 안전하고 효율적으로 전송하기 위한 표준!
JWT
payload :
토큰에 담을 정보가 들어있는 부분, 한 개 이상의 claim으로 이루어짐
claim :
정보의 한 조각, “name” : value
{
"username": "John Doe"
}
subject :
토큰에 대한 제목, 애플리케이션에서는 유저에 대한 식별값이 됨
JWT공식사이트 에서 직접 클레임을 작성하여 JWT를 발급해보았다
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIn0.BrhGCohqhPFYSQMzw3F4OZeN1dgTZwPUBobFOYEVx1SgZ2fuMDNUV36p51aXtxAAPwSzlsORJf-mFWQvdYZTWg
어디서 많이 봤던 결과값이 나왔다
JwtUtil.java
1️⃣ 토큰을 만드는 부분
username을 받아서 액세스 토큰, 리프레쉬 토큰을 만드는 부분을 구현
@Component
public class JwtUtil {
@Value("${haden.jwtSecret}")
private String jwtSecret;
private final long ACCESS_TOKEN_VALID_PERIOD = 1000L*60*30;
private final long REFRESH_TOKEN_VALID_PERIOD = 1000L*60*60*24*7;
public TokenDto generateToken(String username){
Date now = new Date();
Date accessTokenExpireIn = new Date(now.getTime() + ACCESS_TOKEN_VALID_PERIOD);
String accessToken = Jwts.builder()
.setClaims(Jwts.claims().setSubject(username))
.setIssuedAt(now)
.setExpiration(accessTokenExpireIn)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
String refreshToken = Jwts.builder()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_VALID_PERIOD))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
return new TokenDto(accessToken, refreshToken, accessTokenExpireIn.getTime());
}
}
.setClaims(Jwts.*claims*().setSubject(username))
:username 으로 클레임을 만들건데, 그걸 Subject로 설정할거야
.setIssuedAt(now)
:지금 발급되는 토큰이야
.setExpiration(accessTokenExpireIn)
:지금으로부터 ACCESS_TOKEN_VALID_PERIOD
까지 유효해
.signWith(SignatureAlgorithm.*HS512*, jwtSecret)
: HS512
로 인코딩 할거고, jwtSecret
이라는 키를 쓸거야
2️⃣ 토큰으로부터 username을 가져오는 부분
username 클레임이 Subject였으므로 .parseClaimsJws(token).getBody().getSubject()
으로 가져올 수 있음
@Component
public class JwtUtil {
...
public String getUsername(String token){
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
}
3️⃣ 토큰이 유효한지 확인하는 부분
토큰이 jwtSecret
으로 파싱이 된다면 유효한 것으로 판단
@Component
public class JwtUtil {
...
public boolean isTokenValid(String token){
try{
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
}catch (JwtException e){
e.printStackTrace();
}
return false;
}
}
JwtFilter.java
OncePerRequestFilter
를 확장하는 JwtFilter
클래스를 만들어보자
JwtFilter는 request에서 파싱한 토큰을 검증하고, Spring Security에 검증된 유저에 대한 정보를 전달함
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = parseJwt(request);
if(token != null && jwtUtil.isTokenValid(token)){
String username = jwtUtil.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); //UsernamePasswordAuthenticationToken을 생성하는 부분
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request){
String headerAuth = request.getHeader("Authorization");
if(StringUtils.hasText(headerAuth)&&headerAuth.startsWith("Bearer")){
return headerAuth.substring(7);
}
return null;
}
}
parseJwt(request)
:
request로 부터 Jwt 토큰을 추출함
jwtUtil.getUsername(token);
:
토큰이 유효하면 토큰으로부터 Subject인 username을 가져와~
SecurityContextHolder.*getContext*().setAuthentication(authenticationToken)
:
해당 유저 정보로 UsernamePasswordAuthenticationToken
을 만들어 SecurityContextHolder
에 인증 정보 전달
💬 Spring Security는 Authentication과 Authorization에 대한 부분을 Filter로서 처리함
Spring Security는 같은 스레드의 앱 내에서 어디서든 SecurityContextHolder
의 인증 정보를 확인할 수 있도록 구현됨
SecurityContextHolder
는 SecurityContext
를 갖고 있다
SecurityContext
를 만들어 SecurityContextHolder
에 인증 정보를 전달하는 예제
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
💡 용어 정리
SecurityContextHolder
- Spring Security가 인증된 클라이언트의 세부 정보를 보관하는 곳SecurityContextHolder
is where Spring Security stores the details of who is authenticated.SecurityContext
- 현재 인증된 클라이언트의 Authentication을 가지고 있는 객체SecurityContextHolder
and contains the Authentication
of the currently authenticated user.Authentication
- 클라이언트에 대한 일종의 자격 증명AuthenticationManager
to provide the credentials a user has provided to authenticate or the current user from the SecurityContext
.Granted Authority
- 인증된 클라이언트에게 부여된 권한Authentication
(i.e. roles, scopes, etc.)📄 Servlet Authentication Architecture
UserDetailsImpl.java
UserDetails
는 Spring Security에서 사용자의 정보를 담는 객체임
아래와 같이 UserDetails
의 구현체를 만듦
public class UserDetailsImpl implements UserDetails {
private User user;
private Collection<? extends GrantedAuthority> authorities;
public static UserDetailsImpl from(User user){
SimpleGrantedAuthority simpleGrantedAuthority =
new SimpleGrantedAuthority(user.getRole().toString());
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(simpleGrantedAuthority);
UserDetailsImpl userDetails = new UserDetailsImpl();
userDetails.user = user;
userDetails.authorities = collection;
return userDetails;
}
public User getUser(){
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
...
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(()->new UsernameNotFoundException("user not found"));
return UserDetailsImpl.from(user);
}
}
UserDetailsServiceImpl
는 username 을 받아 유저 DB에서 검색후 UserDetailsImpl
객체를 생성함
WebSecurityConfig.java
다 만든 JwtFilter
를 WebSecurityConfig
에 등록
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Autowired
private JwtAuthEntryPoint jwtAuthEntryPoint;
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.cors().disable();
httpSecurity.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest()
.authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}
JwtAuthEntryPoint.java
인증되지 않은 사용자가 secured 리소스를 요청할 때 트리거됨
@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
SC_UNAUTHORIZED
401 에러를 보낸다
User 회원가입 / 로그인 로직을 만들어서 적용해보자
@RestController
public class UserController {
@Autowired
private final UserService userService;
@Autowired
JwtUtil jwtUtil;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/auth/signup")
public ResponseEntity registerUser(@RequestBody SignupForm form){
return ResponseEntity.ok(userService.registerUser(form));
}
@PostMapping("/auth/signin")
public ResponseEntity loginUser(@RequestBody SigninForm form){
return ResponseEntity.ok(userService.loginUser(form));
}
}
@Service
public class UserService {
@Autowired
private final UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtil jwtUtil;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User registerUser(SignupForm form) {
Optional<User> found = userRepository.findByUsername(form.getUsername());
if(found.isPresent()){
throw new IllegalArgumentException("중복중복");
}
UserRoleEnum role = UserRoleEnum.USER;
User user = User.from(form, passwordEncoder.encode(form.getPassword()), role);
return userRepository.save(user);
}
public TokenDto loginUser(SigninForm form) {
User user = userRepository.findByUsername(form.getUsername())
.orElseThrow(()->new IllegalArgumentException("유저가 존재하지 않음"));
//유저가 존재하면
if(passwordEncoder.matches(form.getPassword(), user.getPassword())){ //패스워드 확인 후 맞으면
//토큰 발급
return jwtUtil.generateToken(user.getUsername());
}
throw new IllegalArgumentException("패스워드가 다름");
}
}
기타 사용된 코드
@Getter
@Entity(name = "TUSER")
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
public static User from(SignupForm form, String password, UserRoleEnum role){
User user = new User();
user.username = form.getUsername();
user.password = password;
user.email = form.getEmail();
user.role = role;
return user;
}
}
@Getter
@Setter
public class SigninForm {
private String username;
private String password;
}
@Getter
@Setter
public class SignupForm {
private String username;
private String password;
private String email;
private String adminToken= "";
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
public enum UserRoleEnum {
USER,
ADMIN
;
}
@PostMapping("/auth/signup")
@PostMapping("/auth/signin")
기존에 만들어둔 /auth/**
가 아닌 URL로 리퀘스트 보내봄
응답이 잘 오는 것 = 토큰 인증이 잘 된 걸 확인
SecurityContextHolder
에 저장된 Authentication 정보를 활용해 현재 로그인한(=인증된 토큰으로 요청한) 유저 정보를 가져와보자!
@RestController
public class UserController {
@Autowired
private final UserService userService;
@Autowired
JwtUtil jwtUtil;
public UserController(UserService userService) {
this.userService = userService;
}
...
@GetMapping("/user/search")
public void searchUser(@AuthenticationPrincipal UserDetailsImpl userDetails){
System.out.println("username: " +userDetails.getUsername());
System.out.println("pwd: " +userDetails.getPassword());
}
}
@AuthenticationPrincipal UserDetailsImpl
로 간단하게 활용이 가능함
현재 로그인된 유저 정보가 잘 뜨는 것을 확인