چگونه با استفاده از ابزار Spring Security امنیت REST را افزایش دهیم

چهارشنبه 24 شهریور 1400

ابزار Spring Security یک فریم ورک قدرتمند در زبان جاوا است، در این مطلب قصد داریم کمی بیشتر درباره ابزار Spring Security صحبت کنیم.

 چگونه با استفاده از ابزار Spring Security امنیت REST را افزایش دهیم

ابزار Spring Security یکی از بهترین ابزارها در زبان برنامه نویسی جاوا است که امکانات متنوعی را در اختیار شما قرار می دهد. همان طور که می دانید ایمن سازی و افزایش امنیت اپلیکیشن های تحت وب همواره از اهمیت بسیار زیادی برخوردار بوده است و عملا یک فرایند پیچیده به شمار می آید. ابزار Spring Security یک فریم ورک بسیار قدرتمند را در زبان برنامه نویسی جاوا به شما ارائه می دهد که امنیت اپلیکیشن های تحت وب را تضمین می کند ولی با این حال تا زمانی که به طور کامل این فریم ورک را نیاموخته اید نمی توانید از قدرت کامل آن استفاده کنید.

در این مطلب قصد داریم برخی از کامپوننت های اصلی که برای امن کردن یک REST API با استفاده از ابزار Spring Security وجود دارد را مورد بررسی قرار دهیم. در این مطلب یک اپلیکیشن ساده ایجاد می کنیم که از JSON Web Token برای ذخیره اطلاعات کاربران استفاده می کند.

JWT به علت سادگی و فشردگی بالایی که دارد به سرعت در حال تبدیل شدن به یک رویکرد استاندارد برای نگهداری اطلاعات می باشد.

پس با ما در ادامه این مطلب همراه باشید تا این پروژه را به اتمام برسانیم.

ساخت یک REST API امن ساده

در این بخش قصد داریم اهداف ساده برنامه خود را در اختیار شما قرار دهیم:

-   ابتدا یک دکمه را ایجاد می کنیم که درخواستی را به سمت بک اند ارسال می کند.

-  دو فیلد مربوط به نام کاربری و رمز عبور را ایجاد می کنیم تا کاربران بتوانند وارد اپلیکیشن شوند.

-  زمانی که روی دکمه API کلیک می شود و کاربر هنوز وارد حساب کاربری خود نشده است درخواست او رد شده و سرور عبارت HTTP 401 Forbidden را به عنوان پاسخ بر می گرداند.

-  اگر کاربر وارد حساب کاربری خود شده باشد پاسخ مناسب از سمت سرور برای آنها ارسال می شود.

این اپلیکیشن ساده می تواند تمامی کامپوننت های مورد نیاز برای کار کردن با ابزار Spring Security برای امن کردن یک REST API را در اختیار شما قرار دهد. قبل از شروع کار ابتدا یک نمای کلی از اقداماتی که قصد انجام آنها را دارید ارائه دادیم و در گام بعدی هر یک از فایل های مربوط به این پروژه را یک بار بازنگری کرده و توضیحات مورد نیاز را در اختیار شما قرار می دهیم تا بیشتر با این پروژه و بخش های مختلف آن آشنا شوید.

توضیحات کلی درباره این پروژه مربوط به ابزار Spring Security

شما می توانید فایل ها و layout مربوط به این پروژه را در تصویر زیر به طور کامل مشاهده کنید:

فایل های مربوط به کلاس هایی که در این پروژه ساده مربوط به ابزار Spring Security استفاده شده اند نیز در ادامه به طور کامل لیست شده اند:

-   JwtApplication.java: فایل اصلی این اپلیکیشن که توسط Spring Boot ساخته شده است.

-   JWTTokenService.java: پیاده سازی TokenService که توسط TokenAuthenticationService مورد استفاده قرار گرفته است.

-   MyController.java: کنترل کننده وب که شامل endpoint های محافظت شده است.

-   NoRedirectStrategy: در SecurityConfig.java مورد استفاده قرار گرفته است تا از رفتار redirection پیش فرض مربوط به ابزار Spring Security جلوگیری کند.

-  SecurityConfig.java: مسئول انجام تنظیمات و پیکربندی های مورد نیاز برای این ابزار است.

-  TokenAuthenticationFilter.java: مسئول بررسی و احراز هویت کاربرانی است که درخواستی را به سرور ارسال می کنند و توسط SecurityConfig.java استفاده می شود.

-  TokenAuthenticationProvider.java: توسط SecurityConfig.java در AuthenticationManager  استفاده می شود تا روشی را برای بازیابی کردن کاربر در TokenAuthenticationFilter فراهم کند.

-  TokenAuthenticationService.java: پیاده سازی UserAuthenticationService که مبتنی بر توکن است.

-  TokenService.java: توسط TokenAuthenticationService استفاده می شود تا توکن های JWT را ساخته و اعتبارسنجی کند. این کلاس توسط JWTTokenService پیاده سازی می شود.

- User.java: یک پیاده سازی ساده از اینترفیس UserDetails اسپرینگ که برای نگه داشتن اطلاعات کاربر استفاده می شود.

-  UserAuthenticationService.java: یک سرویس میانی است که توسط UserController.java استفاده می شود تا منطق تجاری ورود کاربران را مدیریت کند که این کار توسط TokenAuthenticationProvider صورت می گیرد تا کاربران را از طریق توکن ها بیابد.

-  UserController.java: کنترل کننده وب که API مربوط به ورود کاربران را فراهم می کند.

-  UserService.java: یک اینترفیس کاربردی است که برای پیدا کردن کاربران مورد استفاده قرار می گیرد. این کلاس توسط TokenAuthenticationService استفاده می شود تا کاربران را از طریق اطلاعات توکن بازیابی کند.

-  UserServiceImpl.java: پیاده سازی UserService.java که در این مثال شامل مجموعه ای از کاربران است.

برای این که همه چیز را تا جای ممکن ساده نگه داریم سعی کردیم تا تمامی کلاس ها را در این بخش به صورت مختصر به شما معرفی کنیم.

کدهای مربوط به فرانت اند با یک صفحه ورود ساده

در زمان استفاده از ابزار Spring Security باید توجه داشته باشید که Spring Web به صورت پیش فرض تمامی فایل ها را در پوشه resources/static ذخیره می کند. این مسیر در واقع همان مسیری است که شما می توانید از طریق فایل index.html به آن دسترسی پیدا کنید. در واقع این فایل به شما نشان می دهد که چگونه می توانید با با استفاده از جاوا اسکریپت در فرانت اند با بخش های مربوط به امنیت سرور در ارتباط باشید.

این فایل index.html نمونه به کاربران اجازه می دهد تا روی یک دکمه کلیک کرده و پیغامی که از endpoint محافظت شده باز میگردد را مشاهده کنند. البته باید توجه کنید که این فایل یک قابلیت مربوط به لاگین و ورود کاربران را نیز برای شما فراهم می کند. شما در بخش بعدی می توانید کدهای مربوط به جاوا اسکریپت برای مدیریت کردن این بخش ها را مشاهده کنید.

بخش اول: API محافظت شده و فراخوانی های لاگین برای استفاده از ابزار Spring Security

<script>
  let token = null;
  async function protectedAPI(){
    let headers = {};
    if (token) headers["Authorization"] = "Bearer " + token;
    let response = await fetch("/protected", {
      headers
    });
    let text = await response.text();
    if (response.ok){
      document.getElementById("protectedMessage").innerHTML = text;
    } else {
      document.getElementById("protectedMessage").innerHTML = "You must log in first";
    }
  }
  async function login(){
    let response = await fetch("/open/login", {
      method: 'post',
      body: 
JSON.stringify({username:document.getElementById("username").value,password:document.getElementById("password").value}),
      headers: {
        "Content-Type": "application/json"
      }
    });
    if (response.ok){
      let text = await response.text();
      token = text;
      document.getElementById("login-msg").innerHTML = "Login success";
    } else {
      document.getElementById("login-msg").innerHTML = "Failed to login: " + response.status;
    }
  }
</script>

بخش اول از کدها به طور کلی مبتنی بر دو API endpoint می باشد که این دو عبارت اند از /open/login و /protected. در واقع این دو endpoint از نتایج مربوط به فراخوانی های لاگین استفاده می کنند تا مقادیر مربوط به متغیرهای توکن ها را تنظیم کنند و در صورتی که توکن ها در دسترس باشند فراخوانی محافظت شده توکن را به header احراز هویت شده ارسال می کند. در ادامه سرور از این توکن استفاده می کند تا اعتبار کاربر را تایید کند و تا کاربر بتواند به endpoint امن دسترسی پیدا کند.

Endpoint محافظت شده(MyController.java)

MyController در واقع یک REST mapping مخصوص وب ابزار Spring Security است که درادامه کدهای مربوط به آن را بررسی خواهیم کرد.

@GetMapping({ "/protected" })
public String protectedEndpoint() {
  return "Protected Endpoint Response";
}

فایل SecurityConfig.java

SecurityConfig.java در واقع اصلی ترین بخش از تنظیمات امنیتی ابزار Spring Security است. به همین علت اجازه دهید تا ابتدا از این فایل آغاز کنیم و سپس اطلاعات دیگر را در اختیار شما قرار دهیم.

این کلاس با استفاده از عبارت های @configuration و @EnableWebSecurity حاشیه نویسی شده است که به ابزار Spring Security اطلاع می دهد که بخش های امنیتی فعال شده است و این کلاس نشان دهنده تنظیمات مربوط به آن است. بخش عمده ای از این اقدامات توسط متد configure انجام می شود که در ادامه کدهای مربوط به آن را می توانید مشاهده کنید:

private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
    new AntPathRequestMatcher("/"), new AntPathRequestMatcher("/open/**")
  );
  TokenAuthenticationProvider provider;
  private static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
  protected void configure(final HttpSecurity http) throws Exception {
    http
      .sessionManagement()
      .sessionCreationPolicy(STATELESS)
      .and()
      .exceptionHandling()
      .defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
      .and()
      .authenticationProvider(provider)
      .addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter.class)
      .authorizeRequests()
      .requestMatchers(PROTECTED_URLS)
      .authenticated()
      .and()
      .csrf().disable()
      .formLogin().disable()
      .httpBasic().disable()
      .logout().disable();
  }
  TokenAuthenticationFilter restAuthenticationFilter() throws Exception {
    final TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
    filter.setAuthenticationManager(authenticationManager());
    filter.setAuthenticationSuccessHandler(successHandler());
    return filter;
  }

در این کد شما می توانید کامنت هایی را مشاهده کنید که اطلاعاتی را در اختیار شما قرار می دهد. متد پیکربندی در این کد از یک تطبیق دهنده الگوی Ant استفاده می کند تا به درخواست هایی که به پوشه ها و مسیرهای استاتیک می روند و همین طور هر چیزی که بعد از مسیر /open/ قرار می گیرد اجازه دهد بدون تایید منتقل شوند. این به آن معناست که شما همچنان می توانید فایل /static/index.html را استفاده کنید و endpoint لاگین نیز می تواند در مسیر /open/login میزبانی شود.

یک نکته مهم در خصوص کدهای بالا درباره ابزار Spring Security

نکته مهمی که باید به آن دقت داشته باشید این است که این تنظیمات در واقع یک provider را نیز اضافه می کنند که در واقع همان TokenAuthenticationProvider می باشد. علاوه بر این یک فیلتر نیز اضافه می شود که  توسط TokenAuthenticationFilter مدیریت می شود. توجه داشته باشید این فیلتر در واقع قبل از AnonymousAuthenticationFilter است و به عنوان بخشی از امنیت ابزار Spring Security شناخته می شود.

فیلتر اعتبارسنجی در ابزار Spring Security

TokenAuthenticationFilter مسئول بررسی درخواست هایی است که وارد URL های محافظت شده می شوند. کدهای مربوط به این فیلتر را در ادامه برای شما آورده ایم:

@Override
  public Authentication attemptAuthentication(final HttpServletRequest request,
                                              final HttpServletResponse response) {
    final String param = ofNullable(request.getHeader(AUTHORIZATION)).orElse(request.getParameter("t"));

    final String token = ofNullable(param).map(value -> removeStart(value, "Bearer"))
      .map(String::trim).orElseThrow(() -> new BadCredentialsException("No Token Found!"));

    final Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
    return getAuthenticationManager().authenticate(auth);
  }

  @Override
  protected void successfulAuthentication(final HttpServletRequest request,
        final HttpServletResponse response, final FilterChain chain,
        final Authentication authResult) throws IOException, ServletException {
      super.successfulAuthentication(request, response, chain, authResult);
      chain.doFilter(request, response);
  }

اساسا این فیلتر توکن که توسط بخش فرانت اند و زبان برنامه نویسی جاوا اسکریپت ارسال شده است را از هدر مربوط به اعتبارسنجی و احراز هویت خارج می کند. در صورتی که توکن در جای خود قرار نداشته باشد یک ارور را برای شما ایجاد می کند و مدیریت آن را نیز به authentication manager واگذار می کند.

بررسی توکن(TokenAuthenticationProvider.java)

TokenAuthenticationProvider وظیفه بازیابی کاربر بر اساس توکن مربوط به اعتبارسنجی را برعهده دارد. این بخش تنها دارای یک متد است که وظایف خود را به UserAuthenticationService واگذار می کند و شما میتوانید کدهای مربوط به آن را در ادامه مشاهده کنید:

@Autowired
UserAuthenticationService auth;
//...
@Override
  protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) {
    final Object token = authentication.getCredentials();
    return Optional.ofNullable(token).map(String::valueOf).flatMap(auth::findByToken)
      .orElseThrow(() -> new UsernameNotFoundException("Couldn't find user: " + token));
  }

در صورتی که کاربر null باشد یک ارور در زمان اجرا به وجود می آید.

فایل های UserAuthenticationService.java و TokenAuthenticationService.java

TokenAuthenticationService پیاده سازی است که به صورت کاملا خودکار به TokenAuthenticationProvider متصل می شود. این کلاس در واقع از متد findByToken استفاده می کند تا کاربر را بازیابی کند.

TokenAuthenticationService در واقع نقطه ای است که جریان لاگین با جریان احراز هویت همسو می شود. این کلاس متد login را فراهم می کند که توسط UserController مورد استفاده قرار می گیرد. شما می توانید هر دو این متدها را در قطعه کد زیر مشاهده کنید:

@Autowired
  TokenService tokenService;
  @Autowired
  UserService users;

  @Override
  public Optional<String> login(final String username, final String password) {
    return users
      .findByUsername(username)
      .filter(user -> Objects.equals(password, user.getPassword()))
      .map(user -> tokenService.newToken(ImmutableMap.of("username", username)));
  }

  @Override
  public Optional<User> findByToken(final String token) {
    System.out.println("$$$$$$$$$$$$$$$$$$$$ token: " + token);
    return Optional
      .of(tokenService.verify(token))
      .map(map -> map.get("username"))
      .flatMap(users::findByUsername);
  }

هر دو متد findByToken و login به TokenService و UserService وابسته هستند. findByToken در واقع یک توکن را دریافت می کند و از tokenService استفاده می کند تا اعتبار آن را بسنجد. در صورتی که توکن مناسب باشد findByToken از UserService استفاده می کند تا شی مربوط به کاربر واقعی را به دست بیاورد.

Login دقیقا برعکس این روند عمل می کند. این متد یک نام کاربری را دریافت می کند و کاربر را با استفاده از userService می گیرد، سپس بررسی می کند که رمز عبوری که وارد کرده است تطابق لازم را داشته باشد و در انتها نیز از tokenService استفاده می کند تا توکن مدنظر را بسازد.

فایل های TokenService.java و JWTTokenService.java

JWTTokenService  در کدهای مربوط به این پروژه درباره ابزار Spring Security محلی است که در آن توکن JWT واقعی مدیریت می شود. این موقعیت به میزان زیادی به کتابخانه JJWT بستگی دارد و شما می توانید کدهای آن را به سادگی در ادامه مشاهده کنید:

JWTTokenService() {
    super();
    this.issuer = requireNonNull("infoworld");
    this.secretKey = BASE64.encode("www.infoworld.com");
  }
  public String newToken(final Map<String, String> attributes) {
    final DateTime now = DateTime.now();
    final Claims claims = Jwts.claims().setIssuer(issuer).setIssuedAt(now.toDate());

    claims.putAll(attributes);

    return Jwts.builder().setClaims(claims).signWith(HS256, secretKey).compressWith(COMPRESSION_CODEC)
      .compact();
  }

  @Override
  public Map<String, String> verify(final String token) {
    final JwtParser parser = Jwts.parser().requireIssuer(issuer).setClock(this).setSigningKey(secretKey);
    return parseClaims(() -> parser.parseClaimsJws(token).getBody());
  }

  private static Map<String, String> parseClaims(final Supplier<Claims> toClaims) {
    try {
      final Claims claims = toClaims.get();
      final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
      for (final Map.Entry<String, Object> e: claims.entrySet()) {
        builder.put(e.getKey(), String.valueOf(e.getValue()));
      }
      return builder.build();
    } catch (final IllegalArgumentException | JwtException e) {
      return ImmutableMap.of();
    }
  }

کتابخانه JJWT ساختن، تجزیه کردن و اعتبارسنجی توکن های JWT را راحت تر می کند. متد newToken از Jwts.claims() استفاده می کند تا برخی از استانداردها را در  ابزار Spring Security تنظیم کند.

ما در این مطلب سعی کردیم اطلاعات جامع و کاملی را درباره ابزار Spring Security با استفاده از یک پروژه ساده و در عین حال کاربردی به شما ارائه دهیم. امیدواریم مطالعه این مطلب درباره ابزار Spring Security برای شما مفید بوده باشد و موفق شده باشید اطلاعات مورد نیاز در این خصوص را به دست بیاورید. برای داشتن اطلاعات بیشتر درباره ابزار Spring Security به شما پیشنهاد می کنیم حتما از سایر بخش های سایت ما نیز دیدن کنید. 

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

تاکنون هیچ کاربری از این پست تشکر نکرده است

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید