07. 스프링 시큐리티 (Spring Security) - OAuth2 를 이용한 네이버, 카카오, 구글 인증 + JWT
OS | Windows 10 Home 64bit 버전 1903 (OS 빌드 18362.836) |
FrameWork | SpringBoot 2.3.1.RELEASE ( 2.4 와 2.5 에서는 profile 전략이 다릅니다 ) |
Security | spring-boot-starter-security 5 |
EditTool | IntelliJ IDEA 2021.3.1 |
BuildTool | Maven |
FrontEnd | Vue 2.x |
요약
구성
- 클래스 목록
- config
- AppProperties.java
- Auth.class : JWT 토큰의 암호키와, 만료기간을 설정할 때 사용
- OAuth2.class : 프론트 엔드 클라이언트가 /oauth2/authorize 요청에서 지정한 redirectUri 입니다
- SecurityConfig.java : 백엔드의 전반적이 보안 설정
- WebMvcConfig.java : cors 를 설정할 때 사용
- AppProperties.java
- controller
- UserApi.java : 내 정보를 조회하기 위해 사용되는 api ( ROLE_USER 로 권한이 제한됨 )
- AuthApi.java : 소셜로그인 이외에 서버에 이메일로 가입하고 싶은 경우 필요한 API
- exception
- BadRequestException.java : 잘못된 요청에 대해서 처리하기 위한 exception 클래스
- OAuth2AuthenticationProcessingException.java :
OAuth2 인증과정 중에서 발생하는 exception 클래스 AuthenticationException 클래스를 상속받고 있다 - ResourceNotFoundException.java : DB에 존재하지 않는 것을 조회할 때 처리하기 위한 exception 클래스
- entity
- user
- User.class : DB 의 User 테이블
- User.class : DB 의 User 테이블
- model
- Provider.Class (Enum) : 각 플랫폼명을 String 이 아닌 Enum 으로 관리하기 위한 클래스
- user
- payload
- ApiResponse.java : 응답을 반환하기 위한 DTO 클래스
- AuthResponse.java : 응답을 반환하기 위한 DTO 클래스
- LoginRequest.java : 요청을 담기 위한 DTO 클래스
- SignUpRequest.java : 요청을 담기 위한 DTO 클래스
- repository
- UserRepository.class (Interface) : DAO (Data Access Object) 클래스
- security
- oauth
- user
- OAuth2UserInfoFactory.java : 각 플랫폼 클래스를 생성하기 위한 Factory 패턴을 사용한 클래스
- OAuth2UserInfo.java (abstract) : 각 플랫폼 확장성을 위한 추상클래스
- NaverOAuth2UserInfo.java : 네이버에 요청하여 유저정보를 응답받는 클래스
- GoogleOAuth2UserInfo.java : 구글에 요청하여 유저정보를 응답받는 클래스
- FacebookOAuth2UserInfo.java : 페이스북에 요청하여 유저정보를 응답받는 클래스
- GithubOAuth2UserInfo.java : 깃허브에 요청하여 유저정보를 응답받는 클래스
- KakaoOAuth2UserInfo.java : 카카오에 요청하여 유저정보를 응답받는 클래스
- CustomOAuth2UserService.java :
Spring OAuth2 에서 제공하는 OAuth2User 을 가공하여 OAuth2UserInfo 로 만들고
OAuth2UserInfo 에 Email 이 있는 지 검사와, A 라는 플랫폼으로 가입이 되어있는 데, B 플랫폼으로 가입 하려는 경우 검사를 진행하며, 이미 존재하는 계정에 경우에는 Update 를 진행하고,
없는 경우에는 새로 Insert 하며, UserPrincipal 을 리턴한다 - HttpCookieOAuth2AuthorizationRequestRepository.java
OAuth2 프로토콜은 CSRF 공격을 방지하기 위해 state 매개 변수 사용을 권장합니다. 인증 중에 애플리케이션은 인증 요청에서 이 매개 변수를 전송하고, OAuth2 공급자는 OAuth2 콜백에서 변경되지 않은 이 매개 변수를 리턴합니다.
응용 프로그램은 OAuth2 공급자에서 반환 된 state 매개 변수의 값을 초기에 보낸 값과 비교합니다. 일치하지 않으면 인증 요청을 거부합니다.
이 흐름을 얻으려면 애플리케이션이 나중에 OAuth2 공급자에서 반환된 상태와 비교할 수 있도록 state 매개 변수를 어딘가에 저장해야합니다.
단기(short-lived) 쿠키에 상태와 redirect_uri를 저장할 것입니다. 다음 클래스는 인증 요청을 쿠키에 저장하고 검색하는 기능을 제공합니다. - 출처 http://yoonbumtae.com/?p=3000 - OAuth2AuthenticationFailureHandler.java :
OAuth2 로그인 과정 중 실패했을 때 처리하는 클래스
에러 메시지를 query 에 담아 redirect_uri 쿠키에 담겨져있던 곳으로 리다이렉트 된다
-> http://localhost:3000/oauth2/redirect?error={errorMessage}
cf ) redirect_uri 가 없는 경우 기본으로 http://localhost:8080/?error={errorMessage} 가 된다 - OAuth2AuthenticationSuccessHandler : OAuth2 로그인 과정이 성공했을 때 처리하는 클래스
-> http://localhost:3000/oauth2/redirect?token={jwt token}
cf ) redirect_uri 가 없는 경우 기본으로 http://localhost:8080/?token={jwt token} 가 된다
- user
- CurrentUser.java (Annotation) :
SecurityContextHolder.getContext().getAuthentication().getPrincipal() 을 통해 UserPrincipal 을
가져오던 것을 간단하게 처리하기 위해 만들어진 어노테이션
물론 @AuthenticationPrincipal 로 가능 하지만 더 짧고 명확한 명칭을 주기 위해 만든 것 뿐 - CustomUserDetailsService.java :
TokenFilter 에서 DB 로 인증을 받기 위해 만들어진 Service 클래스 - RestAuthenticationEntryPoint.java : 이 클래스는 사용자가 인증없이 보안된 리소스에 액세스하려고 할 때 호출됩니다. 이 경우 401 Unauthorized 응답만 반환합니다.
- TokenAuthenticationFilter.java
이 클래스는 request Header에서 Authorization 을 가져다가 JWT 인증 토큰을 읽어 인증(verify)하고, 토큰이 유효한 경우 Spring Security의 SecurityContext를 설정하는 데 사용됩니다. - TokenProvider.java : Json 웹 토큰을 생성하고 인증(verify)하는 코드가 포함되어 있습니다.
- UserPrincipal.java : Security 에서 인증 객체로 사용할 클래스입니다
- oauth
- util
- CookieUtils.java : 쿠키 저장, 삭제를 담당하는 클래스입니다
- CookieUtils.java : 쿠키 저장, 삭제를 담당하는 클래스입니다
- config
Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-social</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>oauth2-demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml 설정
[방법1] application.yml 에 모두 작성하는 방법
spring:
datasource:
url: jdbc:h2:mem:testdb
username: sa
password:
jpa:
show-sql: true
hibernate:
ddl-auto: create
security:
oauth2:
client:
registration:
google:
clientId: [구글 클라이언트 아이디]
clientSecret: [구글 클라이언트 패스워드]
redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/google
scope: # 각 플랫폼마다 scope 가 다르므로 직접 방문해서 api 문서를 참고 바람
- email
- profile
facebook:
clientId: [페이스북 클라이언트 아이디]
clientSecret: [페이스북 클라이언트 패스워드]
redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/facebook
scope: # 각 플랫폼마다 scope 가 다르므로 직접 방문해서 api 문서를 참고 바람
- email
- public_profile
github:
clientId: [깃 허브 클라이언트 아이디]
clientSecret: [깃 허브 클라이언트 패스워드]
redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/github
scope: # 각 플랫폼마다 scope 가 다르므로 직접 방문해서 api 문서를 참고 바람
- user:email
- read:user
naver:
clientId: [네이버 클라이언트 아이디]
client-secret: [네이버 클라이언트 패스워드]
redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/naver
authorization-grant-type: authorization_code
scope:
- name
- email
- profile_image
client-name: Naver
provider:
facebook:
authorizationUri: https://www.facebook.com/v3.0/dialog/oauth
tokenUri: https://graph.facebook.com/v3.0/oauth/access_token
userInfoUri: https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250)
naver:
authorization_uri: https://nid.naver.com/oauth2.0/authorize # api 문서 참고
token_uri: https://nid.naver.com/oauth2.0/token # api 문서 참고
user-info-uri: https://openapi.naver.com/v1/nid/me # api 문서 참고
user_name_attribute: response # api 문서 참고
app:
auth:
tokenSecret: 926D96C90030DD58429D2751AC1BDBBC # JWT를 암호화 하기 위한 암호화 키 (32글자면 된다)
tokenExpirationMsec: 864000000 # token 만료 기간 (24h * 60min * 60s * 1000ms)
oauth2:
# OAuth2 공급자로 성공적으로 인증 한 후 사용자에 대한 인증 토큰을 생성하고 토큰을
# 프론트 엔드 클라이언트가 /oauth2/authorize 요청에서 지정한 redirectUri 입니다.
# 쿠키는 모바일 클라이언트에서 잘 작동하지 않기 때문에 사용하지 않습니다
authorizedRedirectUris:
- http://localhost:3000/oauth2/redirect
- myandroidapp://oauth2/redirect
- myiosapp://oauth2/redirect
[방법2] application.yml 분리 방법
- application.yml
spring:
profiles:
include: h2, oauth2, app
- application-h2.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
username: sa
password:
jpa:
show-sql: true
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
h2:
console:
enabled: true
- application-oauth2.yml
spring:
profiles:
include: naver, kakao, google
---
spring:
profiles: naver
security:
oauth2:
client:
registration:
naver:
clientId: {클라이언트 아이디}
client-secret: {클라이언트 비밀번호}
redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
authorization-grant-type: authorization_code
scope:
- name
- email
- profile_image
client-name: Naver
provider:
naver:
authorization_uri: https://nid.naver.com/oauth2.0/authorize # api 문서 참고
token_uri: https://nid.naver.com/oauth2.0/token # api 문서 참고
user-info-uri: https://openapi.naver.com/v1/nid/me # api 문서 참고
user_name_attribute: response # api 문서 참고
---
spring:
profiles: kakao
security:
oauth2:
client:
registration:
kakao:
clientId: {클라이언트 아이디}
client-secret: {클라이언트 비밀번호}
redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
authorization-grant-type: authorization_code
scope:
- profile_nickname
- profile_image
- account_email
client-authentication-method: POST
client-name: kakao
provider:
kakao:
authorization_uri: https://kauth.kakao.com/oauth/authorize # api 문서 참고
token_uri: https://kauth.kakao.com/oauth/token # api 문서 참고
user-info-uri: https://kapi.kakao.com/v2/user/me # api 문서 참고
user_name_attribute: id # api 문서 참고
---
spring:
profiles: google
security:
oauth2:
client:
registration:
google:
clientId: {클라이언트 아이디}
clientSecret: {클라이언트 비밀번호}
redirectUri: "{baseUrl}/oauth2/callback/{registrationId}"
scope:
- email
- profile
# google, github, facebook 은 기본적으로 provider 를 제공한다
---
spring:
profiles: github
security:
oauth2:
client:
registration:
github:
clientId: {클라이언트 아이디}
clientSecret: {클라이언트 비밀번호}
redirectUri: "{baseUrl}/oauth2/callback/{registrationId}"
scope:
- user:email
- read:user
# google, github 는 기본적으로 provider 를 제공한다
---
spring:
profiles: facebook
security:
oauth2:
client:
registration:
facebook:
clientId: {클라이언트 아이디}
clientSecret: {클라이언트 비밀번호}
redirectUri: "{baseUrl}/oauth2/callback/{registrationId}"
scope:
- email
- public_profile
provider:
facebook:
authorizationUri: https://www.facebook.com/v3.0/dialog/oauth
tokenUri: https://graph.facebook.com/v3.0/oauth/access_token
userInfoUri: https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250)
{baseUrl} : http://localhost:8080
{action} : login
{registrationId} : naver 또는 kakao 또는 google 또는 github 또는 facebook .... 등 등 각 플랫폼명
현재 위 naver, kakao, google include 해서 사용하고 있으며, github 와 facebook 도 사용하려면 include 에 추가하면 된다
- application-app.yml
app:
auth:
tokenSecret: 926D96C90030DD58429D2751AC1BDBBC # JWT를 암호화 하기 위한 암호화 키
tokenExpirationMsec: 864000000 # token 만료 기간 (24h * 60min * 60s * 1000ms)
oauth2:
# OAuth2 공급자로 성공적으로 인증 한 후 사용자에 대한 인증 토큰을 생성하고 토큰을
# 프론트 엔드 클라이언트가 /oauth2/authorize 요청에서 지정한 redirectUri 입니다.
# 쿠키는 모바일 클라이언트에서 잘 작동하지 않기 때문에 사용하지 않습니다
authorizedRedirectUris:
- http://localhost:3000/oauth2/redirect
- myandroidapp://oauth2/redirect
- myiosapp://oauth2/redirect
authorizedRedirectUris 는 프론트엔드에서 url 파라미터로 redirect_uri=주소 했을 때 리다이렉트 되는 주소이다.
파라미터를 넘겨주지 않는 경우 successHandler 와 failureHandler 에서 기본적으로 http://localhost:8080/ 으로 설정한다
- [기타] MySQL Url
url: jdbc:mysql://localhost:3306/{DB명}?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
AppProperties.java
코드
@Getter
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private final Auth auth = new Auth();
private final OAuth2 oauth2 = new OAuth2();
@Getter
@Setter
public static class Auth {
private String tokenSecret;
private long tokenExpirationMsec;
}
@Getter
public static final class OAuth2 {
private List<String> authorizedRedirectUris = new ArrayList<>();
public OAuth2 authorizedRedirectUris(List<String> authorizedRedirectUris) {
this.authorizedRedirectUris = authorizedRedirectUris;
return this;
}
}
}
Security Config
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService customUserDetailsService;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter();
}
/*
By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
the authorization request. But, since our service is stateless, we can't save it in
the session. We'll save the request in a Base64 encoded cookie instead.
*/
@Bean
public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable()
.headers().frameOptions().disable()
.and()
.formLogin()
.disable()
.httpBasic()
.disable()
.exceptionHandling()
.authenticationEntryPoint(new RestAuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/",
"/error",
"/favicon.ico",
"/h2-console/**")
.permitAll()
.antMatchers("/auth/**", "/oauth2/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.authorizationEndpoint()
.baseUri("/oauth2/authorize")
.authorizationRequestRepository(cookieAuthorizationRequestRepository())
.and()
.redirectionEndpoint()
.baseUri("/oauth2/callback/*")
.and()
.userInfoEndpoint()
.userService(customOAuth2UserService)
.and()
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler);
// Add our custom Token based authentication filter
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
- cors 사용
- 세션 매니저 무상태성으로 변경 (세션 사용안함)
- csrf 사용안함
- header <frame></frame> 끄기 -> h2-console 을 위함
- form login 사용안함
- http basic login 사용안함
- 인증 또는 인가에 대한 exception 핸들링 클래스를 정의
- 접근 허용 url 설정 (auth 는 이메일로 가입, oauth 는 social 로 가입할 때 사용)
- authorizationEndpoint 는 각 플랫폼으로 리다이렉트를 하기위한 기본 url
-> http://localhost:8080/oauth2/authorize/naver -> 네이버 로그인화면 이동
- redirectEndpoint
-> 네이버로 부터 accessToken 을 받아왔을 때 우리 서버에서 처리 할 url
-> http://localhost:8080/oauth2/callback/naver -> naver 는 registrationId 가 된다
- userInfoEndpoint
-> accessToken 을 가지고 플랫폼(네이버)에서 해당 유저에 대한 정보를 가져온다 (restTemplate 이용)
-> userService 를 통해 가져온 정보를 가공한다
- successHandler
-> Client 에서 요청한 redirect_uri 파라미터가 서버 application.yml 에 authorizedRedirectUris 에 설정과 같은 지
매칭하는 작업과 토큰을 생성하여 해당 리다이렉트 주소로 토큰을 파라미터로 넘겨주는 일을 한다
-> { 해당 주소 } ?token={ JWT Token }
- failureHanlder
-> 에러를 처리하는 일을 하며, 해당 리다이렉트 주소 리다이렉트 하며 에러메시지를 전달한다
-> { 해당 주소 } ?error={ message }
- TokenAuthenticationFilter 를 UsernameAuthenticationFilter 앞에 놓아 인증을 시도하는 필터로 사용된다
-> 클라이언트에서 Header 에 Authorization Bearer {JWT} 을 넘겨주어야 인증이 된다
/oauth2/authorize/{registrationId} = /oauth2/authorize/naver 로 요청이 들어왔을 때 처리하는 Filter 이다
WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(MAX_AGE_SECS);
}
}
- allowedOrigins(*) : 외부에서 들어오는 모든 url 들을 허용한다 (보통은 파트너 또는 api 를 사용하는 곳만 열어둔다)
http://www.naver.com/** , http://localhost:3000/** 처럼..
- Method 는 허용되는 메소드들을 정의 한다
- Headers 는 허용되는 헤더를 정의한다
- Credentials 는 자격증명을 허용한다
- maxAge 는 최대 1시간 허용
Contoller
AuthController (서버에 이메일로 가입하기 위한 Auth Api)
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TokenProvider tokenProvider;
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.createToken(authentication);
return ResponseEntity.ok(new AuthResponse(token));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if(userRepository.existsByEmail(signUpRequest.getEmail())) {
throw new BadRequestException("Email address already in use.");
}
// Creating user's account
User user = new User();
user.setName(signUpRequest.getName());
user.setEmail(signUpRequest.getEmail());
user.setPassword(signUpRequest.getPassword());
user.setProvider(AuthProvider.local);
user.setPassword(passwordEncoder.encode(user.getPassword()));
User result = userRepository.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentContextPath().path("/user/me")
.buildAndExpand(result.getId()).toUri();
return ResponseEntity.created(location)
.body(new ApiResponse(true, "User registered successfully@"));
}
}
UserController (서버에 로그인이 성공한 경우 접근할 수 있는 User Api)
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/user/me")
@PreAuthorize("hasRole('USER')")
public User getCurrentUser(@CurrentUser UserPrincipal userPrincipal) {
return userRepository.findById(userPrincipal.getId())
.orElseThrow(() -> new ResourceNotFoundException("User", "id", userPrincipal.getId()));
}
}
Entity & Model
AuthProvider
public enum AuthProvider {
local,
facebook,
google,
github,
naver,
kakao
}
User
@Getter
@Entity
@Table(name = "users", uniqueConstraints = {
@UniqueConstraint(columnNames = "email")
})
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Email
@Column(nullable = false)
private String email;
private String imageUrl;
@Column(nullable = false)
private Boolean emailVerified = false;
@JsonIgnore
private String password;
@NotNull
@Enumerated(EnumType.STRING)
private AuthProvider provider;
private String providerId;
@Builder(builderClassName= "social", builderMethodName = "socialBuilder")
private User(String name, @Email String email, String imageUrl, @NotNull AuthProvider provider, String providerId) {
this.name = name;
this.email = email;
this.imageUrl = imageUrl;
this.provider = provider;
this.providerId = providerId;
}
@Builder(builderClassName = "local",builderMethodName = "localBuilder")
public User(String name, @Email String email, String imageUrl, String password, @NotNull AuthProvider provider, String providerId) {
this.name = name;
this.email = email;
this.imageUrl = imageUrl;
this.password = password;
this.provider = provider;
this.providerId = providerId;
}
public void updateNameAndImage(String name, String imageUrl) {
this.name = name;
this.imageUrl = imageUrl;
}
}
Exception
BadRequestException
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
}
OAuth2AuthenticationProcessingException
public class OAuth2AuthenticationProcessingException extends AuthenticationException {
public OAuth2AuthenticationProcessingException(String msg, Throwable t) {
super(msg, t);
}
public OAuth2AuthenticationProcessingException(String msg) {
super(msg);
}
}
ResourceNotFoundException
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private String resourceName;
private String fieldName;
private Object fieldValue;
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}
public String getResourceName() {
return resourceName;
}
public String getFieldName() {
return fieldName;
}
public Object getFieldValue() {
return fieldValue;
}
}
Result(Response) & Payload(Request)
ApiResponse
@Getter
@Setter
@AllArgsConstructor
public class ApiResponse {
private boolean success;
private String message;
}
AuthResponse
@Getter
@Setter
public class AuthResponse {
private String accessToken;
private String tokenType = "Bearer";
public AuthResponse(String accessToken) {
this.accessToken = accessToken;
}
}
LoginRequest
@Getter
public class LoginRequest {
@NotBlank
@Email
private String email;
@NotBlank
private String password;
}
SignUpRequest
@Getter
public class SignUpRequest {
@NotBlank
private String name;
@NotBlank
@Email
private String email;
@NotBlank
private String password;
}
Repository
UserRepository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Boolean existsByEmail(String email);
}
Security
TokenAuthenticationFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
TokenProvider
@Service
@Slf4j
@RequiredArgsConstructor
public class TokenProvider {
private final AppProperties appProperties;
public String createToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + appProperties.getAuth().getTokenExpirationMsec());
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, appProperties.getAuth().getTokenSecret())
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(appProperties.getAuth().getTokenSecret())
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty.");
}
return false;
}
}
CustomUserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() ->
new UsernameNotFoundException("User not found with email : " + email)
);
return UserPrincipal.create(user);
}
@Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id).orElseThrow(
() -> new ResourceNotFoundException("User", "id", id)
);
return UserPrincipal.create(user);
}
}
UserPrincipal
@Getter
public class UserPrincipal implements OAuth2User, UserDetails {
private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
@Setter
private Map<String, Object> attributes;
public UserPrincipal(Long id, String email, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = Collections.
singletonList(new SimpleGrantedAuthority("ROLE_USER"));
return new UserPrincipal(
user.getId(),
user.getEmail(),
user.getPassword(),
authorities
);
}
public static UserPrincipal create(User user, Map<String, Object> attributes) {
UserPrincipal userPrincipal = UserPrincipal.create(user);
userPrincipal.setAttributes(attributes);
return userPrincipal;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@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() {
return authorities;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return String.valueOf(id);
}
}
CurrentUser
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {
}
RestAuthenticationEntryPoint
@Slf4j
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
log.error("승인되지 않은 오류로 응답합니다. 메세지: {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
e.getLocalizedMessage());
}
}
HttpCookieOAuth2AuthorizationRequestRepository
@Component
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
private static final int cookieExpireSeconds = 180;
/** cookie 에 저장되어있던 authorizationRequest 들을 가져온다 authorizationUri, authorizationGrantType, responseType, clientId, redirectUri, scopes, additionalParameters */
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
.map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class))
.orElse(null);
}
/** 플랫폼으로 보내기 위한 Request 를 `oauth2_auth_request` 라는 cookie 에 저장 한다 authorizationUri, authorizationGrantType, responseType, clientId, redirectUri, scopes, additionalParameters */
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
removeAuthorizationRequest(request, response);
return;
}
CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds);
/*
* http://localhost:8080/oauth2/authorize/naver?redirect_uri=http://localhost:3000/oauth/redirect 로 요청 받았을 때
* http://localhost:3000/oauth/redirect 를 가져온다
* 그리고 존재하는 경우 cookie 에 넣어준다
*/
String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds);
}
}
/** remove 를 재정의 해서 cookie 를 가져오고 remove 는 successHandler 또는 failureHandler 에서 할 수 있도록 한다 */
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
return this.loadAuthorizationRequest(request);
}
/**
* 사용자 정보를 다 가지고 온 뒤 이제 리다이렉트를 하면 기존에 남아있던 쿠키들을 제거해주기 위해 사용된다
* OAuth2AuthorizationRequest 와 클라이언트에서 파리미터로 요청한 redirect_uri 가 된다
* */
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
}
}
CustomOAuth2UserService
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
try {
return processOAuth2User(oAuth2UserRequest, oAuth2User);
} catch (AuthenticationException ex) {
throw ex;
} catch (Exception ex) {
// Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
final String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());
if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
}
Optional<User> userOptional = userRepository.findByEmail(oAuth2UserInfo.getEmail());
User user;
// 이미 존재하는 경우
if(userOptional.isPresent()) {
user = userOptional.get();
// 가져온 유저의 공급자명과 넘어온 공급자명이 다른 경우
if(!user.getProvider().equals(AuthProvider.valueOf(registrationId))) {
// 이미 다른 공급자가 존재하기 때문에 가입할 수 없다
throw new OAuth2AuthenticationProcessingException("Looks like you're signed up with " +
user.getProvider() + " account. Please use your " + user.getProvider() +
" account to login.");
}
user = updateExistingUser(user, oAuth2UserInfo);
} else {
user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
}
return UserPrincipal.create(user, oAuth2User.getAttributes());
}
private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {
User user = User.socialBuilder()
.provider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))
.providerId(oAuth2UserInfo.getId())
.name(oAuth2UserInfo.getName())
.email(oAuth2UserInfo.getEmail())
.imageUrl(oAuth2UserInfo.getImageUrl())
.build();
return userRepository.save(user);
}
private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) {
existingUser.updateNameAndImage(oAuth2UserInfo.getName(), oAuth2UserInfo.getImageUrl());
return userRepository.save(existingUser);
}
}
OAuth2AuthenticationSuccessHandler
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
private final AppProperties appProperties;
private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String targetUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
clearAuthenticationAttributes(request, response);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
.map(Cookie::getValue);
if(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
}
String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
String token = tokenProvider.createToken(authentication);
return UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("token", token)
.build().toUriString();
}
protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
private boolean isAuthorizedRedirectUri(String uri) {
URI clientRedirectUri = URI.create(uri);
return appProperties.getOauth2().getAuthorizedRedirectUris()
.stream()
.anyMatch(authorizedRedirectUri -> {
// Only validate host and port. Let the clients use different paths if they want to
URI authorizedURI = URI.create(authorizedRedirectUri);
if(authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
&& authorizedURI.getPort() == clientRedirectUri.getPort()) {
return true;
}
return false;
});
}
}
OAuth2AuthenticationFailureHandler
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
.map(Cookie::getValue)
.orElse(("/"));
targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("error", exception.getLocalizedMessage())
.build().toUriString();
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
OAuth2UserInfoFactory
public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
System.err.println(attributes);
switch (AuthProvider.valueOf(registrationId.toLowerCase())) {
case naver:
return new NaverOAuth2UserInfo(attributes);
case kakao:
return new KakaoOAuth2UserInfo(attributes);
case google:
return new GoogleOAuth2UserInfo(attributes);
case facebook:
return new FacebookOAuth2UserInfo(attributes);
case github:
return new GithubOAuth2UserInfo(attributes);
default:
throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet.");
}
}
}
OAuth2UserInfo
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public Map<String, Object> getAttributes() {
return attributes;
}
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
public abstract String getImageUrl();
}
NaverOAuth2UserInfo
/**
* {
* resultcode=00,
* message=success,
* response={
* id=아이디,
* profile_image=이미지주소.png,
* email=이메일, name=이름
* }
* }
*/
public class NaverOAuth2UserInfo extends OAuth2UserInfo {
/** naver 는 response 안에 담겨져있기 때문에 response 를 해준다 */
public NaverOAuth2UserInfo(Map<String, Object> attributes) {
super((Map<String, Object>) attributes.get("response"));
}
@Override
public String getId() {
return (String) attributes.get("id");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getImageUrl() {
return (String) attributes.get("profile_image");
}
}
KakaoOAuth2UserInfo
/**
* {
* id=11111111,
* connected_at=2021-08-14T14:02:42Z,
* properties={
* nickname=닉네임
* },
* kakao_account={
* profile_nickname_needs_agreement=false,
* profile_image_needs_agreement=false,
* profile={
* nickname=닉네임,
* thumbnail_image_url=xxx.jpg,
* profile_image_url=xxx.jpg,
* is_default_image=true
* },
* has_email=true,
* email_needs_agreement=false,
* is_email_valid=true,
* is_email_verified=true,
* email=xxx@naver.com
* }
* }
* */
public class KakaoOAuth2UserInfo extends OAuth2UserInfo{
/** 카카오는 Integer 로 받아져서 그런지 (String) 또는 (Long) 으로 cascading 이 되지 않는다... 그래서 Integer 로 받아준다 */
private Integer id;
public KakaoOAuth2UserInfo(Map<String, Object> attributes) {
super((Map<String, Object>) attributes.get("kakao_account"));
this.id = (Integer) attributes.get("id");
}
@Override
public String getId() {
return this.id.toString();
}
@Override
public String getName() {
return (String) ((Map<String, Object>) attributes.get("profile")).get("nickname");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getImageUrl() {
return (String) ((Map<String, Object>) attributes.get("profile")).get("thumbnail_image_url");
}
}
GoogleOAuth2UserInfo
public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
super(attributes);
}
@Override
public String getId() {
return (String) attributes.get("sub");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getImageUrl() {
return (String) attributes.get("picture");
}
}
github , facebook, instargram 은 각자찾길...
util
CookieUtils
public class CookieUtils {
public static Optional<Cookie> getCookie(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return Optional.of(cookie);
}
}
}
return Optional.empty();
}
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie: cookies) {
if (cookie.getName().equals(name)) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
}
}
public static String serialize(Object object) {
return Base64.getUrlEncoder()
.encodeToString(SerializationUtils.serialize(object));
}
public static <T> T deserialize(Cookie cookie, Class<T> cls) {
return cls.cast(SerializationUtils.deserialize(
Base64.getUrlDecoder().decode(cookie.getValue())));
}
}
OAuth2AuthorizationRequest 를 base64 로 인코딩해서 각 플랫폼 로그인 url 에 보낼 때 사용한다
프론트 엔드 부분
<p>
<a href="http://localhost:8080/oauth2/authorize/naver?redirect_uri=http://localhost:3000/oauth2/redirect">네이버</a>
</p>
<p>
<a href="http://localhost:8080/oauth2/authorize/kakao?redirect_uri=http://localhost:3000/oauth2/redirect">카카오</a>
</p>
<p>
<a href="http://localhost:8080/oauth2/authorize/google?redirect_uri=http://localhost:3000/oauth2/redirect">구글</a>
</p>
- redirect_uri 를 통해 프론트엔드 쪽으로 token 또는 error 파라미터가 넘어온다
넘어온 token 또는 error 를 받아 LocalStorage 또는 Cookie 또는 vue 의 경우 vuex, react 경우 redux 에 저장 한 뒤
권한이 필요한 요청에 대해서 Header Autorization 에 Bearer { jwt token } 을 같이 포함하여 요청하면 된다
네이버
https://developers.naver.com/main/
카카오
구글
https://console.cloud.google.com/home/dashboard?project=spring-auto-test&hl=ko
'10. Spring > Security' 카테고리의 다른 글
06. 스프링 시큐리티 - Spring Security CORS 와 Spring MVC CORS (0) | 2021.08.13 |
---|---|
05. 스프링 시큐리티 (Spring Security) - Exception (1) | 2020.09.22 |
인프런 - 실전프로젝트 - 인가 프로세스 DB 연동 서비스 계층 구현 (0) | 2020.08.28 |
인프런 - 실전프로젝트 - 인가 프로세스 DB 연동 웹 계층 구현 (0) | 2020.08.25 |
인프런 - 실전프로젝트 - 인증 프로세스 Ajax 인증 구현 (0) | 2020.08.24 |
댓글
이 글 공유하기
다른 글
-
06. 스프링 시큐리티 - Spring Security CORS 와 Spring MVC CORS
06. 스프링 시큐리티 - Spring Security CORS 와 Spring MVC CORS
2021.08.13 -
05. 스프링 시큐리티 (Spring Security) - Exception
05. 스프링 시큐리티 (Spring Security) - Exception
2020.09.22 -
인프런 - 실전프로젝트 - 인가 프로세스 DB 연동 서비스 계층 구현
인프런 - 실전프로젝트 - 인가 프로세스 DB 연동 서비스 계층 구현
2020.08.28 -
인프런 - 실전프로젝트 - 인가 프로세스 DB 연동 웹 계층 구현
인프런 - 실전프로젝트 - 인가 프로세스 DB 연동 웹 계층 구현
2020.08.25