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 |