반응형
스프링으로 '~로 로그인 하기' 를 구현 하고싶어서 처음엔 쌩으로 구글에 요청하고 accessToken, refreshToken을 받아서 진행해봤다. 그런 과정에서 spring security에서 oauth2 인증 로그인을 대신 해준다는 것도 발견했다. 그래서 이번엔 Spring Security를 공부해봤다.
밑에 있는 코드 설명을 단락별로 순서대로 적었다.
일단 WebSecurityConfigurerAdapter를 상속받는 클래스를 config패키지 아래에 만든 후,
@EnableWebSecurity를 붙여 스프링 시큐리티 필터가 작동 되도록 한다.
htt.authorizeRequests().andMathers() 부분은 페이지에 대한 권한을 설정해주는 곳이다.
formLogin()부분은 로그인 정보를 쓸 페이지를 등록하고, 설정한 loginProcessUrl로 보내주면 자동으로 스프링 시큐리티에서 로그인이 되도록 진행된다.
여기에서 UserDetailsService를 implement한 객체의 loadByUsername()메소드를 통해 후처리를 하는 곳으로 보내진다. -구현을 하면 됨. (이 메소드는 UserDetails타입 객체를 반환)
oauth2Login()부분은 ~로 로그인 하기 부분으로, 유저가 로그인 하면 userService()에서 후처리할 객체를 넣어주어 로그인 정보를 받아 처리하도록 된다.
여기도 마찬가지로 DefaultOAuth2UserService를 implemets한 객체가 loadUser()메소드를 통해 후처리를 할 수 있다. (이 메소드는 OAuth2User타입 객체를 반환)
위 두 loadByUsername(), loadUSer()함수가 끝나면 Authentication객체안에 UserDetails, OAuth2User타입 객체가 들어가게 된다.
/* 1.코드받기(인증) 2.엑세스토큰받기(사용자정보가져올수있는 권한)
* 3.사용자프로필 가져오고 4-1.그정보로만 회원가입 자동으로 완료
* 4-2. or 추가적인 회원가입창으로 정보 추가해서 회원가입 시켜줌.
* */
@RequiredArgsConstructor
@Configuration //configuration 에서 AllArgsConstructor가 빠져서 bean등록 오류가 생성됨 -> @AllArgs..추가
@EnableWebSecurity//스프링 시큐리티 필터가 필터체인에 등록이 됨
public class SecurityConfig extends WebSecurityConfigurerAdapter{
final private PrincipalOAuth2UserService principalOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated() /*인증만되면 들어갈수 있는주소*/
.antMatchers("/mannager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANNAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.usernameParameter("username") /*디폴트가 username임*/
.loginProcessingUrl("/login") /*가입후자동로그인: /login주소가 호출되면 스프링시큐리티에서 낚아채(컨트롤러에 안만들어도됨) 대신 로그인진행함 */
.defaultSuccessUrl("/")
.and()
.oauth2Login()
.loginPage("/loginForm") /*Tip. 코드X, 엑세스토큰+사용자프로필정보 받음*/
.userInfoEndpoint()
.userService(principalOAuth2UserService);
}
@Bean //해당 메서드의 리턴되는 오브잭트를 IoC로 등록해준다.
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
}
위의 Authentication객체가 무엇인가?
session에는 security session이 안에 들어가 있다. 그 security session은 Authentication객체만이 들어갈 수 있는데,
Authentication객체 안에 들어갈 수 있는 타입이 정해져 있다.
일반 로그인 시에는 UserDetails 타입이어야 하고, oauth인증 로그인시에는 OAuth2User 타입이어야 한다.
그런데 타입이 두개이기 떄문에 다른 방식으로 로그인할 경우 다른 방식을 취해야 하는 번거로움이 생기기도 하고, 혹 user에 대한 다른 정보도 넣고 싶을때도 있다.
그래서 UserDetails와 OAuth2User를 모두 implements하는 클래스(ex.PrincipalDetails)를 만들어야 하고, 그 클래스에 User에 대한 추가적인 객체(ex.User)를 private로 선언해 주면 하나의 객체로 Authentication에 넣고 가져오기를 자유롭게 할수 있다.
또 추가적으로, 예를 들어 google 로그인시 google프로필을 받아오기 때문에 오버라이드 하면 Map<String, Object> attributes()가 보이는데 이것도 private 객체로 선언해 두어야 나중에 Authentication에서 사용자 정보를 꺼냈을때 정보를 확인할 수있다.
그런데 그 전에, 두 가지 방식 사이에서 가져오는 데이터는 다르니, 로그인 완료전인, 사용자 정보를 후처리 할때는 다른 service클래스를 사용해줘야 한다.
일반로그인은 UserDetailsService를 implements한 객체(ex.PrincipalDetailsService)가.
oauth2로그인은 DefaultOAuth2UserService를 implements한 객체(ex.PrincipalOAuth2UserService)가.
여기서 말한 service클래스가 Authentication에 들어갈 객체를 반환해 주는(loadUser...) 함수를 가지고 있음.
Authentication안에 들어가야 할 객체 PrincipalDetails 이다.
/* 스프링시큐리티가 /login 주소요청오면 낚아채 로그인진행시고
* 로그인 진행이 완료되면 security session(세션내에 시큐리티세션이 있음.Security ContextHolder키값을 가짐)
* 에 넣어줌
* security session에 들어갈 객체는(오브젝트타입) Authentication타입의 객체여야 하고,
* Authentication객체 안에는 유저정보(ex.User)가 있어야 하는데 이는 UserDetails타입객체여야 한다.
* -- security session => Authentication => UserDetails
* */
@Data
public class PrincipalDetails implements UserDetails, OAuth2User{
private User user; //콤포지션: 상속의 문제피하기 위해 private 필드로 기존 클래스의 인스턴스를 참조하게함
private Map<String, Object> attributes;
public PrincipalDetails(User user){//일반로그인
this.user = user;
}
public PrincipalDetails(User user, Map<String, Object> attributes) {//oauth2로그인
this.user = user;
this.attributes = attributes;
}
@Override //해당 유저의 권한을 리턴함
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Map<String, Object> getAttributes() {//oauth2User
return attributes;
}
@Override
public String getName() {//oauth2User
return null;
}
}
일반 로그인시 PrincipalDetailsService
/*시큐리티 설정(SecurityConfig)에서 loginProcessingUrl("/login") 해놔서
* /login 요청오면 자동으로 UserDetailsService타입으로 IoC되어있는
* loadUserByUsername 메서드가 실행되도록 되어있다.
* */
@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService{
private final UserRepository userRepo;
@Override //아래 메서드 결과: SecuritySession(내부 Authentication(내부 UserDetails))
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepo.findByUsername(username);
if(userEntity!=null) {
return new PrincipalDetails(userEntity);
}
return null;
}
}
oauth2 로그인시 PrincipalOAuth2UserService
@RequiredArgsConstructor
@Service
public class PrincipalOAuth2UserService extends DefaultOAuth2UserService{
final private UserRepository userRepo;
//loadUser() = oauth2 로그인시,구글로부터 받은 UserRequest데이터에 대한 후처리 하는 메소드
//메소드 종료시 @AuthienticationPrincipal 어노테이션이 만들어진다!!
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// username= goolge_3047387102293948, password=암호화(겟인데어), email=user@gmail.com,
//role="ROLE_USER", provider=google, providerId=3047387102293948
// 구글로그인 완료-> code리턴(OAuth-client라이브러리가받아)-> accessToken요청해서받음
// userRequest를 가지고 loadUser함수를 호출해->구글로부터 사용자 프로필을 받을 수 있다.
OAuth2User oauth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();//google
String providerId = oauth2User.getAttribute("sub");
String username = provider+"_"+providerId;//google_3938378292
String email = oauth2User.getAttribute("email");
String password = null;
String role = "ROLE_USER";
User userEntity = userRepo.findByUsername(username);
/**
* 존재하지 않는 회원이라면 새로운 회원으로 등록시켜준다. save
*/
if(userEntity==null) {
userEntity = User.builder() /*@Builder*/
.username(username).email(email).password(password)
.role(role).provider(provider).providerId(providerId)
.build();
userRepo.save(userEntity);
}
return new PrincipalDetails(userEntity, oauth2User.getAttributes());//Authentication객체에 들어감
}
}
여기서 주의해야 하는 것은
(=내가 혼돈와서 정리하는 것)
oauth 로그인 service 에서는 기존회원이 아닐때 바로 db에 저장해주는 반면,
왜 일반로그인 service에는 기존회원이 없을때 db에 저장해주지 않았을까?
그렇지 않으면 회원이 틀린 정보로 일반 로그인시,
로그인이 실패하지 않고 무조건 가입이 되기 때문이다.
그래서 회원가입하는 페이지를 따로 만들어서 진행시켜야 한다.
(oauth로그인은 해당 인증서버에서 우리 회원인지 여부를 확인해주고
맞다면 정보를 보내주기 때문에 무조건 db에 없다면 저장해준 것이다 )
userRequest에 들어있는 내용들
super.loadUser(userRequest).ClientRegistration
-> ClientRegistration{ registrationId='google', clientId='...', clientSecret='...',
clientAuthenticationMethod=...,
authorizationGrantType=org.springframework.security.oauth2.core.AuthorizationGrantType@5da5e9f3,
redirectUri='{baseUrl}/{action}/oauth2/code/{registrationId}',
scopes=[email, profile],
providerDetails=...,
clientName='Google'
}
AccessToken:엑세스토큰값을 가짐
Attributes:{ sub=3047387102293948,
name=...,
given_name=..,
family_name=..,
picture=https://lh3.googleuser...,
email=...,
email_verified=true,
locale=ko
}
반응형
댓글