profile: xxx
으로 application-xxx.yml의 설정들을 가져올 수 있다.git rm -r --cached .
git add .
git commit -m "fixed untracked files"
@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(EnumType.STRING)
- JPA로 데이터베이스로 저자알 경우, ENUM 값을 String 형태로 저장한다.
- 기본적으로는 int 형태로 저장된다.
ENUM 클래스인 Role을 생성한다.
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST","손님"),
USER("ROLE_USER","일반 사용자");
private final String key;
private final String title;
}
public interface UserRepository extends JpaRepository<User,Long> {
Optional<User> findByEmail(String email);
}
- 소셜 로그인으로 반환되는 값중에서 email을 통해 이미 생성된 사용자인지 확인하기 위한 메소드이다.
dependencies {
// oauth2
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
}
- 클라이언트 입장에서 소셜 기능 구현시 필요한 의존성이다.
- spring-security-oauth2-client와 spring-security-oauth2-jose를 기본으로 관리해준다.
@RequiredArgsConstructor
// @EnableWebSecurity
// spring security 설정들을 활성화시켜준다.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
// .csrf().disable().headers().frameOptions().disable()
// he-console 화면을 사용하기 위해서 해당 옵션들을 disable 한다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
// authorizeRequests
// URL별 권한 관리를 설정하는 옵션의 시작점이다. authorizeRequests가 선언되어야만 antMatchers 옵션을 사용할 수 있다.
.authorizeRequests()
// antMatchers
// 권한 관리 대상을 지정하는 옵션이다.
// URL, HTTP 메소드별로 관리가 가능하다. 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 관한을 준다.
.antMatchers("/","/css/**","/images/**","/js/**","/h2-console?**").permitAll()
// "/api/v1/**" 주소를 가진 API는 USER 권한을 가진 사람만 가능하도록 한다.
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
// anyRequest
// 설정된 값 이외의 나머지 URL들을 나타낸다. authenticated()를 추가하여 나머지 URL들은 모두 인증된 사용자들(로그인한 사용자들)에게만 허용한다.
.anyRequest().authenticated()
.and()
// .logout().logoutSuccessUrl("/")
// 로그아웃 기능에 대한 여러 설정의 진입점이다. 로그아웃 성공시 "/" 주소로 이동한다.
.logout()
.logoutSuccessUrl("/")
.and()
// oauth2Login
// OAuth 2 로그인 기능에 대한 여러 설정의 진입점이다.
.oauth2Login()
// userInfoEndpoint
// OAuth 2 로그인 성공 이후 사용자 정보를 가져올 떄의 설정들을 담당한다.
.userInfoEndpoint()
// userService
// 소셜 로그인 성공시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록한다.
// 리소스 서버(소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자하는 기능을 명시할 수 있다.
.userService(customOAuth2UserService);
}
}
@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);
// registrationId
// 햔재 로그인 진행 중인 서비스를 구분하는 코드
// 네이버 로그인인지, 구글 로그인인지 구분하기위해 사용한다.
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// userNameAttributeName
// OAuth2 로그인 진행 시 키가 되는 필드값을 말한다. PK와 같은 의미이다.
// 구글의 경우 기본적으로 코드를 지원한다. 구글의 기본 코드는 "sub"이다. (네이버, 카카오 등은 기본 지원하지 않는다.)
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
// OAuthAttributes
// OAuth2UserService를 통해 가져온 OAuth2User의 attributes를 담을 클래스이다.
// 다른 소셜 로그인도 이 클래스를 사용한다.
OAuthAttributes attributes = OAuthAttributes.of(registrationId,userNameAttributeName,oAuth2User.getAttributes());
// SessionUser
// 세션에 사용자 정보를 저장하기 위한 따로 만든 Dto 클래스이다.
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);
}
}
@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;
}
// of()
// OAuth2User 에서 반환하는 사용자 정보는 Map이므로 값 하나하나를 변환해야 한다.
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 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();
}
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();
}
// toEntity()
// User 엔티티를 생성한다.
// OAuthAttributes 에서 엔티티를 생성하는 시점 == 처음 가입할 때
// 가입할 떄의 기본 권한을 GUEST로 주기 위해서 role 빌더 값에는 Role.GUEST를 설정한다.
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
- 가입할 떄의 기본 권한을 GUEST로 주기 위해서 role 빌더 값에는 Role.GUEST를 설정하므로 처음 로그인하면 글쓰기 권한이 없으므로 글을 쓸 수 없다.
// SessionUser 에는 인증된 사용자 정보만 필요하다.
// 그 외의 정보는 필요 없으니 name, email, picture만 필드로 선언한다.
@Getter
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
@Builder
public SessionUser(User user) {
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
}
<!--로그인 기능 영역-->
{{#userName}}
Logged in as : <span id="user">{{userName}}</span>
<a href="/logout" class="btn btn-info active" role="button">Logout</a>
{{/userName}}
{{^userName}}
<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
<a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
{{/userName}}
a href="/logout"
- 스프링 시큐리티에서 기본적으로 제공하는 로그아웃 URL이다
- 개발자가 별도로 logout URL에 해당하는 컨트롤러를 만들 필요가 없다
a href="/oauth2/authorization/google"
- 스프링 시큐리티에서 기본적으로 제공하는 로그인 URL이다.
- 마찬가지로 별도의 컨트롤러를 만들 필요가 없다.
IndexController 클래스
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
// Model 객체
// 서버 탬플릿 엔진에서 사용가능한 객체를 저장할 수 있다.
// 해당 메소드에서는 postsService.findAllDesc()로 가져온 결과를 posts라는 이름으로 index.mustache에 전달한다.
@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
SessionUser user = (SessionUser) httpSession.getAttribute("user");
// 세션에 저장된 값이 있을 경우만 model에 userName으로 등록한다.
if (user != null) {
model.addAttribute("userName",user.getName());
}
return "index";
}
}
- 이부분은 세션값을 가져오는 부부이다.
- index() 메소드 이외의 컨트롤러에서 세션값이 필요하면 그때마다 직접ㅂ 세션에서 값을 가져와야할것이다.
- 같은 코드가 계속 반복된다
- 메소드의 파라미터로 세션값을 바로 받을 수 있도록 코드를 개선해야한다 (어노테이션 기반으로 개선하기)
// @Target(ElementType.PARAMETER)
// 해당 어노테이션이 생성될 수 있는 위치를 지정한다.
// PARAMETER로 지정했으므로 메소드의 파라미터로 선언된 객체에서만 사용가능하다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
// @interface
// 이 파일을 어노테이션 클래스로 지정한다.
public @interface LoginUser {
}
@RequiredArgsConstructor
@Component
// LoginUserArgumentResolver가 스프링에서 인식될 수 있도록 WebMvcConfigurer 에 추가한다.
// WebConfig 클래스를 생성하여 추가한다.
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
// supportsParameter()
// 컨트롤러 메소드의 특정 파라미터를 지원하는지 판단한다.
// parameter에 @LoginUser 어노테이션이 붙어있고, 파라미터 클래스 타입이 SessionUser.class 인 경우만 true를 반환한다.
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
boolean isUserClass = SessionUser.class.equals(parameter.getParameterType());
return isUserClass && isLoginUserAnnotation;
}
// resolveArgument()
// 파라미터에 전달할 객체를 생성한다.
// 여기서는 세션에서 객체를 가져온다.
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return httpSession.getAttribute("user");
}
}
- HttpSession 인터페이스는 둘 이상의 page request에서 사용자를 식별하거나, 웹 사이트를 방문하고 해당 사용자에 대한 정보를 저장하는 방법을 제공한다.
- httpSession.getAttribute("user");
- "user"라는 key로 바인딩된 세션을 반환한다. 만약 없다면 null을 반환한다.
개선된 결과
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
// Model 객체
// 서버 탬플릿 엔진에서 사용가능한 객체를 저장할 수 있다.
// 해당 메소드에서는 postsService.findAllDesc()로 가져온 결과를 posts라는 이름으로 index.mustache에 전달한다.
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user) {
model.addAttribute("posts", postsService.findAllDesc());
// 세션에 저장된 값이 있을 경우만 model에 userName으로 등록한다.
if (user != null) {
model.addAttribute("userName",user.getName());
}
return "index";
}
}
SessionUser user = (SessionUser) httpSession.getAttribute("user");
코드의 중복을 개선하였다.dependencies {
// JPA 가 자동으로 세션 저장용 테이블을 생성하도록 하기위한 의존성
compile('org.springframework.session:spring-session-jdbc')
}
spring:
session:
store-type: jdbc
# spring.session.jdbc.initialize-schema: always 설정이 있어야만
# JPA 가 자동으로 세션 저장용 테이블을 생성한다.
jdbc:
initialize-schema: always