‏نمایش پست‌ها با برچسب spring boot. نمایش همه پست‌ها
‏نمایش پست‌ها با برچسب spring boot. نمایش همه پست‌ها

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

گرفتن 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());
    }
}

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

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

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

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

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

ایجاد فایل برای دانلود در 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;
}