[Spring boot] Spring boot Security๋ฅผ ์ด์ฉํ OAuth2 ์ธ์ฆ ๊ตฌํ 2 - User Entity ์ฐ๋
์น ์๋น์ค๋ฅผ ๊ฐ๋ฐํ๊ฒ ๋๋ค๋ฉด, ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ์ถฐ์ ธ์ผ ํ ๊ฒ์ ๋ฐ๋ก ์ฌ์ฉ์ ์ธ์ฆ์ผ ๊ฒ์ ๋๋ค. ์ฐ๋ฆฌ ์๋น์ค์ ๊ฐ์ ํ ์ฌ์ฉ์์ธ์ง๋ฅผ ๊ตฌ๋ถํด์ผ ํ๋ค๋ ์ ๊ณผ ๋๋ถ์ด, ๋จ๊ณจ์ธ์ง, ์๋์ง ๋ฑ์ด๋ ์ฌ์ดํธ๋ฅผ ๊ด๋ฆฌํ๋ ๊ด๋ฆฌ์ ๋ฑ์ ๊ตฌ๋ถํ๋ ๋ฐ ์์ด, ์ฌ์ฉ์ ์ธ์ฆ์ ํ์์ ์ธ ์์๋ผ๊ณ ํ ์ ์์ต๋๋ค.
๊ทธ๋ฌ๋ ์๋น์ค์ ์์ด ๊ฐ์ฅ ๋ฒ๊ฑฐ๋ก์ด ๊ฒ์ ์ฌ์ฉ์ ๋ฑ๋ก์ ์ํ ํ์ ๊ฐ์ ์ผ ๊ฒ์ ๋๋ค. ์๋ํ๋ฉด ์ฌ์ฉ์๊ฐ ์๋น์ค๋ฅผ ์ด์ฉํ๊ธฐ ์ํด ์์ ์ ๊ฐ์ธ ์ ๋ณด๋ฅผ ์ ๋ ฅํด์ผ ํ๊ณ , ๋ณธ์ธ ์ธ์ฆ ์ ์ฐจ๋ฅผ ๊ฑฐ์ณ์ผ ํ๋ฉฐ, ์ฃผ์์ ์ ํ๋ฒํธ ๋ฑ์ ์ ๋ ฅํจ๊ณผ ๋๋ถ์ด, ๋ ๋ค๋ฅธ ์๋น์ค๋ฅผ ์ด์ฉํ ๋๋ง๋ค ID์ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ์์ฑํ๊ณ ๊ด๋ฆฌํด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด๋ฒ ํฌ์คํธ์์๋ OAuth2 ์ธ์ฆ์ ํตํด ์ ์ํ ์ฌ์ฉ์๋ค์ ์ ์ฅํ๋ User Entity๋ฅผ ์์ฑํ๊ณ , ์ด๋ฅผ ๊ตฌํํ๋ ์๊ฐ์ ๊ฐ์ ธ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
Create User Entity
์ง๋ ํฌ์คํธ์์ ์์ ํ๋ ๋ด์ฉ์ ์ด์ด์ User Entity๋ฅผ ๋ง๋ค์ด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
/** | |
* Created by Neon K.I.D on 8/9/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
@Getter | |
@NoArgsConstructor | |
@Entity | |
@Table | |
public class User implements Serializable { | |
@Id | |
@Column | |
@GeneratedValue(strategy = GenerationType.IDENTITY) | |
private Long idx; | |
@Column | |
private String name; | |
@Column | |
private String password; | |
@Column | |
private String email; | |
@Column | |
private String principal; | |
@Column | |
@Enumerated(EnumType.STRING) | |
private SocialType socialType; | |
@Column | |
private LocalDateTime createdDate; | |
@Column | |
private LocalDateTime updatedDate; | |
@Builder | |
public User(String name, String password, String email, String principal, | |
SocialType socialType, LocalDateTime createdDate, LocalDateTime updatedDate) { | |
this.name = name; | |
this.password = password; | |
this.email = email; | |
this.principal = principal; | |
this.socialType = socialType; | |
this.createdDate = createdDate; | |
this.updatedDate = updatedDate; | |
} | |
} |
๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ์ถฐ์ ธ์ผ ํ Primary Key์ ์์ด๋, ํจ์ค์๋ ๊ทธ๋ฆฌ๊ณ E-mail ์ฃผ์ ์นผ๋ผ์ ๋ฃ์ด์ค๋๋ค. principal์ Spring Security์์ ๊ด๋ฆฌํ๊ฒ ๋ ๊ถํ๋ค์ ๋ฃ์ด ๋์ ์นผ๋ผ์ ๋๋ค.
์ฌ๊ธฐ์ Lombok์์ ์ง์ํ๋ Builder ์ด๋ ธํ ์ด์ ์ ์ด์ฉํ์ฌ Builder ํจํด์ ์ด์ฉํ์ฌ ๊ฐ์ฒด๋ฅผ ์์ฑํ๋๋ก ์ถ๊ฐํด์ค์๋ค.
/** | |
* Created by Neon K.I.D on 8/11/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
public interface UserRepository extends JpaRepository<User, Long> { | |
User findByEmail(String email); | |
} |
Spring Data JPA์์ ์ ๊ณตํ๋ JpaRepository๋ฅผ ์์ ๋ฐ๊ณ , E-mail์ ์ด์ฉํ์ฌ User ์ ๋ณด๋ฅผ ์กฐํํ ์ ์๋๋ก findByEmail ๋ฉ์๋๋ฅผ ์ ์ํฉ๋๋ค.
๋ค์ํ ์์ ๋ก๊ทธ์ธ ๊ณ์ ์ ์ง์ํ๊ธฐ ์ํด ๊ฐ ์์ ํ์ ์ ์ ์ํ๋ ๋ฐฉ์์ผ๋ก ์ด๊ฑฐ์ฒด๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
Argument Resolver
๋ง์ฝ ๋ก๊ทธ์ธ ํ์ ๋ ๊ณ์ ์ด ์์ ๋ก๊ทธ์ธ๋ ๊ณ์ ์ด๋ผ๋ฉด, ๊ธฐ์กด์ ๊ณ์ ๊ณผ ๋งคํํด์ฃผ๊ฑฐ๋, ๊ทธ๋ ์ง ์์ผ๋ฉด ์๋ก์ด ์ฌ์ฉ์๋ก ์์ฑํด์ผ ํฉ๋๋ค. ์ผ๋ฐ์ ์ธ ํ์ ๊ฐ์ ์ Form์ ์ด์ฉํด์ ๊ฐ์ข ์ ๋ณด๋ฅผ ๋ฐ๊ณ , JPA๋ฅผ ์ด์ฉํด ์ ๋ณด๋ฅผ ๋ฃ๊ณ DB์ ๋ฃ์ผ๋ฉด ๋ฉ๋๋ค.
๊ทธ๋ฐ๋ฐ, ์์ ๋ก ๋ก๊ทธ์ธ๋ ๊ณ์ ์ ์ด๋ป๊ฒ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์๊น์? ๋ด๊ฐ ๋ง๋ View์์ Form์ด ์ ๋ ฅ๋๋ ๊ฒ๋ ์๋๊ณ , ๊ทธ๋ ๋ค๋ฉด ์์ ๊ณ์ ์๋ฒ๋ก๋ถํฐ ์ ๋ณด๋ฅผ ๋ฐ์์์ผ ํ๋๋ฐ, ์ด๋ฏธ Google ๊ณ์ ๊ณผ์ ์ฐ๋์ Spring Security์์ ๋ง๋ค์ด์ง ๊ฒ์ ์ฌ์ฉํ์ผ๋ ์ด๊ฒ์ ์ฌ๊ตฌํํด์ผ ํ๋ ๊ฑธ๊น์?
์ด๋ด ๋๋ Java์ ์ด๋ ธํ ์ด์ ๊ณผ ํจ๊ป Spring์์ ์ ๊ณตํ๋ Argument Resolver๋ฅผ ์ด์ฉํด ๋ณผ ์ ์์ต๋๋ค. oAuth2Login ๋ฉ์๋ ๋ฐ์ผ๋ก defaultSuccessfulUrl์ ์ง์ ํ์ฌ ์์ ์ฌ์ดํธ๋ก๋ถํฐ ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ์์ ๋, API๋ฅผ ํ ๊ฐ ์์ฑํ๊ณ , ํด๋น ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ฅผ ํน์ ์ด๋ ธํ ์ด์ ์ ํ๋ผ๋ฏธํฐ ํ์ ์ผ๋ก ์ค์ ํด์ฃผ๋ฉด, ํด๋น Resolver๊ฐ ํธ์ถ๋๊ฒ๋ ํ ์ ์์ต๋๋ค. ์ด์ธ์๋ ์ธํฐ์ ํฐ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ ๊ฐ๋ฅํฉ๋๋ค.
๋จผ์ ๊ธฐ์กด์ ์ฐ๋ฆฌ๊ฐ ์ค์ ํ๋ SecurityConfig ํ์ผ์ ์กฐ๊ธ ๊ณ ์ณ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
/** | |
* Created by Neon K.I.D on 8/9/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
@Configuration | |
@EnableWebSecurity | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Override | |
protected void configure(HttpSecurity httpSecurity) throws Exception { | |
httpSecurity.authorizeRequests() | |
.antMatchers("/", "/oauth2/**", "/login/**", "/css/**", | |
"/images/**", "/js/**", "/console/**").permitAll() | |
.anyRequest().authenticated() | |
.and() | |
.oauth2Login().defaultSuccessUrl("/loginSuccess").failureUrl("/loginFailure") | |
.and() | |
.headers().frameOptions().disable() | |
.and() | |
.exceptionHandling() | |
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) | |
.and() | |
.formLogin().successForwardUrl("/board") | |
.and() | |
.logout().logoutUrl("/logout").logoutSuccessUrl("/").deleteCookies("JSESSIONID").invalidateHttpSession(true) | |
.and() | |
.csrf().disable(); | |
} | |
} |
์ด์ ์ ์์ฑํ๋ OAuth2Login์์ ๋ก๊ทธ์ธ ์ฑ๊ณตํ์ ๋ ํฌ์ธํธ์ ์คํจํ์ ๋ ํฌ์ธํธ๋ฅผ ์ก์์ต๋๋ค. ์ฌ๊ธฐ์ ์ถ๊ฐ๋ก ๋ก๊ทธ์์ ๋ถ๋ถ๊น์ง ์ค์ ํ๋ฉด ์ข๊ฒ ์ฃ .
์ง๋ ํฌ์คํธ์์ ์์ฑํ๋ ๊ฒ ๊ทธ๋๋ก Argument Resolver๋ฅผ ์์ฑํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ํน์๋ผ๋ ํฌ์คํธ๋ฅผ ๋ณด์ง ๋ชปํ๋ค๋ฉด ์๋์ ๋งํฌ๋ฅผ ๊ผญ ์ฐธ๊ณ ํด์ ๋ณด๊ณ ์งํํด์ฃผ์ธ์.
[Spring] Argument Resolver๋ฅผ ์ด์ฉํ ์ ์ฐ์ฑ ์๋ ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌ
์๋น์ค๋ฅผ ์ด์ํ๋ค๋ณด๋ฉด ๋ค์ํ ์ข ๋ฅ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ด ๋๋ง๋ค Controller ๋ถ๋ถ์์ ์ด๋ฅผ ์ ์ฒ๋ฆฌํ๊ฒ ๋๋๋ฐ, ์ด๋ ๊ฒ ๋๋ฉด ๊ฐ Controller์ ์ ์ฒ๋ฆฌ ํด์ผ ํ๋ ์ฝ๋๋ฅผ ํจ์ํ ํ๊ฑฐ๋ Utils ํด
blog.neonkid.xyz
๋จผ์ ์ด๋ ธํ ์ด์ ๋ถํฐ ๋ง๋ค์ด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์ ๋นํ ์ด๋ฆ์ ์ฃผ๊ณ , Target์ ํ๋ผ๋ฏธํฐ๋ก ํ์ฌ๊ธ ์ด๋ ธํ ์ด์ ์ ํ ๊ฐ ๋ง๋ค์ด์ค๋๋ค.
/** | |
* Created by Neon K.I.D on 8/11/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
@RequiredArgsConstructor | |
@Component | |
public class UserArgumentResolver implements HandlerMethodArgumentResolver { | |
private final UserRepository userRepository; | |
@Override | |
public boolean supportsParameter(MethodParameter parameter) { | |
return parameter.getParameterAnnotation(SocialUser.class) != null && | |
parameter.getParameterType().equals(User.class); | |
} | |
@Override | |
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, | |
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { | |
HttpSession session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) | |
.getRequest().getSession(); | |
User user = (User) session.getAttribute("user"); | |
return getUser(user, session); | |
} | |
private User getUser(User user, HttpSession session) { | |
if (user == null) { | |
try { | |
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); | |
Map<String, Object> map = token.getPrincipal().getAttributes(); | |
User convertUser = convertUser(token.getAuthorizedClientRegistrationId(), map); | |
user = userRepository.findByEmail(convertUser.getEmail()); | |
if (user == null) | |
user = userRepository.save(convertUser); | |
setRoleIfNotSame(user, token, map); | |
session.setAttribute("user", user); | |
} catch (ClassCastException ex) { | |
return user; | |
} | |
} | |
return user; | |
} | |
private User convertUser(String authority, Map<String, Object> map) { | |
if (GOOGLE.getValue().equals(authority)) | |
return getModernUser(GOOGLE, map); | |
return null; | |
} | |
private User getModernUser(SocialType socialType, Map<String, Object> map) { | |
return User.builder() | |
.name(String.valueOf(map.get("name"))) | |
.email(String.valueOf(map.get("email"))) | |
.principal(String.valueOf(map.get("id"))) | |
.socialType(socialType) | |
.createdDate(LocalDateTime.now()) | |
.updatedDate(LocalDateTime.now()) | |
.build(); | |
} | |
private void setRoleIfNotSame(User user, OAuth2AuthenticationToken token, Map<String, Object> map) { | |
if (!token.getAuthorities().contains( | |
new SimpleGrantedAuthority(user.getSocialType().getRoleType()))) { | |
SecurityContextHolder.getContext().setAuthentication( | |
new UsernamePasswordAuthenticationToken(map, "N/A", | |
AuthorityUtils.createAuthorityList(user.getSocialType().getRoleType()))); | |
} | |
} | |
} |
์์์ ๋ง๋ Repository๋ฅผ ์์กด์ฑ ์ฃผ์ ํ์ฌ ์ฌ์ฉํ๋๋ก ํฉ๋๋ค. ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋์ถฉ ์์๊ฒ ์ง๋ง ๊ฐ๋จํ๊ฒ ์ค๋ช ์ ๋๋ฆฐ๋ค๋ฉด, ๋จผ์ ์์ ์ ์ ๋ก ๋ก๊ทธ์ธ ํ ๋ค, ์ธ์ ์์ ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์ต๋๋ค. ๊ธฐ์กด ์ฌ์ฉ์๊ฐ ํด๋น ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๊ฐ์ง๊ณ ๊ฐ์ ์ ํ๋ค๋ฉด ํด๋น ์ ์ ์ ์์ ๋ก๊ทธ์ธ ํ ์ ์๋๋ก ์ฐ๋๋์ด ์ง๊ณ , ๊ทธ๋ ์ง ์๋ค๋ฉด ์๋ก์ด ์ฌ์ฉ์๋ฅผ ๋ง๋๋ ๋ฐฉํฅ์ผ๋ก ๊ตฌํํ์์ต๋๋ค.
/** | |
* Created by Neon K.I.D on 8/9/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
@Controller | |
public class LoginController { | |
@GetMapping({"", "/"}) | |
public String getAuthorizationMessage() { | |
return "home"; | |
} | |
@GetMapping("/login") | |
public String login() { | |
return "login"; | |
} | |
@GetMapping("/board") | |
public String board() { | |
return "board"; | |
} | |
@GetMapping("/loginSuccess") | |
public String loginComplete(@SocialUser User user) { | |
return "redirect:/board"; | |
} | |
} |
๋ง์ง๋ง์ผ๋ก Controller์ ์๊น ์์์ ์ฐ๋ํ ๋ก๊ทธ์ธ ์ฑ๊ณต ์๋ํฌ์ธํธ์ ์คํํ ๋ฉ์๋๋ฅผ ๊ตฌํํ๊ณ ํด๋น ํ๋ผ๋ฏธํฐ๋ฅผ ์์์ ๋ง๋ ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ ์ฐ๋ํด์ค๋๋ค.
๊ทธ๋ฆฌ๊ณ , ๋ง๋ Argument Resolver๋ฅผ ๋ฆฌ์คํธ์ ์ถ๊ฐํด์ฃผ๋ฉด ๋์ ๋๋ค. ์ด ๋ถ๋ถ์ ์ง๋ ํฌ์คํธ์์ ์ค๋ช ์ ํ๊ธฐ ๋๋ฌธ์ ์๋ตํ๋๋ก ํฉ๋๋ค.
Test
ํ ์คํธ๋ฅผ ์ํด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ค์ ์ ์๋์ ๊ฐ์ด ๋ณ๊ฒฝํ์ฌ H2 Database๋ฅผ ์ด์ฉํ์ฌ ์ฌ์ฉ์ ๋ฐ์ดํฐ๊ฐ ์ ์ฝ์ ์ด ๋์ด์ง๋์ง๋ฅผ ๋ณด๋๋ก ํฉ๋๋ค.
spring: | |
datasource: | |
url: jdbc:h2:file:~/test;AUTO_SERVER=TRUE | |
driver-class-name: org.h2.Driver | |
h2: | |
console: | |
enabled: true | |
path: /console |
home ๋๋ ํฐ๋ฆฌ ๋ฐ์ผ๋ก test ํ์ผ์ ๋ง๋ค๊ณ , ์๋ฒ๊ฐ ์คํ๋ ๋๋ง๋ค ์์ฑ๋๋๋ก ํฉ๋๋ค.

console ํ์ด์ง์ ์ ๊ทผํฉ๋๋ค. ์ฐ๋ฆฌ๋ ์๋ฌด๋ฐ ์ฌ์ฉ์ ์ด๋ฆ๊ณผ ํจ์ค์๋๋ ์ค์ ํ์ง ์์์ผ๋ฏ๋ก ๋ฐ๋ก Connect๋ฅผ ๋๋ฌ ์ ์ํฉ๋๋ค.

๋ณด๋ค์ํผ User ํ ์ด๋ธ์ด ๋ง๋ค์ด์ก๊ณ , ์กฐํ๋ฅผ ํ๊ฒ ๋๋ฉด ์๋ฌด๋ฐ ๋ฐ์ดํฐ๊ฐ ์์์ ์ ์ ์์ต๋๋ค.

๋ฉ์ธ ํ์ด์ง์ ์ ์ํ์ฌ ๋ก๊ทธ์ธ์ ์๋ํ ํ ์ฝ์ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.

๊ทธ๋ฌ๋ฉด ์์ ๊ฐ์ด ์๋ก์ด ๊ณ์ ์ด ์์ฑ์ด ๋์์์ ์ ์ ์์ต๋๋ค.
๋ง์น๋ฉฐ...
๋ญ๊ฐ ์คํ๋ง ๊ธ์ ๊ณ์ ์์ฑํ๋ฉด์๋ถํฐ ๊ฐ๋ ์ ๋ํ ๊ธ ๋ณด๋ค๋ ๊ธฐ๋ฅ ๊ตฌํ์ ์ค์ฌ์ผ๋ก ๊ธ์ ์ฐ๊ฒ ๋ ๊ฒฝ์ฐ, ์ ๋ฌ์ด ์ ์๋๋ ๋๋์ ๋ฐ๋ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ์์ฃผ ๋ค๊ฒ ๋ฉ๋๋ค.
์๋ฌด๋๋ ๋ทฐ์ ์ค์ฌ์ด ์๋ ๊ธฐ์ ์ ๋ํ ์ดํด๋์ ์ ์๋ฅผ ์ ๋ฌํ๋ ๋ฐ ๊ทธ ์์๋ฅผ ๋๊ณ ์ ํ๋ ๊ฒ์ ๋ชฉํ๋ก ํด์ ๊ทธ๋ฐ๊ฑด์ง ๋ค์ ๋ฒ ๊ธ์ ์ธ ๋๋ ์ข ๋ ์ ๋ฌ๋ ฅ์ด ํฅ์๋ ๋ชจ์ต์ผ๋ก ๊ธ์ ์จ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
OAuth2 ๋ก๊ทธ์ธ์ ์ต๊ทผ ์น์์ ์์ฃผ ์ฌ์ฉ๋๋ ๋ก๊ทธ์ธ ๋ชจ๋ธ์ ๋๋ค. ์ฌ์ฉ์์ ํธ์๋ฅผ ์ํด์๋ ์ ๊ณต๋๋ ๊ธฐ๋ฅ์ด๋ ์น ๊ฐ๋ฐํ์๋ ๋ถ๋ค์๊ฒ ์์ด ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค.