사용자 정보 담당할 도메인 User 클래스
package com.springAWS.domain.user;
import com.springAWS.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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();
}
}
- @Enumerated(Enum Type.STRING)
- JPA로 데베 저장할 때 Enum 값 어떤 형태로 저장할지 결정함
- 기본적으로 int - 데베 확인할 때 그 값이 무슨 코드 의미하는지 알 수 없음 -> 문자열로 저장될 수 있게 선언함
Role Enum 클래스
package com.springAWS.domain.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
스프링 시큐리티에서는 권한 코드에 항상 ROLE_이 앞에 있어야 함
UserRepository 클래스
User의 CRUD 책임짐
package com.springAWS.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
//email 통해 이미 생성된 사용잔지 아닌지 판단 위한 메소드
}
스프링 시큐리티 설정
build.gradle에 추가
클라이언트 입장에서 소셜 로그인 등과 같은 소셜 기능 구현 시 필요한 의존성
spring-security-oauth2-client, spring-security-oauth2-jose 기본으로 관리해줌
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
config.auth 패키지
시큐리티 관련 클래스 모두 여기에 담기
SecurityConfig 클래스
package com.springAWS.config.auth;
import com.springAWS.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@RequiredArgsConstructor
@EnableWebSecurity //spring security 설정 활성화시켜줌
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**",
"/js/**", "/h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout().logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint().
userService(customOAuth2UserService);
}
}
- crsf().disable().headers().frameOptions().disable()
- h2-console 화면 사용 위해 옵션들 disable 해줌
- authorizeRequests
- URL별 권한 관리 설정하는 옵션 시작점
- 이게 선언되어야 antMatchers 옵션 사용 가능
- antMatchers
- 권한 관리 대상 지정하는 옵션
- URL, HTTP 메소드별로 관리 가능
- permitAll() : 전체 열람 권한
- hasRole(Role.USER.name()) : USER 권한 가진 사람만 가능
- anyRequest
- 설정된 값들 이외 나머지 URL
- authenticated() 추가 : 나머지 URL들은 모두 인증된 사용자들에게만 허용하게 함 (로그인한 사용자)
- logout().logoutSuccessUrl("/")
- 로그아웃 기능 대한 설정들 진입점
- 성공 시 / 주소로 이동
- oauth2Login
- OAuth2 로그인 기능 대한 여러 설정 진입점
- userInfoEndpoint
- 로그인 성공 이후 사용자 정보 가져올 떄의 설정들 담당
- userService
- 소셜 로그인 성공 후 후속 조치 진행할 UserService 인터페이스 구현체 등록
- 리소스 서버 (소셜 서비스)에서 사용자 정보 가져온 상태에서 추가로 진행하려고 하는 기능 명시 가능
CustomOAuth2UserService
package com.springAWS.config.auth;
import com.springAWS.config.auth.dto.OAuthAttributes;
import com.springAWS.config.auth.dto.SessionUser;
import com.springAWS.domain.user.User;
import com.springAWS.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.Collections;
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
private User saveOrUpdate(OAuthAttributes attributes) {
User user = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
- registrationId
- 현재 로그인 진행 중인 서비스 구분하는 코드 (네이버인지 구글인지)
- userNameAttributeName
- OAuth2 로그인 진행 시 키가 되는 필드값 == Primary Key
- 구글의 기본 코드 "sub"
- OAuthAttributes
- OAuth2UserService로 가져온 OAuth2User의 attribute 담을 클래스
- SessionUser
- 세션에 사용자 정보 저장하기 위한 Dto 클래스
- User 클래스 사용하지 않고 새로 만드는 이유
- User 클래스 사용 시 User 클래스에 직렬화 구현하지 않았다는 에러 뜸
- User 클래스에 직렬화 코드 넣으면 자식 엔티티까지 직렬화해야 해서 성능 이슈, 부수 효과가 발생하게 됨
- 직렬화 기능 가진 Dto 하나 추가로 만드는 것이 운영과 유지보수에 많은 도움이 됨
OAuthAttributes 클래스
package com.springAWS.config.auth.dto;
import com.springAWS.domain.user.Role;
import com.springAWS.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name,
String email, String picture) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
}
public static OAuthAttributes of(String registrationId, String userNameAttributeName,
Map<String, Object> attributes) {
//OAuth2User에서 반환하는 사용자 정보는 Map 이어서 값 하나하나 변환해야 함
return ofGoogle(userNameAttributeName, attributes);
}
private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
- toEntity()
- User 엔티티 생성
- OAuthAttributes 에서 엔티티 생성 시점은 처음 가입할 때
- 기본 권한을 GUEST로 주기 위해 role 빌더값에 Role.GUEST 사용
SessionUser 클래스
인증된 사용자 정보만 필요함
package com.springAWS.config.auth.dto;
import com.springAWS.domain.user.User;
import lombok.Getter;
import java.io.Serializable;
@Getter
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
public SessionUser(User user) {
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
}
mustache
- {{#userName}}
- 머스테치는 if문을 제공하지 않고 true/false 여부만 판단함
- 항상 최종값을 넘겨줘야 함
- userName 있으면 usreName 노출하도록 구성
- {{^userName}}
- 머스테치에서 해당 값이 존재하지 않으면 ^ 사용
- a href = "/logout" , a href = "/oauth2/authorization/google"
- 스프링 시큐리티에서 기본적으로 제공하는 로그아웃, 로그인 URL
- 별도로 컨트롤러 만들 필요 없음
IndexController
index.mustache에서 userName 사용 가능하게 IndexController에서 userName을 model에 저장하는 코드 추가
@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
SessionUser user = (SessionUser) httpSession.getAttribute("user");
if (user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
- (SessionUser) httpSession.getAttribute("user")
- CustomOAuth2UserService에서 로그인 성공 시 세션에 SessionUser 저장하게 구성
- 로그인 성공 시 httpSession.getAttribute("user")에서 값 가져올 수 있음
- if(user!=null)
- 세션에 저장된 값 있을 때만 model에 userName으로 등록함
- 저장값 없으면 model은 값이 없는 상태여서 로그인 버튼 보이게 됨