Spring Security 6 ๋ฒ์ ๊ธฐ์ค
์ต๊ทผ Spring Security ๋ฅผ ์ด์ฉํ JWT๊ธฐ๋ฐ ์ธ์ฆ ์๋ฒ๋ฅผ ๋ฆฌํฉํ ๋ง ํ๋ฉด์ ์ธ์ฆ/์ธ๊ฐ์ ๋ํ ๋ถ๋ถ์ ๋ค๋ค๋ณด์๋ค.
์ฌ๋ฌ ๋ ํฌ์ ๋ธ๋ก๊ทธ ๊ธ์ ๋ง์ด ๋ณด์์ง๋ง ๊ตฌํ ๋ฐฉ๋ฒ์ ๋ชจ๋ ๋ฌ๋๋ค.
๊ทธ๋์ Spring Security ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ฌ ๊ตฌํํด ๋ณด์๋ค.
๊ทธ ์ค ์ธ๊ฐ(Authorization) ๋ฐฉ๋ฒ์ ์ค์ ์ ์ผ๋ก ๋ค๋ค๋ณผ ๊ฒ์ด๋ค.
1. AuthUser
์ด ํด๋์ค๋ ์๋น์ค๋จ์์ ์ฌ์ฉํ ์ฌ์ฉ์์ uniqueํ ์๋ณ์๋ค์ ์ ๋ณด๋ฅผ ๋ฃ์ด ์ ๋ฌ๋๋ ์์ ๊ฐ์ฒด๋ก,
ํ์ฌ ์ธ๊ฐ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ด์ ์ฌ์ฉ๋ ๊ฒ์ด๋ค.
@Getter
@AllArgsConstructor
public class AuthUser {
private final Long id;
private final String email;
@JsonIgnore
private final String password;
private final List<Role> roles;
}
ํด๋์ค ์ด๋ฆ์ LoginUser, CurrentUser, CurrentAuthUser ๋ฑ๋ฑ.. ์ฌ์ฉํด๋ ๋๊ฒ ๋ค.
ํ๋ ๊ตฌ์ฑ ๋ํ ์๋น์ค ๋ก์ง์์ ์ฌ์ฉํ ์ ๋ณด๋ค์ ์ํ๋๋๋ก ๋ด์ผ๋ฉด ๋๊ฒ ๋ค.
๋จ, ์๋น์ค ๋ด์์ ์ฌ์ฉํ ์ฌ์ฉ์๋ฅผ ์๋ณํ ๊ฐ(id, email ๋ฑ) ์ ํ์๋ก ํฌํจํด์ผ ํ๋ค.
์ฌ๊ธฐ์ Role๋ ์๋ฒ์์ ์ฌ์ฉํ๋ enumํ์ ์ด๋ค.
public enum Role {
ROLE_USER, ROLE_ADMIN
}
์ธ์ฆ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ๋ Authority๋ก ๋ณํํ์ฌ ์ ๋ฌํ๊ณ , JWT์๋ ๋ฃ์ ์์ ์ด๋ค.
ํ ํฐ์ ์ด์ฉํ ์ธ์ฆ/์ธ๊ฐ์ ํน์ง์ ๋ฌด์ํ์ฑ(STATELESS)์ด๋ค.
ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ/์ธ๊ฐ ์๋ฒ๋ DB ์กฐํ๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๊ธฐ ๋๋ฌธ์,
์ธ๊ฐ ๊ณผ์ ์์ DB์์ ๊ฐ์ ธ์จ User๋ฅผ ์ง์ ์ฌ์ฉํ ์ ์๋ค.
๋ฐ๋ผ์ ์ธ๊ฐ์ฉ ์์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ ๊ฒ์ด๋ค.
์ธ๊ฐ ๊ณผ์ ๋ง๋ค DB๋ฅผ ๊ฑฐ์ณ ์ฌ์ฉ์ ๊ฐ์ฒด๋ฅผ ์ง์ ๊ฐ์ ธ์ฌ ์ ์๊ฒ ์ง๋ง, ๊ทธ๋ ๊ฒ ๋๋ฉด ํ ํฐ์ ์ฌ์ฉํ๋ ์๋ฏธ๊ฐ ์๋ค.
๋ํ, ํน์ ์๋น์ค ๋ก์ง์ ์ ์ธํ๊ณ User์ ๋ชจ๋ ์ ๋ณด๋ฅผ ์ฌ์ฉํ ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์ ๋งค๋ฒ ์ฌ์ฉ์๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ ๋น ํจ์จ์ ์ด๋ค.
2. CustomUserDetails
public class CustomUserDetails extends AuthUser implements UserDetails {
//์ธ๊ฐ์ฉ ๊ฐ์ฒด ์์ฑ์
public CustomUserDetails(Long id, String email, String password, List<Role> roles) {
super(id, email, password, roles);
}
//์ธ์ฆ์ฉ ๊ฐ์ฒด ์์ฑ์
public CustomUserDetails(User user) {
super(user.getId(), user.getEmail(), user.getPassword(), user.getRoles());
}
//AuthUser์์ List<Role>์ ๊ฐ์ ธ์์ ์ถ๊ฐ
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<GrantedAuthority>(getRoles().stream().map(Role::toString).map(SimpleGrantedAuthority::new).toList());
}
@Override
public String getUsername() {
return getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Spring Security์์ ์ฌ์ฉํ ์ธ์ฆ์ฉ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค.
ํด๋น ํด๋์ค๋ AuthUser๋ฅผ ์์ํ๊ณ Spring Security์ UserDetiails์ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ค.
์ฌ์ฉ์์ ๊ถํ๋ค์ ๊ฐ์ ธ์ค๋ getAuthorities()์์๋ enum์ ๋ด์ List๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ String์ผ๋ก ๋ณํ ํ SimplGrantedAuthority ๊ฐ์ฒด๋ก ๋ง๋ค์ด์ List๋ก ๋ฐํํ๋ค.
3. UserRepositoryUserDetailsService (CustomUserDetailsService)
@Service
@RequiredArgsConstructor
public class UserRepositoryUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
//Username์ email๋ก ์ฌ์ฉํจ
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<User> optionalUser = userRepository.findByEmail(email);
if (optionalUser.isPresent()) {
User user = optionalUser.get();
return new CustomUserDetails(user);
}
throw new UsernameNotFoundException("์ฌ์ฉ์๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.");
}
}
ํด๋น ํด๋์ค์์๋ ์ธ์ฆ(Authentication) ๊ณผ์ ์ค์ ์ฌ์ฉ๋ loadUserByUsername ๋ฉ์๋๋ฅผ ์ ์ํ๋ค.
์๋น์ค์์ ์ฌ์ฉํ๋ UserRepo๋ฅผ ์ฌ์ฉํ์ฌ ์ค์ User ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์ค๊ณ , CustomDetails ์ธ์ฆ์ฉ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ์ธ์ฆ์ ์งํํ๊ฒ ๋๋ค.
์ด์ ์ธ์ฆ/์ธ๊ฐ์ฉ ๊ฐ์ฒด ์ ์์ ๋ฉ์๋ ๊ตฌํ์ ์๋ฃํ๊ณ , ์ธ์ฆ/์ธ๊ฐ ๊ณผ์ ์ ์ดํด๋ณด๊ฒ ๋ค.
3. ํ ํฐ ๋ฐ๊ธ
ํ ํฐ์ io.jsonwebtoken ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค.
public String createToken(CustomUserDetails customUserDetails, Instant expiration) {
Instant issuedAt = Instant.now();
List<String> roles = customUserDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
return Jwts.builder()
.header()
.add("typ", "JWT") // JWT ํ์
.and()
.subject(customUserDetails.getUsername()) //email
.claim("id", customUserDetails.getId()) //id
.claim("roles", roles) //๊ถํ
.issuedAt(Date.from(issuedAt)) // ํ์ฌ ์๊ฐ
.expiration(Date.from(expiration)) //๋ง๋ฃ ์๊ฐ
.signWith(secretKey)
.compact();
}
์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ์ ๋ ๋ฐ๊ธํด์ค ํ ํฐ์ ์์ฑํ๋ค.
subject์๋ email์, claim์๋ id์ roles ์ ๋ฃ์ด์ฃผ์๋ค.
์ต์ข ์ ์ผ๋ก ํ ํฐ์ ์์ฑํ๋ฉด payload๋ ์๋์ฒ๋ผ ๋๋ค.
์ถํ์ ์ธ๊ฐ ๊ณผ์ ์์ id, email, role์ ๊ฐ์ ธ์ค๋ ํจ์๋ ๋ค์๊ณผ ๊ฐ๋ค.
//ID ์ถ์ถ
public Long getIdFromToken(String token) throws SignatureException {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("id", Long.class);
}
//email ์ถ์ถ
public String getEmailFromToken(String token) throws SignatureException {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
//Role ์ถ์ถ
public List<Role> getRolesFromToken(String token) throws SignatureException{
List<String> roles = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("roles", List.class);
return roles.stream().map(Role::valueOf).toList();
}
4. ์ธ๊ฐ
์ด์ ์ธ๊ฐ ๊ณผ์ ์ ์ดํด๋ณด๊ฒ ๋ค.
์๋๋ ์ธ๊ฐ์ฉ ํํฐ JwtAuthorizationFilter์ ์ผ๋ถ์ด๋ค.
private void authenticateAccessToken(String accessToken) {
//AccessToken ์ ํจ์ฑ ๊ฒ์ฆ
jwtUtil.validateToken(accessToken);
//CustomUserDetail ๊ฐ์ฒด ์์ฑ
CustomUserDetails userDetails = new CustomUserDetails(
jwtUtil.getId(accessToken),
jwtUtil.getEmail(accessToken),
null,
jwtUtil.getRoles(accessToken)
);
//Spring Security ์ธ์ฆ ํ ํฐ ์์ฑ
Authentication authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
//์ธ์ฆ ๊ฐ์ฒด ์ ์ฅ
SecurityContextHolder.getContext().setAuthentication(authToken);
}
์๋ ์์ฒญ์ผ๋ก๋ถํฐ access token์ ์ถ์ถํ์ฌ ๊ฒ์ฆํ๋ ๋ฉ์๋์ด๋ค.
ํ ํฐ์ ์ ํจ์ฑ์ ๊ฒ์ฌํ ๋ค, CustomUserDetails๋ฅผ ์์ฑ์๋ฅผ ํตํด ์์ฑํ ํ ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ์ ์ฅํ๋ค.
๋ง์ง๋ง์ SecurityContextHolder์ ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ์ ์ฅํด๋๋ฉด ์ธ๊ฐ ๊ณผ์ ์ด ์๋ฃ๋๋ค.
5. ์ธ๊ฐ๋ ๊ฐ์ฒด ์ ๋ฌ๋ฐ๊ธฐ
์ธ๊ฐ๋ ๊ฐ์ฒด๋ ์ด๋ ธํ ์ด์ ์ ํตํด ๋ฐ์ ๊ฒ์ด๋ค.
@AuthenticationPrincipal
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
์ด๋ ธํ ์ด์ ์ ์ ์ํ๊ณ , @AuthenticationPrincipal์ ๋ถ์ด๋ฉด SecurityContextHolder์ ๋ด๊ธด ์ธ์ฆ ๊ฐ์ฒด๊ฐ ์ ๋ฌ๋๊ฒ ๋๋ค.
@PutMapping("/name")
public ResponseEntity<?> updateName(@CurrentUser AuthUser authUser, @RequestParam String name) {
userService.updateName(authUser, name);
return new ResponseEntity<>(HttpStatus.OK);
}
์ด์ ์ธ๊ฐ๊ฐ ํ์ํ Controller์์ ๋ฏธ๋ฆฌ ์ ์ํด๋ ์ด๋ ธํ ์ด์ ์ ํตํด AuthUser๋ฅผ ์ ๋ฌ๋ฐ์ผ๋ฉด ๋๋ค.
์ฌ๊ธฐ์ ๋์ฌ๊ฒจ ๋ณผ ๊ฒ์ด ์๋๋ฐ,
์ธ๊ฐ ๊ณผ์ ์์๋ SecurityContextHolder์ CustomUserDetails๋ฅผ ๋ฃ์์ง๋ง @CurrentUser๋ AuthUser๋ฅผ ๋ฐ๋๋ค.
public class CustomUserDetails extends AuthUser implements UserDetails {
CustomUserDetails๋ AuthUser๋ฅผ ์์๋ฐ๊ณ ์๊ธฐ ๋๋ฌธ์ AuthUser๋ก ์นํ์ด ๊ฐ๋ฅํ๋ค. (๋ฆฌ์ค์ฝํ ์นํ ์์น)
6. ์ฃผ์ํ ์
์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ AuthUser๋ ์ธ๊ฐ๋ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ ์์ ๊ฐ์ฒด์ผ ๋ฟ, ์ค์ ์ฌ์ฉ์๊ฐ ์๋๋ค.
์๋ ์์์ฒ๋ผ AuthUser์ ํ๋๊ฐ๋ง ์ฌ์ฉํ๊ณ , ์ค์ ์ฌ์ฉ์๋ DB์กฐํ๋ฅผ ๊ฑฐ์ณ์ผํ๋ค.
@Transactional
public void updateName(AuthUser authUser, String name) {
User userByEmail = userRepository.findByEmail(authUser.getEmail()).orElseThrow(
() -> new CustomException(ErrorCode.USER_NOT_FOUND)
);
userByEmail.setName(name);
}
@Transactional
public void updateName(AuthUser authUser, String name) {
User userById = userRepository.findById(authUser.getId()).orElseThrow(
() -> new CustomException(ErrorCode.USER_NOT_FOUND)
);
userById.setName(name);
}
@Transactional
public void updateName(AuthUser authUser, String name) {
List<Role> roles = authUser.getRoles();
if (roles.contains(Role.ROLE_USER)) {
log.info("์ผ๋ฐ ์ฌ์ฉ์ ๊ฐ์ง");
}
//...
}