๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Project

[ํ”„๋กœ์ ํŠธ ๊ธฐ๋ณธ ์„ธํŒ…] 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์— ๋„˜๊ธด๋‹ค.