origoni's Blog from Millky

origoni의 스프링 블로그 입니다.

[블로그개발_12] 소셜 로그인 붙이기 (spring-social-facebook)

블로그개발 시리즈 - 다른글 : http://millky.com/@origoni/folder/30/post/list

라이브 데모 : http://blog.millky.com/post/list

자바 웹 개발 시작하기 : http://www.slideshare.net/origoni/presentations




지난 시간에 글 수정 삭제 기능을 붙여 보았다.

원래는 소셜 로그인은 좀 뒤에 진행하려 했었다.. 주인 아이디 비번만 하드코딩하고.... 가려 했지만. 너무 없어보이는 나머지... 댓글 부분에서나 도입하려 했던 소셜로그인을!!

오늘은 간단하게 넣어보자. ^^


이번 글은

글과 코드를 참고했다.


라이브 데모 : http://blog.millky.com/post/list

오늘 작업 코드 : https://github.com/origoni/Spring-Blog/releases/tag/v0.0.12


그럼 시작해보자.



pom 설정


우선 pom.xml 에 스프링 소셜 라이브러리를 추가해보자.


    org.springframework.social
    spring-social-facebook


아래와 같이 추가 해도 된다.


    org.springframework.boot
    spring-boot-starter-social-facebook


차이가 뭘까?



보면 결과적으로 같다. 우리가 이미 spring-boot-starter-web 을 사용하기 있기 때문이다.

(다르게 말하면 .. spring-boot-starter-social-facebook을 사용하면 spring-boot-starter-web 이 없어도 된다는 사실!)

라이브러리 디펜던시 부분은 이클립스 pom.xml에서 아패 Dependency Hierarchy 탭을 확인해보기 바란다.

[블로그개발_08] 글에 해당 이슈가 언급되어 있다.


다음으로 스프링 시큐리티리 라이브러리를 추가해보자.


    org.springframework.boot
    spring-boot-starter-security

시큐리티의 경우 코어와 웹 두가지를 사용해야 해서... 그냥 부트 스타터로...


스프링 소셜 사용을 위한 라이브러리가 준비되었으면 이제 키를 받아와야 한다.




페이스북 등록


우선 페이스 북에 어플리케이션을 등록해야 한다.

http://spring.io/guides/gs/register-facebook-app/ 요 가이드에 설명이 있긴 한데.. 옛날 버전이고 지금은 화면이 많이 바꿔있다.

당연히.. 페이스북에 가입되어있는 상황에서 http://developers.facebook.com 에 방문하면


위와 같은 화면을 볼 수 있다. (내가 이미 등록되어있고 앱이 있어서 좀 다를수도 있다... 만약 최초 등록 부분이 나온다면. http://spring.io/guides/gs/register-facebook-app/ 글을 참고 부탁드린다.)

My Apps -> Add a New App 로 들어가자.

우리는 일단 웹으로 보는 블로그를 만들 예정이니 웹사이트를 선택하자.

이름을 정하는데 우리는 SpringBlog를 만들고 있으니 그렇게 적어보자.

다른 것의 테스트가 아니니 그냥 두고. 분류는 커뮤니케이션으로 하겠다.

자바스크립트 설정 부분이 나오는데 우리는 스프링 소셜을 사용할 예정인 아래 웹사이트 정보를 적자.

우리가 만들고 있는 스프링 블로그는 반응형 웹이니 PC나 모바일 사이트 같은 주소를 적어주면 되겠다.

이제 여기 발급된 App ID와 App Secret 을 스프링 부트 설정파일에 정의해야 한다.


application.yml 파일에 소셜 부분을 추가해 보자.

spring:
    view:
        prefix: /WEB-INF/jsp/
        suffix: .jsp
    social:
        facebook:
            appId: {App ID} 이부분은 각자 발급받은 값으로 넣어주시면 됩니다.
            appSecret: {App Secret} 이부분은 각자 발급받은 값으로 넣어주시면 됩니다.


여기까지 지정이 되었으면 페이스북 등록은 끝났다.

참고로.. 테스트 앱을 만들면 로컬 호스트에서 쉽게 테스트 가능하다. (yml 파일 --- 로 나누는건.. 나중에...)




스프링 시큐리티


이번에는 시큐리티 세팅을 해보자.

스프링 시큐리티에 대한 부분은 오늘 주제에서 넘어가는 부분이라 나중에 다시 설명하도록 하겠지만 간단하게 보면. 정말 그냥 영어 읽으면 기본적인 답은 보인다.


코드를 보자.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.formLogin()
				.loginPage("/user/login")
			.and()
				.logout()
					.logoutUrl("/user/logout")
					.deleteCookies("JSESSIONID")
					.logoutSuccessUrl("/post/list")
			.and()
				.authorizeRequests()
					.antMatchers("/**/write*", "/**/edit*", "/**/delete*").authenticated()
					.antMatchers("/**").permitAll();
	}
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/console/**");
	}
}

HttpSecurity 세팅 부분은. 로그인 URL 정의. 그리고 로그아웃시 자바 세션 쿠키 삭제하고 URL과 돌아갈 URL도 정의. 권한을 write, edit, delete가 포함되는 URL일때 보고 나머지 주소는 열려있다... 라고 써있다. (사실 기본 로그인 URL은 signin 이다... 이부분은 기본값으로 쓰는것이 여러모로? 나을 수도 있다 일단은 이렇게 가고.. 나중에 상황을 지켜보자.)

그리고. WebSecurity 부분은. console 로 시작하는 부분은 그냥 무시하도록 한다. 이부분은 08번글에서 설명한 h2console 때문에 들어갔다.

스프링 시큐리티는 기본적으로 CSRF 방어가 활성화 되어있다. CSRF도.. 오늘 범위에 벗어나니.. 일단 검색 부탁드린다. (나중에!!!! 다 설명해 보겠다 ㅠㅠ 언제 ㅠㅠ)




스프링 소셜


우선은 페이스북 한곳만 연동 한다고 생각하고 좀 간단하게 진행해 보겠다. (다음 다음에 트위터 등 다른곳도 넣도록 하겠다.)

먼저 설정을 보면.

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
	@Override
	public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
		InMemoryUsersConnectionRepository repository = new InMemoryUsersConnectionRepository(connectionFactoryLocator);
		repository.setConnectionSignUp(new QuickConnectionSignup());
		return repository;
	}
	private static class QuickConnectionSignup implements ConnectionSignUp {
		@Override
		public String execute(Connection connection) {
			return connection.getKey().getProviderUserId();
		}
	}
	@Bean
	public SignInAdapter signInAdapter() {
		return new QuickSignInAdapter(new HttpSessionRequestCache());
	}
}

사용자 정보를 저장하는곳을 우선 메모리에 할 것이다. 그리고 providerUserId 키로 바로 가입을 진행한다.


다음으로 SignInAdapter 를 구현해보자.

public class QuickSignInAdapter implements SignInAdapter {
	private final RequestCache requestCache;
	@Autowired
	public QuickSignInAdapter(RequestCache requestCache) {
		this.requestCache = requestCache;
	}
	@Override
	public String signIn(String localUserId, Connection connection, NativeWebRequest request) {
		String providerUserId = connection.getKey().getProviderUserId();
		signin(providerUserId);
		return extractOriginalUrl(request);
	}
	private String extractOriginalUrl(NativeWebRequest request) {
		...
	}
	private void removeAutheticationAttributes(HttpSession session) {
		...
		session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
	}
	private void signin(String userId) {
		SecurityContextHolder.getContext().setAuthentication(
				new UsernamePasswordAuthenticationToken(userId, null, null));
	}
}

이름처럼 로그인은 간단한게 providerUserId를 사용하여 로그인 한다.

나머지 부분은 로그인 전 URL을 기억했다가 로그인 후 돌려주는 부분이다. (우선 오버라이드 된 부분만 보자.)


마지막으로 로그인에 관련된 컨트롤러를 만들어보자.

@Controller
public class UserController {
	@RequestMapping(value="/user/login", method=RequestMethod.GET)
	public String login() {
		return "connect/login";
	}
}

매우 간단하다.

/user/login 로 들어오면 connect/login 으로 가서 로그인을 하라는것이다.


그럼 JSP파일을 보자.

핵심은 아래 부분이다. /signin/facebook으로 POST 질의를 날리면 로그인이 된다.

You aren't connected to Facebook yet. Click the button to connect this application with your Facebook account.


한가지.. 스프링 시큐리티 덕분에 보안이 강화 되어

이부분이 추가된다. 앞으로 모든 포스트 리퀘스트에 추가 할 것이다.

csrf 다들 검색해보셨죠?




POST


요렇게 간단하게(? 소셜 로그인 치고는 간단하다!) 설정이 되었으면. 이제 글 쓰기, 수정, 삭제 부분을 보자.


우선 Post Entity 를 보자

String userId;
String name;
...
String _csrf;


위의 사용자 아이디와 이름 저정하는 부분과 csrf토큰 저장하는 부분이 추가되었다.

사용자 이름이 name로 정해졌으니...

post.jsp, list.jsp 에서 이름 나오는 부분을 Origoni 에서 ${post.name} 로 변경하자


다음으로 컨틀롤러 글쓰기 부분을 보자.

@RequestMapping(value = "/write", method = RequestMethod.POST)
public String write(@Valid Post post, BindingResult bindingResult) {
	User user = getConnect();
	if (bindingResult.hasErrors()) {
		return "form";
	}
	post.setRegDate(LocalDateTime.now());
	post.setUserId(user.getProviderUserId());
	post.setName(user.getDisplayName());
	return "redirect:/post/" + postDao.save(post).getId();
}

보면 getConnect()를 이용해 사용자 정보를 가지고 오고.

글 작성 시 해당 정보를 넣어서 저장한다.

private User getConnect() {
	Connection connection = connectionRepository.findPrimaryConnection(Facebook.class);
	if (connection == null) {
		return null;
	}
	ConnectionData data = connection.createData();
	return new User(data.getProviderUserId(), data.getDisplayName());
}

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class User {
	String providerUserId;
	String displayName;
}

getConnect 는 connectionRepository 에서 페이스북 연결정보를 가지고 온다.


삭제를 보면.

@RequestMapping("/{id}/delete")
public String delete(@PathVariable int id) {
	User user = getConnect();
	Post post = postDao.findOne(id);
	if (post.getUserId().equals(user.getProviderUserId())) {
		postDao.delete(id);
	}
	return "redirect:/post/list";
}

이런식으로 사용자 정보를 가지고 와서 지금 지우려는 글이 정말 그 사용자가 작성한 글인지 확인하고 맞으면 지워준다.


수정도 비슷하다.

@RequestMapping(value = "/{id}/edit", method = RequestMethod.POST)
public String edit(@Valid Post post, BindingResult bindingResult) {
	User user = getConnect();
	if (bindingResult.hasErrors()) {
		return "form";
	}
	Post oldPost = postDao.findOne(post.getId());
	if (oldPost.getUserId().equals(user.getProviderUserId())) {
		oldPost.setTitle(post.getTitle());
		oldPost.setSubtitle(post.getSubtitle());
		oldPost.setContent(post.getContent());
		return "redirect:/post/" + postDao.save(oldPost).getId();
	}
	return "form";
}

사용자 정보를 확인하고 맞으면.. 제목과 부제목과 내용만 수정한다.


그리고 날짜 부분을 좀 수정해야 한다. 지난시간 날짜 부분에 잘못 설명한 부분 좀 있다.

지난시간 처럼 하면 DB에 Date 타입으로 저장되는것이 아니었다.....

아직 JPA가 Java8 의 time 부분을 지원하지 못한다...

http://stackoverflow.com/questions/23718383/jpa-support-for-java-8-new-date-and-time-api

그래서 링크에 나온 것 처럼 해당 부분을 변환해줄 수 있는 LocalDateTimeConverter 를 만들었다.

@Slf4j
@Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter, Date> {
    @Override
    public Date convertToDatabaseColumn(LocalDateTime date) {
    	log.debug("date = {}", date);
        Instant instant = Instant.from(date.atZone(ZoneId.systemDefault()));
        return Date.from(instant);
    }
    @Override
    public LocalDateTime convertToEntityAttribute(Date value) {
    	log.debug("value", value);
        Instant instant = value.toInstant();
        return LocalDateTime.from(instant.atZone(ZoneId.systemDefault()));
    }
}


convertToDatabaseColumn, convertToEntityAttribute 를 구현해 줘야 한다. 말 그대로 DB에 들어갈때 나올때 컨버팅 해주는 역할을 한다...


추가로. 지난번에 Date가 이상하게 나와 넣었던...

@DateTimeFormat(iso = ISO.DATE_TIME)


저장시 에러나서 넣었던 (이건 값이 없어서 그런거였는데...)

 

부분들 제거하였다.


날짜가 간단해 보이지만 정말 어려운 부분중에 하나이다.

거기다가 여러 나라에 하나의 플랫폼을 서비스를 하다 보면 고민할것도 많다.

스프링 블로그도 나중에 다국어와 여러나라에서 글을 쓸때 날자를 어떻게 하면 좋을지 고민해서 만들어보자 (아직 밀키에도 해당부분이 잘 적요이 안되어 있다.)


날짜.. 표시 부분도 jsp fmt 사용이 안된다...

http://stackoverflow.com/questions/28516766/jstl-formatdate-and-java-time-localdate


그래서 아래처럼 작업해야 한다;;; (그냥 참고만.. 코드에 적용되지 않았다.)





날짜를 저장할때 작성자의 타임존을 저장해야.......

이부분은 나중에 다시 이야기 해보자.




테스트


여기까지 작업이 되었으면. 이제 프로젝틀를 돌려보자~

글쓰기 버튼을 누르면.



로그인 할 수 있는 버튼이 있다. 해당 버튼을 눌러 로그인이 가능하다.


처음 로그인하면 위와 같은 화면을 만날 수 있다. 확인 을 누르면 바로 글을 작성할 수 있다.


다른 사람것을 지워보자.

일단 로컬에서는 혼자 테스트 할 수 없어서 ^^; 와이프 계정을 빌려서 테스트 해 보았다.



다른사람 글을 수정하려면.. 


이렇게 나온다. 사실 다른 사람 글은 버튼 자체가 안보여야 하는건데 ㅎㅎ 그부분은... 나중에 해결하겠다~




로그아웃


마지막으로 로그아웃 버튼을 넣겠다.

사용자 정보가 없으면... 로그아웃 버튼이 보이고. 누를 수 있게 되어있다. (POST방식이기에 _csrf도 꼭 넣자)


	
	


정보는 포스트 컨트롤러에서

@RequestMapping("/list")
public String list(Model model) {
	...
	User user = getConnect();
	model.addAttribute("user", user);
	return "list";
}
@RequestMapping("/{id}")
public String view(Model model, @PathVariable int id) {
	...
	User user = getConnect();
	model.addAttribute("user", user);
	return "post";
}


이렇게 주면 된다.

그리고 로그아웃버튼을 누르면 글 리스트로 돌아오게 된다. 왜 리스트로 돌아오는지는 위에 설명이 있다~


끝!




오늘 소셜로그인(페이스북 연동)과 날짜.. 등에 대해 알아보았다.

쉬운부분이 아닌데.. 일단 쉽게 보이게 하고 술렁술러 넘어왔다.

아직 부족한 부분이 많이 있지만 차차 개선할 것이니 걱정하지 말자~




너무 목표없(어보이게)이 글을 쓰고 있나 해서 점검을 좀 해본다.

책 보고 이해는 되는데 실제 어떻게 만들어야 하지?

인터넷 튜토리얼들은 너무 기초적인 것만 있네.. 더 나아가려면.. 어떻게 해야 하지?

라는 물음에 답을? 찾기 위해... 글을 쓰기 시작했다. 그래서 실용적인 예제를 만들어 보자. 로 블로그롤 만들어 보자. 로..//(라고 하지만.. 밀키 를 오픈소스화 해보고싶은 욕심? 목적? 도 있었고... 사실 이게 더 크다고 고백해본다. 지금은 초기라.. 내용도 없고 구조도 이상해 피드백 받을것도 없지만. 나중에 좀 정리가 되고 하면. 다른분들과 같이 만들어 가게 될 수 있지 않을까? 포크, 푸시 부탁드려요 ㅠ)

이 글을 쓰기 시작할때 생각했던 블로그 모습은.

  • 기본적인 블로그 뷰.
  • 글쓰기, 수정, 삭제. 페이징.
  • 댓글, 답댓글, 수정?, 삭제. (소셜로그인을 이용)
  • 카테고리.
  • 태그.
  • 간단한 검색. (DB)
  • RSS 뷰.
  • 국제화(i18n 다국어 지원).


이정도였다. 이정도 되면 블로그라고 불러볼 수 있을까 해서다 ^^;

사실 이런 리스팅은 외형적인 부분이고.. 스프링으로 웹을 개발하면서 접할 수 있는 상황을 풀어 가 보자는 것이다. 지금은 위에 부분만 보여지지만.. 차차 기능을 추가하면서 내부 모습도 보게 될 것이다. (탑 다운 접근 방식. 요방식에 대한 이야기도 나중에 해 볼 시간이 있을 듯~)

거기다 기왕이면 모던 자바(8) 를 사용하는 것도 넣어보고... (그런데 만들다보니.. 비지니스 로직이 아직은 별로 없다보니 Java8의 정말 좋은 점인 람다, 스트림등을 사용할 일이 없네;; ㅋ 회사에서 요즘 프로젝트 하는데.. 람다, 스트림 없이 이전에 어떻게 개발했나 싶을 정도다;;; 뭐 만들다보면 분명 이야기 할 날이 올 것이다.)


보면 이제 가장 기본적인 글쓰기 정도가 되었으니 앞으로도 갈길이 멀구나.

우선 위에 보이는것들이 마무리 되면.

  • 간단한 캐싱. (EHCache ?)
  • DB를 MySQL로.. (사실 이부분은 JPA사용중이라.. 간단할 수도 있지만. 아니기도 하다 ^^;)
  • 호트팅 받아서 올려보기...
  • 검색엔진 사용 (ES)....

요런 시스템 적인 부분도 진행해 봐야겠다.




감사한 분이 이런 글을 적어 주셨다.



제가 더 감사 드린다. 정말 감사합니다.


오늘은 여기까지 ^^




오늘 작업한 코드는 https://github.com/origoni/Spring-Blog/tree/v0.0.12 여기를 참고 부탁드린다.


라이브 데모 : http://blog.millky.com/post/list

변경사항은 : https://github.com/origoni/Spring-Blog/commit/b5ad3ed528f685bd785532c7af047333a4e28bfc

깃헙 : https://github.com/origoni/Spring-Blog

프로젝트 다운받아 시작하는 방법은(for Windows User)? http://millky.com/@origoni/post/1145

블로그개발 시리즈 - 다른글 : http://millky.com/@origoni/folder/30/post/list

역순 정렬도 있어요.. : http://millky.com/@origoni/folder/30/post/list?sort=regDate,asc






origoni 2015-06-28 10:36:30

글 작성한지 일주일이 지나서야 발견을 했네요. ㅠㅠ
라이브 데모에 정상적으로 글을 쓸 수 없는 버그가 있었습니다.
코드상의 버그는 아니고 제가 페이스북 설정을 잘못 하였습니다.

https://developers.facebook.com/apps/{AppID}/review-status/ 페이지에서
- Do you want to make this app and all its live features available to the general public? 요걸 Yse로 변경해야 합니다. (기본값이 No 입니다.)

지난주 회사일이 바빠서 따로 모니터링 못해서 ㅠㅠ

sisi님 제보 감사합니다.

코드 보셔서 아시겠지만 제가 따로 저장하는 정보는 없습니다.
페이스북에서 주는 providerUserId (프로바이더 사용자 아이디)의 경우 서비스 마다 다르기 때문에. 따로 사용할 수 없습니다. ^^;
많은 테스트 부탁드립니다.
(전 글이 없어서.. 혹시 정보 유출이 불안하셔서 안쓰시나 하고 생각했네요;;;)

다음글은 7월 2주차에 더욱 좋은 내용으로 찾아 뵙겠습니다.

감사합니다.

origoni 2015-06-28 10:58:56

라이브 데모 : http://blog.millky.com/post/list

gaad 2016-06-16 20:43:51

ggggjhjhj

seungdols 2015-09-09 16:29:57

궁금한 점이 있다면, 소셜 로그인 말고 기본 로그인 처리는 강좌에서 빠지는 건가요? - 밀키 버그라면, 로그인 안하고 그냥 댓글 쓰기시 CSRF 에러 페이지가 뜨는데 redirect 처리 해주시는 것을 깜빡하신 것인지 궁금합니다.
back to top