Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 |
Tags
- Java
- 자바스크립트
- 시큐리티
- Oracle
- springboot
- DB
- postgresql
- 리눅스
- baekjoon
- javascript
- 백준
- CS지식
- 프로그래머스
- 데이터베이스
- Flutter
- python
- 데이터
- 자료구조
- 스프링부트
- 플러터
- spring
- 네트워크
- 스프링 부트 쇼핑몰 프로젝트 with JPA
- 자바
- 파이썬
- CS
- Spring Security
- backjoon
- JPA
- 스프링
Archives
- Today
- Total
Jin's Dev Story
[Spring Security] Jwt Token 사용 본문
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'
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 |