۱۴۰۰ دی ۶, دوشنبه

فعال سازی پروتکل های قدیمی TSL/SSL در جاوا

 همیشه این امکان وجود دارد که با یک پروژه قدیمی روبرو شویم و مجبور باشیم تا آنرا اصلاح و یا بروزرسانی کنیم. معمولا استک آنها خیلی قدیمی و منسوخ شده هستند، مانند پروژه ای که هفته پیش روی آن کار کردم، دیتابیس این پروژه SQL Server بود که برای ارتباط امن از پروتکل TSL 1.0 استفاده می‌کرد! من روی سیستم ام JDK 8 آپدیت 312 را نصب کرده بودم، و روی این نسخه از جاوا، TSL 1.0 به دلایل امنیتی، غیر فعال شده است. نتیجه این که پروژه اسپرینگ بوت قدیمی، امکان برقراری ارتباط با دیتابیس را نداشت و خطای زیر را نمایش میداد:


com.microsoft.sqlserver.jdbc.SQLServerException: The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption. Error: "The server selected protocol version TLS10 is not accepted by
client preferences [TLS12]". ClientConnectionId:38242ee7-8809-47a1-b020-2df5e3cc8683

برای بروز کردن یکی از سرویس های پروژه نیازمند اجرای آن روی سیستم ام بودم، و امکان بروز کردن دیتابیس وجود نداشت. راه حل بعدی نصب نسخه قدیمی تر از JDK بود که با این پروتکل سازگار باشد (!). اما، یک راه دیگر نیز هست، فعال کردن این پروتکل روی جاوای موجود.

برای این منظور، از یار قدیمی، گوگل استفاده کردم و دنبال راه حلی برای فعال سازی پروتکل TSL 1.0 گشتم که در آدرس زیر راه حل آنرا پیدا کردم: https://www.qvera.com/kb/index.php/2689/how-to-re-enable-tlsv1-and-tlsv1-1-in-java-8

برای فعال سازی این پروتکل، باید وارد مسیر JAVA_HOME جاوای مد نظرتان شوید (مسیری که جاوا نصب شده و شامل پوشه های اصلی برنامه نظیر bin، bundle و غیره میشود) و فایل java.security را در آن مسیر جستجو کنید و آنرا برای ویرایش باز کنید.

اکنون دنبال مقدار jdk.tls.disabledAlgorithms در فایل بگردید، این کلید مقادیر الگوریتم های غیر فعال شده را نگهداری می‌کند که با حذف نام الگوریتم، دوباره در جاوا فعال میشود، که برای پروژه من TSL1 باید حذف میشد:


jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, include jdk.disabled.namedCurves

مقدار jdk.tls.disabledAlgorithms بعد از اصلاح:


jdk.tls.disabledAlgorithms=SSLv3, TLSv1.1, RC4, DES, MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, include jdk.disabled.namedCurves

اکنون میتوانید براحتی از پروتکل مد نظر استفاده کنید. توجه داشته باشید که این روش به هیچ وجه برای نسخه های جدید اپلیکیشن یا پروژه های جدید استفاده نکنید، زیرا شامل خطرات امنیتی بسیار بالایی می‌شود.

۱۴۰۰ آذر ۴, پنجشنبه

گرفتن IP کاربر در Spring Boot

به دلیل امنیتی، لازم که بتونیم IP کاربران رو ثبت کنیم. در یک برنامه معمولی Spring Boot مستقیما با کاربران تعامل دارد (بدون پراکسی، لود بالانس و غیره)، می‌توانیم به راحتی با استفاده متد getRemoteHost() از HttpServletRequest اقدام به گرفتن IP کاربران کنیم.

اما در بیشتر موارد، باید اپلیکیشن را پشت Nginx یا آپاچی قرار دهیم و درخواست ها رو به اپلیکیشن بفرستیم‌. در این حالت HttpServletRequest.getRemoteHost() آدرس IP پراکسی سرور را برمی‌گرداند. در نتیجه ما باید IP کاربران را در هدر درخواست ها ست کنیم. معمولا X-FORWARDED-FOR برای این منظور استفاده میشود، ولی تنها این گزینه نیست:

  • X-Forwarded-For
  • Proxy-Client-IP
  • WL-Proxy-Client-IP
  • HTTP_X_FORWARDED_FOR
  • HTTP_X_FORWARDED
  • HTTP_X_CLUSTER_CLIENT_IP
  • HTTP_CLIENT_IP
  • HTTP_FORWARDED_FOR
  • HTTP_FORWARDED
  • HTTP_VIA
  • REMOTE_ADDR

این گزینه ها را در فایل properties اپلیکیشن قرار می‌دهیم و سپس به راحتی، مانند کد پایین، میتوانیم آنها را چک کنیم:


application:
  header-ip-candidates: X-Forwarded-For,Proxy-Client-IP,WL-Proxy-Client-IP,HTTP_X_FORWARDED_FOR,HTTP_X_FORWARDED,HTTP_X_CLUSTER_CLIENT_IP,HTTP_CLIENT_IP,HTTP_FORWARDED_FOR,HTTP_FORWARDED,HTTP_VIA,REMOTE_ADDR

@RestController
@RequestMapping("/ip")
public class IPResource {

    @Value("${application.header-ip-candidates}")
    private String[] headerCandidates;

    @GetMapping
    public ResponseEntity getHome() {
        if (RequestContextHolder.getRequestAttributes() == null) {
            return ResponseEntity.ok("0.0.0.0");
        }

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        for (String header: headerCandidates) {
            String ipList = request.getHeader(header);
            if (ipList != null && ipList.length() != 0 && !"unknown".equalsIgnoreCase(ipList)) {
                String ip = ipList.split(",")[0];
                return ResponseEntity.ok(ip);
            }
        }
        return ResponseEntity.ok(request.getRemoteAddr());
    }
}

۱۴۰۰ آبان ۲۸, جمعه

تبدیل فایل به base64 در سمت کلاینت با VueJS

آپلود فایل از سمت کلاینت به سرور، یک تسک عادی است. تقریبا همه دولپر ها آنرا انجام دادند. پست کردن فایل با استفاده از نوع درخواست form data یک راه حل عادی است. ولی اگه بخواهیم یک عکس رو قبل از آپلود به سرور به کاربر نمایش بدیم باید چکار کنیم؟ یک باکس برای نمایش تصویر داریم و می‌خواهیم کاربر قبل از ذخیره و ارسال فایل به سرور بتواند تصویر انتخابی خودش را در آن ببیند.

من یک راه حل بر مبنای base64 پیدا کردم. بدینصورت که تصویر رو به base64 تبدیل کنم و سپس آنرا در یک متغیر ذخیره کنم و این متغیر را به باکس تصویر وصل کنم. البته می‌توانیم مقدار base64 را هم به سرور ارسال کنیم و آنجا آنرا به فرمت باینری تبدیل و ذخیره کنیم!

در VueJS، من از watch برای مانیتور کردن تغییرات روی فیلد انتخاب فایل استفاده میکنم تا بعد از تغییر از طرف کاربر، در اپ کلاینت، تصویر را به base64 تبدیل و در یک متغیر دیگر که به باکس تصویر وصل است، ذخیره میکنم.

اجازه بدهید آنها را در کد بررسی کنیم. در قدم اول من دو متغیر تعریف میکنم:

  1. image: متصل شده به فیلد انتخاب فایل
  2. base64: برای نگهداری نتیجه تبدیل 
سپس یک watch روی متغیر image برای بروز کردن base64، تعریف میکنم:


    image: function (newVal, oldVal) {
      if(newVal) {
        this.createBase64Image(newVal);
      } else {
        this.base64 = null;
      }
    }

در انتها، یک تابع برای تبدیل تصویر به base64 و بروزرسانی متغیر آن تعریف میکنم:


    createBase64Image: function(FileObject) {
      const reader = new FileReader();
      reader.onload = (event) => {
        this.base64 = event.target.result;
      }
      reader.readAsDataURL(FileObject);
    }

برای بررسی یک نمونه آنلاین میتوانید لینک https://codepen.io/glinboy/pen/KKvEzjy را چک کنید :)

۱۴۰۰ آبان ۲۷, پنجشنبه

سطر های کلیک شونده با ستونهایی که مستثتا هستند در VueJS

 به تجربه، ترجیح میدم سطرهای جدول قابل کلیک کردن باشند تا اینکه یک دکمه اضافه برای اینکار در آخرین ستون داشته باشم. در VueJS به راحتی میتوانیم @click را به تگ <tr> اضافه کنیم، مانند زیر

اما با دکمه های ستون آخر باید چکار کنیم؟ بعنوان مثال، اگر یک دکمه برای حذف و یک دکمه برای ویرایش داشته باشیم که کاربران مان راحتتر با جدول داده ها کار کنند.

بعد از یک جستجوی نسبتا طولانی، متوجه شدم که میتوانیم به راحتی از تگ @click.stop در تگ <td> استفاده کنیم، که در نتیجه آن دکمه ها میتوانند تابع مدنظرشان را اجرا کنند:

میتوانید آدرس https://codepen.io/glinboy/pen/YzxBQEN را بررسی کنید.

۱۳۹۹ اسفند ۵, سه‌شنبه

دسترسی به role های کاربران در Spring/Jhipster از Keycloak

 برای ایجاد محدودیت روی کنترلر ها، یا فیلتر کردن نتایج بسته به نقش کاربر، با محدودیت هایی در JHipster 5.x روبرو شدیم. توکنی که از Keycloak میگرفتیم شامل نقشهای کاربر بود ولی در مسیر اشتباهی! Keycloak نقش ها رو در مسیر realm_access.roles داخل توکن قرار میداد در حالی که Spring Security، در روت playload توکن دنبال roles میگرده.

من این مشکل رو سمت از سمت Keycloak حل کردم و roles رو به روت playload اضافه کردم، که در دنباله این مطلب به این موضوع میپردازم.

۱۳۹۹ دی ۹, سه‌شنبه

سرویس های ضروری Keycloak

Keycloak یکی ازسرور های احراز هویت قدرتمند حال حاضر است. اپن سورس و در نتیجه رایگان است، اما امکان تهیه پشتیبان حرفه ای از ردهت هم موجود است.
وقتی در حال طراحی اپلیکیشن به صورت میکروسرویس هستیم، هر بیزینسی را به صورت یک سرویس میبینیم، و یکی از مهمترین این سرویس ها، مدیریت کاربران است. Kecloak این رو برای شما هندل میکنه و سرویس های شما رو امن میکنه.
حالا من میخوام یک سری از سرویس های کلی Keycloak رو براتون به اشتراک بزارم،‌که کارتون برای شروع راحتتر بشه.

۱۳۹۹ فروردین ۱۲, سه‌شنبه

چطور پیامک میفرستادیم

وقتی که برنامه نویسی رو شروع کردم، تسکی گرفتم برای ارسال پیامک به مشتری هامون. خیلی هم عالی! فکر میکنم باید با جاوا ۵ یا ۶ پیاده میشد، چیزی که مطمئن هستم این که سرویس دهنده ما از روش‌های قدیمی برای دریافت پیامک از طرف ما استفاده میکرد. من این کد رو از آقای مجیدی (یکی از مدیران اون مجموعه، که واقعاً کارش درست بود) گرفتم، و حالا اون با شما هم به اشتراک میزارم،‌ شاید روزی بدرد کسی خورد:

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

public class SMS {
    public static void sendSms(String mobile, String msg) throws Exception {
        String url = "{SERVER_URL}";
        URL obj = new URL(url);
        HttpURLConnection con = (HttpURLConnection)obj.openConnection();
        con.setRequestMethod("POST");
        con.setRequestProperty("User-Agent", "Mozilla/5.0");
        con.setRequestProperty("Accept-Language", "en-US,en;q=0.5");
        con.setRequestProperty("accept-charset", "UTF-8");
        String urlParam =
                "HEADER={YOUR_SMS_NUMBER}&DESTINATION=" + "{COUNTRY_CODE}" + mobile.substring(1) +
                        "&MESSAGEID=1&MESSAGE=" + msg + "&DCS=8";
        con.setDoOutput(true);
        DataOutputStream wr = new DataOutputStream(con.getOutputStream());
        BufferedWriter writer =
                new BufferedWriter(new OutputStreamWriter(wr, "UTF-8"));
        writer.write(urlParam);
        writer.close();
        wr.close();
        int responseCode = con.getResponseCode();
        BufferedReader in =
                new BufferedReader(new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();
    }
}

۱۳۹۷ اسفند ۷, سه‌شنبه

ایجاد فایل برای دانلود در Spring

یکی از نیازمندی هام این بود که یک متنی رو در قالب فایل برای دانلود، به کاربر ارایه بدم، بعنوان مثال فرض کنیم یه سریال نامبر داریم که کاربر میتونه به صورت فایل متنی دانلود اش کنه. بعد از کلی گشت و گذار به این راه حل رسیدم که کار میکنه:

@GetMapping(path = "/file/get", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity verificationFile() {
 HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_PLAIN);
    headers.setContentDispositionFormData("attachment", "test.txt");
 InputStreamResource inputStreamResource = new InputStreamResource(new ByteArrayInputStream("Hello File".getBytes(StandardCharsets.UTF_8)));
 return ResponseEntity.ok().headers(headers).body(inputStreamResource);
}

بعد از فراخوانی آدرس http://{SERVER-ADDRESS}:{PORT}/file/get فایلی با عنوان test.txt و با محتوای Hello File دانلود میشه.

۱۳۹۷ اسفند ۴, شنبه

محدود کردن جستجوی الستیک روی داده های کاربر جاری در JHipster

وقتی یه پروژه رو با JHipster ایجاد میکنیم، یکی از بخش های مهم که برامون انجام نمیده محدود کردن نمایش داده های هر کاربر برای خود اون کاربر. به صورت پیشفرض وقتی کاربری درخواست نوع داده ای رو میکنه، تمام داده های موجود رو برای کاربر ارسال میکنه (که راه حل برطرف کردن این مشکل با یه سرچ کوچیک پیدا میکنید).
اما وقتی Elasticsearch رو به پروژه اضافه میکنیم و وظیفه جستجو رو به اون واگذار میکنیم، یه مشکل جدید هم ایجاد میشه، اینکه جستجوی کاربر روی تمام داده ای موجود همه کاربران اتفاق میافتد در صورتیکه هر کاربر فقط مجاز به جستجو و مشاهده داده های خودش، نه تمام داده های موجود! خب به صورت پیشفرض نحوه جستجو رو به اینصورت پیاده سازی کرده که تولید مشکل میکند:

return StreamSupport.stream(categorySearchRepository.search(queryStringQuery(query))
    .spliterator(), false).collect(Collectors.toList());

و ما باید یه کار کوچیک روش انجام بدیم و اون رو تبدیل کنیم به این کد:

BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().must(queryStringQuery(query));
if (SecurityUtils.isAuthenticated() && !SecurityUtils.isCurrentUserInRole(AuthoritiesConstants.ADMIN)) {
 queryBuilder = queryBuilder.filter(matchQuery("user.login", SecurityUtils.getCurrentUserLogin().orElse("")));
}
return StreamSupport.stream(categorySearchRepository.search(queryBuilder).spliterator(), false).collect(Collectors.toList());
تو کد بالا، چک میکنیم اگه کاربر، ادمین نبود (AuthoritiesConstants.ADMIN)، به quryBuilder یک فیلتر اضافه میکنیم که براساس کاربر جاری جستجو انجام بشه.
راه حل این مشکل تو اپلیکیشن نمونه 21-point روی گیت هاب وجود داره که منبع منم همون کد بود (PointsResource.java).

۱۳۹۷ بهمن ۱۸, پنجشنبه

Lombok، Hibernate و خطای 'StackOverflowError: null'!

یکی از کتابخانه های محبوبم در جاوا Project Lombok، کمک بزرگی برای تمیز و خوانا بودن کد، برای اینکه متوجه منظورم بشید فقط یه نگاه به نمونه کد اش برای @Data (اینجا) بندازید و اونوقت شما هم به من حق میدید!
 ولی استفاده از @Data روی یک entity و استفاده از این شی داخل یک entity دیگه (مثلا یک Set از کامنتهای یک پست بلاگ)، شما رو با خطای java.lang.StackOverflowError: null مواجه میکنه! @Data یک میانبری برای این انوتیشن های Lombok:
  • @Getter
  • @Setter
  • @ToString
  • @RequiredArgsConstructor
  • @EqualsAndHashCode
 و همه چی بخاطر @EqualsAndHashCode! من یک راه حل برای این مشکل در آخرین کامنت این سوال StackOverflow پیدا کردم: https://stackoverflow.com/questions/34972895 که میگه باید بعد از @Data از این انوتیشن استفاده کنیم و فیلد های رابطه ای رو از متدهای Equals و Hash حذف کنیم:

@EqualsAndHashCode(exclude="entries")
من تو پروژه Feader ام از این کتابخانه استفاده کردم و میتونید به عنوان یه نمونه روش بررسی هاتون رو انجام بدید. Feed.java و FeedEntry.java کلاسهایی هستند که مورد استفاده قرار گرفتند، FeedEntry داخل Feed استفاده شده (مثل کامنتهای یک پست):

package com.glinboy.feader.model;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Entity
@Data
@EqualsAndHashCode(callSuper=true, exclude="entries")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Feed extends BaseModel {

 private static final long serialVersionUID = 7720285113632455831L;
 
 @Column(length=50)
 private String title;
 
 @Column(length=256)
    private String link;
    
    @Lob
    private String description;
 
    @ManyToMany
    @JoinTable(name="feed_category",
      joinColumns = @JoinColumn(name = "feed_id", nullable=false),
            inverseJoinColumns = @JoinColumn(name = "category_id", nullable=false))
    private Set category;

    @OneToMany(mappedBy="feed")
    private Set entries;
}

package com.glinboy.feader.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Entity
@Data
@EqualsAndHashCode(callSuper=true, exclude="feed")
public class FeedEntry extends BaseModel {

 private static final long serialVersionUID = -7086809764656937022L;
 
 @Column(length=128)
 private String title;
 
 @Lob
 private String description;
 
 @Column(length=512)
 private String link;
 
 @Column(length=64)
 private String author;
 
 @Column(length=40)
 private String guid;
 
 @ManyToOne
 @JoinColumn(name="feed_id", nullable=false)
 private Feed feed;
}

۱۳۹۷ دی ۲۴, دوشنبه

Hello World!

بزارید به روش برنامه نویس ها شروع کنم!
به عنوان یک توسعه دهنده جاوا:

class Main {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}
به عنوان یک برنامه نویس C++:

#include <iostream>
using namespace std;

int main() 
{
    cout << "Hello, World!";
    return 0;
}
به عنوان یک توسعه دهنده پایتون:

print 'hello world!'
به عنوان یک طرفدار Rust:

fn main() {
    println!("Hello World!");
}
به عنوان یک طرفدار Go:

package main

import "fmt"

func main() {
 fmt.Println("Hello World!")
}
به عنوان یک طرفدار Elixir:

IO.puts "Hello, World!"
و برای سایر دوستانم:

{
  "from": "GLinBoy",
  "to": [
    "Developers",
    "Programmers",
    "Geeks",
    "Hackers",
    "Machines",
    "etc"
  ],
  "message": "Hello World!"
}