간편로그인 부분은 인증, 인가, 권한부여 등의 부분이 들어가기 때문에
Spring Security 에 대해 알고있다면 수월하게 작업할 수 있을 것입니다.
1. 관련 포스팅
* Google 간편로그인 등록 (feat. OAuth)
* 카카오 간편로그인 등록 (포스팅 예정)
2. 테스트 환경
* MacOS (M1)
* Spring Boot 3.1.1
* Spring Security 6.1.1
* Spring Security OAuth2 Client 6.1.1
* jjwt 0.11.5
3. 구현
구현 부분은 다른 분들 글을 많이 참고하였습니다.
참고한 글에도 상세한 설명이 많으니 코드와 간략한 설명만 하고 넘어가도록 하겠습니다.
a. Gradle
dependencies {
/* Data */
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
/* Security & OAuth2.0 */
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.1.2'
implementation 'org.springframework.boot:spring-boot-starter-security:3.1.2'
/* JWT */
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
/* Static Web Page */
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
/* Lombok */
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
/* Database */
runtimeOnly 'com.h2database:h2'
/* Test */
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
Spring Boot Starter Security, OAuth2 Client 를 3.1.2 버전으로 작성하면,
Security 와 OAuth2 Client 가 6 버전 대로 설치 됩니다.
b. YMAL
데이터가 매핑되기 위한 yaml 파일을 작성해줍니다.
application.yaml
spring:
profiles:
include: oauth # application-oauth.yaml 파일 포함
# 기타 설정 Database, Jpa ...
...
application-oauth.yaml
spring:
security:
oauth2:
client:
registration:
google:
client-id: {google-client-id} #(클라이언트 ID)
client-secret: {google-client-secret} #(클라이언트 보안 비밀번호)
scope:
- profile
- email
kakao:
client-id: {kakao-client-id}
# Spring OAuth2 기본 리다이렉트 url 형태 ( {base-url}/login/oauth2/code/{registrationId} )
redirect-uri: http://localhost:8080/login/oauth2/code/{registrationId}
authorization-grant-type: authorization_code
client-name: kakao
# POST 로 작성하기도 하지만 (Spring security 5.6 이전 버전 ~5.5)
# 현재 버전(6.1.1)에서는 client_secret_post 로 해야 적용이됨
# client-authentication-method: POST
client-authentication-method: client_secret_post
scope:
- profile_nickname
- profile_image
- account_email
# OAuth 사용에 필요한 값에 대한 매핑 정보
provider:
google:
authorizationUri: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent
kakao:
authorizationUri: https://kauth.kakao.com/oauth/authorize
tokenUri: https://kauth.kakao.com/oauth/token
userInfoUri: https://kapi.kakao.com/v2/user/me
# 카카오가 회원정보를 json으로 넘겨주는데, id라는 키값으로 리턴해준다.
userNameAttribute: id
jwkSetUri: https://kauth.kakao.com/oauth/token
Spring OAuth2 에서는 구글, 페이스북 등 기본 내제된 서비스들이 있습니다.
하지만 카카오의 경우, 내제된 데이터가 없기 때문에 작성 내용이 더 필요합니다.
(위에 작성된 yaml 파일을 참고할 것!)
client-id, client-password : 각 서비스(구글, 카카오..) 에서 제공하는 키
redirect-uri : 각 서비스에 입력했던 Redirect URI 를 입력해준다.
(인증 서버로 부터 받는 일회용 코드를 받는 본인 서버의 주소)
scope : 각 서비스에서 미리 설정해두었던 받고자 하는 데이터 범위
이전 구글 간편로그인 글에서도 언급하였지만,
redirect-uri의 디폴트 값은
{base-url}/login/oauth2/code/{registrationId} 이다.
변경하지 않는 이상, 구글에는 작성하지 않아도 기본 설정이 되어 있다.
c. 권한 Enum
권한 정보를 담고 있는 Enum Class를 만들어서 관리하도록 합니다.
[Enum] Role
import lombok.Getter;
@Getter
public enum Role {
GUEST("ROLE_GUEST","비 로그인 사용자"),
COMMON("ROLE_USER","일반 사용자"),
API("ROLE_API","API Key 사용자"),
VIP("ROLE_VIP","특별 사용자"),
ADMIN("ROLE_ADMIN","관리자");
private String key;
private String desc;
Role(String key, String desc) {
this.key = key;
this.desc = desc;
}
}
여기서 key 변수에 'ROLE_' 을 Prefix 로 붙이는 이유가 있는데,
Spring Security 에서 권한 체크 시에는 .hasAuthority(), .hasRole() 등의 메소드를 이용해서 처리하게 됩니다.
만약 .hasRole() 사용 시,
'ROLE_' 이 Prefix 로 붙어 있어야 처리가 가능합니다.
(만약 붙여주지 않는다면 'ROLE_' 이 아니라고 에러가 발생합니다.)
d. 회원 정보
먼저 회원의 정보를 객체 형태로 담을 클래스를 만들어줄건데,
간편 로그인 Attribute 정보를 담을 DTO 객체와, 데이터베이스에 저장할 Entity 객체를 만들어 주는데, 아래 코드와 같이 구성합니다.
[DTO] OAuthAttributes
구글, 카카오 두가지를 사용하고 있기 때문에 매핑되는 키가 다르다.
이를 통합하기 위해 OAuthAttributes DTO 를 만든다.
import com.example.oauth2login.model.Role;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Getter
@ToString
@Slf4j
public class OAuthAttributes {
private Map<String, Object> attributes; // Attributes 원본
private String nameAttributeKey; // attribute key : google(sub), kakao(id)
private String attributeId; // 고유 id
private String name;
private String email;
private String picture;
private String registrationId; // 간편로그인 서비스 구분 (ex : google, kakao)
private Role role; // 권한 값
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey,
String attributeId, String name, String email, String picture,
String registrationId, Role role) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.attributeId = attributeId;
this.name = name;
this.email = email;
this.picture = picture;
this.registrationId = registrationId;
this.role = role != null ? role : Role.COMMON;
}
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
// 제공 타입에 따른 객체 세팅
if (registrationId.equals("google")) {
return ofGoogle(userNameAttributeName, attributes, registrationId);
} else if (registrationId.equals("kakao")) {
return ofKakao(userNameAttributeName, attributes, registrationId);
}
return null;
}
private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes, String registrationId) {
return OAuthAttributes.builder()
.attributeId(String.valueOf(attributes.get(userNameAttributeName)))
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.registrationId(registrationId)
.build();
}
private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes, String registrationId) {
// 카카오의 계정 정보 추출
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
// 카카오의 프로필 정보 추출
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
return OAuthAttributes.builder()
.attributes(attributes)
.attributeId(String.valueOf(attributes.get(userNameAttributeName)))
.nameAttributeKey(userNameAttributeName)
.name((String) profile.get("nickname"))
.email((String) kakaoAccount.get("email"))
.picture((String) profile.get("profile_image_url"))
.registrationId(registrationId)
.build();
}
}
of() 로 해당 클래스에서 구글, 카카오 뿐만 아니라 앞으로 더 추가될 것(Apple, Naver 등)에 대한 데이터 매핑을 이 클래스에서 처리하도록 합니다.
참고한 다른 분 코드를 참고하여 이렇게 처리하였는데,
또 다른 분들 글 중에 Enum으로 처리하시는 분도 계셨고,
인증 관련 클래스를 상속받아 인증 인가 관련 로직 많은 곳에 사용하시는 분도 있었습니다.
만드는 분마다 성향차이니 구현하시는 분의 생각에 따라 자신에게 맞는 코드를 만들면 될것 같습니다.
[Entity] Member
데이터베이스에 저장할 Member Entity를 만들어줍니다.
만약 AccessToken, RefreshToken 도 저장하고자 한다면 입맛에 맞게 커스텀 하면 됩니다.
import com.example.oauth2login.dto.OAuthAttributes;
import com.example.oauth2login.model.Role;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Entity
@Table(name = "MEMBER",uniqueConstraints = @UniqueConstraint(
columnNames = {"attribute_id", "registration_id"}
))
@NoArgsConstructor
@Getter
@ToString
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "attribute_id")
private String attributeId; // 간편로그인 계정의 고유 ID
@Column(name = "registration_id")
private String registrationId; // 간편로그인 고유 계정 (ex, google, kakao)
@Column
private String name; // attribute name
@Column
private String email; // attribute email
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role; // 권한 Role
@Builder
public Member(Long id, String attributeId, String registrationId, String name, String email, Role role) {
this.id = id;
this.attributeId = attributeId;
this.registrationId = registrationId;
this.name = name;
this.email = email;
this.role = role;
}
public static Member toEntity(OAuthAttributes attributes) {
return Member.builder()
.attributeId(attributes.getAttributeId())
.registrationId(attributes.getRegistrationId())
.name(attributes.getName())
.email(attributes.getEmail())
.role(attributes.getRole())
.build();
}
public Member updateNameAndEmail(String name, String email) {
this.name = name;
this.email = email;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
}
[Repository] MemberRepository
@Repository
public interface MemberRepository extends JpaRepository<Member,Long> {
// Member의 유니크 값을 조회하기 위한 조회 메서드
Optional<Member> findByAttributeIdAndRegistrationId(String attributeId, String registrationId);
}
예전이었다면,
email 값을 PK나 UK로 잡았겠지만 카카오에서 email 값을 필수값으로 받을수 없게 되면서 고유한 값으로 잡기에는 애매모호 해졌습니다.
개인정보 보호가 화두가 되면서 점점 받을 수 있는 정보가 줄어들고 있고,
이런 부분을 우회하는 차원에서 회원가입 하면서 이메일, 핸드폰 번호 인증 절차를 넣는 앱 서비스들을 자주 볼 수 있습니다.
e. 서비스 로직 구현
위에서 만든 것들을 활용하기 위해
OAuth2UserService 를 상속 받아 loadUser() 메소드를 재 정의하는 작업을 진행합니다.
[Service] CustomOAuth2UserService
@RequiredArgsConstructor
@Service
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 로그인 서비스 구분 값 google, kakao, naver, apple ...
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 시 키 값이 된다. 구글은 키 값이 "sub"이고, 네이버는 "response"이고, 카카오는 "id"이다. 각각 다르므로 이렇게 따로 변수로 받아서 넣어줘야함.
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// 각기 다른 서비스를 통한 OAuth2 로그인을 통합 객체로 만들어준다.
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
Member member = saveOrUpdate(attributes);
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(member.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
/**
* Member 조회 후 저장 및 세팅
* @param attributes
* @return
*/
private Member saveOrUpdate(OAuthAttributes attributes) {
// Member 객체 조회 및 세팅
Member member = memberRepository.findByAttributeIdAndRegistrationId(attributes.getAttributeId(), attributes.getRegistrationId())
.map(entity -> entity.updateNameAndEmail(entity.getName(),entity.getEmail()))
.orElseGet(() -> Member.toEntity(attributes));
return memberRepository.save(member);
}
}
(23.08.14, saveOrUpdate() 메소드에서 orElse -> orElseGet 으로 수정하였습니다.
위 코드상에는 수정 되어있으니 이전에 보신분들은 참고해주세요!)
위에서 재정의한 메소드는 로그인 이후 동작합니다.
loadUser() : Member 저장 및 권한 설정에 대한 부분을 재정의
saveOrUpdate() : 처음 로그인 한 경우 Member를 저장해주고, 기존 회원의 경우 업데이트를 진행
f. JWT Token Util
서비스 토큰을 발행 시켜주기 위한 작업을 해줍니다.
[Util] JwtTokenUtil
@Component
public class JwtTokenUtil {
// JWT 비밀키 (임의로 설정)
// JWT 키는 되도록이면 길고 쉽게 풀지 못하는 것으로!
private String secret = "aschnhkghgrrHRoiwoASqfo123kl1l23jlwfmnan19047ahnfgklalkwwikdrkACACjsjUIUKJBlhWAFWASFascWAfaollas";
// JWT 유효 시간 설정 (30분으로 설정)
private long expiration = 1800000;
// JWT 토큰 생성
public String generateToken(Authentication authentication) {
// Key 세팅
byte[] keyBytes = Decoders.BASE64.decode(secret);
Key key = Keys.hmacShaKeyFor(keyBytes);
// 현재시간, 만료시간 세팅
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
// google, kakao ...
String clientRegistrationId = getRegistrationIdFromAuthentic(authentication);
return Jwts.builder()
.setSubject(authentication.getName())
.setIssuedAt(now)
.setExpiration(expiryDate)
.claim("registration_id",clientRegistrationId)
.signWith(key,SignatureAlgorithm.HS512) //or signWith(Key, SignatureAlgorithm)
.compact();
}
// JWT 토큰으로부터 사용자 ID 추출
public String getUserIdFromToken(String token) {
Jws<Claims> claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return claims.getBody().getSubject();
}
// JWT 토큰으로부터 registrationId 추출 (google, kakao)
public String getRegistrationIdFromToken(String token) {
Jws<Claims> claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return (String) claims.getBody().get("registration_id");
}
// JWT 토큰 유효성 검사
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
// RegistrationId 를 얻기 위한 메서드 (From. Authentic)
private String getRegistrationIdFromAuthentic(Authentication authentication) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
return oauthToken.getAuthorizedClientRegistrationId();
}
}
generateToken() : 토큰 생성 메서드
getUserIdFromToken() : 토큰으로 부터 고유 ID 를 얻기 위한 메서드 (subject 에 넣어둔 데이터이다.)
getRegistrationIdFromToken() : 토큰으로 부터 RegistrationId 를 얻기 위한 메서드 (google, kakao)
getRegistrationIdFromAuthentic() : Authentication 객체로 부터 RegistrationId를 얻기위한 메서드
이 토큰은 API 요청 시, 권한 부여에 사용될 예정입니다.
쉽게 생각해서 서비스 자체 AccessToken 을 만들어 준다 라고 생각하면 됩니다.
만들어진 토큰을 복호화 하면 다음과 같이 정보가 보여진다.
sub : 토큰 제목 (본 글에서는 사용자의 고유 ID 를 넣어두었다.)
iat : 발행 일자
exp : 만료 일자
registration_id : 사용자가 선택한 인증 서버 주체 (google, kakao)
JWT 에 대해 더 궁금한점이 있다면 아래 공식 홈페이지 참고!
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
g. Handler 구현
로그인 성공과 실패에 대한 Handler 를 구현하여,
Redirect 시킬 위치를 지정해주거나, 토큰 발행 등의 처리를 구현 합니다.
[Hadler] OAuth2AuthenticationSuccessHandler
@Component
@RequiredArgsConstructor
@Slf4j
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// Get the access token and refresh token from the authorized client
log.info("OAuth2 로그인에 성공하였습니다.");
// 토큰 발행
String token = jwtTokenUtil.generateToken(authentication);
// 토큰 헤더에 포함시켜 전송
// 헤더 말고 바디에 포함 시켜도 무관!
response.addHeader("Authorization","Bear "+token);
// Redirect URL 세팅
response.sendRedirect("/login/login-success");
super.onAuthenticationSuccess(request, response, authentication);
}
}
성공 시에 Header에 token 을 포함시켜주었는데,
사실 인증 인가의 주체는 서버에 있기 때문에
서버에서 클라이언트로 보내는데 굳이 Header에 포함 시키지 않고 Body에 담아 보내는것도 좋을 것 같습니다.
(이 부분은 클라이언트 작업을 하시는 분들과 정하는걸로!)
[Handler] OAuth2AuthenticationFailureHandler
@Slf4j
@Component
public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.warn("소셜 로그인에 실패했습니다. 에러 메시지 : {}", exception.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.sendRedirect("/login/login-failure");
}
}
h. Filter 구현
SecurityConfig 작성 전에,
필터 부분을 작성할건데, JWT Token, API Key, None 세가지 경우로 분기 처리하여 작업하였습니다.
각 분기마다 다르게 권한 부여를 하여 API 사용 시에 권한에 맞는지 체크할 수 있도록 합니다.
[Filter] AuthenticationFilter
@Slf4j
@RequiredArgsConstructor
@Component
public class AuthenticationFilter extends GenericFilterBean {
// Token Util
private JwtTokenUtil jwtTokenUtil = new JwtTokenUtil();
private final MemberRepository memberRepository;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Request Header 에 Token 또는 API Key 를 추출
String authorizationHeader = httpRequest.getHeader("Authorization");
String apiKeyHeader = httpRequest.getHeader("X-API-KEY");
UsernamePasswordAuthenticationToken authentication;
// API Key 또는 Bearer Token 둘 중 하나로 인증 하기 위한 작업
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String bearerToken = authorizationHeader.substring(7); // "Bearer " 이후의 토큰 부분 추출
if (jwtTokenUtil.validateToken(bearerToken)) {
// 유효기간에 대한 예외처리
// ...
}
// 토큰에서 Member 조회를 위한 값 추출
String attributeId = jwtTokenUtil.getUserIdFromToken(bearerToken);
String registrationId = jwtTokenUtil.getRegistrationIdFromToken(bearerToken);
// 등록된 Member 조회
Member member = memberRepository.findByAttributeIdAndRegistrationId(attributeId, registrationId)
.orElseThrow(() -> new RuntimeException("Bearer Token 인증 실패!!"));
// 유저 인가 정보 설정 -> API 별 권한 체크에 사용됨
authentication = new UsernamePasswordAuthenticationToken(bearerToken, null,
// 조회된 Member 에서 권한(Role) 입력
Collections.singleton(new SimpleGrantedAuthority(member.getRoleKey())));
} else if (apiKeyHeader != null) {
String apiKey = apiKeyHeader;
// API Key를 이용하여 처리 (예: 인증 처리)
// ...
authentication = new UsernamePasswordAuthenticationToken(apiKey, null,
Collections.singleton(new SimpleGrantedAuthority(Role.API.getKey())));
} else {
// 예외처리
// Guest 권한으로 처리(혹은 권한 인증 실패에 대한 예외처리를 할 것)
authentication = new UsernamePasswordAuthenticationToken(UUID.randomUUID(), null,
Collections.singleton(new SimpleGrantedAuthority(Role.GUEST.getKey())));
}
// authentication 세팅
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
i. Config 파일 작성
위에 진행한 일련의 과정들을 적용할 차례입니다.
해당 클래스 파일에서 API 별 권한 설정 및 Handler, Filter 적용을 하게 됩니다.
[Config] SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final AuthenticationFilter authenticationFilter;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize // 권한 지정
.requestMatchers("/").permitAll()
.requestMatchers("/login/**").permitAll()
.requestMatchers("/users/**").permitAll()
.requestMatchers("/api/**").hasAuthority(Role.COMMON.getKey()))
// 세션 사용 설정
.sessionManagement(session -> session
// 세션을 사용하지 않겠다는 의미
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// OAuth2 적용
http
.oauth2Login(oauth2 -> oauth2
// CustomService 적용
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService))
// Handler 적용
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
);
// 필터 적용
http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
authorizeHttpRequests() : Path 별, 권한 설정
sessionManagement() : Session 사용에 관한 정책 설정
(이 포스팅에서는 사용하지 않는 것으로 설정하였습니다.)
oauth2Login() : OAuth2.0 로그인 관련 설정
addFiterBefore() : 필터 적용 관련 설정
위에서 작업한
CustomOAuth2UserService, OAuth2AuthenticationSuccessHadler(& Failure) 는 oauth2Login() 설정에,
AutheticationFilter는 addFilterBefore()에 설정한다.
5. 실행
{base-url}/login 에 접속하면,
위에서 yaml 파일에 구글, 카카오 정보를 입력하였기에 따로 페이지를 만들지 않더라도 해당 페이지를 자동으로 만들어줍니다.
카카오 로그인 진행 후에,
SuccessHandler에서 지정해둔 Redirect URL 로 이동하는 모습을 볼 수 있고
Header에 Token과 Location 에 /login/login-success 가 포함되어 있는 것을 볼 수 있습니다.
로그인 성공, 실패에 대한 웹페이지는 Thymeleaf를 이용하였습니다.
해당 부분은 깃허브에 함께 포함되어 있으니 참고바랍니다!
6. 마무리
개발 진행하면서 로그인 관련 기능과는 연이 없었어서 공부 겸 관련 공부를 해보았는데
생각보다 알아야할 부분도 많고 흐름에 대해 고민해봐야 할 부분들이 많았습니다.
적용해보시면서 그대로 따라하는 것이 아닌
자신의 생각이 담긴 기능과 흐름을 만들어 보시면 좋을것 같습니다!
(만들면서 미흡한 점이나 구현이 덜 된 부분이 있어 저도 리팩토링 중입니다)
7. 깃허브
GitHub - mk1-p/spring-boot-oauth2-login: Test for Spring Boot Security And OAuth2 Client
Test for Spring Boot Security And OAuth2 Client. Contribute to mk1-p/spring-boot-oauth2-login development by creating an account on GitHub.
github.com
* 소스에 레디스 관련 파일들이 있는데 해당 포스팅과는 무관하니 지우고 실행하셔도 무관합니다!
8. 참고 자료
OAuth2 :: Spring Security
Spring Security provides comprehensive OAuth 2 support. This section discusses how to integrate OAuth 2 into your servlet based application.
docs.spring.io
[Spring Boot] OAuth2 + JWT + React 적용해보리기
오늘 팀원이랑 이야기를 해보다가 우려했던 일이 벌어졌다.. 우려했던 일이란?우려했던 일(현재 문제점)개선 방안OAuth2란?With Spring Boot구현현재까지 구현되었던 프로젝트의 로그인과정을 살펴보
velog.io
Spring Security + OAuth2.0 소셜 인증 예제(Google, Naver, Kakao)
사이드 프로젝트를 만들며 소셜 로그인의 OAuth2.0 인증방식을 개발해보려고 한다. 인터넷에 소개되어있는 블로그 글들은 서칭 후 코딩해보고 정리하였다. 카카오, 네이버, 구글만 구현할것인데
devbksheen.tistory.com
'DEV > Spring' 카테고리의 다른 글
[Spring] Data Rest Event 사용하기 (0) | 2023.08.22 |
---|---|
[JPA] @JoinColumns 두 개 이상의 조인 컬럼 설정하기 (0) | 2023.08.06 |
[Spring] Spring Security 란? (0) | 2023.07.26 |
[Spring] Exception Handler (with. Spring Data Rest) (1) | 2023.07.12 |
[Spring] Spring Data Rest 란? (0) | 2023.07.10 |