[프로젝트] 개발자 포트폴리오 - 3. [백엔드] Account 관련 객체, 인증 관련 객체 생성

개발자 포트폴리오 프로젝트

3. [백엔드] Account 관련 객체, 인증 관련 객체 생성

전체 소스 - https://github.com/vividswan/Portfolio-For-Developers


Account 관련 객체 & UserDetails 관련 객체 생성

source commit - cb45f17

인증된 사용자의 정보를 담을 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 설정

source commit - 42e3aeb

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에 넘긴다.