dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
security:
oauth2:
client:
registration:
google:
client-id: your-client-id
client-secret: your-client-secret
scope:
- email
- profile
만약, 분명히 Id, secret 부분 잘 넣었는데도 구글인증을 할 때 엑세스가 거부되었다고 나온다면??
OAuth동의 화면에서 게시상태를 반드시 확인해보자(다른 곳에 오타가 없다면 ㅠㅠ )
앱 게시를 한 뒤에야 OAuth인증이 가능한 상태가 된다!!
HttpSession session = req.getSession() -> XXX(HttpSession session)
session.setAttribute("s_name","홍길동")
session.getAttribute("s_name")
Authentication안(세션)에 user 저장 시 반드시 UserDetails타입이어야 받아줄 수 있음.
우리는 실습을 통해 UserDetails를 구현하기 위한 User클래스와 PrincipalDetails 클래스를 생성하였음.
- User : user 정보갖고 있는 Model 클래스
- PrincipalDetails : UserDetails를 구현한 클래스 => Spring Security에서 사용자 인증 및 권한 부여에 활용
스프링 시큐리티는 SecurityConfig.java에서 loginProcessingUrl("/loginProcess")에 등록
<form action=/loginProcess>
스프링 시큐리티는 별도 세션을 관리하진 않는다. 본진은 캐시 메모리 하나인데, 톰캣이란 was가 해주고 있다.
결국 톰캣이 관리하는 것에 접근해서 사용하는 것인데
사용하는 약속은 Authentication이고, 그 안의 타입을 UserDetails로 정해져 있는 것이다.
이런 흐름으로 세션에 접근하여 사용할 수 있다는 큰 그림을 그릴 수 있으면 된다.
2024-01-22 10:33:57.602 [INFO] 24689 [PrincipalDetailService.java : 28] {지연1}
2024-01-22 10:33:57.620 [INFO] 24689 [UserDao.java : 18] {login}
2024-01-22 10:33:57.797 [INFO] 24689 [ViewController.java : 45] {indextrue}
2024-01-22 10:33:57.797 [INFO] 24689 [ViewController.java : 46] {indexfalse}
2024-01-22 10:35:01.767 [INFO] 24689 [ViewController.java : 34] {principalDetails: PrincipalDetails(user=User(id=14, username=지연1, password=$2a$10$uuuJscXvSjd1OKCL0MeAJO7czRYN7OXYmZMl4z5Lkvemk8umH/IKW, email=test@hot.com, role=ROLE_USER, provider=, providerId=, createDate=2024-01-22))}
2024-01-22 10:35:01.769 [INFO] 24689 [ViewController.java : 36] {principalDetails.getUser(): User(id=14, username=지연1, password=$2a$10$uuuJscXvSjd1OKCL0MeAJO7czRYN7OXYmZMl4z5Lkvemk8umH/IKW, email=test@hot.com, role=ROLE_USER, provider=, providerId=, createDate=2024-01-22)}
2024-01-22 10:35:01.769 [INFO] 24689 [ViewController.java : 38] {userDetails.getUser(): User(id=14, username=지연1, password=$2a$10$uuuJscXvSjd1OKCL0MeAJO7czRYN7OXYmZMl4z5Lkvemk8umH/IKW, email=test@hot.com, role=ROLE_USER, provider=, providerId=, createDate=2024-01-22)}
의존성 설정:
spring-boot-starter-security
와 구글 OAuth 2.0 클라이언트 의존성을 추가합니다. build.gradle dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.apache.tomcat.embed:tomcat-embed-jasper"
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1'
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0'
runtimeOnly 'com.oracle.database.jdbc:ojdbc11'
implementation 'com.oracle.ojdbc:orai18n:19.3.0.0'
implementation 'com.zaxxer:HikariCP:5.1.0'
implementation 'org.mybatis:mybatis:3.5.15'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'jakarta.el:jakarta.el-api:5.0.1'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
application.properties 또는 application.yml 설정:
server:
tomcat:
additional-tld-skip-patterns: "*.jar"
port: 8000
error:
path: /error
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
output:
ansi:
enabled: always
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
datasource:
hikari:
jdbc-url:
username:
password:
driver-class-name: oracle.jdbc.OracleDriver
connection-timeout: 20000
validation-timeout: 3000
minimum-idle: 5
maximum-pool-size: 12
idle-timeout: 300000
max-lifetime: 1200000
auto-commit: true
pool-name: oraPool
jpa:
open-in-view: true
hibernate:
ddl-auto: create
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
servlet:
multipart:
max-file-size: 10MB
enabled: true
security:
oauth2:
client:
registration:
google:
client-id: your-client-id
client-secret: your-client-secret
scope:
- email
- profile
구글 로그인 사용자 정보를 담을 모델 클래스 생성:
package com.example.demo.model;
import lombok.Builder;
import lombok.Data;
@Data
public class User {
public User(){}
@Builder
public User(String username, String password, String email, String role, String provider, String providerId, String createDate){
this.id = id;
this.username = username;//소셜로그인-강제회원가입처리-provider_providerId 예)googel_123232123123
this.password = password;
this.email = email;
this.role = role;
this.provider = provider;
this.providerId = providerId;
this.createDate = createDate;
}
private int id;
private String username="";
private String password;
private String email;
private String role;
private String provider;//google, kakao, github
private String providerId;//userId, uid
private String createDate;//java.sql.Timestamp
}
CustomUserDetailsService 구현:
UserDetailsService
를 상속받아 구현한 클래스를 생성합니다. PrincipalDetailspackage com.example.demo.auth;
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.example.demo.model.User;
import lombok.Data;
@Data //@Getter + @Setter - 외부 클래스에서 User객체 정보 접근을 위해 추가함
//일반로그인과 구글 로그인을 한 가지 타입으로 묶어서 양쪽 타입 모두를 처리할 수 있도록 클래스 재정의한다
//구글 로그인을 처리하기 위해서 OAuth2User를 추가하였다
public class PrincipalDetails implements UserDetails, OAuth2User {
private User user; //캡슐레이션
//구글 로그인시 구글서버에서 넣어주는 정보가 Map의 형태임
private Map<String,Object> attributes;
public PrincipalDetails(User user){
this.user = user;
}
//일반로그인과 구글 로그인 두가지를 모두 처리한다
//OAuth로그인시 사용하는 생성자 이다 - GitHub, 네이버
public PrincipalDetails(User user, Map<String,Object> attributes){
this.user = user;
this.attributes = attributes;
}
//해당 User의 권한을 리턴함
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
//세션에 담을 다른 컬럼 정보도 추가 가능하다
public int getId(){
return user.getId();
}
public String getEmail(){
return user.getEmail();
}
public String getRole(){
return user.getRole();
}
//데이터베이스와 매칭이 안되면 loginFaill.jsp 호출됨
@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;
}
//구글 로그인 후에 프로필 정보를 담을 변수
//{sub=구글에서 할당하는 나에대한 고유식별자숫자값, name=이름, picture=, email=, email_verified=true, lokale=ko}
@Override
public Map<String,Object> getAttributes(){
return attributes;
}
@Override
public String getName() {
return null;
}
}
OAuth2UserDetailsService 구현:
package com.example.demo.auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.example.demo.dao.UserDao;
import com.example.demo.model.User;
//스프링 시큐리티가 낚아채서 로그인 진행해줌 - 왜냐면 세션관리를 해야 하니까
//SecurityConfig설정에서 loginProcessingUrl("/loginProcess") 이부분
//loginProcess 요청이 오면 자동으로 UserDetailsService타입으로 IoC되어 있는
//loadUserByUsername함수가 실행된다 -스프링 시큐리티 컨벤션
//시큐리티 session <- Authentication <- UserDetails@Service
public class PrincipalDetailService implements UserDetailsService {
Logger logger = LoggerFactory.getLogger(PrincipalDetailService.class);
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info(username);//파라미터로 사용자가 입력한 이름이 담김
User user = userDao.login(username);
if(user !=null){//DB에서 가져온 값이 있으면
return new PrincipalDetails(user);
}
return null;
}
}
SecurityConfig 설정:
Controller에서 로그인 및 사용자 정보 처리:
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터 체인에 등록됨
@EnableMethodSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public static BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private PrincipalOauth2UserService principalOauth2UserService;
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
//큰 권한 순서로 '>'를 사용하여 입력해준다. 띄어쓰기도 중요
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_TEACHER > ROLE_USER");
return roleHierarchy;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(requests -> requests
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
.requestMatchers("/user/**").authenticated()// 인증만되면 들어가는 주소
.requestMatchers("/teacher/**").hasRole("TEACHER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/login", "/join", "/joinForm").permitAll()//권한없이 접근가능함
.anyRequest().authenticated())
.formLogin(login -> login
.loginPage("/login")
.loginProcessingUrl("/loginProcess")//로그인 버튼을 요청했을 때
.failureUrl("/login-error")//비번이 틀렸을 때
.defaultSuccessUrl("/", true)
.permitAll())
.oauth2Login(oauth -> oauth
.loginPage("/login") //구글로그인 완료된 후 후처리가 필요함
.userInfoEndpoint(end -> end
.userService(principalOauth2UserService))
)
.logout(logout -> logout
//현재 페이지에서 로그아웃 눌렀을 때 로그인 페이지가 아니라 메인페이지로 이동하기
.logoutSuccessUrl("/")
.permitAll())
.exceptionHandling(exception -> exception.accessDeniedPage("/access-denied"));
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
}```
> 위의 코드에서는 구글 로그인을 쉽게 구현하기 위한 주요 단계를 설명하고 있습니다
1. **구글 클라이언트 설정**: 구글 로그인을 사용하기 위해 클라이언트 설정을 하세요. 클라이언트 ID와 시크릿 키 등이 필요합니다.
2. **User 클래스 (모델 클래스)**: 사용자 정보를 담을 클래스를 만들어주세요. 사용자의 기본 정보와 권한을 저장합니다.
3. **UserDetailsService 및 OAuth2UserService의 구현**: `UserDetailsService`는 사용자 정보를 가져오는 역할, `OAuth2UserService`는 OAuth 2.0 프로토콜을 통해 사용자 정보를 가져오는 역할을 합니다.
4. **SecurityConfig 설정**: Spring Security 설정 클래스에서 구글 로그인에 관한 설정을 하세요. 여기에는 로그인 및 콜백 URL, 사용자 정보를 가져오는 서비스 등이 포함됩니다.
> 이렇게 설정된 구성은 사용자가 구글 로그인을 시도할 때, Spring Security가 클라이언트 설정과 서비스를 통해 구글 서버와 통신하고, 사용자 정보를 가져와서 인증 및 권한을 부여합니다. 이렇게 간단한 설정으로 안전하고 효율적인 구글 로그인을 구현할 수 있습니다.