구글 로그인 연동
clientId, clientSecret : 아래 링크에서 인증 정보를 발급받는다.
- 클라우드 플랫폼에 신규 프로젝트 생성하기
새 프로젝트 생성 후, API 및 서비스 > 사용자 인증 정보 > 사용자 인증 정보 만들기 > OAuth 클라이언트 ID > OAuth 동의 화면 구성
- OAuth 동의 화면에서 앱 이름에는 구글 로그인 시 사용자에게 노출될 어플리케이션 이름을 작성한다.
- 지원 이메일은 사용자 동의 화면에서 노출될 이메일 주소를 작성한다.
- 구글 API 범위는 등록할 구글 서비스에서 사용할 범위 목록으로 기본값은 email, profile, openid이다
2. 사용자 인증 정보 만들기
: API 및 서비스 > 사용자 인증정보 > 사용자 인증정보 만들기 > OAuth 클라이언트 ID 만들기
URL 주소등록: 승인된 리디렉션 URI는 서비스에서 파라미터로 인증정보를 주었을 때 인증에 성공할 시 구글에서 리다이렉트 할 URL를 뜻한다.
리디렉션 URI : https://localhost:8080/login/oauth2/code/google
스프링 부트 2 제공 디폴트 리다이렉트 URL : {도메인}/login/oauth2/code/{소셜서비스코드}
따라서 사용자가 별도로 리다이렉트 URL 지원 Controller 생성할 필요는 없다.
AWS 서버에 배포하면 localhost 외 주소를 추가해야 하지만 개발 단계에서는 보통 localhost 로 놓고 테스트
application-oauth 등록하기
src/ main/ resource/ … application.properties 있는 디렉토리에 application-oauth.properties 을 생성한다.
- 클라이언트 ID, 클라이언트 보안 비밀번호 등록해준다.
- scope의 기본 값은 openid, email, profile 이다.
spring.security.oauth2.client.registration.google.client-id={클라이언트 id}
spring.security.oauth2.client.registration.google.client-secret={클라이언트-secret}
spring.security.oauth2.client.registration.google.scope=profile,email
gitignore 등록하기
: 클라이언트 ID, 보안 비밀번호는 외부에 노출되면 안되는 중요 정보이기 때문에 gitignore로 관리를해준다.
깃허브 연동 시 노출되지 않도록 application-oauth.properties 파일 등록을 방지한다.
.gitignore → application-oauth.properties 추가하여 등록한다.
User 엔티티를 생성한다.
: 사용자 정보 담당할 도메인을 관리한다.
@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column
private String picture;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@Builder
public User(String name, String email, String picture, Role role) {
this.name = name;
this.email = email;
this.picture = picture;
this.role = role;
}
public User update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
Role Enum 클래스를 생성한다.
: 각 사용자의 권한을 관리할 Enum 클래스
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
UserRepository를 생성한다.
: User 관련 CRUD 를 담당한다.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
어노테이션 기반으로 개선하기
스프링에서 프로그램에 관한 데이터를 제공하거나, 코드에 정보를 추가할 때 사용하는 것을 어노테이션이라 한다. 이미 만들어진 어노테이션 뿐만 아니라 직접 커스텀해서 만들 수 있는데, 이를 커스텀 어노테이션이라 한다.
IndexController 부분에서 세션값이 필요하면 그때마다 아래 세션에서 값을 가져오는 코드를 반복하게 된다. 같은 코드가 계속 반복되므로, 커스컴 어노테이션을 만들어 개선해보자.
SessionUser user = (SessionUser) httpSession.getAttribute("user");
→ 메소드 인자로 세션값을 바로 받을 수 있도록 변경해보자.
- config/auth/@LoginUser
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
- 어노테이션이 생성될 수 있는 위치를 지정한다
- PARAMETER로 지정하였으므로, 메소드의 파라미터로 선언된 객체에서만 사용 가능
- TYPE : 클래스 선언문에 사용 가능
- @interface
- 해당 파일을 어노테이션 클래스로 지정
- LoginUser라는 이름의 어노테이션이 생성되었다
- @Retention
- 어노테이션이 언제까지 유효할지 결정하는 어노테이션
config/auth/LoginUserArgumentResolver : HandlerMethodArgumentResolver 인터페이스 구현
@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
boolean isUserClass = SessionUser.class.equals(parameter.getParameterType());
return isLoginUserAnnotation && isUserClass;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return httpSession.getAttribute("user");
}
}
- HandlerMethodArgumentResolver
- 조건에 맞는 경우, 메소드가 있다면 HandlerMethodArgumentResolver 구현체가 지정한 값으로 해당 메소드의 파라미터로 넘길 수 있다.
- supportsParameter()
- 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단한다
- @LoginUser 어노테이션이 붙어있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true를 반환한다.
- resolveArgument()
- 파라미터에 전달할 객체를 생성
- 여기서는 세션에서 객체를 가져온다.
config.WebConfig : 생성된 LoginUserArgumentResolver가 스프링에서 인식되게 설장 추가
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
}
}
- HandlerMethodArgumentResolver는 항상 WebMvcConfigurer의 addArgumentResolvers()로 추가해준다.
IndexController 코드 수정
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user){
model.addAttribute("posts", postsService.findAllDesc()); // 메서드 결과로 가져온 posts를 index.mustache에 전달한다
if (user != null){
model.addAttribute("userName", user.getName());
}
return "index";
}
- @LoginUser SessionUser user
- 기존에 (SessionUser) httpSession.getAttribute("user");로 가져오던 세션 정보값이 개선
- 어느 컨트롤러든지 @LoginUser만 사용하면 세션 정보를 가져올 수 있다.
Spring Security란?
Spring 기반의 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크이다.
인증과 권한에 대한 부분을 Filter 흐름에 따라 처리하며, 기본적으로 인증(Authentication) 절차를 거친 후, 인증이 성공하면 인가(Authorization) 절차를 진행한다. (인증 → 인가)
인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인한다. 이런 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증방식을 사용한다.
- Principals : 접근 주체. 보호받는 resource에 접근하는 대상
- Credentials : 비밀번호, resource에 접근하는 대상의 비밀번호
✨ Spring Security 주요 모듈
- SecurityContextHolder
- 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부정보가 저장된다.
- SecurityContext
- Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.
- Authentication
- 현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다.
- Authentication 객체는 Security Context에 저장된다.
- SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근할 수 있다.
- UsernamePasswordAuthenticationToken
- Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스
- user의 ID가 Principal 역할을 하고, password가 Credential 역할을 한다.
- 인증 완료 전의 객체를 생성하고, 인증 완료 후의 객체를 생성하는 역할을 한다.
- AuthenticationProvider
- 실제 인증에 대한 부분을 처리한다.
- Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다.
- AuthenticationManager
- 인증은 Authentication Manager를 통해서 처리하는데, 실질적으로는 Authentication Manager에 등록된 AuthenticationProvider에 의해 처리된다.
- 인증이 성공하면 생성자를 통해 인증이 성공한 객체를 생성하여 Security Context에 저장한다.
- 인증상태 유지를 위해 세션에 보관하며, 인증이 실패한 경우 AuthenticationException을 발생시킨다.
- UserDetails
- 인증에 성공하여 생성된 UserDetails 객체는 Authentication객체를 구현한 UsernamePasswordAuthenticationToken을 생성하기 위해 사용된다.
- UserDetails 인터페이스에는 정보를 반환하는 메서드들을 갖고 있다.
- UserDetailsService
- UserDetailsService 인터페이스는 UserDetails객체를 반환하는 단 하나의 메소드, loadUserByUsername을 갖고 있다.
- 일반적으로 이를 구현한 클래스의 내부에 UserRepository를 주입받아 db와 연결하여 처리해준다.
- Password Encoding
- 패스워드 암호화에 사용될 PasswordEncoder 구현체를 지정할 수 있다.
- GrantedAuthority
- 현재 principal(사용자)이 가지고 있는 권한을 뜻한다.
- ROLE_* 의 형태로 사용하며, UserDetailsService에 의해 불러올 수 있다.
- 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 결정한다.
네이버 로그인
네이버 오픈 API (https://developers.naver.com/apps/#/register) 이용하여 로그인 구현 가능
- 애플리케이션 등록
- Callback URL: 구글에서 등록한 redirection URL과 같은 역할
등록 완료 후 위와 같이 ClientID, ClientSecret이 생성됨
- application-oauth.properties에 생성된 키값 등록
# registration
spring.security.oauth2.client.registration.naver.client-id="클라이언트 ID 값"
spring.security.oauth2.client.registration.naver.client-secret="클라이언트 비밀 값"
spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email,profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver
# provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user_name_attribute=response
네이버는 스프링 시큐리티를 공식 지원하지 않음 → Common OAuth2Provider에서 해주던 값들도 모두 입력해야 함
- user_name_attribute=response이때 스프링 시큐리티에서는 하위 필드를 명시 불가 → response를 user_name으로 지정 후, response의 id를 user_name으로 지정하는 방식으로 해결!
- 네이버의 회원 조회 시 반환되는 JSON 형태에는 최상위 필드가 3개 존재 (resultCode, message, response)
- 스프링 시큐리티 설정 등록
구글 로그인 등록 시 사용한 OAuthAttributes에 네이버 판단 코드, 네이버 생성자 추가
// OAuthAttributes.java 내 코드 추가
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
**if ("naver".equals(registrationId)) {
return ofNaver("id", attributes);
}**
return ofGoogle(userNameAttributeName, attributes);
}
**private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuthAttributes.builder()
.name((String) response.get("name"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}**
- 네이버 로그인 버튼 추가
index.mustache에 작성
<a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
- /oauth2/authorization/naver
- 네이버 로그인 URL: redirect_uri_template (application-oauth.properties에 등록) 값에 맞춰 자동 등록됨
'Group Study (2022-2023) > Spring 입문' 카테고리의 다른 글
[spring 입문] 6주차 스터디 - EC2에서 소셜 로그인 하기 + 자바 웹 애플리케이션의 동작과 배포 (0) | 2022.11.20 |
---|---|
[spring 입문] 5주차 스터디 - AWS EC2, RDS (0) | 2022.11.13 |
[Spring 입문] 3주차 스터디 - 머스테치로 화면 구성하기 (0) | 2022.11.01 |
[Spring 입문] 2주차 스터디 - 스프링 부트에서 JPA로 데이터베이스 다뤄보기 (0) | 2022.10.10 |
[Spring 입문] 1주차 스터디 - 스프링부트 시작 / 테스트 코드 작성하기 (1) | 2022.10.02 |