μ΄λ²μ ν μ΄ νλ‘μ νΈλ‘ λ°±μ μμ½ μΉ μ ν리μΌμ΄μ
μ μ μ μ€μ μκ³ μ΄λ―Έ μΈμ¦ λΆλΆμ ꡬνλ μνμ
λλ€. μΈμ
μ μ΄μ©νλ Stateful
μνλ‘ κ΅¬νλμμ΅λλ€.
μμ½ μ ν리μΌμ΄μ
μ κ²½μ° νΉμ λ μ§, νΉμ μκ°μ μ¬μ©μκ° λͺ°λ¦¬λ κ²½μ°κ° λ§μ΅λλ€. λλ¬Έμ scale-out
μ΄ μ©μ΄ν΄μΌ ν©λλ€. νμ§λ§ μΈμ
λ°©μμ μ¬μ©νλ κ²½μ° νμ₯μ μ ν μ©μ΄νμ§ μμ΅λλ€.
νλ‘μ νΈ μμ± ν νμΈμ
(?)μ μλνκΈ° μν΄ ν ν°λ°©μ μΈμ¦μ κ°μ₯ λνμ νμ€μΈ JWT
λ₯Ό μ΄μ©ν΄ μΈμ¦μ ꡬνν΄λ³΄κ³ μ ν©λλ€.
Spring Security
μ μ€μ μ 컀μ€ν
νκΈ° μν μ€μ νμΌμ μμ±ν©λλ€.
/auth
κ²½λ‘λ‘ μΈμ¦μμ²μ ν κ²μ΄λ―λ‘ μ κ·Όμ νκ°νκ³ κ·Έ μΈ λͺ¨λ μμ²μ μΈμ¦μ΄ νμνλλ‘ νμ΅λλ€.
sessionCreationPolicy
STATELESS
λ‘ μ€μ ν΄μ£Όλ―λ‘ μΈμ
μ μ¬μ©νμ§ μλλ‘ μ€μ
λ€μμΌλ‘ μΈμ¦κ°μ²΄λ₯Ό λ§λ€μ΄μ€ UserDetailsService
λ₯Ό μ μν©λλ€.
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService customUserDetailsService;
private final JwtAuthenticateFilter jwtAuthenticateFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/auth").permitAll()
.anyRequest().authenticated();
// Stateless (μΈμ
μ¬μ©X)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// UsernamePasswordAuthenticationFilter μ λλ¬νκΈ° μ μ 컀μ€ν
ν νν°λ₯Ό λ¨Όμ λμμν΄
http.addFilterBefore(jwtAuthenticateFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
λ³΄ν΅ μ΄ λΆλΆμμλ DBμμ username
μΌλ‘ User
λ₯Ό μ‘°νν΄μΌ νλλ° ν
μ€νΈλ‘ κ°λ¨νκ² μ§μ User
κ°μ²΄λ₯Ό λ§λ€μμ΅λλ€.
μ¬κΈ°μ User
κ°μ²΄λ μν°ν°κ° μλλλ€ !!
import org.springframework.security.core.userdetails.User;
userdetails
μ User
ν΄λμ€μ
λλ€.
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final PasswordEncoder encoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User("kim", encoder.encode("1234"), AuthorityUtils.createAuthorityList());
}
}
ꡬνν κΈ°λ₯μ μλ 6κ° μ λλ€.
generate
- create
)isValidToken
)getAllClaims
)μ¬κΈ°μ Claim
μ JWT
μ νμ΄λ‘λ λΆλΆμ ν΄λΉν©λλ€.
Map
μ μμνκ³ μκΈ° λλ¬Έμ νΈνκ² key-value
λ‘ λ£μ΄μ£Όλ©΄ λ©λλ€.
νμ¬λ username
λ§ Claim
μΌλ‘ μ€μ νμ΅λλ€.
Claim
μ κ°μ΄ λ€μ΄κ° λ Base64
λ‘ μΈμ½λ© λ©λλ€. Base64
λ 곡κ°λ μΈμ½λ© κΈ°λ²μ΄κΈ° λλ¬Έμ λμ½λ© λν κ°λ₯ν©λλ€. λλ¬Έμ Claim
μλ critialν μ 보λ λ΄μ§ μλλ‘ μ£Όμν©λλ€.
JWT
λ 3κ° λΆλΆμΌλ‘ λλ μ§λλ€.
@Slf4j
@Component
public class JwtUtil {
// μ€μ νμΌλ‘ λΉΌμ νκ²½λ³μλ‘ μ¬μ©νλ κ²μ κΆμ₯
private final String SECRET = "secret";
/**
* ν ν° μμ±
*/
public String generateToken(UserDetails userDetails) {
Claims claims = Jwts.claims();
claims.put("username", userDetails.getUsername());
return createToken(claims, userDetails.getUsername()); // usernameμ subjectλ‘ ν΄μ token μμ±
}
private String createToken(Claims claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 1μκ°
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
/**
* ν ν° μ ν¨μ¬λΆ νμΈ
*/
public Boolean isValidToken(String token, UserDetails userDetails) {
log.info("isValidToken token = {}", token);
String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
/**
* ν ν°μ Claim λμ½λ©
*/
private Claims getAllClaims(String token) {
log.info("getAllClaims token = {}", token);
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
/**
* Claim μμ username κ°μ Έμ€κΈ°
*/
public String getUsernameFromToken(String token) {
String username = String.valueOf(getAllClaims(token).get("username"));
log.info("getUsernameFormToken subject = {}", username);
return username;
}
/**
* ν ν° λ§λ£κΈ°ν κ°μ Έμ€κΈ°
*/
public Date getExpirationDate(String token) {
Claims claims = getAllClaims(token);
return claims.getExpiration();
}
/**
* ν ν°μ΄ λ§λ£λμλμ§
*/
private boolean isTokenExpired(String token) {
return getExpirationDate(token).before(new Date());
}
}
@Slf4j
@RequiredArgsConstructor
@RestController
public class HomeController {
private final CustomUserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
private final AuthenticationManager authenticationManager; // authenticate λ©μλ : username, password κΈ°λ° μΈμ¦ μν
@GetMapping("/home")
public String home() {
return "home";
}
@PostMapping("/auth")
public ResponseEntity<LoginSuccessResponse> authenticateTest(@RequestBody LoginRequest loginRequest) {
log.info("/auth νΈμΆ");
try {
// username, password μΈμ¦ μλ
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
} catch (BadCredentialsException e) {
throw new BadCredentialsException("λ‘κ·ΈμΈ μ€ν¨");
}
// μΈμ¦ μ±κ³΅ ν μΈμ¦λ userμ μ 보λ₯Ό κ°κ³ μ΄
UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.username);
// subject, claim λͺ¨λ UserDetailsλ₯Ό μ¬μ©νλ―λ‘ κ°μ²΄λ₯Ό κ·Έλλ‘ μ λ¬
String token = jwtUtil.generateToken(userDetails);
// μμ±λ ν ν°μ μλ΅ (Test)
return ResponseEntity.ok(new LoginSuccessResponse(token));
}
// μΈμ¦μμ² κ°μ²΄
@AllArgsConstructor
@Data
static class LoginRequest{
private String username;
private String password;
}
// μΈμ¦μμ²μ λν μλ΅ κ°μ²΄
@AllArgsConstructor
@Data
static class LoginSuccessResponse {
private String token;
}
}
/auth
λ‘ username=kim
, password=1234
μμ²
ν ν°μ΄ μ μμ μΌλ‘ λ°κΈλμμ΅λλ€.
μ΄μ ν΄λΌμ΄μΈνΈκ° λ°κΈλ ν ν°μ λ€κ³ μΈμ¦μμ²μ νμ λ μλ²μΈ‘ μ²λ¦¬λ₯Ό ꡬνν΄λ³Όκ»μ.
μ΄μ μΈμ¦μ νμλ‘ νλ κ²½λ‘λ‘ μμ²μ΄ λ€μ΄μμ λ username
, password
λ₯Ό κΈ°λ°μΌλ‘ μΈμ¦νλ λ°©μμ΄ μλκ³ ν΄λΌμ΄μΈνΈκ° λ€κ³ μ¨ ν ν°
μ μ ν¨μ¬λΆλ‘ νλ¨ν΄μΌ ν©λλ€.
Spring security
λ μ¬λ¬ κ° νν°κ° λ±λ‘λμ΄ μλλ° κ·Έ μ€ νλμΈ UsernamePasswordAuthenticationFilter
κ° μ λ¬λ username
κ³Ό password
λ‘ μΈμ¦μ μ§νν©λλ€.
κ·ΈλΌ μ°λ¦¬λ UsernamePasswordAuthenticationFilter
μμͺ½μ ν ν°μ κ²μ¦νλ νν°λ₯Ό λ£μ΄μ£Όλ©΄ λ©λλ€. μΌλ¨ νν°λΆν° λ§λ€μ΄λ³Όκ»μ.
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticateFilter extends OncePerRequestFilter {
private final CustomUserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization"); // ν€λ νμ±
String username = "", token = "";
if (authorization != null && authorization.startsWith("Bearer ")) { // Bearer ν ν° νμ±
token = authorization.substring(7); // jwt token νμ±
username = jwtUtil.getUsernameFromToken(token); // username μ»μ΄μ€κΈ°
} else {
filterChain.doFilter(request, response);
}
// νμ¬ SecurityContextHolder μ μΈμ¦κ°μ²΄κ° μλμ§ νμΈ
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// ν ν° μ ν¨μ¬λΆ νμΈ
log.info("JWT Filter token = {}", token);
log.info("JWT Filter userDetails = {}", userDetails.getUsername());
if (jwtUtil.isValidToken(token, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request,response);
}
}
νν° μ§ν νλ¦
1. μμ² ν€λμ Authorization
μ΄ μλκ° ?
2. Authorization
μ κ°μ΄ Bearer
ν ν° νμ
μΈκ°?
3. (1,2) λ§μ‘±μ μ λ¬λ ν ν°μ λ°μμ€κ³ subjectλ₯Ό νμ±
4. νμ¬ SecurityContext
μ μΈμ¦λ κ°μ²΄κ° μλκ°?
5. (4) λ§μ‘±μ UserDetails
get
6. ν ν°μ΄ μ ν¨νκ° ?
7. ν ν°μ΄ μ ν¨νλ€λ©΄ μΈμ¦κ°μ²΄λ₯Ό μμ±νμ¬ SecurityContext
μ μΈν
OncePerRequestFilter?
OncePerRequestFilter
λ μ΄λ¦ κ·Έλλ‘ ν λ²μ μμ² λΉ ν λ²λ§ μ€νμ 보μ₯νλ νν°μ
λλ€.
"κ°μ request
κ°μ²΄λ₯Ό μ¬μ©νλ ν μ΄ νν°λ₯Ό λ€μ νμ§ μλλ€" λΌκ³ μ΄ν΄ν΄λ λ κ² κ°λ€μ.
Bearer Token?
ν ν°μ νμ
μ€ νλλ‘ jwt
, oauth2
μμ Access tokenμΌλ‘ μ¬μ©λ©λλ€.
νν° μ μ©
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/auth").permitAll()
.anyRequest().authenticated();
// Stateless (μΈμ
μ¬μ©X)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// UsernamePasswordAuthenticationFilter μ λλ¬νκΈ° μ μ 컀μ€ν
ν νν°λ₯Ό λ¨Όμ λμμν΄
http.addFilterBefore(jwtAuthenticateFilter, UsernamePasswordAuthenticationFilter.class);
}
ν ν° λ°κΈ
λ°κΈλ ν ν°μΌλ‘ μΈμ¦ μμ² (/home μΌλ‘ μμ²)
μμ£Ό κ°λ¨νκ² JWTμ λν΄ μμ보μμ΅λλ€.
λ€μμλ DB, JPA, MVC λ±μ μΆκ°ν΄μ λ³΄λ€ μ€λ¬΄μ μΈ μμ λ₯Ό μκ°ν΄λ³΄κ² μ΅λλ€.
κ°μ¬ν©λλ€. π
μ’μκΈ κ°μ¬ν©λλ€!