Home Spring Security, Spring Security Otp(3)
Post
Cancel

Spring Security, Spring Security Otp(3)

otp_part_2

  • 인증필터, JWT 필터 구현

  • 인증필터 기능 : 요청을 가로채고 인증 논리를 적용

    • 1 ] 인증 서버가 수행하는 인증을 처리할 필터 구현
    • 2 ] JWT기반 인증 필터 구현


InitialAuthenticationFiler

  • 첫번째 인증단계 처리 필터
    • 인증 책임을 위임할 AuthenticationManager 주입
      • 해당 주입 부분을 spring security 6.x.x 버전에서 구현하다가 포기하고 5.x.x로 구현(TODO 항목으로 남겨둠)
    • shouldNotFilter() 해당 재정의를 통해 특정 경로에 따른 필터 분기 설정
      • 해당 필터에서는 /login경로에 대해서만 모든 요청을 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Component
public class InitialAuthenticationFilter extends OncePerRequestFilter {

  private final AuthenticationManager manager;

  public InitialAuthenticationFilter(AuthenticationManager manager) {
    this.manager = manager;
  }

  @Value("${jwt.signing.key}")
  private String signingKey;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String username = request.getHeader("username");
    String password = request.getHeader("password");
    String code = request.getHeader("code");

    if (code == null) {
      Authentication a = new UsernamePasswordAuthentication(username, password);
      manager.authenticate(a);
    } else {
      Authentication a = new OtpAuthentication(username, code);
      manager.authenticate(a);

      SecretKey key = Keys.hmacShaKeyFor(signingKey.getBytes(StandardCharsets.UTF_8));
      String jwt = Jwts.builder()
        .setClaims(Map.of("username", username))
        .signWith(key)
        .compact();
      response.setHeader("Authorization", jwt);
    }

  }

  @Override
  protected boolean shouldNotFilter(HttpServletRequest request) {
    return !request.getServletPath().equals("/login");
  }
}
  • 코드 분석

  • HTTP 요청에 OTP가 없으면 사용자 이름과 암호로 인증후 AuthenticationManager호출

1
2
3
4
    if (code == null) {
      Authentication a = new UsernamePasswordAuthentication(username, password);
      manager.authenticate(a);
    }


  • OTP코드가 null이 아닌 경우 분기를 추가, 이때, 인증 서버가 OTP를 보냈다고 가정한후.
    OtpAuthenticationAuthenticationManager 호출
1
2
3
4
5
  } else {
    Authentication a = new OtpAuthentication(username, code);
    manager.authenticate(a);
    //  ..
  }
  • JWT를 구축하고 헤더에 추가하는 코드
1
2
3
4
5
6
7
8
9
    } else {
        //  ..
        SecretKey key = Keys.hmacShaKeyFor(signingKey.getBytes(StandardCharsets.UTF_8));
        String jwt = Jwts.builder()
            .setClaims(Map.of("username", username))
            .signWith(key)
            .compact();
        response.setHeader("Authorization", jwt);
    }


JwtAuthenticationFilter

  • /login외에 다른 모든 경로에 대한 요청을 처리하는 필터 추가
    • 해당 필터는 엔드포인트에 대한 접근 권한을 헤더에 담긴 JWT 토큰을 통해 검증한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

  @Value("${jwt.signing.key}")
  private String signingKey;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String jwt = request.getHeader("Authorization");

    SecretKey key = Keys.hmacShaKeyFor(signingKey.getBytes(StandardCharsets.UTF_8));
    Claims claims = Jwts.parserBuilder()
      .setSigningKey(key)
      .build()
      .parseClaimsJws(jwt)
      .getBody();

    String username = String.valueOf(claims.get("username"));

    GrantedAuthority a = new SimpleGrantedAuthority("user");
    var auth = new UsernamePasswordAuthentication(username, null, List.of(a));
    SecurityContextHolder.getContext().setAuthentication(auth);

    filterChain.doFilter(request, response);
  }

  @Override
  protected boolean shouldNotFilter(HttpServletRequest request) {
    return request.getServletPath().equals("/login");
  }
}


SecurityConfig

  • Filter,Provider 주입 및 설정 구성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private InitialAuthenticationFilter initialAuthenticationFilter;

  @Autowired
  private JwtAuthenticationFilter jwtAuthenticationFilter;

  @Autowired
  private OtpAuthenticationProvider otpAuthenticationProvider;

  @Autowired
  private UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(otpAuthenticationProvider)
      .authenticationProvider(usernamePasswordAuthenticationProvider);
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();

    http.addFilterAt(
        initialAuthenticationFilter,
        BasicAuthenticationFilter.class)
      .addFilterAfter(
        jwtAuthenticationFilter,
        BasicAuthenticationFilter.class
      );

    http.authorizeRequests()
      .anyRequest().authenticated();
  }

  @Override
  @Bean
  protected AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
  }
}


테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {

  @Autowired
  private MockMvc mvc;

  @MockBean
  private MockBean restTemplate;

  @MockBean
  private AuthenticationServerProxy authenticationServerProxy;

  @Test
  @DisplayName("Test /login with username and password")
  public void testLoginWithUsernameAndPassword() throws Exception {
    mvc.perform(get("/login").servletPath("/login")
        .header("username", "bill")
        .header("password", "12345")
      )
      .andExpect(status().isOk());

    verify(authenticationServerProxy)
      .sendAuth("bill", "12345");
  }

  @Test
  @DisplayName("Test /login with username and otp")
  public void testLoginWithUsernameAndOtp() throws Exception {
    when(authenticationServerProxy.sendOTP("bill", "5555"))
      .thenReturn(true);

    mvc.perform(get("/login").servletPath("/login")
        .header("username", "bill")
        .header("code", "5555")
      )
      .andExpect(header().exists("Authorization"))
      .andExpect(status().isOk());
  }

  @Test
  @DisplayName("Test /test with Authorization header")
  public void testRequestWithAuthorizationHeader() throws Exception {
    when(authenticationServerProxy.sendOTP("bill", "5555"))
      .thenReturn(true);

    var authorizationHeaderValue =
      mvc.perform(get("/login").servletPath("/login")
          .header("username", "bill")
          .header("code", "5555")
        )
        .andReturn().getResponse().getHeader("Authorization");

    mvc.perform(get("/test")
        .header("Authorization", authorizationHeaderValue))
      .andExpect(status().isOk());
  }
}
  • oAuth2 인증으로 넘어간 후 못다룬 부분을 자세히 알아볼 예정.
This post is licensed under CC BY 4.0 by the author.