안녕하세요 오늘도 열심히 프로젝트를 개발 중에 있습니다. 최근 매번 개인 프로젝트를 개발할 때마다 인증구현을 해줘야함에 번거로움을 느껴 통합인증(SSO) 시스템을 만드려고 합니다. 이에 먼저 Spring Security를 공부하고자 하는 마음으로 해당 내용을 정리해보았습니다.
Spring Security는 Filter단에서 실행되며 DelegatingFilterProxy단에서 일어나 Spring 프레임워크에서 해당 인증 구현을 할 수 있게 한다. (FilterChain)
JSESSIONID를 톰캣에서 발급해주는데 Spring Security에서 로그인 성공시 이 JSESSONID에 로그인 정보를 담게 된다.
import java.util.Iterator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cos.securityex01.config.auth.PrincipalDetails;
import com.cos.securityex01.model.User;
import com.cos.securityex01.repository.UserRepository;
@Controller
public class IndexController {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping({ "", "/" })
public @ResponseBody String index() {
return "인덱스 페이지입니다.";
}
@GetMapping("/user")
public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principal) {
System.out.println("Principal : " + principal);
System.out.println("OAuth2 : "+principal.getUser().getProvider());
// iterator 순차 출력 해보기
Iterator<? extends GrantedAuthority> iter = principal.getAuthorities().iterator();
while (iter.hasNext()) {
GrantedAuthority auth = iter.next();
System.out.println(auth.getAuthority());
}
return "유저 페이지입니다.";
}
@GetMapping("/admin")
public @ResponseBody String admin() {
return "어드민 페이지입니다.";
}
//@PostAuthorize("hasRole('ROLE_MANAGER')")
//@PreAuthorize("hasRole('ROLE_MANAGER')")
@Secured("ROLE_MANAGER")
@GetMapping("/manager")
public @ResponseBody String manager() {
return "매니저 페이지입니다.";
}
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/join")
public String join() {
return "join";
}
@PostMapping("/joinProc")
public String joinProc(User user) {
System.out.println("회원가입 진행 : " + user);
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
user.setRole("ROLE_USER");
userRepository.save(user);
return "redirect:/";
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.cos.securityex01.config.oauth.PrincipalOauth2UserService;
@Configuration // IoC 빈(bean)을 등록
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private PrincipalOauth2UserService principalOauth2UserService;
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')")
//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/loginProc")
.defaultSuccessUrl("/")
.and()
.oauth2Login()
.loginPage("/login")
.userInfoEndpoint()
.userService(principalOauth2UserService);
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr/>
<!-- 시큐리티는 x-www-form-url-encoded 타입만 인식 -->
<form action="/loginProc" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<button>로그인</button>
</form>
<br />
<h1>Social Login</h1>
<br />
<!-- javascript:; 는 클릭해도 반응을 없게 하는 키워드 -->
<a href="/oauth2/authorization/google" >
<img src="https://pngimage.net/wp-content/uploads/2018/06/google-login-button-png-1.png"
alt="google" width="357px" height="117px">
</a>
<br />
<a href="/oauth2/authorization/facebook">
<img src="https://pngimage.net/wp-content/uploads/2018/06/login-with-facebook-button-png-transparent-1.png"
alt="facebook" width="357px" height="117px">
</a>
<br />
<a href="/oauth2/authorization/naver">
<img src="https://blogfiles.pstatic.net/MjAyMDA4MDRfMzMg/MDAxNTk2NTEyOTY4MDMx.vhXHCulffijGUnvlaBR2jW4__Lkz8R3ZTaEDcTeNV2gg.Wt_HNl_zktUJUMrYGkVmqJ-PhxKv_s4A7gG1uPKMZaQg.PNG.getinthere/naver_button.png"
alt="naver" width="357px" height="50px">
</a>
<br />
</body>
</html>
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import com.cos.securityex01.model.User;
import lombok.Data;
// Authentication 객체에 저장할 수 있는 유일한 타입
public class PrincipalDetails implements UserDetails, OAuth2User{
private static final long serialVersionUID = 1L;
private User user;
private Map<String, Object> attributes;
// 일반 시큐리티 로그인시 사용
public PrincipalDetails(User user) {
this.user = user;
}
// OAuth2.0 로그인시 사용
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
public User getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collet = new ArrayList<GrantedAuthority>();
collet.add(()->{ return user.getRole();});
return collet;
}
// 리소스 서버로 부터 받는 회원정보
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
// User의 PrimaryKey
@Override
public String getName() {
return user.getId()+"";
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.cos.securityex01.model.User;
import com.cos.securityex01.repository.UserRepository;
@Service
public class PrincipalDetailsService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if(user == null) {
return null;
}else {
return new PrincipalDetails(user);
}
}
}
server:
port: 8080
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
datasource:
driver-class-name: org.h2.Driver
url: [db url]
username: [db id]
password: [db password]
mvc:
view:
prefix: /templates/
suffix: .mustache
jpa:
hibernate:
ddl-auto: update #create update none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
security:
oauth2:
client:
registration:
google: # /oauth2/authorization/google 이 주소를 동작하게 한다.
client-id: [client id]
client-secret: [client password]
scope:
- email
- profile
facebook:
client-id: [client id]
client-secret: [client password]
scope:
- email
- public_profile
# 네이버는 OAuth2.0 공식 지원대상이 아니라서 provider 설정이 필요하다.
# 요청주소도 다르고, 응답 데이터도 다르기 때문이다.
naver:
client-id: [client id]
client-secret: [client password]
scope:
- name
- email
- profile_image
client-name: Naver # 클라이언트 네임은 구글 페이스북도 대문자로 시작하더라.
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/naver
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response # 회원정보를 json의 response 키값으로 리턴해줌.
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import com.cos.securityex01.config.auth.PrincipalDetails;
import com.cos.securityex01.config.oauth.provider.FaceBookUserInfo;
import com.cos.securityex01.config.oauth.provider.GoogleUserInfo;
import com.cos.securityex01.config.oauth.provider.NaverUserInfo;
import com.cos.securityex01.config.oauth.provider.OAuth2UserInfo;
import com.cos.securityex01.model.User;
import com.cos.securityex01.repository.UserRepository;
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
// userRequest 는 code를 받아서 accessToken을 응답 받은 객체
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest); // google의 회원 프로필 조회
// code를 통해 구성한 정보
System.out.println("userRequest clientRegistration : " + userRequest.getClientRegistration());
// user의 accesstoken
//System.out.println("userRequest = " + userRequest.getAccessToken());
// 받은 실제 유저 정보 attribute
//System.out.println("super.loadUser(userRequest).getAttributes() = " + super.loadUser(userRequest).getAttributes());
// token을 통해 응답받은 회원정보
System.out.println("oAuth2User : " + oAuth2User);
return processOAuth2User(userRequest, oAuth2User);
}
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
// Attribute를 파싱해서 공통 객체로 묶는다. 관리가 편함.
OAuth2UserInfo oAuth2UserInfo = null;
if (userRequest.getClientRegistration().getRegistrationId().equals("google")) {
System.out.println("구글 로그인 요청~~");
oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
} else if (userRequest.getClientRegistration().getRegistrationId().equals("facebook")) {
System.out.println("페이스북 로그인 요청~~");
oAuth2UserInfo = new FaceBookUserInfo(oAuth2User.getAttributes());
} else if (userRequest.getClientRegistration().getRegistrationId().equals("naver")){
System.out.println("네이버 로그인 요청~~");
oAuth2UserInfo = new NaverUserInfo((Map)oAuth2User.getAttributes().get("response"));
} else {
System.out.println("우리는 구글과 페이스북만 지원해요 ㅎㅎ");
}
//System.out.println("oAuth2UserInfo.getProvider() : " + oAuth2UserInfo.getProvider());
//System.out.println("oAuth2UserInfo.getProviderId() : " + oAuth2UserInfo.getProviderId());
Optional<User> userOptional =
userRepository.findByProviderAndProviderId(oAuth2UserInfo.getProvider(), oAuth2UserInfo.getProviderId());
User user;
if (userOptional.isPresent()) {
user = userOptional.get();
// user가 존재하면 update 해주기
user.setEmail(oAuth2UserInfo.getEmail());
userRepository.save(user);
} else {
// user의 패스워드가 null이기 때문에 OAuth 유저는 일반적인 로그인을 할 수 없음.
user = User.builder()
.username(oAuth2UserInfo.getProvider() + "_" + oAuth2UserInfo.getProviderId())
.email(oAuth2UserInfo.getEmail())
.role("ROLE_USER")
.provider(oAuth2UserInfo.getProvider())
.providerId(oAuth2UserInfo.getProviderId())
.build();
userRepository.save(user);
}
return new PrincipalDetails(user, oAuth2User.getAttributes());
}
}
스프링 시큐리티 oauth 방식이 여러개 있다.
(code Grant Type으로 구현함)
[HTML] 이미지 비율 설정하여 보여주는 방법 (0) | 2022.05.04 |
---|---|
[Keycloak] 설치 및 간단한 SSO 인증 시스템 구현 (0) | 2022.05.03 |
크롬 개발자 도구 사용법 및 소개, 쉽게 핵심만 알아보자 (0) | 2022.04.29 |
[알고리즘] 문자열 4번 문제 - 김태원 자바 알고리즘 (0) | 2022.04.28 |
[aws] 가비아 도메인 연결/호스팅 방법, 포트 생략 까지 알아보자 (0) | 2022.04.27 |