< 스프링 시큐리티 (Spring Security) >
막강한 인증(Authentication)과 인가(Authorization) (권한부여)를 가진 프레임워크
스프링부터 2.0에서부터 크게 변경되었다.
1.5는 많이 설정하지만 2.0버전에서는 client인증정보만 입력하면 된다.
ex)
spring.security.oauth2.client.registration.google.client-id= 인증정보
spring.security.oauth2.client.registration.google.client-secret=인증정보CommonOAuth2Provider 라는 enum이 추가되어 구글,깃허브,페이스북,옥타의 기본설정값은 모두 여기서 제공
이외에 다른 소셜 로그인(네이버,카카오 등) 은 직접 다 추가해야한다.
< 구글 서비스 등록>
구글 서비스에 신규 서비스를 생성하여 발급된 인증정보(clientId, clientSecret)을 통해 로그인 기능과 소셜 서비스 기능을 사용 할 수 있다
https://console.cloud.goole.com에 가서 프로젝트 선택
새 프로젝트
프로젝트 이름 생성
API 및 서비스 > 사용자 인증정보 > 사용자 인증정보 만들기 버튼
OAuth 클라이언트ID 만들기 > 동의화면구성 버튼
OAuth 동의화면 탭에서 애플리케이션 이름 작성
OAuth 클라이언트 ID 만들기 > 웹 어플리케이션 에 체크
승인된 리디렉션 URI 확인
http://localhost:8080/login/oauth2/code/google
서비스에서 인증정보를 주었을 때 성공하면 구글에서 리다이렉트할 URL
기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드} 로 URL지원
저장 버튼 클릭 후 프로젝트 이름과 , 클라이언트ID, 클라이언트 보안비밀 확인
< 소스 ( 키 등록 및 User 엔티티 관련 ) >
src/main/resources/application-oauth.properties
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
application.properties 에 application-oauth.properties 설정 파일을 포함하도록 추가
spring.profiles.include=oauth
보안 파일이니까 .gitignore 에 소스 추가
application-oauth.properties
domain/user/User : 사용자 정보를 담당할 도메인 (Entity)
package com.anse.book.springboot.domain.user;
import com.anse.book.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
public class User extends BaseTimeEntity {
strategy = GenerationType.IDENTITY) (
private Long id;
nullable = false) (
private String name;
nullable = false) (
private String email;
private String picture;
// 기본적으로 JPA로 DB에 저장될때 Enum 은 int로 저장
// 숫자로 저장되면 DB로 확인할때 값의 의미를 정확히 알지 못하므로
// 문자열로 저장할 수 있게 함
EnumType.STRING) (
nullable = false) (
private Role role;
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();
}
}
각 사용자의 권한을 관리할 Enum 클래스 : domain/user/Role
package com.anse.book.springboot.domain.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
public enum Role {
// 스프링 시큐리티에서는 권한코드에 항상 ROLE_이 앞에 붙어야 함
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
domain/user/UserRepository : User의 CRUD Interface
package com.anse.book.springboot.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// 소셜 로그인으로 반환되는 값 중
// email을 통해 이미 생성된 사용자인지, 첫가입하는 사용자인지
// 판단하기 위한 메서드
Optional<User> findByEmail(String email);
}
< 소스( 스프링 시큐리티 설정) >
build.gradle 에 스프링 시큐리티 의존성 추가
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
config/auth/SecurityConfig : OAuth 라이브러리를 이용한 소셜 로그인 설정 코드
package com.anse.book.springboot.config.auth;
import com.anse.book.springboot.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;
// Spring Security 설정들을 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable().headers().frameOptions().disable() // 1
.and()
.authorizeRequests() // 2
.antMatchers("/","/css/**","/images/**","/js/**","/h2-console/**").permitAll() // 3
.antMatchers("/api/v1/**").hasRole(Role.USER.name()) // 4
.anyRequest().authenticated() // 5
.and()
.logout()
.logoutSuccessUrl("/") // 6
.and()
.oauth2Login() // 7
.userInfoEndpoint() // 8
.userService(customOAuth2UserService); // 9
/*
* 1. h2-console화면을 사용하기 위해 해당 옵션들을 disable
* 2. URL별 권한 관리를 설정하는 옵션의 시작점, 선언되야만 antMatcher를 사용할 수 있다.
* 3. permitAll()로 전체 권한 줌
* 4. USER 권한을 가진 사람만 가능
* 5. 나머지 URL들은 모두 인증된 사용자들에게만 허용(로그인 사용자)
* 6. 로그아웃 성공시 "/" 주소로 이동
* 7. Oauth2 로그인 기능에 대한 설정의 진입점
* 8. Oauth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
* 9. 소셜로그인 성공시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록
* 리소스 서버(소셜서비스)에서 사용자 정보를 가져온 상태에서
* 추가로 진행하고 하는 기능을 명시할 수 있음
*
*/
}
}
config/auth/CustomOAuth2UserService
구글 로그인 이후 가져온 사용자의 정보를 기반으로 가입 및 정보수정, 세션 저장 등의 기능을 지원
package com.anse.book.springboot.config.auth;
import com.anse.book.springboot.config.auth.dto.OAuthAttributes;
import com.anse.book.springboot.config.auth.dto.SessionUser;
import com.anse.book.springboot.domain.user.User;
import com.anse.book.springboot.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;
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
//서비스구분하기 위한 코드 ,ex)네이버로그인인지 구글로그인인지
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 진행시 키가 되는 필드값 , primary key와 같은 의미
// 구글은 기본적인 코드를 지원하지만 ("sub"), 네이버 카카오등은 지원하지 않는다
// 이후 네이버,구글 로그인을 동시 지원할 때 사용
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
// SessionUser : 세션에 사용자정보를 저장하기 위한 Dto 클래스
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);
}
}
config/auth/dto/OAuthAttributes : DTO
package com.anse.book.springboot.config.auth.dto;
import com.anse.book.springboot.domain.user.Role;
import com.anse.book.springboot.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
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) {
return ofGoole(userNameAttributeName, attributes);
}
private static OAuthAttributes ofGoole(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();
}
// 첫 가입 시점에 User 엔티티를 생성
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST) //가입할때 기본원한을 GUEST
.build();
}
}
config/auth/dto/SessionUser : 인증된 사용자 정보
User 클래스는 엔티티 클래스로 직렬화를 구현하지 않았다
그래서 직렬화기능을 가진 세션 Dto를 하나 추가로 만드는 것이 운영,유지보수에 좋다
package com.anse.book.springboot.config.auth.dto;
import com.anse.book.springboot.domain.user.User;
import lombok.Getter;
import java.io.Serializable;
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();
}
}
< 소스 (로그인 테스트) >
web/IndexController
import javax.servlet.http.HttpSession;
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
"/") (
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
// CustomOAuth2UserService에서 로그인 성공시 세션에 SessionUser를 저장
// 즉, 로그인 성공 시 httpSession.getAttribute("user")에서 값을 가져올 수 있다
SessionUser user = (SessionUser) httpSession.getAttribute("user");
// 세션에 값이 있을때만 model 에 userName 등록
if(user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
}
resoources/templates/index.mustache
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글등록</a>
{{#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>
{{/userName}}
</div>
</div>{{#userName}}
머스테치는 if를 제공하지 않는다. true/false 여부만 판단할 뿐
그래서 {{#userName}} 으로 값이 있을 때만 노출
{{^userName}}
해당값이 존재하지 않는 경우 안에 있는 태그 노출
a href="logout"
스프링 시큐리티에서 기본적으로 제공하는 로그아웃URL
개발할 필요 없다 (SecurityConfig에서 URL을 변경할 수 도 있다)
a href = "/oauth2/authorization/google"
스프링 시큐리티에서 기본적으로 제공하는 로그인 URL
logout과 마찬가지