Jwt Token 로그인
💡 Use Case Specification (명세서)
- 우선 login, join을 제외한 페이지를 전부 막는다.
- 사용자가 login하면 id, pw를 검증하고 Token을 생성하여 발급한다.
- 발급 받은 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'
configuration
- 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 |
| @RequiredArgsConstructor |
| public class AuthenticationConfig { |
| |
| |
| private final LoginService loginService; |
| |
| @Value("${jwt.secret}") |
| private String secretKey; |
| |
| |
| @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() |
| ) |
| |
| |
| .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) |
| .addFilterBefore(new JwtFilter(loginService, secretKey), UsernamePasswordAuthenticationFilter.class); |
| |
| return httpSecurity.build(); |
| } |
| } |
- 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; |
| |
| @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; |
| |
| if(JwtUtil.isExpired(token, secretKey)){ |
| log.error("token이 만료 되었습니다."); |
| filterChain.doFilter(request, response); |
| return; |
| } |
| |
| String userName = JwtUtil.getUserName(token, secretKey); |
| log.info("userName:{}", userName); |
| |
| UsernamePasswordAuthenticationToken authenticationToken; |
| if(userName.equals("admin")) { |
| |
| authenticationToken = new UsernamePasswordAuthenticationToken |
| (userName, null, List.of(new SimpleGrantedAuthority("ADMIN"))); |
| }else { |
| |
| 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); |
| } |
| } |
| 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; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| String userName = JwtUtil.getUserName(token, secretKey); |
| log.info("userName:{}", userName); |
| |
| UsernamePasswordAuthenticationToken authenticationToken; |
| if(userName.equals("admin")) { |
| |
| authenticationToken = new UsernamePasswordAuthenticationToken |
| (userName, null, List.of(new SimpleGrantedAuthority("ADMIN"))); |
| }else { |
| |
| 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
- 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; |
| |
| |
| @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("회원가입 완료"); |
| } |
| } |
- 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"; |
| } |
| |
| @PostMapping("/admin") |
| @PreAuthorize("hasRole('ROLE_ADMIN')") |
| public String adminPage() { |
| return "adminPage"; |
| } |
| } |
- 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
- 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
- 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; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| @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; |
| } |
| |
| |
| |
| |
| |
| public JoinResponseDTO join(JoinRequest joinRequest) { |
| DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); |
| |
| |
| 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
- 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); |
| } |
| |
| |
| |
| |
| 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(); |
| 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해줌.