목차
[이전 게시글] 꼭! 봐주세여
간단히 요약하면
1. 아이디, 비밀번호를 가진 요청이 들어온다
2. Form 로그인이면 UserDetailsService의 loadUserByUsername메서드가 실행되고
OAuth2 로그인이면 OAuth2UserService의 loadUserByUsername메서드가 실행된다.
3. loadUserByUsername메서드는 "이런 정보가 들어왔는데 얘 혹시 회원이야?" 라고 묻는 메서드이다.
= loadUserByUsername에서는 회원을 찾아주는 로직을 구현하면된다.
4. 이때, 회원정보는 Form 로그인이면 UserDetails타입으로, OAuth2 로그인이면 OAuth2User타입으로 반환해준다.
5. UserDetails 또는 OAuth2User를 반환하면 Spring에서 알아서 Session에 저장해준다.
++
FormLogin으로 로그인 하는 경우엔,
사용자가 직접 회원가입을 거친 후 로그인하기때문에 loadUserByUsername에서 회원을 찾고 정보를 반환해주었다.
OAuth2로 로그인 하는 경우엔,
사용자가 회원가입을 하지 않기때문에 loadUserByUsername에서 회원을 찾고,
없는 회원이라면 회원가입 처리해주어야한다!!
있는 회원이라면 FormLogin과 동일하게 정보를 반환해주면된다.
Spring Security - OAuth2 구글 로그인하기
완성된 모습
구글 설정
https://console.cloud.google.com/apis/dashboard 접속
1. 상단 프로젝트부분 클릭
2. 새 프로젝트 클릭 > 만들기 클릭
3. 방금 생성한 프로젝트 선택 > OAuth 동의화면 > 외부 > 만들기
4. 1단계 필수부분 작성,
2단계 범위 설정 건들지 않고 다음,
3단계 테스트 사용자 건들지 않고 다음,
4단계 요약 확인
5. 사용자 인증정보 > 사용자 인증정보 만들기 클릭
6. 애플리케이션 유형 : 웹 애플리케이션 선택, 승인된 리디렉션URI 입력
승인된 리디렉션URI에는 "http://localhost:8080/login/oauth2/code/google" 을 입력한다. (빨간색은 정해진 내용이다)
스프링 부트 공식문서에 나와있는 내용이다.
https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html
7. 클라이언트ID와 보안 비밀번호를 기억해두자
OAuth2 설정
build.gradle > dependency 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:2.6.2'
application.properties
# google
spring.security.oauth2.client.registration.google.client-id = 7번의 클라이언트ID 입력
spring.security.oauth2.client.registration.google.client-secret = 7번의 클라이언트 비밀번호 입력
spring.security.oauth2.client.registration.google.scope = profile, email
패키지 > config 폴더 > SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private PrincipalOauth2UserService principalOauth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manager/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/admin/**").hasRole("ROLE_ADMIN")
.anyRequest().permitAll()
.and() //추가
.oauth2Login() // OAuth2기반의 로그인인 경우
.loginPage("/loginForm") // 인증이 필요한 URL에 접근하면 /loginForm으로 이동
.defaultSuccessUrl("/") // 로그인 성공하면 "/" 으로 이동
.failureUrl("/loginForm") // 로그인 실패 시 /loginForm으로 이동
.userInfoEndpoint() // 로그인 성공 후 사용자정보를 가져온다
.userService(principalOauth2UserService); //사용자정보를 처리할 때 사용한다
}
}
.oauth2Login() | OAuth2기반으로 로그인 할 경우의 설정을 추가할 수 있다. OAuth2LoginConfigurer를 불러온다 |
.loginPage(String url) | 로그인 페이지 경로를 호출한다. |
.defaultSuccessUrl(String url) | 로그인 성공 시 url로 이동한다. |
.failureUrl(String url) | 로그인 실패 시 url로 이동한다. |
.userInfoEndpoint() | 로그인 성공 후 사용자 정보를 가져온다 |
.userService(Class) | userInfoEndpoint()로 가져온 사용자 정보를 처리할 때 사용한다. |
OAuth2Login loadUserByUsername
패키지 > auth 폴더 > PrincipalOauth2UserService.java
OAuth2UserService ← DefaultOAuth2UserService (DefaultOAuth2UserService는 OAuth2UserService 를 구현한다)
DefaultOAuth2UserService는 OAuth2로그인 시 loadUserByUsername메서드로 로그인한 유저가 DB에 저장되어있는지를 찾는다.
OAuth2로 로그인하는 사용자는 회원가입을 거치지 않기 때문에 DB에 유저가 없다면 회원가입처리,
유저가 있다면 Authentication(OAuth2User를 구현한 PrincipalDetails)를 반환하여 SecurityContextHolder에 저장할 수 있게 한다.
package com.example.demo.oauth;
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Autowired private UserRepository userRepository;
@Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId(); //google
String providerId = oAuth2User.getAttribute("sub");
String username = provider+"_"+providerId; // 사용자가 입력한 적은 없지만 만들어준다
String uuid = UUID.randomUUID().toString().substring(0, 6);
String password = bCryptPasswordEncoder.encode("패스워드"+uuid); // 사용자가 입력한 적은 없지만 만들어준다
String email = oAuth2User.getAttribute("email");
Role role = Role.ROLE_USER;
User byUsername = userRepository.findByUsername(username);
//DB에 없는 사용자라면 회원가입처리
if(byUsername == null){
byUsername = User.oauth2Register()
.username(username).password(password).email(email).role(role)
.provider(provider).providerId(providerId)
.build();
userRepository.save(byUsername);
}
return new PrincipalDetails(byUsername, oAuth2User.getAttributes());
}
}
++ 참고
public class OAuth2UserRequest {
private final ClientRegistration clientRegistration;
private final OAuth2AccessToken accessToken;
private final Map<String, Object> additionalParameters; //이건 여기서 안쓴다
....
}
//인증서버(구글)의 정보를 가져온다
userRequest.getClientRegistration();
결과 : ClientRegistration{
registrationId='google',
clientId='~',
clientSecret='~',
clientAuthenticationMethod=org.springframework.security.oauth2.core.ClientAuthenticationMethod@4fcef9d3,
authorizationGrantType=org.springframework.security.oauth2.core.AuthorizationGrantType@5da5e9f3,
redirectUri='{baseUrl}/{action}/oauth2/code/{registrationId}',
scopes=[profile, email],
providerDetails=org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails@5b8ec6e5,
clientName='Google'
}
//인증 토큰값을 가져온다
userRequest.getAccessToken().getTokenValue();
결과 : Tokenvalue~~
//유저의 정보를 가져온다
super.loadUser(userRequest).getAttributes();
결과 : {
sub=~(PK 같은거),
name=~,
given_name=~,
family_name=~,
picture=~,
email=~,
email_verified=true,
locale=ko
}
OAuth2Login Authentication OAuth2User
패키지 > auth 폴더 > PrincipalDetails.java
FormLogin 방법, OAuth2Login 방법 구분없이 한 객체로만 관리하기위해
기존 PrincipalDetails에 OAuth2User도 implements한다
package com.example.demo.auth;
@Getter
@ToString
public class PrincipalDetails implements UserDetails, OAuth2User {
private User user;
private Map<String, Object> attributes;
//UserDetails : Form 로그인 시 사용
public PrincipalDetails(User user) {
this.user = user;
}
//OAuth2User : OAuth2 로그인 시 사용
public PrincipalDetails(User user, Map<String, Object> attributes) {
//PrincipalOauth2UserService 참고
this.user = user;
this.attributes = attributes;
}
/**
* UserDetails 구현
* 해당 유저의 권한목록 리턴
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole().toString();
}
});
return collect;
}
/**
* UserDetails 구현
* 비밀번호를 리턴
*/
@Override
public String getPassword() {
return user.getPassword();
}
/**
* UserDetails 구현
* PK값을 반환해준다
*/
@Override
public String getUsername() {
return user.getUsername();
}
/**
* UserDetails 구현
* 계정 만료 여부
* true : 만료안됨
* false : 만료됨
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* UserDetails 구현
* 계정 잠김 여부
* true : 잠기지 않음
* false : 잠김
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* UserDetails 구현
* 계정 비밀번호 만료 여부
* true : 만료 안됨
* false : 만료됨
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* UserDetails 구현
* 계정 활성화 여부
* true : 활성화됨
* false : 활성화 안됨
*/
@Override
public boolean isEnabled() {
return true;
}
/**
* OAuth2User 구현
* @return
*/
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
/**
* OAuth2User 구현
* @return
*/
@Override
public String getName() {
String sub = attributes.get("sub").toString();
return sub;
}
}
OAuth2Login Domain구현
패키지 > domain 폴더 > Role.java : FormLogin과 같음
패키지 > domain 폴더 > User.java
package com.example.demo.domain;
@Entity
@Getter @ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@Setter
private String password;
private String email;
@Enumerated(EnumType.STRING)
@Setter
private Role role;
@CreationTimestamp //자동으로 만들어준다
private Timestamp createTime;
private String provider; // oauth2를 이용할 경우 어떤 플랫폼을 이용하는지
private String providerId; // oauth2를 이용할 경우 아이디값
@Builder(builderClassName = "UserDetailRegister", builderMethodName = "userDetailRegister")
public User(String username, String password, String email, Role role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
@Builder(builderClassName = "OAuth2Register", builderMethodName = "oauth2Register")
public User(String username, String password, String email, Role role, String provider, String providerId) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.provider = provider;
this.providerId = providerId;
}
}
OAuth2Login Controller 구현 (Controller에서 인증 객체 가져오기)
패키지 > controller 폴더 > UserController.java
package com.example.demo.controller;
@Controller
public class UserController {
@Autowired private UserRepository userRepository;
@Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/loginForm")
public String loginForm(){
return "login";
}
@GetMapping("/joinForm")
public String joinForm(){
return "join";
}
@PostMapping("/join")
public String join(@ModelAttribute User user){
user.setRole(Role.ROLE_USER);
String encodePwd = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(encodePwd);
userRepository.save(user); //반드시 패스워드 암호화해야함
return "redirect:/loginForm";
}
@GetMapping("/user")
@ResponseBody
public String user(){
return "user";
}
@GetMapping("/manager")
@ResponseBody
public String manager(){
return "manager";
}
@GetMapping("/admin")
@ResponseBody
public String admin(){
return "admin";
}
// !!!! OAuth로 로그인 시 이 방식대로 하면 CastException 발생함
@GetMapping("/form/loginInfo")
@ResponseBody
public String formLoginInfo(Authentication authentication, @AuthenticationPrincipal PrincipalDetails principalDetails){
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
User user = principal.getUser();
System.out.println(user);
//User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
User user1 = principalDetails.getUser();
System.out.println(user1);
//User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
//user == user1
return user.toString();
}
@GetMapping("/oauth/loginInfo")
@ResponseBody
public String oauthLoginInfo(Authentication authentication, @AuthenticationPrincipal OAuth2User oAuth2UserPrincipal){
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
Map<String, Object> attributes = oAuth2User.getAttributes();
System.out.println(attributes);
// PrincipalOauth2UserService의 getAttributes내용과 같음
Map<String, Object> attributes1 = oAuth2UserPrincipal.getAttributes();
// attributes == attributes1
return attributes.toString(); //세션에 담긴 user가져올 수 있음음
}
@GetMapping("/loginInfo")
@ResponseBody
public String loginInfo(Authentication authentication, @AuthenticationPrincipal PrincipalDetails principalDetails){
String result = "";
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
if(principal.getUser().getProvider() == null) {
result = result + "Form 로그인 : " + principal;
}else{
result = result + "OAuth2 로그인 : " + principal;
}
return result;
}
}
OAuth2Login View구현
resources > templates > join.html : FormLogin과 같음
resources > templates > login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr/>
<h2>로그인 유저 : </h2>
<p sec:authentication="principal"></p>
<div sec:authorize="isAnonymous()" style="background-color:pink; padding:1em;">
<form action="/login" method="post" >
<input type="text" name="username" />
<input type="password" name="password" />
<button>로그인</button>
</form>
<a href="/oauth2/authorization/google">구글 로그인</a>
<br><br>
<a href="/joinForm">회원가입하기</a><br>
</div>
<div sec:authorize="isAuthenticated()" style="background-color:pink; padding:1em;">
<a href="/logout">로그아웃</a>
</div>
<br><br>
<a href="/user">유저</a><br>
<a href="/manager" sec:authorize="hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')">매니저</a><br>
<a href="/admin" sec:authorize="hasRole('ROLE_ADMIN')">어드민</a><br>
<br><br>
<a href="/form/loginInfo">Form 로그인 정보</a>
<a href="/oauth/loginInfo">OAuth2 로그인 정보</a>
<a href="/loginInfo">로그인 정보</a>
</body>
</html>
'🌿 Spring' 카테고리의 다른 글
[OAuth2] 동작과정 (0) | 2022.02.05 |
---|---|
[Spring Security] OAuth 네이버 로그인하기 (10) | 2022.02.02 |
[Spring Security] 동작방법 및 Form, OAuth 로그인하기 (Feat.Thymeleaf 타임리프) (8) | 2022.02.01 |
Spring + MariaDB + Mybatis 연동 및 실행해보기 (0) | 2021.12.22 |
홈페이지 최초접속 시 url에 jsessionid 자동으로 안붙게하기 (0) | 2021.12.20 |