오늘은 간편로그인 구현. 이부분을 하려고 합니다. 전에 로그인구현의 내용을 다지우고... 구글 간편로그인. 이런 것으로 바꾸려고 하네요. 참조한 책은 이 책입니다.
스프링부트 aws – Daum 검색
Daum 검색에서 스프링부트 aws에 대한 최신정보를 찾아보세요.
search.daum.net
스프링 부트와 AWS로 혼자 구현하는 웹서비스' 라는 책입니다. 스프링부트 JPA의 기본적인 내용, 무중단 배포하는 방법등, 웹 서비스를 만들어나가기 위한 작업들이 잘 기록되어 있기 때문에, 실제 이 책의 내용으로 공부하는 것을 추천하네요.
간편로그인 구현. ...
로그인 할때에, 회원정보에서 각 역할정보를 따로 실을 수 있어요. Spring Boot의 기능이군요. Role이라는 칼럼이 있는데, ADMIN이라고 하면, 관리자. 그렇게 정해진 것은 없구요. 이런 식으로 역할별 네이밍을 해주는 것이죠.
ADMIN, COMPANY, USER 이렇게 3가지 롤을 만들어주기로 합시다. 그리고, 로그인 한 후, 페이지가 각각 다르겠죠.
관리자, 기업회원, 회원. 이렇게 구분해보기로 합시다. 그리고, 우선은 간편로그인을 만들기 위한 작업을 해주어야겠죠~.
구글 서비스 등록을 해줌니다.
https://console.cloud.google.com/ 여기에서, 프로젝트 선택.
적당한 이름으로 프로젝트 이름을 적은 뒤, 만들어 줌니다. 그런 후에, API 및 서비스 메뉴를 클릭해서 이동 해줌니다.
사용자 인증정보를 클릭해서 이동해줌니다.
OAuth 동의 화면, 외부로 만들어 줌니다.
별표 쳐진 부분만 적으면서 죽 이어 적습니다.
범위는 여기까지만 선택해줌니다. 책에 나온 범위를 선택했네요.
저장 후 계속. ...
테스트 사용자는 인증 전까지 100명까지 사용가능하다네요. 입력하지 않고, 저장 후 계속 버튼을 눌려서 완료해 줌니다.
그 다음, 사용자 인증 정보를 만듬니다. 사용자 인증 정보에서, OAuth 클라이언트 ID.
웹 애플리케이션을 선택하고, 이름에는 프로젝트 이름을 적어 줌니다. 아래로 내려와서, 승인된 리디렉션 URI에 다음과 같이 적어 줌니다.
http://localhost:8080/login/oauth2/code/google
이렇게 생성하게 되면, 클라이언트ID와 비밀번호를 생성해 주는데요.
이것을 application-oauth.properties 파일에 등록해줌니다. 파일을 만들어야겠죠~. src/main/resources/ 디렉토리에 만들어 줌니다.
spring.security.oauth2.client.registration.google.client-id= 클라이언트id
spring.security.oauth2.client.registration.google.client-secret= 클라이언트 비밀번호
spring.security.oauth2.client.registration.google.scope= profile,email
클라이언트 id와 비밀번호를 적어주세요.
application.properties 파일에 이 한줄을 적어줌니다. 그럼으로서, application-oauth.properties파일을 접근할 수 있게 됨니다.
spring.profiles.include=oauth
oauth2를 사용가능하게 한 클라이언트 id, 클라이언트 비밀번호는 Github에 올라가면 안되기 때문에, .gitignore파일에 한줄의 파일을 추가해줌니다.
application-oauth.properties
구글 로그인 연동하기.
사용자 정보를 담을 User 클래스를 만들어 줌니다. 그 다음, Role, Enum 클래스도 만들어 줌니다. UserRepository 클래스 생성.
User 클래스. ...
package org.example.domain.user;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.example.domain.BaseTimeEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@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 클래스. ...
package org.example.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;
}
UserRepository.java 파일. ...
package org.example.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);
}
이제 시큐리티 설정. ...
build.gradle 파일에 의존성 설정을 추가해줌니다.
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
config.auth 패키지를 만들어 줌니다. SecurityConfig 클래스를 생성해 줌니다. config.auth.SecurityConfig
package org.example.config.auth;
import lombok.RequiredArgsConstructor;
import org.example.domain.user.Role;
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
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/**", "/profile").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
CustomOAuth2UserService 클래스도 만들어 줌니다.
package org.example.config.auth;
import lombok.RequiredArgsConstructor;
import org.example.domain.user.User;
import org.example.domain.user.UserRepository;
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 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);
}
}
임포트를 해야하는 OAuthAttributes, SessionUser클래스도 만들어 줌니다.
패키지, config.auth.dto.OAuthAttributes 클래스.
package org.example.config.auth.dto;
import lombok.Builder;
import lombok.Getter;
import org.example.domain.user.Role;
import org.example.domain.user.User;
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) {
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();
}
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
패키지, config.auth.dto.SessionUser 클래스.
package org.example.config.auth.dto;
import lombok.Getter;
import org.example.domain.user.User;
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();
}
}
이제, html페이지에 로그인 버튼과 로그인 성공시 사용자 이름을 보여주는 코드를 만들어 줌니다.
html파일이름을 index_log_leaf.html 로 만들었고, 타임리프 형식입니다. 책의 내용과 다르네요. 참조할 점은, model의 값. 뷰페이지로 데이터를 넘길때에 값의 이름을 ${}이런 값으로 타임리프에서는 인식하는데요. 머스테치에서는, {{#userName}}. 이렇게 표현해줌니다.
index_log_leaf.html 파일.
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/css/nav.css">
<title>오늘, 일 하시는 것은 어떤가요. 좋은 하루되세요.</title>
<style>
.navbar-brand {
font-size: 1rem;
}
.card {
margin-bottom: 10px;
}
.nav_bottom {
margin-bottom: 40px;
}
</style>
<script th:inline="javascript">
/*<![CDATA[*/
let result = [[${workPlanList}]]
/*]]>*/
</script>
</head>
<body>
<div class="fixed-bottom">
<nav class="navbar navbar-expand-lg nav1">
<div class="container-fluid">
<a class="navbar-brand" href="#">스마트 팩토리</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon" style="color:black;margin-top:5px;"><i class="bi bi-justify"></i></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a sec:authorize="isAnonymous()" class="nav-link active" aria-current="page" href="#">일 찾아보기</a>
</li>
<li class="nav-item">
<a sec:authorize="isAuthenticated()" class="nav-link" href="#">출근 하기</a>
</li>
<li class="nav-item">
<a sec:authorize="isAuthenticated()" class="nav-link" href="#">마이 페이지</a>
</li>
<li class="nav-item">
<a sec:authorize="isAuthenticated()" class="nav-link" th:href="@{/test/board}">test 게시판</a>
</li>
<li>
<a sec:authorize="isAuthenticated()" class="nav-link">안녕하세요. <span th:text="${userName}"></span>님</a>
</li>
<li>
<a sec:authorize="isAnonymous()" class="nav-link" th:href="@{/oauth2/authorization/google}">Google 로그인</a>
</li>
<li>
<a sec:authorize="isAuthenticated()" class="nav-link" th:href="@{/logout}">로그아웃</a>
</li>
<li>
<a sec:authorize="isAnonymous()" class="nav-link" th:href="@{/signup}">회원가입</a>
</li>
<li class="nav-item" style="display:none" id="email">{{email}}</li>
</ul>
</div>
</div>
</nav>
</div>
<div class="container">
<!-- <h5> A 푸드. 음식. 김치를 만들어보세요.</h5>-->
<br />
<br/>
<th:block th:each="coList: ${coperationLists}">
<h5><span th:text="${coList.coperationName}"></span>, <span th:text="${coList.catchPrice}"></span></h5>
<br/>
<div class="row justify-content-center" >
<div class="col d-flex justify-content-center" th:each="list : ${workPlanLists}" th:if="${coList.coperationName == list.coperation.coperationName}" >
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title" th:text="${list.coperation.coperationName}"></h5>
<p class="card-text" th:text="${list.workPlanTitle}"></p>
<a href="#" class="btn btn-primary">보기</a>
</div>
<div class="card-footer">
<span th:text="${list.workPlanTag}"></span> | <span th:text="${#temporals.format(list.workPlanStartDate, 'yyyy년 MM월 dd일')}"></span> ~
</div>
</div>
</div>
</div>
<br />
<hr />
<br />
</th:block>
</div>
<br />
<br />
<br />
<br />
<nav class="navbar nav_bottom">
<div class="container-fluid">
<div class="navbar-text" href="#">
<i class="bi bi-emoji-smile"></i>
스마트 팩토리, UI 만들어봤습니다. 2021년 6월 21일 ~ 2021 7월 9일., 2022년 12월 29일 ~ ... <br />한번 만들어보세요~. 일하기가 더 좋아졌으면 좋겠습니다. 좋은 개발되세요~.
감사합니다.<br />작성자: 최현일
|
Github주소 <a href="https://github.com/infott2t/smartFactory-ex">@infott2t</a>
<br/>
</div>
</div>
</nav>
</div>
<!-- Optional JavaScript; choose one of the two! -->
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
<script src="/js/jquery-3.6.0.js"></script>
<script>
$(document).ready(function(){
var email = $("#email").text()
setTimeout(function () {
window.ReactNativeWebView.postMessage('{"email" : "'+email+'"}')
}, 2000)
});
</script>
</body>
</html>
그리고, 컨트롤러의 "/"부분을 위의 html 파일과 연결시켜줌니다.
BaseController.java
private final HttpSession httpSession
@GetMapping("/")
public String indexDefault(Model model, HttpSession session){
....
...
SessionUser user = (SessionUser) httpSession.getAttribute("user");
model.addAttribute("userName", user.getName());
return "index_log_leaf"
}
실제 로그인하면, 구글 로그인이 된 것을 확인 할 수 있습니다~~.
실제로 책의 내용을 보고서 하는 것을 추천하네요~.
Github에는 이미 완성된 부분이 올려져 있으니, 코드를 작성하지 않아도 될검니다. 단지 gitignore가 된, application-oauth.properties 파일의 생성과, 구글 간편로그인을 위한 구글 클라우드 등록이 필요할 거예요~.
infott2t/ex05-springboot-querydsl (github.com)
GitHub - infott2t/ex05-springboot-querydsl
Contribute to infott2t/ex05-springboot-querydsl development by creating an account on GitHub.
github.com
공부해보세요~.
--
저의 글, 봐 주셔서 감사합니다.
'프로그래밍' 카테고리의 다른 글
[웹 프로그래밍] 스프링부트JPA 5. 회원 기본정보 확장하기. 연락처와 주소. (2) | 2023.01.07 |
---|---|
[웹 프로그래밍] 스프링부트JPA 4. 세션, Role(역할)에 따라, 페이징해주기. - 롤 역할 별 페이지 뷰 만들기. (2) | 2023.01.05 |
[웹 프로그래밍] 스프링부트JPA 2. 엔티티 심화. 맵핑하고, 타임리프 each문 작성해서 리스트 출력하기. (2) | 2023.01.02 |
[웹 프로그래밍] 스프링부트JPA 1. 데이터를 클래스화 하기. 자동 생성. (2) | 2023.01.01 |
[웹호스팅] 따라해보기 1. 무료 웹호스팅, 클라우드타입. 스프링부트 JPA설치해보자. (0) | 2022.12.30 |