Spring + Vue 로그인(1)

shinhyocheol·2021년 7월 15일
1

Vue 게시판

우선 간단한 게시판을 그리는 작업은 위 링크에서 확인하면 된다. 이글에서는 로그인 화면과 회원등록 화면을 추가하고, API 접근시 해당 회원정보가 접근해도 되는 사용자인지 간단한 검증과정까지의 과정을 담아보려고 한다.

SpringSecurity+JWT

사실상 위 글에서 화면이 추가되었으며, 이 때보다 좀 더 퀄리티 높고, 가독성 좋게 코드를 개선하며 로그인 기능을 구현하고자 한다.(저 글을 보면 쓸 당시의 나는 도대체 무슨 뻔뻔함이 가득했길래 글을 저렇게 써놓고 뿌듯해했을까 싶다...)

그렇다고 글을 지우기보단 저 글을 작성했던 당시의 나와 이 글을 쓰고 있는 나를 비교할 수 있는 좋은 증거이기도 하며, 후에는 지금 쓴 글 조차도 부끄러워 할 날이 분명 오겠지..

우선 서버쪽에 로그인에 필요한 API와 그 외 파일을 작성했다.
나는 로그인 과정을 이전과 마찬가지로 Spring security + jwt를 이용해 작업을 진행했다.

build.gradle

compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.7.0'

SpringSecurityConfig

@RequiredArgsConstructor
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{

  private final JwtAuthProvider jwtAuthProvider;
	
  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  } 

  @Bean
  public BCryptPasswordEncoder bCryptPasswordEncoder(){
    return new BCryptPasswordEncoder();
  } 
	
  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
		
      CorsConfiguration configuration = new CorsConfiguration();

      configuration.addAllowedOrigin("*");
      configuration.addAllowedMethod("*");
      configuration.addAllowedHeader("*");
      configuration.setMaxAge((long) 3600);
      configuration.setAllowCredentials(false);
      configuration.addExposedHeader("accessToken");
      configuration.addExposedHeader("content-disposition");

      UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
      source.registerCorsConfiguration("/**", configuration);
		
      return source;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
		
      http
	.httpBasic().disable()
		.csrf().disable()
	.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		.and()
	.authorizeRequests()
	.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
		.antMatchers("/signup").permitAll()			// 회원가입
		.antMatchers("/signin/**").permitAll() 		// 로그인
		.antMatchers("/exception/**").permitAll() 	// 예외처리 포인트
		.anyRequest().hasRole("USER")				// 이외 나머지는 USER 권한필요
		// .anyRequest().permitAll()
	  	.and()
	.cors()
		.and()
	.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedPoint())
		.and()
	.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
		.and()
	.addFilterBefore(new JwtAuthenticationFilter(jwtAuthProvider), UsernamePasswordAuthenticationFilter.class);
	}

JwtAuthenticationFilter

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private JwtAuthProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtAuthProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpReq = (HttpServletRequest)request;
        HttpServletResponse httpRes = (HttpServletResponse) response;

        try {
            if("OPTIONS".equalsIgnoreCase(httpReq.getMethod())) {
                httpRes.setStatus(HttpServletResponse.SC_OK);
            } else {

                String token = jwtTokenProvider.resolveToken(httpReq);
                if (token != null) {
                    if(jwtTokenProvider.validateToken(token)) {

                        /** 사용자 인증토큰 검사 */
                        Authentication auth = jwtTokenProvider.getAuthentication(token);
                        SecurityContextHolder.getContext().setAuthentication(auth);
                    }
                }
                filterChain.doFilter(request, response);

            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

JwtAuthProvider

@RequiredArgsConstructor
@Component
public class JwtAuthProvider {

    @Value("${spring.jwt.secret.at}")
    private String atSecretKey;

    @PostConstruct
    protected void init() {
        atSecretKey = Base64.getEncoder().encodeToString(atSecretKey.getBytes());
    }

    private final UserDetailsService userDetailsService;

    /**
     * @throws Exception
     * @method 설명 : jwt 토큰 발급
     */
    public String createToken(
            long userPk,
            String email,
            String nickname) {
    	
    	/**
    	 * 토큰발급을 위한 데이터는 UserDetails에서 담당
    	 * 따라서 UserDetails를 세부 구현한 CustomUserDetails로 회원정보 전달
    	 */
    	CustomUserDetails user = new CustomUserDetails(
    			userPk, 	// 번호
    			email);		// 이메일
    	
    	// 유효기간설정을 위한 Date 객체 선언
    	Date date = new Date();
        
        final JwtBuilder builder = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject("accesstoken").setExpiration(new Date(date.getTime() + (1000L*60*60*12)))
                .claim("userPk", userPk)
                .claim("email", email)
                .claim("roles", user.getAuthorities())
                .signWith(SignatureAlgorithm.HS256, atSecretKey);

        return builder.compact();
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(atSecretKey).parseClaimsJws(token).getBody().getSubject();
    }

    /**
     * @method 설명 : 컨텍스트에 해당 유저에 대한 권한을 전달하고 API 접근 전 접근 권한을 확인하여 접근 허용 또는 거부를 진행한다.
     */
    @SuppressWarnings("unchecked")
    public Authentication getAuthentication(String token) {

        // 토큰 기반으로 유저의 정보 파싱
        Claims claims = Jwts.parser().setSigningKey(atSecretKey).parseClaimsJws(token).getBody();

        long userPk = claims.get("userPk", Integer.class);
        String email = claims.get("email", String.class);

        CustomUserDetails userDetails = new CustomUserDetails(userPk, email);
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    /**
     * @method 설명 : request객체 헤더에 담겨 있는 토큰 가져오기
     */
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("accesstoken");
    }

    /**
     * @method 설명 : 토큰 유효시간 만료여부 검사 실행
     */
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(atSecretKey).parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

}

간단하게 정리해보자면 spring security 설정을 통해 JwtAuthenticationFilter를 필터로 사용하고

JwtAuthenticationFilter 에서 인증토큰을 검증과정을 JwtAuthProvider를 통해 진행한다.

이 과정에 문제 발생 시 accessDeniedHandlerauthenticationEntryPoint 각각 지정포인트를 통해

각 상황에 맞게 예외처리를 한다.

그리고 로그인이니까 당연히 회원 엔티티가 필요하겠지??

Members

@Getter
@Entity
@DynamicUpdate
@DynamicInsert
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Members {

    @Id
    @GeneratedValue
    private Long id;

    @Column(length = 100, nullable = false)
    private String email;

    @Column(length = 200, nullable = false)
    private String password;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 100, nullable = false)
    private String mobile;

    @Column(length = 100, nullable = true)
    private String nickname;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;

    @Builder
    public Members(Long id, String email, 
    		String password, String name, 
    		String mobile, String nickname) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.name = name;
        this.mobile = mobile;
        this.nickname = nickname;
    }

MemberRepository

public interface MemberRepository extends JpaRepository<Members, Long> {

    Optional<Members> findByEmail(String email);

}

자 로그인이 이루어지기 위한 환경은 마련이 됐다. 이제 로그인 API를 만들어보자!!

AuthenticationDto

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationDto {

    private Long id;

    private String email;

    private String name;

    private String nickname;

    private String mobile;

    private String regDate;

    private String modDate;

}

LoginService

public AuthenticationDto loginService(LoginDto loginDto) {
		
	// dto -> entity
	Members loginEntity = loginDto.toEntity();

	// 회원 엔티티 객체 생성 및 조회시작
	Members member = memberRepository.findByEmail(loginEntity.getEmail())
				.orElseThrow(() -> new UserNotFoundException("User Not Found"));

	if (!passwordEncoder.matches(loginEntity.getPassword(), member.getPassword()))
		throw new ForbiddenException("Passwords do not match");

	// 회원정보를 인증클래스 객체(authentication)로 매핑
	AuthenticationDto authentication = modelMapper.toDto(member, AuthenticationDto.class);

	return authentication;
}

LoginDto

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {

	@NotBlank(message = "'email' is a required input value")
	@Email(message = "It is not in email format")
	private String email;

	@NotBlank(message = "'password' is a required input value")
	private String password;

	
	public Members toEntity() {

		Members build = Members.builder()
				.email(email)
				.password(password)
				.build();
		
		return build;
	}
	
}

LoginController

/**
* @method 설명 : 로그인
* @param loginDto
* @throws Exception
*/
@PostMapping(value = {"/signin"})
public ResponseEntity<AuthenticationDto> appLogin(
	@Valid @RequestBody LoginDto loginDto) throws Exception {

	AuthenticationDto authentication = apiSignService.loginService(loginDto);

	return ResponseEntity.ok()
			.header("accesstoken", jwtProvider
				.createToken(
					authentication.getId(),
					authentication.getEmail())
			.body(authentication);
}

자 로그인 API까지 완성한 상태다. 다음 글에서는 화면과 테스트까지 진행해볼것이다.

profile
놀고싶다

0개의 댓글