์ฒ์ ํด๋น ์ฝ๋ ์์ฑํ ๋, ์ ๋ง ์ค๋ ์๊ฐ์ด ๊ฑธ๋ ธ์ต๋๋ค
๊ด๋ จ๋ ์๋ฃ๊ฐ ์๋ฒ์ฌ์ด๋ ๋๋๋ง์ ํตํด ํ์๊ฐ์ ์ ์งํํ๋ ์์๋ฐ์ ์์๊ณ , REST API๋ฅผ ์ฌ์ฉํ์ฌ ํต์ ํ ๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํด์ผ ํ๋์ง์ ๋ํ ์๋ฃ๊ฐ ์์ด์ ๋๊ฒ ๊ณ ์ ๋ง์ดํ์์ต๋๋ค.
์ ์ ๊ฐ์ ์ฌ๋์ด ๋ง์๊น๋ด ์ด๋ ๊ฒ ์ง๊ธ๊น์ง ํ๋ ๊ฒ๋ค์ ์จ๋ณด๊ฒ ์ต๋๋ค.
๋์ ์๋ฆฌ
ํ๋ก ํธ์๋๋ ๋ฆฌ์กํธ๋ฅผ ์ฌ์ฉํ๊ณ (๋ฆฌ์กํธ๊ฐ ์๋์ด๋ ์๊ด์์ต๋๋ค), ๋ฐฑ์๋๋ก๋ ์คํ๋ง์ ์ฌ์ฉํ์ฌ ๋ก๊ทธ์ธ์ ์งํํ ๊ฒ์ ๋๋ค. ๋ฆฌ์กํธ๋ ํ๋๋ฒ์ ๋ชจ๋ฅด๊ธฐ์ ๋์ด๊ฐ๊ณ , ๋ฐฑ์๋์ ๊ตฌ์ฑ ์ฝ๋๋ง ์์ฑํ๊ฒ ์ต๋๋ค.
์ฐ์ ํ๋ก ํธ์์ ์์ ๋ก๊ทธ์ธ API๋ฅผ ์ฌ์ฉํ์ฌ AccessToken์ ๋ฐ์์ค๋ ์ฝ๋๊น์ง๋ ์์ฑํด ์ฃผ์ด์ผ ํฉ๋๋ค.
ํ๋ก ํธ์์๋ AccessToken์ ๋ฐ๊ธ๋ฐ์ ์๋ฒ์ ์ ๋ฌํด์ฃผ๊ณ , ์๋ฒ์์๋ ํด๋น AccessToken์ ์ฌ์ฉํด์ ํ์์ ์ ๋ณด๋ฅผ ์กฐํํ์ฌ ํ์๊ฐ์ ํน์ ๋ก๊ทธ์ธ์ ์งํํ๋ ๋ฐฉ์์ ๋๋ค.
(ํน์๋ ๊ทธ๋ฅ ์๋ฒ์์ ๋ ๋ค ํ๋ฉด ์๋๋์? ๋ผ๊ณ ํ ์๋ ์๋๋ฐ, ์ ๋ ์๋์์ต๋๋ค. CORS ์ค๋ฅ๊ฐ ๋ฏธ์น๋ฏ์ด ๋ฐ์ํ์๊ณ ๊ฒฐ๊ตญ ์คํจํ์ต๋๋ค. ์ดํ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋, ํ๋ก ํธ์ ๋ฐฑ์ ๋ถ๋ฆฌํด์ ์งํํ๋ ๊ฒฝ์ฐ์๋ ํ๋ก ํธ์์ AccessToken๊น์ง ๋ฐ์์ค๊ณ , ๋ฐฑ์ ํด๋น AccessToken์ ํตํด ์ ๋ณด๋ฅผ ์กฐํํ๋ ๋ฐฉ์์ผ๋ก ์งํํ๋ ๊ฒ์ด ์ ์์ธ ๊ฒ ๊ฐ์์ต๋๋ค)
์ฆ ์ ๋ฆฌํ๋ฉด
ํ๋ก ํธ
- ๋ก๊ทธ์ธ API ์๋ฒ์์ ์ธ๊ฐ ์ฝ๋ ๋ฐ์์ค๊ธฐ
- ๋ฐ์์จ ์ธ๊ฐ ์ฝ๋๋ฅผ ํตํด AccessToken ๋ฐ์์ค๊ธฐ
- ๋ฐ์์จ AccessToken์ Header์ ํฌํจํ์ฌ ์๋ฒ์ ์ ์กํ๊ธฐ
๋ฐฑ
- ํด๋ผ์ด์ธํธ๊ฐ ์ ํด์ง ์ฃผ์๋ก AccessToken์ ํค๋์ ๋ด์์ ์์ฒญ์ ๋ณด๋ด๋ฉด, ํด๋น AccessToken์ ๊ฐ์ง๊ณ ํ์ ์ ๋ณด ์กฐํํ๊ธฐ
- ์กฐํํ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก, ํ์๊ฐ์ ์ด๋, ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ฑ์ ๋ก์ง ๊ตฌํํ๊ธฐ
Domain
์๋ฌด ๊ธฐ๋ฅ ์์ด ํ์๊ฐ์ ๊ณผ ๋ก๊ทธ์ธ๋ง ์งํํ ๊ฒ์ด๋ฏ๋ก Member ํด๋์ค ๋จ ํ๋๋ง ๋ง๋ค๊ฒ ์ต๋๋ค.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@AllArgsConstructor
@Builder
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private String password;
private String name;
private String socialId;
@Enumerated(EnumType.STRING)
private SocialType socialType;
@Enumerated(EnumType.STRING)
private Role role;
}
socialId๋ ๊ฐ ์์ ๋ก๊ทธ์ธ์ ์๋ณ๊ฐ์ ๋๋ค. (์นด์นด์ค์ ๋ค์ด๋ฒ๋ id, ๊ตฌ๊ธ์ email์ ์ฌ์ฉํฉ๋๋ค.)
SocialType
public enum SocialType {
KAKAO(
"kakao",
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET
),
GOOGLE(
"google",
"https://www.googleapis.com/oauth2/v3/userinfo",
HttpMethod.GET
),
NAVER(
"naver",
"https://openapi.naver.com/v1/nid/me",
HttpMethod.GET
);
private String socialName;
private String userInfoUrl;
private HttpMethod method;
SocialType(String socialName, String userInfoUrl, HttpMethod method) {
this.socialName = socialName;
this.userInfoUrl = userInfoUrl;
this.method = method;
}
public HttpMethod getMethod() {
return method;
}
public String getSocialName() {
return socialName;
}
public String getUserInfoUrl() {
return userInfoUrl;
}
}
KAKAO์ GOOGLE, NAVER๋ง ์ฌ์ฉํ๊ฒ ์ต๋๋ค.
userInfoUrl ์ AccessToken์ ํตํด ํ์์ ์ ๋ณด๋ฅผ ์กฐํํ URL์ ๋๋ค.
method๋ ํด๋น url๋ก ์์ฒญ์ ๋ณด๋ผ ๋์ HttpMethod๋ฅผ ์ง์ ํ ๊ฒ์ ๋๋ค.
Role
public enum Role {
USER("ROLE_USER"),
GUEST("ROLE_GUEST"),
ADMIN("ROLE_ADMIN");
private String grantedAuthority;
Role(String grantedAuthority) {
this.grantedAuthority = grantedAuthority;
}
public String getGrantedAuthority() {
return grantedAuthority;
}
}
MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findBySocialTypeAndSocialId(SocialType socialType , String socialId);
}
socialId๋ ์์ ๋ก๊ทธ์ธ์ ์๋ณ๊ฐ์ด๋ฏ๋ก, socialType๊ณผ socialId๋ก ํ์์ ์ฐพ์ผ๋ฉด ์ ๋ ํ์์ด ์ค๋ณตํด์ ๋์ฌ ์ ์์ต๋๋ค.
์ ๋ ์ด๋ฅผ ํตํด ํ์์ ์กฐํํ๋ ์ฝ๋๋ฅผ ์์ฑํ์ต๋๋ค.
SecurityConfig
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final OAuth2AccessTokenAuthenticationFilter oAuth2AccessTokenAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//== ์ ๊ทผ ์ ํ ==//
http.authorizeRequests()
.antMatchers("/login/oauth2/*").permitAll() //๋ก๊ทธ์ธ ํ๋ฉด ์ ๊ทผ ๊ฐ๋ฅ
.antMatchers("/").permitAll() //๋ฉ์ธ ํ๋ฉด ์ ๊ทผ ๊ฐ๋ฅ
.anyRequest().hasRole("USER");
http.addFilterBefore(oAuth2AccessTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
๋ง์ ์ค์ ์ ํ์ง๋ ์์์ผ๋ฉฐ, login/oauth2/๋ก ์ค๋ ์์ฒญ๋ง ํ์ฉํ์ฌ ๋ก๊ทธ์ธ์ ์งํ์ํค๋๋ก ํ ๊ฒ์ด๋ค.
addFilterBefore์ ํตํด UsernamePasswordAuthenticationFilter ์ด์ ์ ์๋ก์ด ํํฐ๋ฅผ ๋ฑ๋กํด ์ฃผ์๋๋ฐ, UsernamePasswordAuthenticationFilter๊ฐ Authentication์ ์ํํ๋ ํํฐ ์ค์์๋ ๊ฐ์ฅ ๋จผ์ ์คํ๋๊ธฐ ๋๋ฌธ์,
์ด ์ ์ ๋ฑ๋กํด์ฃผ์ด ์ฐ๋ฆฌ์ ์ปค์คํ ํํฐ๋ฅผ ๊ฐ์ฅ ๋จผ์ ์ฌ์ฉํ๊ฒ ํ ๊ฒ์ด๋ค.
์ฝ๋์ ๋์ ์์
๋ก๊ทธ์ธ ์์ฒญ์ด ๋ค์ด์ค๋ฉด AbstractAuthenticationProcessingFilter์ด ์๋ํฉ๋๋ค.
-> AbstractAuthenticationProcessingFilter๊ฐ attemptAuthentication()๋ฅผ ํธ์ถํ๋๋ฐ,
์ด๋ ์ถ์ ๋ฉ์๋๋ก AbstractAuthenticationProcessingFilter๋ฅผ ๊ตฌํํ ํ์ ํด๋์ค์์ ๊ตฌํํด์ฃผ์ด์ผ ํฉ๋๋ค.
-> ์ ํฌ๋ AbstractAuthenticationProcessingFilter๋ฅผ ๊ตฌํํ OAuth2AccessTokenAuthenticationFilter๋ฅผ ๋ฑ๋กํ ๊ฒ์ด๊ณ , ๋ฐ๋ผ์ OAuth2AccessTokenAuthenticationFilter์ attemptAuthentication()์ด ์๋ํฉ๋๋ค.
ํด๋น ๋ฉ์๋์์๋ ์คํ์ค์ AutenticationManager์ authenticate()๋ฅผ ํธ์ถํ์ฌ ์ธ์ฆ์ ์งํํ๋ค.
->AutenticationManager์ authenticate()๊ฐ ํธ์ถ๋ ๋ ์ธ์๋ก๋ Authentication์ ์ปค์คํ ํ๊ฒ ๊ตฌํํ AccessTokenSocialTypeToken๋ฅผ ์ ๋ฌํ๋ค.
->AutenticationManager์๋ ์ฐ๋ฆฌ๊ฐ ์ปค์คํ ํ Provider์ธ AccessTokenAuthenticationProvider๊ฐ ๋ค์ด์์ผ๋ฉฐ, AutenticationManager๋ authenticate()๋ฉ์๋ ์คํ ์ค์ AccessTokenAuthenticationProvider์ authenticate()๋ฅผ ํธ์ถํฉ๋๋ค.
->AccessTokenAuthenticationProvider์ authenticate() ๋ฉ์๋ ์์์, ์ ํฌ๋ AccessToken์ ๊ฐ์ง๊ณ ํ์์ ์ ๋ณด๋ฅผ ๋ฐ์์์ผ ํฉ๋๋ค. ์ด ์ญํ ์ LoadUserService์ getOAuth2UserDetails()๊ฐ ์คํํฉ๋๋ค.
-> getOAuth2UserDetails()์์๋ SocialLoadStrategy ๋ฅผ ์ฌ์ฉํ์ฌ socialType๋ง๋ค ๋ค๋ฅด๊ฒ RestTemplate์ ํตํด ์ ๋ณด๋ฅผ ์กฐํํ๋ ์์ฒญ์ ๋ณด๋ ๋๋ค. SocialLoadStrategy ์ getSocialPk() ๋ฅผ ํตํด ํด๋น ์์ ๋ก๊ทธ์ธ API์ ์๋ณ๊ฐ์ ๋ฐ์์ต๋๋ค.
-> LoadUserService์์๋ ๋ฐ์์จ ์๋ณ์์ socialType์ ํตํด OAuth2UserDetails์ ์์ฑํ์ฌ ๋ฐํํ๋ค.(OAuth2UserDetails๋ UserDetails ๊ตฌํ์ฒด๋ก, ํ์ ContextHolder์์ ๊บผ๋ด์ด์ ์ฌ์ฉํ ๋, UserDetailํ์์ผ๋ก ํต์ผํ์ฌ ์ฌ์ฉํ๊ธฐ ์ํด ์ด๋ ๊ฒ ๊ตฌํํ์์ต๋๋ค.)
->AccessTokenAuthenticationProvider์์๋ ๋ฐ์์จ OAuth2UserDetails์์ ์๋ณ์์ socialType์ผ๋ก DB์์ ํ์์ ์กฐํํ๋ค. ๋ง์ฝ ์กฐํ๋๋ ํ์์ด ์๋ค๋ฉด ์๋ก ๋ฑ๋กํด์ค๋๋ค. ์ดํ Authentcitaion ๊ตฌํ์ฒด์ธ AccessTokenSocialTypeToken๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ๋ฐํํฉ๋๋ค.
->AccessTokenSocialTypeToken๋ principal๋ก OAuth2UserDetails๋ฅผ ๊ฐ์ง๋ค. ํด๋น AccessTokenSocialTypeToken๋ AccessTokenAuthenticationProvider์์ ๋ฐํ๋์ด AutenticationManager๋ก ๋ฐํ๋๊ณ , ์ต์ข ์ ์ผ๋ก AbstractAuthenticationProcessingFilter์๊ฒ ๋ฐํ๋ฉ๋๋ค.
-> ๋ฌธ์ ์์ด ๋ก๊ทธ์ธ์ด ์ ๋์๋ค๋ฉด successfulAuthentication()๊ฐ ์คํ๋ฉ๋๋ค.
ํด๋น ๋ฉ์๋์์๋ ์ธ์ฆ๋ Authentication ๊ฐ์ฒด๋ฅผ SecurityContextHolder์ ์ ์ฅํ๊ณ , AuthenticationSuccessHandler๋ฅผ ํธ์ถํ์ฌ ๋ก๊ทธ์ธ ์ฑ๊ณต ์ฒ๋ฆฌ๋ฅผ ํฉ๋๋ค.
์ด์ ์ ํฌ๊ฐ ์ปค์คํ ํ์ฌ ์ฌ์ฉํ ์ํคํ ์ฒ๋ค์ ์์๋ณด๊ฒ ์ต๋๋ค.
์ฐ์ AbstractAuthenticationProcessingFilter ๋ฅผ ์ปค์คํ ํ OAuth2AccessTokenAuthenticationFilter ํํฐ์ ๋๋ค.
AbstractAuthenticationProcessingFilter์ ์ฝ๋๋ฅผ ์กฐ๊ธ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์ต์๋จ์ ๋นจ๊ฐ ๋ฐ์ค์์ ์คํ๋๋ attemptAuthentication์ด ๋ณด์ด์๋์?
AbstractAuthenticationProcessingFilter์ ์ถ์ ํด๋์ค์ ๋๋ค.
ํ ํ๋ฆฟ ๋ฉ์๋ ํจํด์ ์ฌ์ฉํ์ฌ, Authentication์ ์๋ํ๋ ๊ฒ์ ๊ตฌํ์ฒด๋ค์๊ฒ ๋งก๊ธด ๊ฒ์ ๋๋ค.
๋ฐ๋ผ์ ์ ํฌ๋ ํด๋น ๋ฉ์๋๋ฅผ ๊ตฌํํ์ฌ Authentication ๊ฐ์ฒด๋ฅผ ๋ฐํํด ์ฃผ์ด์ผ ํฉ๋๋ค.
OAuth2AccessTokenAuthenticationFilter
@Component
@Slf4j
public class OAuth2AccessTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_OAUTH2_LOGIN_REQUEST_URL_PREFIX = "/login/oauth2/"; // /login/oauth2/ + ????? ๋ก ์ค๋ ์์ฒญ์ ์ฒ๋ฆฌํ ๊ฒ์ด๋ค
private static final String HTTP_METHOD = "GET"; //HTTP ๋ฉ์๋์ ๋ฐฉ์์ GET์ด๋ค.
private static final String ACCESS_TOKEN_HEADER_NAME = "Authorization"; //AccessToken์ ํด๋์ ๋ณด๋ผ ๋, ํด๋์ key๋ Authorization์ด๋ค.
private static final AntPathRequestMatcher DEFAULT_OAUTH2_LOGIN_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher(DEFAULT_OAUTH2_LOGIN_REQUEST_URL_PREFIX +"*", HTTP_METHOD); //=> /oauth2/login/* ์ ์์ฒญ์, GET์ผ๋ก ์จ ์์ฒญ์ ๋งค์นญ๋๋ค.
public OAuth2AccessTokenAuthenticationFilter(AccessTokenAuthenticationProvider accessTokenAuthenticationProvider, //Provider๋ฅผ ๋ฑ๋กํด์ฃผ์๋ค. ์ด๋ ์กฐ๊ธ ์ด๋ฐ ์ค๋ช
ํ๊ฒ ๋ค.
AuthenticationSuccessHandler authenticationSuccessHandler, //๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ฒ๋ฆฌํ handler์ด๋ค
AuthenticationFailureHandler authenticationFailureHandler) { //๋ก๊ทธ์ธ ์คํจ ์ ์ฒ๋ฆฌํ handler์ด๋ค.
super(DEFAULT_OAUTH2_LOGIN_PATH_REQUEST_MATCHER); // ์์์ ์ค์ ํ /oauth2/login/* ์ ์์ฒญ์, GET์ผ๋ก ์จ ์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ค์ ํ๋ค.
this.setAuthenticationManager(new ProviderManager(accessTokenAuthenticationProvider));
//AbstractAuthenticationProcessingFilter๋ฅผ ์ปค์คํฐ๋ง์ด์ง ํ๋ ค๋ฉด ProviderManager๋ฅผ ๊ผญ ์ง์ ํด ์ฃผ์ด์ผ ํ๋ค(์๊ทธ๋ฌ๋ฉด ์์ธ๋จ!!!)
this.setAuthenticationSuccessHandler(authenticationSuccessHandler);
this.setAuthenticationFailureHandler(authenticationFailureHandler);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//AbstractAuthenticationProcessingFilter ์ ์ถ์ ๋ฉ์๋๋ฅผ ๊ตฌํํ๋ค. Authentication ๊ฐ์ฒด๋ฅผ ๋ฐํํด์ผ ํ๋ค.
SocialType socialType = extractSocialType(request);
//์ด๋ค ์์
๋ก๊ทธ์ธ์ ์งํํ ๊ฒ์ธ์ง๋ฅผ uri๋กค ํตํด ์ถ์ถํ๋ค. kakao, google, naver๊ฐ ์์ผ๋ฉฐ, ์๋ฅผ ๋ค์ด /oauth2/login/kakao๋ก ์์ฒญ์ ๋ณด๋ด๋ฉด kakao๋ฅผ ์ถ์ถํ๋ค
String accessToken = request.getHeader(ACCESS_TOKEN_HEADER_NAME); //ํค๋์ AccessToken์ ํด๋นํ๋ ๊ฐ์ ๊ฐ์ ธ์จ๋ค.
log.info("{}",socialType.getSocialName());
return this.getAuthenticationManager().authenticate(new AccessTokenSocialTypeToken(accessToken, socialType));
//AuthenticationManager์๊ฒ ์ธ์ฆ ์์ฒญ์ ๋ณด๋ธ๋ค. ์ด๋ Authentication ๊ฐ์ฒด๋ก๋ AccessTokenSocialTypeToken์(์ง์ ์ปค์คํ
ํจ) ์ฌ์ฉํ๋ค.
}
private SocialType extractSocialType(HttpServletRequest request) {//์์ฒญ์ ์ฒ๋ฆฌํ๋ ์ฝ๋์ด๋ค
return Arrays.stream(SocialType.values())//SocialType.values() -> GOOGLE, KAKAO, NAVER ๊ฐ ์๋ค.
.filter(socialType ->
socialType.getSocialName()
.equals(request.getRequestURI().substring(DEFAULT_OAUTH2_LOGIN_REQUEST_URL_PREFIX.length())))
//subString์ ํตํด ๋ฌธ์์ด์ ์๋ผ์ฃผ์๋ค. ํด๋น ์ฝ๋๋ฅผ ์คํํ๋ฉด ~~~/kakao์์ kakao๋ง ์ถ์ถ๋๋ค
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("์๋ชป๋ URL ์ฃผ์์
๋๋ค"));
}
}
(์ฃผ์์ ์ฐธ๊ณ ํด์ฃผ์ธ์)
ํด๋น ํํฐ์ ๋ฑ๋กํ Provide๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค.
AuthenticationManager
์ด๋ฆ ๊ทธ๋๋ก Authentication์ ๊ด๋ฆฌํฉ๋๋ค. AuthenticationManager์ ๊ตฌํ์ฒด๋ก ๋ํ์ ์ผ๋ก ์ฌ์ฉ๋๋ ๊ฒ์ดProviderManager์ ๋๋ค.
AuthenticationManager๋ ์์ OAuth2AccessTokenAuthenticationFilter์์ ๋ณด์๋ฏ์ด authenticate๋ผ๋ ๋ฉ์๋๋ฅผ ํตํด์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋๋ฐ,
ProviderManager์ ์ธ์ฆ ์ฒ๋ฆฌ ๋ถ๋ถ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๋ณด๋ฉด AuthenticationManager๋ Provider(์ ํํ๋ AuthenticationProvider)์ List๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.
์ธ์ฆ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ํด๋น Provider์ ๋ฆฌ์คํธ ์ค์์ ๋งค๊ฐ๋ณ์๋ก ๋ฐ์์จ Authentication ๊ฐ์ฒด์ ์ฒ๋ฆฌ๋ฅผ ์ง์ํ๋ Provider๋ฅผ ์ฐพ์ต๋๋ค.
์ดํ Provider์ authenticate๋ฅผ ํตํด Authentication ๊ฐ์ฒด๋ฅผ ๋ฐ์์จ๋ค.
์ ํฌ๋ AuthenticationManager๋ฅผ ๊ตฌํํ ๊ฒ์ด ์๋๋ผ, AuthenticationManager์ ๊ตฌํ์ฒด์ธ ProviderManager์ ์ ํฌ๊ฐ์์ฑํ ProviderManager๋ฅผ ํ๋ ๋๊ฒจ์ค ๊ฒ์ ๋๋ค.
๋๋จธ์ง ๋ก์ง์ ProviderManager๊ฐ ์ฒ๋ฆฌํด์ค๋๋ค.
AccessTokenAuthenticationProvider
@RequiredArgsConstructor
@Component
public class AccessTokenAuthenticationProvider implements AuthenticationProvider {//AuthenticationProvider์ ๊ตฌํ๋ฐ์ authenticate์ supports๋ฅผ ๊ตฌํํด์ผ ํ๋ค.
private final LoadUserService loadUserService; //restTemplate๋ฅผ ํตํด์ AccessToken์ ๊ฐ์ง๊ณ ํ์์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ญํ ์ ํ๋ค.
private final MemberRepository memberRepository;//๋ฐ์์จ ์ ๋ณด๋ฅผ ํตํด DB์์ ํ์์ ์กฐํํ๋ ์ญํ ์ ํ๋ค.
@SneakyThrows
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {//ProviderManager๊ฐ ํธ์ถํ๋ค. ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ค
OAuth2UserDetails oAuth2User = loadUserService.getOAuth2UserDetails((AccessTokenSocialTypeToken) authentication);
//OAuth2UserDetails๋ UserDetails๋ฅผ ์์๋ฐ์ ๊ตฌํํ ํด๋์ค์ด๋ค. ์ดํ ์ผ๋ฐ ํ์๊ฐ์
์ UserDetails๋ฅผ ์ฌ์ฉํ๋ ๋ถ๋ถ๊ณผ์ ๋คํ์ฑ์ ์ํด ์ด๋ ๊ฒ ๊ตฌํํ์๋ค.
//getOAuth2UserDetails์์๋ restTemplate๊ณผ AccessToken์ ๊ฐ์ง๊ณ ํ์์ ๋ณด๋ฅผ ์กฐํํด์จ๋ค (์๋ณ์ ๊ฐ์ ๊ฐ์ ธ์ด)
Member member = saveOrGet(oAuth2User);//๋ฐ์์จ ์๋ณ์ ๊ฐ๊ณผ social๋ก๊ทธ์ธ ๋ฐฉ์์ ํตํด ํ์์ DB์์ ์กฐํ ํ ์๋ค๋ฉด ์๋ก ๋ฑ๋กํด์ฃผ๊ณ , ์๋ค๋ฉด ๊ทธ๋๋ก ๋ฐํํ๋ค.
oAuth2User.setRoles(member.getRole().name());//์ฐ๋ฆฌ์ Role์ name์ ADMIN, USER, GUEST๋ก ROLE_์ ๋ถ์ฌ์ฃผ๋ ๊ณผ์ ์ด ํ์ํ๋ค. setRolse๊ฐ ๋ด๋นํ๋ค.
return AccessTokenSocialTypeToken.builder().principal(oAuth2User).authorities(oAuth2User.getAuthorities()).build();
//AccessTokenSocialTypeToken๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค. principal์ OAuth2UserDetails๊ฐ์ฒด์ด๋ค. (formLogin์์๋ UserDetails๋ฅผ ๊ฐ์ ธ์์ ๊ฒฐ๊ตญ ContextHolder์ ์ ์ฅํ๊ธฐ ๋๋ฌธ์)
//์ด๋ ๊ฒ ๊ตฌํํ๋ฉด UserDetails ํ์
์ผ๋ก ํ์์ ์ ๋ณด๋ฅผ ์ด๋์๋ ์กฐํํ ์ ์๋ค.
}
private Member saveOrGet(OAuth2UserDetails oAuth2User) {
return memberRepository.findBySocialTypeAndSocialId(oAuth2User.getSocialType(),
oAuth2User.getSocialId()) //socailID(์๋ณ๊ฐ)๊ณผ ์ด๋ค ์์
๋ก๊ทธ์ธ ์ ํ์ธ์ง๋ฅผ ํตํด DB์์ ์กฐํ
.orElseGet(() -> memberRepository.save(Member.builder()
.socialType(oAuth2User.getSocialType())
.socialId(oAuth2User.getSocialId())
.role(Role.GUEST).build()));//์๋ค๋ฉด ๋ฉค๋ฒ๋ฅผ ์๋ก ๋ง๋๋๋ฐ, USER๊ฐ ์๋๋ผ GUEST๋ก ์ค์ ํ๋ค. ์ด๋ ์๋ํด์ ์ค๋ช
ํ๋ค
}
@Override
public boolean supports(Class<?> authentication) {
return AccessTokenSocialTypeToken.class.isAssignableFrom(authentication); //AccessTokenSocialTypeTokenํ์
์ authentication ๊ฐ์ฒด์ด๋ฉด ํด๋น Provider๊ฐ ์ฒ๋ฆฌํ๋ค.
}
/**
* ์ GUEST๋ก ์ค์ ํ์๋??
*
* ์์
๋ก๊ทธ์ธ์์ ํ์๋ก ์ ๊ณตํ๋ ํ์์ ์ ๋ณด๋ค์ด ๋ค ๋ค๋ฅด๋ค.
* ์๋ฅผ ๋ค๋ฉด ์นด์นด์ค๋ ์ด๋ฆ์ email์ ํ์๋ก ์ ๊ณตํ์ง ์๋๋ฐ, ๊ตฌ๊ธ์ email์ด ํ์์ผ ๋ฟ๋๋ฌ ์๋ณ๊ฐ์ผ๋ก ์ฌ์ฉํ๋ค.
* ์ด๋ ๊ฒ ์์
๋ก๊ทธ์ธ๊ฐ์ ์ ๋ณด์ ๋ถ๊ท ํ์ ํด์ํ๊ธฐ ์ํด ์ฐ๋ฆฌ๋ ์์
๋ก๊ทธ์ธ ์์ ์๋ฌด๋ฐ ์ ๋ณด๋ ๋ฐ์์ค์ง ์๊ณ ,
* ๋จ์ง ์๋ณ๊ฐ๋ง์ ์ฌ์ฉํ์ฌ ํ์์ ์ ์ฅํ๋ค.
* ํ์ฌ GUEST๋ก ์ ์ฅ๋ ์ํฉ์ ํด๋ผ์ด์ธํธ๊ฐ ์ ๋ณด ์ ๊ณต์ ๋์๋ฅผ ๋๋ฅธ ๊ฒฝ์ฐ์ ํด๋น ์ฝ๋๊ฐ ์คํ๋๋ค.
*
* ์ ๋ณด ๋์๋ฅผ ํ ์ดํ ์ฐ๋ฆฌ๋ ์ถ๊ฐ ์ ๋ณด๋ฅผ ์
๋ ฅ๋ฐ์ผ๋ฌ ์ถ๊ฐ ํผ ์
๋ ฅ ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ์ํจ๋ค.
* ๊ทธ๋ผ ํด๋ผ์ด์ธํธ๋ ์ถ๊ฐ ์ ๋ณด๋ฅผ ์
๋ ฅํ๊ณ AccessToken๊ณผ ํจ๊ป ๋ค์ ์ถ๊ฐ ์ ๋ณด๋ฅผ ์๋ฒ์ ์ ๋ฌํ๋ฉด ๋๋ค.
*
* ๊ทธ๋ฐ๋ฐ ์ฐ๋ฆฌ๋ ํด๋น ํ์์ด ์ด๋ฏธ ๊ฐ์
์ ํ ํ์์ธ์ง, ์๋๋ฉด ์ ๋ณด ์ ๊ณต์ ๋์๋ง ํ๊ณ ๊ฐ์
์ ์งํํ๊ธฐ ์ ํ์์ธ์ง ์ ์ ์๋ ๋ฐฉ๋ฒ์ด ๋ฑํ ์๋ค.
* ๊ทธ๋์ ๊ทธ๋ฅ ๋ฌผ๋ก ๋ค๋ฅธ ๋ฐฉ๋ฒ๋ ์๊ฒ ์ง๋ง ๊ฐ๋จํ๊ฒ ROLE์ GUEST๋ก ์ฃผ์ด์ ์์ง ํ์๊ฐ์
์ ์งํํ์ง ์๊ณ DB์ ์ ์ฅ๋ ์ํ๋ผ๋ ๊ฒ์ ํ์ํ๋ ๊ฒ์ด๋ค.
*
* (๊ทผ๋ฐ ์ง๊ธ ์๊ฐํด๋ณด๋๊น, ์ธ๊ฐ ์ฝ๋๋ฅผ ์ด๋ฏธ ํ๋ก ํธ์๋์์ ๋ฐ์์ค๊ธฐ ๋๋ฌธ์ ๊ตณ์ด ์ด๋ด ํ์๊ฐ ์๋ ๊ฑฐ ๊ฐ๊ธด ํ๋ค.. ์ฅ..ใ
ใ
์ดํ ์ ๋๋ก ์คํํด๋ณธ ํ ์์ ํ๊ฒ ๋ค.)
*
*
*/
}
์ด์ ํด๋น Provider์ AuthenticationManager์ ๋๊ฒจ์ฃผ๋ Authentication ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค.
AccessTokenSocialTypeToken
public class AccessTokenSocialTypeToken extends AbstractAuthenticationToken {
private Object principal;//OAuth2UserDetails ํ์
private String accessToken;
private SocialType socialType;
public AccessTokenSocialTypeToken(String accessToken, SocialType socialType) {
super(null);
this.accessToken = accessToken;
this.socialType = socialType;
setAuthenticated(false);
}
@Builder
public AccessTokenSocialTypeToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
public String getAccessToken() {
return accessToken;
}
public SocialType getSocialType() {
return socialType;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
}
OAuth2UserDetails
์ด๋ ์ธ์ฆ ํ SecurityContextHolder์ ๋ด์ ๋ชฉ์ ์ผ๋ก ๋ง๋ค์์ต๋๋ค.
@AllArgsConstructor
@Builder
public class OAuth2UserDetails implements UserDetails {
private SocialType socialType;
private String socialId;
private String username;
private Set<GrantedAuthority> authorities;
public SocialType getSocialType() {
return socialType;
}
public String getSocialId() {
return socialId;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return username;
}
public void setRoles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList<>(roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith("ROLE_"),
() -> role + " cannot start with ROLE_ (it is automatically added)");
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
this.authorities = Set.copyOf(authorities);
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
์ด์ RestTemplate๋ฅผ ์ฌ์ฉํ์ฌ ํ์์ ๋ณด๋ฅผ ๋ฐ์์ค๋ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค.
LoadUserService
@Service
@RequiredArgsConstructor
public class LoadUserService {
private final RestTemplate restTemplate = new RestTemplate();
public OAuth2UserDetails getOAuth2UserDetails(AccessTokenSocialTypeToken authentication) {
SocialType socialType = authentication.getSocialType();
SocialLoadStrategy socialLoadStrategy = getSocialLoadStrategy(socialType);//SocialLoadStrategy ์ค์
String socialPk = socialLoadStrategy.getSocialPk(authentication.getAccessToken());//PK ๊ฐ์ ธ์ค๊ธฐ
return OAuth2UserDetails.builder() //PK์ SocialType์ ํตํด ํ์ ์์ฑ
.socialId(socialPk)
.socialType(socialType)
.build();
}
private SocialLoadStrategy getSocialLoadStrategy(SocialType socialType) {
return switch (socialType){
case KAKAO -> new KakaoLoadStrategy();
case GOOGLE -> new GoogleLoadStrategy();
case NAVER -> new NaverLoadStrategy();
default -> throw new IllegalArgumentException("์ง์ํ์ง ์๋ ๋ก๊ทธ์ธ ํ์์
๋๋ค");
};
}
}
SocialLoadStrategy
public abstract class SocialLoadStrategy {
ParameterizedTypeReference<Map<String, Object>> RESPONSE_TYPE = new ParameterizedTypeReference<>(){};
protected final RestTemplate restTemplate = new RestTemplate();
public String getSocialPk(String accessToken) {
HttpHeaders headers = new HttpHeaders();
setHeaders(accessToken, headers);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
return sendRequestToSocialSite(request);//๊ตฌ์ฒด ํด๋์ค๊ฐ ๊ตฌํ
}
protected abstract String sendRequestToSocialSite(HttpEntity request);
public void setHeaders(String accessToken, HttpHeaders headers) {
headers.set("Authorization", "Bearer " + accessToken);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
}
}
๋ค์ด๋ฒ์์ ์ ๋ณด ๋ฐ์์ค๊ธฐ
@Slf4j
public class NaverLoadStrategy extends SocialLoadStrategy{
protected String sendRequestToSocialSite(HttpEntity request){
try {
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(SocialType.NAVER.getUserInfoUrl(),//
SocialType.NAVER.getMethod(),
request,
RESPONSE_TYPE);
Map<String , Object> response2 = ( Map<String , Object>)response.getBody().get("response");
return response2.get("id").toString();
} catch (Exception e) {
log.error("AccessToken์ ์ฌ์ฉํ์ฌ NAVER ์ ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค๋ ์ค ์์ธ๊ฐ ๋ฐ์ํ์ต๋๋ค {}" ,e.getMessage() );
throw e;
}
}
}
๋ค์ด๋ฒ๋ response ์์ id ๊ฐ์ผ๋ก ์๋ณ๊ฐ์ ์ ๊ณตํฉ๋๋ค.
์นด์นด์ค์์ ์ ๋ณด ๋ฐ์์ค๊ธฐ
@Slf4j
public class KakaoLoadStrategy extends SocialLoadStrategy{
protected String sendRequestToSocialSite(HttpEntity request){
try {
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(SocialType.KAKAO.getUserInfoUrl(),// -> /v2/user/me
SocialType.KAKAO.getMethod(),
request,
RESPONSE_TYPE);
return response.getBody().get("id").toString();//์นด์นด์ค๋ id๋ฅผ PK๋ก ์ฌ์ฉ
} catch (Exception e) {
log.error("AccessToken์ ์ฌ์ฉํ์ฌ KAKAO ์ ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค๋ ์ค ์์ธ๊ฐ ๋ฐ์ํ์ต๋๋ค {}" ,e.getMessage() );
throw e;
}
}
}
์นด์นด์ค๋ ์๋ณ๊ฐ์ id๋ก ๋ฐ๋ก ์ ๊ณตํฉ๋๋ค.
๊ตฌ๊ธ์์ ์ ๋ณด ๋ฐ์์ค๊ธฐ
@Slf4j
public class GoogleLoadStrategy extends SocialLoadStrategy{
protected String sendRequestToSocialSite(HttpEntity request){
try {
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(SocialType.GOOGLE.getUserInfoUrl(),
SocialType.GOOGLE.getMethod(),
request,
RESPONSE_TYPE);
return (response.getBody().get("email")).toString();//๊ตฌ๊ธ์ email๋ฅผ PK๋ก ์ฌ์ฉ
} catch (Exception e) {
log.error("AccessToken์ ์ฌ์ฉํ์ฌ GOOGLE ์ ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค๋ ์ค ์์ธ๊ฐ ๋ฐ์ํ์ต๋๋ค {}" ,e.getMessage() );
throw e;
}
}
}
๊ตฌ๊ธ์ email์ ํตํด ์๋ณ๊ฐ์ ์ ๊ณตํฉ๋๋ค.