내가 보려고 정리한 것
일단 시큐리티를 쓰려면
WebSecurityConfigurerAdapter 를 구현한 컨피그레이션 클래스를 만드러야한다~
그리고 configure 오버라이딩 해준다
(configure 메소드에서 시큐리티 설정을 함)
@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터 체인에 등록
@RequiredArgsConstructor
/**
* secured
* preAuthorize(postAuthorize)
* 메소드에 직접 권한 걸기 true
*/
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final PrincipalOauth2UserService principalOauth2UserService;
@Bean
public static BCryptPasswordEncoder encoderPwd() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/user/**").authenticated() //인증만 받으면 접속 가능
// 인증뿐만 아니라, 권한도 있어야함
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll() //나머지 요청은 다 허용할게요
.and()
.formLogin()
.loginPage("/loginForm") // 로그인 페이지 명시
// .usernameParameter("id") // username 파라미터 이름 명시(기본 username)
.loginProcessingUrl("/login") //시큐리티가 대신 로그인 진행해줌(컨트롤러에 /login 안 만들어도 됨)
.defaultSuccessUrl("/") //로그인이 성공하면, 이 페이지로 가주세요
/**
* 1. 코드 받기(인증)
* 2. 액세스 토큰 받기(권한)
* 3. 사용자 프로필 정보 가져오기
* 4-1. 정보를 토대로 자동 회원가입
* 4-2. 추가 정보 필요하다면 작성
*/
/**
* 구글 로그인?
* 액세스 토큰 + 사용자 프로필 정보 한꺼번에 가져옴
* 코드 필요 X
* username = google_(sub)
* password = (암호화)겟인데어
* email = 구글이메일
* role = ROLE_USER
*/
.and()
.oauth2Login() //oauth2 로그인 허용
.loginPage("/loginForm") //구글 로그인 인증 페이지는?
.userInfoEndpoint() //로그인이 성공했다면?
.userService(principalOauth2UserService) //후처리 이렇게 해주세요
;
}
}
그리고 얘는 신기한게
로그인 인증을 자동으로 해준다...;;
주석 보면 되긴 하는데
/loginForm 이
아디비번 적는 view 페이지이고(여기서 로그인 정보 친다고 알려줌)
usernameParameter은 form에서
아이디를 치는 input text의 name이 기본이 username인데
다른 걸로 바꿀래? 머 이런 뜻이다
loginProcessingUrl은 "/login" 이 URL로 로그인 들어간단 뜻인데
컨트롤러에 로그인 안 만들어도 된다
걍 시큐리티가 알아서해줌;;;
마지막 디폴트석세스유알엘은 로그인이 성공하면 갈 페이지를 얘기하는 건데
시큐리티가 좀 신기한게
/loginForm 으로 처음 접속하거나 했으면 설정한 defaulltSuccessUrl 로 보내주는데
만약에 내가 다른 페이지를 들어가려고 했다가
로그인 인증이 막혀서 /loginForm이 떴고
로그인을 완료했다면
다른 페이지로 자동 리다이렉트해준다 ;;;;
내가 직접 http 헤더에서 전에 있었던 페이지 저장해줄 필요가 읍다....
이뇨석 매우 편리했음
어쨌든
보통 시큐리티 로그인 순서는
* 1. 코드 받기(인증)
* 2. 액세스 토큰 받기(권한)
요것인데
SNS로그인을 하면
* 1. 코드 받기(인증)
* 2. 액세스 토큰 받기(권한)
* 3. 사용자 프로필 정보 가져오기
* 4-1. 정보를 토대로 자동 회원가입
* 4-2. 추가 정보 필요하다면 작성
4-1 까지는 자동으로 해준당ㅎ.ㅎ
4-2 는 필요하다면 추가해주기
* 만약 구글 로그인이라면 ?
* 구글이 액세스 토큰 + 사용자 프로필 정보 한꺼번에 제공
* 로그인에 관한 코드는 필요 없음~
* 회원가입을 한다면, 원하는 정보만 가져와서 save 하면 됨~
나는 sns명_아이디로 회원가입을 하고 싶었고
provideId = 해당 SNS 내 아이디의 PK
password = 대충대충
email = 해당sns 이메일
role = 자동으로 user 인증으로
아까 SecurityConfig의 configure 메소드에 보면
후처리를 하는 부분이 있다
얘는 sns로그인을 할 경우 후처리를 담당하는데~
OAuth2UserService 타입만 들어올 수 있답니당 ^0^
PrincipalOauth2UserService.class
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final UserRepository userRepository;
public PrincipalOauth2UserService(BCryptPasswordEncoder bCryptPasswordEncoder, UserRepository userRepository) {
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.userRepository = userRepository;
}
//로그인 후처리
// 구글로 받은 userRequest 데이터에 대한 후처리
/**
* loadUser 는, 기본적으로 서비스 객체를 만들지 않아도 자동 발동하여
* 로그인을 가능하게 함
* 근데 우리가 굳이 다시 한 번 구현하는 이유?
* return 을 PrincipalDetails 로 하기 위하여,
* UserDetails , OAuth2UserService 를 둘 다 접근가능하게 하기 위하여
* PrincipalDetailsService 의 loadUserByUsername 도 마찬가지...
* 해당 메소드 종료시 @AuthenticationPrincipal 어노테이션 만들어짐
*/
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("userRequest = {}", userRequest);
// registrationId 로 어떤 OAuth 로 로그인했는지 확인 가능
log.info("getClientRegistration = {}", userRequest.getClientRegistration()); // 클라이언트가 누구니? (구글..)
log.info("getAccessToken = {}", userRequest.getAccessToken()); // 토큰(토큰 정보)
OAuth2User oAuth2User = super.loadUser(userRequest);
// 구글 로그인 버튼 클릭 -> 구글 로그인 창 -> 로그인 완료 -> code 리턴(OAuth - Client 라이브러리) -> AccessToken 요청
// userRequest 정보 -> loadUser() -> 구글로부터 회원 프로필 받아줌
log.info("getAttributes = {}", super.loadUser(userRequest).getAttributes()); // 유저 값(이름, 이메일...) // sub=115894611263003886842 -> primaryKey , 구글 회원 아이디 PK
// 회원 가입 강제 진행
OAuth2UserInfo oAuth2UserInfo = null;
if(userRequest.getClientRegistration().getRegistrationId().equals("google")) {
log.info("구글 로그인 요청");
oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
} else if(userRequest.getClientRegistration().getRegistrationId().equals("facebook")) {
log.info("페이스북 로그인 요청");
oAuth2UserInfo = new FacebookUserInfo(oAuth2User.getAttributes());
} else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")) {
log.info("네이버 로그인 요청");
oAuth2UserInfo = new NaverUserInfo((Map) oAuth2User.getAttributes().get("response"));
} else if(userRequest.getClientRegistration().getRegistrationId().equals("kakao")) {
log.info("카카오톡 로그인 요청");
oAuth2UserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
} else {
log.info("구글과 페이스북만 지원 가능");
}
String provider = oAuth2UserInfo.getProvider(); //google
String providerId = oAuth2UserInfo.getProviderId();
String username = provider + "_" + providerId; // google_115894611263003886842
String password = bCryptPasswordEncoder.encode("겟인데어"); //의미는 없지만 그래도
String email = oAuth2UserInfo.getEmail();
String role = "ROLE_USER";
User userEntity = userRepository.findByUsername(username);
// 찾은 userEntity가 없다며는 회워가입
if (userEntity == null) {
log.info("SNS 로그인 최초 = {}", provider);
userEntity = User.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userRepository.save(userEntity);
} else {
log.info("이미 로그인되어 있는 사람입니다");
}
// Authentication 객체 안으로 들어감
return new PrincipalDetails(userEntity, oAuth2User.getAttributes());
}
loadUser 메소드 핵심 구성은 대충 이렇게 돼있다
복잡해보이지만,
request 온 sns에 따라 구분해서 1 - 2 - 3 순으로 동작한다
일단 loadUser()에 매개변수로 request를 담고
이를 OAuth2UserInfo(인터페이스) 타입으로 받는다
얘는 pk , sns명, 이메일, 이름 등을 받는다
public interface OAuth2UserInfo {
String getProviderId(); //PK
String getProvider(); //google, facebook...
String getEmail(); //이메일
String getName(); //이름
}
그리고,
각 sns에 맞게 인터페이스를 구현한다!
제일 까다로웠던 카카오..
public class KakaoUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes; //oAuth2User.getAttributes
private Map<String, Object> attributesAccount;
private Map<String, Object> attributesProfile;
public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
this.attributesAccount = (Map<String, Object>) attributes.get("kakao_account");
this.attributesProfile = (Map<String, Object>) attributesAccount.get("profile");
}
@Override
public String getProviderId() {
return String.valueOf(attributes.get("id"));
}
@Override
public String getProvider() {
return "kakao";
}
@Override
public String getEmail() {
return (String) attributesAccount.get("email");
}
@Override
public String getName() {
return attributesProfile.get("nickname").toString();
}
}
카카오는 정보가 좀 특이하게 온다
providerId 는 그대로 오는 대신 Long 타입으로 와서
String.valueOf() 로 타입캐스팅 해줘야하고 ( toString() 은 안먹음 )
email은 kakao_account 맵 형식으로 안에 들어있고
nickname은 또 그 안에 profile 맵 안에 들어있어서
하나하나 맵으로 꺼낸 다음 또 값을 다시 꺼내줘야한다 ㅠㅠ(하...)
페이스북과 구글 같은 경우는
글로벌 사이트라 그런지 시큐리티에서
기본으로 제공되는 녀석들이라
걍 매우 무난하고(ProvideId 이름만 조금씩 달라서 그거만 바꾸면 됨)
참고로
구글은 sub
네이버, 카카오, 페북은 id 이다
public class GoogleUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes; //oAuth2User.getAttributes
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String) attributes.get("sub");
}
@Override
public String getProvider() {
return "google";
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
네이버도 좀 짜증나는디
얘는 provideId 등의 값이
response 라는 이름으로 들어있어서
요렇게 response를 맵으로 한번 감싼 후 꺼내야한다
( NaverUserInfo 자체는 위와 동일함 )
그리고 그냥 로그인을 할 때
시큐리티컨피그에서 loginProcessingUrl() 설정을 해주게 되는데
* 시큐리티 설정에서 loginProcessingUrl("/login") 하면
* /login 으로 요청이 됐을 때
* UserDetailsService 타입으로 Ioc 되어있는
* loadUserByUsername 함수가 실행된다
ㄴ 무조건 자동임
근데, 얘를 구현 안 해도 자동으로 시큐리티가 해주긴 하는디
sns와 그냥 로그인을 편하게 하려면
구현하는 것이 좋다(뒤에 설명하겟음 ㅋ)
UserDetailsService 를 구현한 PrincipalDetailsService.class
/**
* 시큐리티 설정에서 loginProcessingUrl("/login")
* /login 요청이 오면
* UserDetailsService 타입으로 Ioc 되어있는
* loadUserByUsername 함수가 실행
*/
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
* (Session (Security session (Authentication (UserDetails))))
* 해당 메소드 종료시 @AuthenticationPrincipal 어노테이션 만들어짐
*/
@Override
// form 에서 name 이 username 이라고 되어 있는 거랑 매핑됨
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
// username 값이 들어왔다면
if (username != null) {
// UserDetails 객체로 검증하세요!
return new PrincipalDetails(userEntity);
}
// 안 들어왔다면 null
return null;
}
}
해당 메소드가 다 실행되고 나면
@AuthenticationPrincipal 메소드가 생긴다(활성화 된다? 사용 가능하다?고 하는게 맞을듯)
그리고 얘는
view에서 form으로 넘길 때,
name="username" 이라고 되어있는 거랑만 매핑되니까
이름 정할 때 조심히 ㅈ ㅓㅇ해야한담...ㅎㅎ
뭐 어쨋든 이렇게 로그인을 하면?
* loadUser 는, 기본적으로 서비스 객체를 만들지 않아도 자동 발동하여
* 로그인을 가능하게 함
* 근데 우리가 굳이 다시 한 번 구현하는 이유?
* return 을 PrincipalDetails 로 하기 위하여,
왜 PrincipalDetails 로 하느냐?
* UserDetails , OAuth2UserService 를 둘 다 접근가능하게 하기 위하여
* PrincipalDetailsService 의 loadUserByUsername 도 마찬가지...
* 해당 메소드 종료시 @AuthenticationPrincipal 어노테이션 만들어짐
Session 안에는
Security Session 을 위한 공간이 따로 있고,
시큐리티 세션에는 Authentication 만 담을 수 있음,
근데 또 어선티케이션 얘는
UserDetails 과 OAuth2User 만 담을 수 있다 ㅡ.ㅜ
그래서 원래 일반 로그인한 정보를 가지고 오고 싶을 때는
UserDetails 타입을 이용하고
sns로그인은
OAuth2User 타입을 이용해야한다
.
그래서
컨트롤러에서 일반 로그인, sns로그인 매핑을 따로 구현해서
각각 타입캐스팅 해줘야한다ㅠㅠ
일반 로그인
(PrincipalDetails 는 UserDetails 를 구현한 클래스)
sns 로그인
각각 타입 캐스팅을 해줘야하는 어려움이 있었는데,
이를 해결하기 위해서는?
UserDetails , OAuth2User 를 한꺼번에 구현하면 되지롱~
/**
* Security Session -> Authentication -> UserDetails
*/
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
private User user; //컴포지션
private Map<String, Object> attributes;
// 일반 로그인
public PrincipalDetails(User user) {
this.user = user;
}
// OAuth 로그인
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
// 해당 유저의 권한 return
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collection;
}
@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() {
/**
* 사이트에서 n년간 로그인 안 하면
* 휴면 계정이 된다면? ....
*/
return true;
}
// OAuth 로그인 할 때 들어오는 유저 정보 가져오는...
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null; // pk
}
}
이로케 설정만 해놓으면 ?
시큐리티가 알아서 로그인 인증 기타 등등을 해준다...(개신기)
아~그리고 시큐리티 설정도 해야한다
나는 application.yml 로 했다
이건 검색하면 다 나오긴 하는데
머 굳이 쓰면...
security:
oauth2:
client:
registration:
google:
client-id: 아이디
client-secret: 비번
scope:
- email
- profile
facebook:
client-id: 아이디
client-secret: 비번
scope:
- email
- public_profile
naver:
client-id: 아이디
client-secret: 비번
scope:
- name
- email
client-name: Naver
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/naver #주소 고정이 안 되어 있지만 관례를 따름
kakao:
client-id: 아이디(Rest Api용)
client-secret: 비번(제품설정-보안에 있는 코드임)
scope:
- profile_nickname
- account_email
client-name: Kakao
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/kakao
client-authentication-method: POST
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize #얘로 요청하면 네이버 아이디 뜸
token-uri: https://nid.naver.com/oauth2.0/token #토큰은 여기서 발급함
user-info-uri: https://openapi.naver.com/v1/nid/me #프로필 정보는 이 주소를 호출
user-name-attribute: response # 회원 정보를 json 으로 받음, response 키값으로 네이버가 return 해줌
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: kakao_account
그리고 네이버랑 카카오는
시큐리티가 기본 제공(?)하는 글로벌 사이트가 아니어서
provider 를 따로 설정해야한다
그리고 @@@카카오 할 때 @@@
registration 설정에 카카오는
client-authentication-method: POST
이거 반드시 넣어줘야한다;;;
안그러면 오류남..하.....
***
아 그리고 DB 설정할 때,
싴큐리티 인증, 권한 설정할 때 젤 궁금햇던게
hasRole('권한')
이거 DB에 어케 넣어야함??
어케 넣어야 시큐리티가 알아들음??
자동으로 알아들음???
이거였는디
걍 DB 테이블에 role 이라는 이름의 컬럼 넣고
내가 지정한 권한명을 넣어주면
시큐리티가 해당 유저 테이블의 role 컬럼을 확인해서
내가 집어넣은
hasRole('ROLE_ADMIN') 등을 찾으면 그 유저한테 권한을 준다;;;;
너무 완벽하게 자동화가 되어있어서 이해를 못했었음...ㅎㅎ;;;;
그래서 걍 디비에는
1.멤버 테이블에 role 칼럼 추가함
2.권한 이름을 정하고, 적절하게 넣어줌
(난 (인증만받은)일반유저 ROLE_USER,
쪼렙어드민 ROLE_MANAGER,
최고어드민 ROLE_ADMIN 이라고 줌)
3. .access() 로 role에 해당하는 단어(?)가 있으면 그게 권한이 있다고 인식하고 그 권한 가진 유저만 접근 가능하게 함
어쨋든 머...대충 정리한거같다
벌써 10시 52분이다......
내일 하건가기 싫다ㅠㅠ
'혼자서 개발새발' 카테고리의 다른 글
Spring Security ) JWT 토큰 로그인 구현을 해보았다 (0) | 2023.03.25 |
---|---|
thymeleaf 템플릿을 사용해서 메일로 코드 발송하기! (0) | 2023.03.12 |
페이징 할 때 Entity로 받고, Dto로 변환하자! (1) | 2023.01.05 |
JPA ) QueryDsl , Pageble 을 이용해 페이징을 하다! (0) | 2023.01.04 |
게시판을 손수 페이징해보자 ^^! (0) | 2022.12.22 |