[ํ๋ก์ ํธ ๊ธฐ๋ณธ ์ธํ ] Account ๊ด๋ จ ๊ฐ์ฒด, ์ธ์ฆ ๊ด๋ จ ๊ฐ์ฒด ์์ฑ
ํ๋ก์ ํธ ๊ธฐ๋ณธ ์ธํ
3. [๋ฐฑ์๋] Account ๊ด๋ จ ๊ฐ์ฒด, ์ธ์ฆ ๊ด๋ จ ๊ฐ์ฒด ์์ฑ
Account ๊ด๋ จ ๊ฐ์ฒด & UserDetails ๊ด๋ จ ๊ฐ์ฒด ์์ฑ
์ธ์ฆ๋ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ด์ Entity ๊ฐ์ฒด์ธ Account
์ Account์ ๊ด๋ จ๋ ์ฟผ๋ฆฌ๋ฉ์๋๋ฅผ ์ฌ์ฉํ AccountRepository
๋ฅผ ๋ง๋ค์ด์ฃผ์๋ค.
Account.java
package com.portfolio.backend.account;
// ... import ์๋ต
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Entity
public class Account extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nickname;
@Column(nullable = false, unique = true)
private String email;
private boolean isCertify;
@Column(nullable = false)
private String emailToken;
@Column(nullable =false)
private String password;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
public void acquireCertification(){
this.isCertify = true;
}
public void changePassword(String password){
this.password = password;
}
public void changeNickname(String nickname) {
this.nickname = nickname;
}
}
์ถํ์ ๊ณ์ ์ด๋ฉ์ผ ์ธ์ฆ์ ๊ฐ๋ฐํด์ผ ํ๋ฏ๋ก ๊ด๋ จ๋ ๋ณ์์ ๋ฉ์๋๋ฅผ ๋ง๋ค์๊ณ , ๊ณ์ ์ ๋๋ค์, ํจ์ค์๋ ๋ณ๊ฒฝ ๋ฉ์๋ ์ญ์ ๋ง๋ค์๋ค.
AccountRepository.java
package com.portfolio.backend.account;
// ... import ์๋ต
public interface AccountRepository extends JpaRepository<Account, Long> {
Optional<Account> findAccountByEmail(String email);
}
CustomUsersService์์ ํ์ ์กฐํ๋ฅผ ์ด๋ฉ์ผ๋ก ํ ๊ฒ์ด๋ฏ๋ก findAccountByEmail
๋ฉ์๋๋ฅผ ๋ง๋ค์ด์ค๋ค.
Account ๊ฐ์ฒด๋ฅผ ๋ฐํ์ผ๋ก UserDetails์ ๋ง๋ค์ด ์ฃผ๋ CustomUserDetails
๊ฐ์ฒด์ ์ด๋ฅผ ๋ฐํ์ผ๋ก loadUserByUsername
๋ฉ์๋๋ฅผ ๊ตฌํํ๋ CustomUserDetailsService
๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ค๋ค.
CustomUserDetails.java
package com.portfolio.backend.config.security;
// ... import ์๋ต
public class CustomUserDetails implements UserDetails {
private Account account;
public CustomUserDetails(Account account){
this.account = account;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.account.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@Override
public String getPassword() {
return account.getPassword();
}
@Override
public String getUsername() {
return account.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
CustomUserDetailsService.java
package com.portfolio.backend.config.security;
// ... import ์๋ต
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final AccountRepository accountRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Account account = accountRepository.findAccountByEmail(email)
.orElseThrow(CustomUserNotFoundException::new);
return new CustomUserDetails(account);
}
}
JWT ํ ํฐ ๊ด๋ จ ๊ฐ์ฑ & Spring Security ์ค์
JWT๋ฅผ ์์ฑํ๋ ๊ฐ์ฒด, ํํฐ๋ง์ ํด์ฃผ๋ ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ Spring Security์ ์ค์ ์ ์ถ๊ฐํด ์ค๋ค.
JwtTokenProvider.java
package com.portfolio.backend.config.security;
// ... import ์๋ต
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
private final UserDetailsService userDetailsService;
@Value("jwt.secret")
private String secretKey;
// ํ ํฐ ์ ํจ ์๊ฐ -> ํ ์๊ฐ
private long tokenValidity = 60 * 60 * 1000L;
@PostConstruct
protected void init(){
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createToken(String username, List<String> roles){
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles",roles);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidity))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public String getUsername(String token){
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public Authentication getAuthentication(String token){
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
}
public String resolveToken(HttpServletRequest req){
return req.getHeader("X-AUTH-TOKEN");
}
public boolean validateToken(String jwtToken){
try{
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claimsJws.getBody().getExpiration().before(new Date());
}catch (Exception e){
return false;
}
}
}
JWT ํ ํฐ์ ์์ฑํด ์ฃผ๋ Provider ๊ฐ์ฒด๋ฅผ ๋ง๋ ๋ค.
Request ํด๋์ X-AUTH-TOKEN
๋ผ๋ ์ด๋ฆ์ผ๋ก ์ ์ธ๋๋ฉฐ HS256 ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ์ํธํ ์ํจ๋ค.
ํ ํฐ์ ํ์ด๋ก๋์ ์ธ์ฆ๋ ์ฌ์ฉ์์ username(์ด๋ฒ ํ๋ก์ ํธ์์ ์ด๋ฉ์ผ), Roles, ํ ํฐ ์์ฑ ์๊ฐ์ ๋ฃ์ด์ค๋ค.
JwtAuthenticationFilter.java
package com.portfolio.backend.config.security;
// ... import ์๋ต
public class JwtAuthenticationFilter extends GenericFilterBean {
private JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider){
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
if(token != null && jwtTokenProvider.validateToken(token)){
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
๊ธฐ์กด MVC ํจํด์ ์คํ๋ง ๋ถํธ ํ๋ก์ ํธ์์๋ ์ธ์ฆ์ด ํ์ํ ๊ฒฝ์ฐ UsernamePasswordAuthenticationFilter
์ ์ํด ๋ก๊ทธ์ธ ํผ์ผ๋ก ์ด๋ํ์ง๋ง, ์ด๋ฒ ํ๋ก์ ํธ๋ ๋ก๊ทธ์ธ ํผ์ ํ๋ก ํธ์๋์์ ์์ฑํ๋ฏ๋ก UsernamePasswordAuthenticationFilter
๊ฐ ์ฒ๋ฆฌํ๊ธฐ ์ ์ ๋ ์๋จ์ ์ฌ์ฉํ ํํฐ๋ฅผ ๋ง๋ค์ด์ค์ผ ํ๋ค.
์์ ๊ฐ์ด token์ด ์์ผ๋ฉด ํ์ธ์ ํด์ฃผ๋ ํํฐ๋ฅผ ๋ง๋ค๊ณ SecurityConfig
์์ ํํฐ์ ์์๋ฅผ ์ง์ ํด ์ค๋ค.
SecurityConfig.java
package com.portfolio.backend.config.security;
// ... import ์๋ต
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
@Value("${mapping.url}")
private String mappingUrl;
@Override // ignore check swagger resource
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/v2/api-docs", "/swagger-resources/**",
"/swagger-ui.html", "/webjars/**", "/swagger/**");
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(mappingUrl)
.allowedMethods("GET", "POST", "OPTIONS", "PUT")
}
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/sign-up", "/api/sign-in").permitAll()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.anyRequest().hasRole("USER")
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
์์ ์ฝ๋๋ก ํํฐ์ ์์๋ฅผ ์ ํด์ค๋ค.
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
ํจ์ค์๋ ์ธ์ฝ๋ ์ค์
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(mappingUrl)
.allowedMethods("GET", "POST", "OPTIONS", "PUT")
}
}
Cors ๊ด๋ จ ์ค์
@Value("${mapping.url}")
Cors๋ฅผ ํ์ฉํ๋ url์ yml์ ๋๊ธด๋ค.