
// SecurityFilterChain
http.requestMatchers("/user/**")
// .hasAnyRole("MEMBER", "MANAGER", "ADMIN")
.hasAnyAuthority("MEMBER", "MANAGER", "ADMIN");
// UserDetailsService
manager.createUser(
User.withUsername("user")
.password(encoded)
// .roles("ADMIN")
.authorities("ADMIN")
.build()
);
hasAnyRole() : 가변인자로 권한을 받는다. 인자 중 하나의 권한만 가져도 URL에 접근할 수 있다. Role로 권한을 주면 내부에서 ROLE_을 붙여서 처리한다.hasAnyAuthority() : 가변인자로 권한을 받는다..roles() : 역할을 지정해줄 수 있다. ROLE_이 붙은채로 만들어진다..authorities() : 역할을 지정해줄 수 있다. .authorities("ADMIN")으로 권한을 설정해주고, requestMatchers에서 hasAnyRole("ADMIN") 으로 설정해두었다고 가정해보자.
둘 다 ADMIN으로 권한을 부여했기 때문에 접근할 수 있을 것을 예상하지만, 접근이 되지 않아 403 에러가 뜬다. -> 내부에서 ROLE_ADMIN만 접근할 수 있도록 해뒀기 때문에 접근할 수 없는 것이다.
@PostMapping("/signup")
public String doSignUp(SignUpForm signUpForm) {
log.info("signUpForm = {}", signUpForm);
memberService.save(signUpForm);
return "redirect:/";
}
Member 그 자체로 둘 수도 있지만, 컨트롤러에서 엔티티 자체를 사용하는 것은 정보 보호를 위해 권장하지 않는다. -> 그래서 DTO를 만들어서 전달한다. redirect:/" 로 로그인 후 index 페이지로 리다이렉트 되도록 한다.@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final PasswordEncoder passwordEncoder;
private final MemberRepository memberRepository;
public void save(SignUpForm signUpForm) {
Member member = Member.builder()
.username(signUpForm.getUsername())
.password(passwordEncoder.encode(signUpForm.getPassword()))
.email(signUpForm.getEmail())
.build();
memberRepository.save(member);
}
SecurityConfig에서 @Bean으로 등록한 PasswordEncoder를 주입 받는다. /login에서 로그인을 시도해도 로그인은 되지 않는다 ! ❌🔗 UserDetailsService를 구현해주지 않았고, 로그인 시 Spring Security가 사용자 정보를 조회할 수 없어 인증에 실패하게 된다. 따라서 로그인을 가능하게 하려면 loadUserByUsername()을 구현해서 UserDetails 객체를 반환하도록 해야 한다 !
public class MemberDetails implements UserDetails {
private final String username;
private final String password;
private final String role;
public MemberDetails(Member member) {
this.username = member.getUsername();
this.password = member.getPassword();
this.role = member.getRole();
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(this.role));
}
MemberDetails라는 DTO를 만들어서 UserDetails를 구현해준다.Getter 메서드를 오버라이딩해야 한다. Member를 받고 그 멤버의 유저네임, 비밀번호, 권한을 넣어준다. new SimpleGrantedAuthority() : GrantedAuthority를 구현한 구현체로서, 하나의 Role을 설정해준다. public class MemberService implements UserDetailsService {
// 중간 코드 생략
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> memberOptional = memberRepository.findByUsername(username);
Member findMember = memberOptional.orElseThrow(
() -> new UsernameNotFoundException("Username " + username + " not found")
);
return new MemberDetails(findMember);
}
}
MemberService가 UserDetailsService를 구현하도록 한다. -> loadUserByUsername을 오버라이딩 해야 한다.loadUserByUsername은 UserDetails를 반환해야 한다. (사용자 이름, 비밀번호, 권한을 포함)MemberRepository에서 findByUsername 메서드를 추가해주고, 이 메서드를 이용해 입력받은 아이디(사용자명)의 유저를 DB에서 찾는다.loadUserByUsername()은 사용자 정보 조회만 해준다고 이해하면 된다.loadUserByUsername()이 아니라 Spring Security 내부에서 자동으로 처리된다. 리턴된 UserDetails 객체에서 getPassword()로 비밀번호를 꺼내고, 사용자가 폼에 입력한 비밀번호와 비교한다.@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(formLogin -> formLogin.disable() )
.oauth2Login(Customizer.withDefaults()) //
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/login", "/sign-up")
.anonymous()
.requestMatchers("/user/**")
.hasAnyAuthority("USER")
.requestMatchers("/admin/**")
.hasAnyAuthority("ADMIN")
.anyRequest()
.authenticated();
})
.build();
}
.formLogin(formLogin-> formLogin.disable()) : 폼 로그인 방식을 사용하지 않는다고 설정한다.oauth2Login(Customizer.withDefaults()) : 인증 방식으로 OAuth 인증 방식을 설정하는 코드이다. 별도의 커스터마이징 없이 Spring Security가 제공하는 기본 설정을 사용하는 것이다.OAuth 방식을 사용하기 위해서는 Client를 Resource Server에 등록해줘야 한다.
원하는 플랫폼에서 Client를 등록하고, ClientID와 Client 보안 비밀번호 를 받아와야 한다. 이 과정에서 RedirectURL도 반드시 설정해줘야 한다.
Redirect URL ❓
인증이 완료된 후 사용자를 다시 어디로 보내줄지 알려주는 주소이다.
Authorization Server가 인증을 마친 뒤 사용자를 다시 돌려보낼 주소이다. 외부 플랫폼은 redirect_uri로 사용자를 돌려보내면서 Authorization Code를 쿼리 파라미터에 붙여서 전송한다. 그렇게 되면 우리 서버는 이code를 받아서 토큰을 얻는 다음 단계로 넘어가게 된다.🎯 redirect url 이 중요한 이유
Authorization Server는 이 URL이 사전에 등록된 값인지 확인하고, 다르면 요청 자체를 거부한다. 임의의 URL로 리다이렉트 못하게 막는 것이다.
security:
oauth2:
client:
registration:
google :
client-id: {발급받은 클라이언트 아이디}
client-secret: {발급받은 클라이언트 비밀 번호}
scope:
- email
- profile
scope : Resource Server에서 얻어오고 싶은 데이터를 설정해준다.
@Getter
public class MemberDetails implements OAuth2User {
private String name;
private Map<String, Object> attributes;
@Setter
private String role;
@Builder
public MemberDetails(String name, Map<String, Object> attributes, String role) {
this.name = name;
this.role = role;
this.attributes = attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(this.role));
}
}
OAuth2AuthenticatedPrincipal를 상속한 OAuth2User를 구현한다. getName(), getAttributes(), getAuthorities()를 오버라이딩 해줘야 한다. public class MemberService extends DefaultOAuth2UserService {
UserDetailsService를 구현했던 것처럼, OAuth 방식에서는 DefaultOAuth2UserService 를 상속한다. OAuth2UserService도 있지만, Spring Security에서 제공하는 기본 OAuth2 서비스를 상속받았다. -> loadUser 를 오버라이딩 해야 한다.loadUser 메서드는 사용자가 OAuth2로 로그인했을 때 호출된다 ❗️
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
String provider = userRequest.getClientRegistration().getRegistrationId();
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String findName = attributes.get("name").toString();
String email = attributes.get("email").toString();
Optional<Member> memberOptional = memberRepository.findByEmail(email);
Member member = memberOptional.orElseGet( // 있으면 있는 거 반환해주고 없으면 람다식 안에 걸 반환해준다.
() -> {
Member saved = Member.builder()
.name(findName)
.email(email)
.provider(provider)
.build();
return memberRepository.save(saved);
}
);
MemberDetails memberDetails = MemberDetails.builder()
.name(member.getName())
.attributes(attributes)
.role(member.getRole())
.build();
return memberDetails;
}
OAuth2UserRequest : ClientRegistration(구글인지 네이버인지), AccessToken 등의 정보를 담은 객체이다. userRequest.getClientRegistration().getRegistrationId() : 어떤 사이트에서 정보를 받아오는지 제공자의 정보를 가져온다.OAuth2User : OAuth2User 는 google, kakao 등에서 가져온 정보를 담고 있는 객체이다. 즉, Resource Server에서 정보를 받아온 객체로, 요청의 scope에 설정해두었던 정보들이 담긴다. oAuth2User에는 attributes가 사용자 정보를 담고 있다. scope에 설정해두었던 정보들이 담긴다. (email, profile)findByEmail() : Resource Server에서 제공한 attributes 중 UNIQUE하다고 판단되는 걸로 DB에서 유저를 찾는다. (이미 회원가입 했는지 여부를 확인하기 위해).orElseGet() : Optional 내에 객체가 있으면 객체를 반환해주고, 없으면 안의 람다식에서 리턴해서 넣어준다. 즉, 이미 회원가입한 멤버면 그 객체를 반환해주고, 신규 멤버라면 멤버 객체를 DB에 저장한 후 반환해준다.MemberDetails를 빌더 패턴으로 만들면서, 권한도 부여해주고, 이를 반환해준다.Q1. Redirect 와 Forward가 뭔지 잘 모르겠다. 이것들이 뭔지, 차이점은 뭔지 알아보자.
↔️ Forward 방식은 다음 이동한 URL로 요청정보를 그대로 전달한다. 그렇기 때문에 사용자가 최초로 요청한 요청 정보는 다음 URL에서도 유효하다.
↪️ Redirect 방식은 최초 요청을 받은 URL1에서 클라이언트에 redirect 할 URL2를 리턴하고, 클라이언트에게 전혀 새로운 요청을 생성하여 URL2에 다시 요청을 보낸다. 따라서 처음 보냈던 최초의 요청정보는 더이상 유효하지 않게 된다.
✅ 정리
| 방식 | URL 변화여부 | 객체의 재사용 여부 |
|---|---|---|
Forward | ❌ | ✔️ |
Redirect | ✔️ | ❌ |
Q2. loadUserByUsername, loadUser가 언제 호출되는 것인지,, 뭘 하는 메서드인지 한 번 더 정리하기
⏱️ 호출 시점 (폼 로그인 과정 중):
POST /login (또는 loginProcessingUrl)UsernamePasswordAuthenticationFilter가 동작해서 username & password를 추출AuthenticationManager가 UserDetailsService를 사용해 사용자 정보 조회loadUserByUsername() 호출됨📌 역할
⏱️ 호출 시점 (OAuth2 로그인 과정 중):
loadUser() 호출됨📌 역할
어제 잘 따라잡고 오늘 수업을 들어서인지 오늘 수업은 잘 이해가 됐다 !! 이해하고 보니 더 재밌는 너낌 ㅎ 근데 아직 익숙하진 않아서, 계속 다시 정리한 거 다시 읽어보고, 또 다시 이해하고 실습이랑 매칭하고.. 그랬던 것 같다. 뭔가 인증 방식 여러가지 나오니까 조금 헷갈리는 것도 있고 그랬다.. 익숙해지려면 반복이 답이겠지?
오늘도 절거운 수업이었다 ! :)