스프링 시큐리티는 스프링 프레임워크에서 제공하는 보안 프레임워크.
WebSecurityConfig.java
스프링 시큐리트 설정 클래스
/*
@Configuration
스프링의 설정 정보를 포함하고 있음을 나타냄
@Bean
빈 객체를 생성, 빈 객체는 스프링 컨테이너에 등록되어 다른 빈 객체에서 참조할 수 있음.
@EnableWebSecurity
스프링 시큐리티의 설정 정보를 포함하는 클래스임을 나타냄.
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
// 소셜(OAuth2) 로그인 성공 후, 처리 되는 로직을 수행하는 서비스단
private final CustomOAuthUserService oAuthUserService;
// 회원의 비밀번호를 DB에 저장하기 전, 해쉬 알고리즘으로 암호화를 시킬 때, 사용되는 객체
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/* SecurityFilterChain는 스프링 시큐리티에서 사용되는 인터페이스로,
HttpServletRequest와 매칭되는 필터 체인을 정의한다.
이 필터 체인은 FilterChainProxy를 구성하는 데 사용된다.
*/
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception{
// HttpSecurity에 빌더 패턴으로 보안 옵션들을 설정한다.
/* authorizeHttpRequests 유저의 권한에 따라 요청(request)에 대한 접근 가능 여부를 판별.
.hasAuthority("USER") USER 권한을 가진 회원만 접근 허용
.anonymous() 비회원인 경우만 접근 허용
.permitAll() 회원 여부에 상관 없이 접근 허용
*/
http.authorizeHttpRequests((req) -> req
.requestMatchers("/member/mypage**", "planner/**").hasAuthority("USER")
.requestMatchers("/member/join**", "/member/login").anonymous()
.anyRequest().permitAll())
// formLogin 로그인 시
.formLogin(formLogin -> formLogin
.loginPage("/member/login")
.defaultSuccessUrl("/"))
// logout 로그 아웃 시
.logout((logout) -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true))
// oauth2Login 소셜 로그인 시
.oauth2Login((oauth) -> oauth
.loginPage("/member/login")
.userInfoEndpoint((endpoint)->endpoint
.userService(oAuthUserService)));
return http.build();
}
}
MemberService.java
스프링 Security에 formLogin를 설정할 경우, Controller단에서 로그인 인증을 위한 RequestMapping을 따로 지정하지 않아도, loadUserByUsername(String username)
메소드로 유저 이름을 입력받아 해당 유저의 인증 정보(UserDetails
)를 반환합니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Member member = memberRepository.findByUsername(username);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("USER"));
return new MemberDetails(member, authorities);
}
}
MemberDetails.java
UserDetails 클래스는 유저의 세부 정보와 권한 정보를 포함하는 유저 세부 정보 클래스.
public class MemberDetails implements UserDetails, OAuth2User {
// 회원 객체
private Member member;
// 권한
private final Set<GrantedAuthority> authorities;
// OAuth의 속성
private Map<String, Object> attributes;
public MemberDetails(Member member, Collection<? extends GrantedAuthority> authorities){
this.member = member;
this.authorities = Set.copyOf(authorities);
}
public MemberDetails(Member member, Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes){
this.member = member;
this.authorities = Set.copyOf(authorities);
this.attributes = attributes;
}
}
application.properties
# KaKao OAuth2 설정
spring.security.oauth2.client.registration.kakao.client-id={카카오 REST API 키}
spring.security.oauth2.client.registration.kakao.client-secret={카카오 로그인 Client Secret 키}
spring.security.oauth2.client.registration.kakao.redirect-uri={카카오 로그인 Redirect URI}
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post
spring.security.oauth2.client.registration.kakao.client-name=Kakao
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,profile_image,account_email
# Kakao Provider 등록
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
CustomOAuthUserService.java
카카오 로그인 시 카카오
@Service
@RequiredArgsConstructor
public class CustomOAuthUserService extends DefaultOAuth2UserService{
private final MemberRepository memberRepository;
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String userId = String.valueOf(attributes.get("id"));
Member member = memberRepository.findByUsername(userId);
if(member == null){
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
String nickname = String.valueOf(profile.get("nickname"));
String email = String.valueOf(kakaoAccount.get("email"));
member = new Member();
member.setCreateDate(LocalDateTime.now());
member.setMemberType(MemberType.Kakao);
member.setUsername(userId);
member.setRealname(nickname);
member.setEmail(email);
memberRepository.save(member);
}
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("USER"));
return new MemberDetails(member, authorities, attributes);
}
}
oAuth2User.getAttributes()
예시{
id=1000000000,
connected_at=2023-10-30T02:00:00Z,
properties={
nickname=이름,
profile_image=http://k.kakaocdn.net/dn/WTAaw/btssfTs84YC/.../img_640x640.jpg,
thumbnail_image=http://k.kakaocdn.net/dn/WTAaw/btssfTs84YC/.../img_110x110.jpg
},
kakao_account={
profile_nickname_needs_agreement=false,
profile_image_needs_agreement=false,
profile={
nickname=이름,
thumbnail_image_url=http://k.kakaocdn.net/dn/WTAaw/btssfTs84YC/.../img_110x110.jpg,
profile_image_url=http://k.kakaocdn.net/dn/WTAaw/btssfTs84YC/.../img_640x640.jpg,
is_default_image=false
},
has_email=true,
email_needs_agreement=false,
is_email_valid=true,
is_email_verified=true,
email=user@email.com
}
}
회원가입 페이지다.
로그인 화면. 카카오 로그인도 구현했기에 아래에 뜨게 해놓았다.