[Spring Security] Jwt Token 사용

2023. 10. 17. 15:58·Web & Android/Spring Security
목차
  1. SourceCode & Explanation
  2. AuthenticationConfig
  3. JwtFilter
  4. UserController
  5. HomeController
  6. ReviewController
  7. LoginRequest
  8. UserService
  9. JwtUtil

Jwt Token 로그인

💡 Use Case Specification (명세서)

  1. 우선 login, join을 제외한 페이지를 전부 막는다.
  2. 사용자가 login하면 id, pw를 검증하고 Token을 생성하여 발급한다.
  3. 발급 받은 Token 권한에 따라 해당 페이지를 접근할 수 있다.
💡 [build.gradle]
implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'javax.xml.bind:jaxb-api:2.3.0' implementation 'org.springframework.boot:spring-boot-starter-security'

SourceCode & Explanation

configuration

AuthenticationConfig

  • Security 사용을 선언 및 설정
  • 모든 요청을 받아 필터링하는 securityFilterChain을 세팅하여 등록함
import com.example.walkingmate_back.login.service.LoginService;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
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;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@Configuration // 설정파일 선언
@EnableWebSecurity // security사용 선언
@RequiredArgsConstructor
public class AuthenticationConfig {
// @EnableWebSecurity를 선언함으로 써 모든 api 요청을 security가 관리하게 됨.
private final LoginService loginService;
@Value("${jwt.secret}")
private String secretKey;
// api 요청이 들어오면 검사하는 security의 FilterChain설정.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request
.requestMatchers("/api/login", "/api/join", "/buyHistory/**", "/board/**", "/user/**", "/battle/**", "/checkList/**", "/run/**", "/team/**").permitAll() // 인증 필요없음
.requestMatchers(HttpMethod.POST, "/api/**").authenticated() // 인증 있어야함
)
// .requestMatchers(HttpMethod.POST, "/api/v1/home/user").hasRole("USER") // USER 권한 있어야함
// .requestMatchers(HttpMethod.POST, "/api/v1/home/admin").hasRole("ADMIN") // ADMIN 권한 있어야함
.sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtFilter(loginService, secretKey), UsernamePasswordAuthenticationFilter.class); // FilterChain 앞에 JwtFilter 추가
return httpSecurity.build();
}
}

JwtFilter

  • securityFilterChain 앞에서 처리하는 Custom Filter
  • Token 확인, 접근제한, 권한부여, Detail추가 등의 선처리 역할
package com.example.test.configuration;
import com.example.test.service.UserService;
import com.example.test.utils.JwtUtil;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private final UserService userService;
private final String secretKey;
// 인증받기 위한 내부Filter - 여기를 통해야 들어갈 수 있다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// request에서 토큰 추출
final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("authorization : {}", authorization);
// Token이 없다면 그냥 반환시킴
if(authorization == null){
log.error("[접근불가] authorization이 없습니다.");
filterChain.doFilter(request, response);
return;
}
// Token 꺼내기
String token = authorization;
// Token Expired 되었는지 여부
if(JwtUtil.isExpired(token, secretKey)){
log.error("token이 만료 되었습니다.");
filterChain.doFilter(request, response);
return;
}
// UserName Token에서 꺼내기
String userName = JwtUtil.getUserName(token, secretKey);
log.info("userName:{}", userName);
// 권한 부여
UsernamePasswordAuthenticationToken authenticationToken;
if(userName.equals("admin")) {
// id가 admin이면 관리가(ADMIN)권한 부여
authenticationToken = new UsernamePasswordAuthenticationToken
(userName, null, List.of(new SimpleGrantedAuthority("ADMIN")));
}else {
// 아니라면 일반 사용자(USER)권한 부여
authenticationToken = new UsernamePasswordAuthenticationToken
(userName, null, List.of(new SimpleGrantedAuthority("USER")));
}
log.info("Role : {}", authenticationToken.getAuthorities());
// Detail을 넣어줍니다.
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
import com.example.walkingmate_back.login.service.LoginService;
import com.example.walkingmate_back.login.utils.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private final LoginService loginService;
private final String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("authorization : {}", authorization);
if(authorization == null){
log.error("[접근불가] authorization이 없습니다.");
filterChain.doFilter(request, response);
return;
}
String token = authorization;
// TODO
//
// if(JwtUtil.isExpired(token, secretKey)){
// log.error("token이 만료 되었습니다.");
// filterChain.doFilter(request, response);
// return;
// }
// Token에서 UserName추출
String userName = JwtUtil.getUserName(token, secretKey);
log.info("userName:{}", userName);
UsernamePasswordAuthenticationToken authenticationToken;
if(userName.equals("admin")) {
// 관리자(ADMIN)권한 부여
authenticationToken = new UsernamePasswordAuthenticationToken
(userName, null, List.of(new SimpleGrantedAuthority("ADMIN")));
}else {
// 일반 사용자(USER)권한 부여
authenticationToken = new UsernamePasswordAuthenticationToken
(userName, null, List.of(new SimpleGrantedAuthority("USER")));
}
log.info("Role : {}", authenticationToken.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}

controller

UserController

  • login 페이지 mapping, 요청 값 리턴
package com.example.test.controller;
import com.example.test.domain.LoginRequest;
import com.example.test.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// login 버튼 클릭 시 id, pw를 받으며 호출됨.
// dto로 (id,pw)값을 태워서 service의 login메서드 호출
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest dto) {
return ResponseEntity.ok().body(userService.login(dto.getUserName(), dto.getPassword()));
}
@PostMapping("/join")
public ResponseEntity<String> join() {
return ResponseEntity.ok().body("회원가입 완료");
}
}

HomeController

  • user, admin 페이지 mapping, 요청 값 리턴
  • USER, ADMIN 페이지 권한 설정
package com.example.test.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/home")
public class HomeController {
@PostMapping("/user")
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public String userPage() {
return "userPage";
}
// ADMIN 권한을 가져야 접근 가능
@PostMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String adminPage() {
return "adminPage";
}
}

ReviewController

  • reviews 페이지 mapping, 요청 값 리턴
package com.example.test.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/reviews")
public class ReviewController {
@PostMapping
public ResponseEntity<String> writeReview(Authentication authentication) {
return ResponseEntity.ok().body(authentication.getName() + "님의 리뷰 등록이 완료되었습니다.");
}
}

domain

LoginRequest

  • login 매핑 시 Request값을 받는 dto
package com.example.test.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class LoginRequest {
private String userName;
private String password;
}

 


service

UserService

  • UserController에서 service작업을 처리하기 위해 호출하는 클래스
  • login메서드로 값을 전달받아 JwtUtil을 호출하여 Token을 생성해옴
import com.example.walkingmate_back.login.domain.JoinRequest;
import com.example.walkingmate_back.login.domain.JoinResponseDTO;
import com.example.walkingmate_back.login.domain.LoginRequest;
import com.example.walkingmate_back.login.domain.LoginResponse;
import com.example.walkingmate_back.login.utils.JwtUtil;
import com.example.walkingmate_back.user.entity.UserBody;
import com.example.walkingmate_back.user.entity.UserEntity;
import com.example.walkingmate_back.user.repository.UserBodyRepository;
import com.example.walkingmate_back.user.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
/**
* 로그인, 회원가입
* - 서비스 로직
*
* @version 1.00 / 2023.09.10
* @author 전우진, 이인범
*/
@Service
@Slf4j
@Transactional
public class LoginService {
@Value("${jwt.secret}")
private String secretKey;
private Long expiredMs = 1000 * 60 * 60l;
private final UserRepository userRepository;
private final UserBodyRepository userBodyRepository;
private LoginResponse loginResponse;
private JoinResponseDTO joinResponseDTO;
@Autowired
public LoginService(UserRepository userRepository, UserBodyRepository userBodyRepository) {
this.userRepository = userRepository;
this.userBodyRepository = userBodyRepository;
}
/**
* 회원가입
* - 전우진, 이인범 2023.09.10
*/
public JoinResponseDTO join(JoinRequest joinRequest) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
// 문자열 -> LocalDate
LocalDate date = LocalDate.parse(joinRequest.getBirth(), formatter);
joinResponseDTO = new JoinResponseDTO();
if(userRepository.existsById(joinRequest.getId()) == false) {
userRepository.save(UserEntity.builder()
.id(joinRequest.getId())
.pw(joinRequest.getPw())
.name(joinRequest.getName())
.phone(joinRequest.getPhone())
.birth(date)
.build());
// 신체정보 저장
UserBody userBody = new UserBody();
userBody.setUserId(joinRequest.getId());
userBody.setHeight(joinRequest.getHeight());
userBody.setWeight(joinRequest.getWeight());
userBodyRepository.save(userBody);
joinResponseDTO.data.code = joinResponseDTO.success;
joinResponseDTO.data.message = "회원가입 성공";
return joinResponseDTO;
} else {
joinResponseDTO.data.code = joinResponseDTO.fail;
joinResponseDTO.data.message = "중복된 아이디";
return joinResponseDTO;
}
}
/**
* 로그인
* - 이인범
*/
public LoginResponse login(LoginRequest loginRequest) {
String userId = loginRequest.getUserId();
String password = loginRequest.getPassword();
log.info("userName:{}, password:{}", userId, password);
loginResponse = new LoginResponse();
// 인증 과정
Optional<UserEntity> user = userRepository.findById(userId);
if (user.isPresent()) {
loginResponse.data.userId = user.get().getId();
if((user.get().getPw()).equals(password)) {
loginResponse.data.jwt = JwtUtil.createJwt(userId, secretKey, expiredMs);
loginResponse.data.code = loginResponse.success;
loginResponse.data.message = "generate token";
return loginResponse;
}
loginResponse.data.message = "잘못된 비밀번호";
loginResponse.data.code = loginResponse.fail;
return loginResponse;
}
loginResponse.data.message = "존재하지 않는 사용자";
loginResponse.data.code = loginResponse.fail;
return loginResponse;
}
}

utils

JwtUtil

  • Jwt을 사용할때 필요한 부가 작업 메서드로 구현
  • Token 생성, 만료, 값 추출 등의 작업
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {
public static String getUserName(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
.getBody().get("userName", String.class);
}
// TODO
// Token의 만료 여부를 체크하는 isExpired 메서드
// Error: .parseClaimsJws(token)에서 token parsing 과정에서 Json타입 에러 발생.
public static boolean isExpired(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
.getBody().getExpiration().before(new Date());
}
public static String createJwt(String userName, String secretKey, Long expiredMs) {
Claims claims = Jwts.claims(); // username을 저장할 map?
claims.put("userName", userName);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}
총정리 : configuration에서 security세팅 및 적용하고, controller에서 service의 함수 호출하면, service가 utils에서 토큰 만들어와서 다시 controller로 return해줌.
저작자표시 비영리 변경금지 (새창열림)

'Web & Android > Spring Security' 카테고리의 다른 글

[Spring Security] 기본 동작 구조  (0) 2023.10.17
[Spring Security] Spring Security란?  (0) 2023.10.17
[Spring Security] Authentication의 메커니즘  (0) 2023.10.17
[Spring Security] Authentication의 구조  (0) 2023.10.17
[Spring Security] Security 사용  (1) 2023.10.17
  1. SourceCode & Explanation
  2. AuthenticationConfig
  3. JwtFilter
  4. UserController
  5. HomeController
  6. ReviewController
  7. LoginRequest
  8. UserService
  9. JwtUtil
'Web & Android/Spring Security' 카테고리의 다른 글
  • [Spring Security] Spring Security란?
  • [Spring Security] Authentication의 메커니즘
  • [Spring Security] Authentication의 구조
  • [Spring Security] Security 사용
woojin._.
woojin._.
여러가지 개발을 해보며 발생하는 이야기들에 대한 블로그입니다:)
  • woojin._.
    Jin's Dev Story
    woojin._.
  • 전체
    오늘
    어제
    • 분류 전체보기 (794)
      • Tools (25)
        • eGovFrame (3)
        • GeoServer (3)
        • QGIS (2)
        • LabelImg (2)
        • Git (6)
        • GitHub (1)
        • Eclipse (7)
        • Visual Studio (1)
      • Web & Android (121)
        • SpringBoot (37)
        • Three.js (2)
        • Spring Data JPA (9)
        • 스프링 부트 쇼핑몰 프로젝트 with JPA (25)
        • Thymeleaf (4)
        • Spring Security (15)
        • Flutter (29)
      • Programming Language (61)
        • JAVA (27)
        • JavaScript (14)
        • Dart (2)
        • Python (15)
        • PHP (3)
      • Database (43)
        • PostgreSQL (32)
        • MYSQL (7)
        • Oracle (3)
        • MSSQL (1)
      • SERVER (17)
        • TCP_IP (3)
        • 리눅스 (7)
        • AWS (7)
      • Coding Test (410)
        • 백준[JAVA] (76)
        • 프로그래머스[JAVA] (257)
        • 알고리즘 고득점 Kit[JAVA] (3)
        • SQL 고득점 Kit[ORACLE] (74)
      • CS 지식 (49)
        • [자료구조] (14)
        • [네트워크] (12)
        • [데이터베이스] (10)
        • [알고리즘] (9)
        • [운영체제] (4)
      • 기타 (6)
      • 자격증 & 공부 (62)
        • 정보처리기사 (2)
        • SQLD (6)
        • 네트워크관리사 2급 (5)
        • 리눅스마스터 1급 (44)
        • 리눅스마스터 2급 (1)
        • ISTQB (3)
        • 시스템보안 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 인기 글

  • 태그

    JPA
    Java
    스프링 부트 쇼핑몰 프로젝트 with JPA
    baekjoon
    springboot
    Oracle
    리눅스
    프로그래머스
    Flutter
    스프링부트
    리눅스마스터 1급
    DB
    backjoon
    programmers
    spring
    postgresql
    플러터
    데이터
    자바
    시큐리티
    백준
    Spring Security
    Linux
    python
    CS지식
    pcce 기출문제
    CS
    스프링
    데이터베이스
    리눅스마스터
  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
woojin._.
[Spring Security] Jwt Token 사용

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.