সরাসরি পর্বে যান
পর্ব ৫৩ · ৫৯-এর মধ্যে · মডিউল ৭

Spring Security — authentication, authorization আর নিরাপদ API

Spring Security — Filter Chain, Password Hashing, JWT, Method Security, CSRF, CORS and Secure API Habits

Taught in Bangla, this lesson covers Spring Security: authentication versus authorization and 401 versus 403, the servlet filter chain, configuring SecurityFilterChain, storing passwords with BCrypt or Argon2, sessions, tokens and JWT, method security and ownership checks, CSRF, CORS and security headers, and secrets, HTTPS, rate limits and security testing.

এই পর্বে যা শিখবেন

  • একটি সাধারণ API কোন হুমকির মুখে থাকে, আর প্রতিটির প্রতিরক্ষা কী
  • Spring Security-র filter chain, SecurityContextHolder আর Authentication object কীভাবে কাজ করে
  • SecurityFilterChain bean দিয়ে কোন endpoint সবার জন্য, কোনটি লগইন করা মানুষের, কোনটি শুধু ADMIN-এর
  • পাসওয়ার্ড hash, session বনাম token, JWT, OAuth2 আর “Google দিয়ে লগইন”-এর মূল ধারণা
  • method security, ownership check, CSRF, CORS, security header, secret, log আর security টেস্ট

১ · দুটি প্রশ্ন — “আপনি কে?” আর “আপনি কী করতে পারেন?”

পর্ব ৫১-এ আপনি একটি অনলাইন দোকানের API বানিয়েছেন। এখন সেটা ইন্টারনেটে খোলা। যে কেউ পণ্যের তালিকা দেখতে পারবেন — এটা ভালো। কিন্তু একই দরজা দিয়ে যে কেউ যদি চালের দাম ১ টাকা করে দিতে পারেন, বা অন্যের অর্ডারের ঠিকানা আর ফোন নম্বর দেখতে পারেন, তাহলে দোকানটি এক দিনও টিকবে না। এই পর্বের বিষয় সেই দরজাগুলো পাহারা দেওয়া।

নিরাপত্তার শুরুতেই দুটি প্রশ্ন আলাদা করতে হয়। প্রথমটি authentication — “আপনি কে?” পাসওয়ার্ড, OTP বা token দিয়ে এর উত্তর মেলে। দ্বিতীয়টি authorization — “পরিচয় তো জানলাম, এই কাজটি কি আপনি করতে পারেন?” প্রথম প্রশ্নের উত্তর না মিললে HTTP-তে 401, দ্বিতীয়টির উত্তর “না” হলে 403. পর্ব ৫১-এর status code-এর টেবিলে এই দুটি দেখেছেন; এবার সেগুলো আসলে কে, কীভাবে দেয়, তা দেখবেন।

মতিঝিলের একটি অফিস ভবনের কথা ভাবুন। গেটে নিরাপত্তাকর্মী আইডি কার্ড দেখে নিশ্চিত হন, আপনি এই অফিসেরই কর্মী — এটা authentication. ভেতরে ঢোকার পর আপনার কার্ড দিয়ে শুধু নিজের তলার দরজা খোলে; ক্যাশ রুম খোলে শুধু হিসাব বিভাগের কার্ডে — এটা authorization. ভবনে ঢুকতে পারা মানেই সব ঘরে ঢুকতে পারা নয়। আর এই নিয়ম পরীক্ষা হয় দরজায়, ঘরের ভেতরের কাজ শুরুর আগে।

Spring Security ঠিক এই গেট আর দরজাগুলো বানায়। প্রতিটি request আপনার controller-এ পৌঁছানোর আগে কয়েকটি পাহারাদারের সারি পার হয়: কেউ পরিচয় যাচাই করে, কেউ নিয়ম মিলিয়ে দেখে, কেউ ব্যর্থ হলে ঠিক উত্তরটি পাঠায়। আপনার controller-এর কোড চলে কেবল সব পাহারা পার হলে।

কোন বিপদ থেকে বাঁচাচ্ছি — একটি ছোট threat model

নিরাপত্তার কাজ শুরু হয় একটি প্রশ্ন দিয়ে: কে, কী ক্ষতি করতে চাইতে পারে, আর কোন পথে? এই চিন্তাকে বলে threat model. আমাদের দোকানের API-র জন্য প্রধান ঝুঁকি আর তাদের প্রতিরক্ষা এক টেবিলে:

ঝুঁকিকী ঘটতে পারেপ্রতিরক্ষাকোথায়
ডেটাবেস ফাঁসপাসওয়ার্ডের তালিকা বাইরে চলে যায়; মানুষ একই পাসওয়ার্ড অন্য সাইটেও ব্যবহার করেনsalt-সহ ধীর hashঅংশ ৪
অনুমান করে লগইনএকের পর এক পাসওয়ার্ড চেষ্টা, বা অন্য সাইট থেকে ফাঁস হওয়া জোড়া দিয়ে চেষ্টাচেষ্টার সীমা, MFAঅংশ ৪, ৮
ভাঙা অনুমতিসাধারণ গ্রাহক দাম বদলান, বা URL-এর নম্বর বদলে অন্যের অর্ডার দেখেনডিফল্টে নিষেধ, role, ownership checkঅংশ ৩, ৬
Injectionইনপুটের লেখা SQL-এর অংশ হয়ে যায়parameterized query, ইনপুট যাচাইপর্ব ৪৯, ৫১
সংবেদনশীল ডেটা ফাঁসtoken পথে পড়ে ফেলা, log-এ পাসওয়ার্ড, উত্তরে অপ্রয়োজনীয় fieldHTTPS, DTO, সাবধানী logঅংশ ৮
জাল বা চুরি হওয়া tokenকেউ token বদলে নিজেকে ADMIN বানাতে চায়স্বাক্ষর যাচাই, ছোট মেয়াদঅংশ ৫

তৃতীয় সারিটির নাম Broken Access Control. OWASP Top 10-এর ২০২১ সালের তালিকায় এটি প্রথম স্থানে উঠে আসে — পরীক্ষা করা অ্যাপগুলোতে অন্য যেকোনো ধরনের দুর্বলতার চেয়ে এটি বেশি পাওয়া গেছে। আর SQL injection-এর প্রতিরক্ষা আপনি পর্ব ৪৯-এ PreparedStatement দিয়ে শিখেছেন; এই পর্বে সেটা আবার বলব না।

মূল ধারণা

নিরাপত্তার বেশিরভাগটাই নতুন চালাক কৌশল নয় — নিরাপদ ডিফল্ট রাখা, আর সেগুলো অকারণে বন্ধ না করা। Spring Security শুরু থেকেই সব দরজা বন্ধ রাখে (deny by default); আপনি জেনে-বুঝে একটি একটি করে খোলেন। আর প্রতিটি মানুষ বা প্রোগ্রাম পাবে কাজের জন্য ঠিক যতটুকু অনুমতি দরকার, ততটুকুই — least privilege. একটি পাহারায় ভুল থাকতে পারে ধরে নিয়ে কয়েকটি স্বাধীন স্তর রাখাকে বলে defense in depth.

নিজে নিরাপত্তার যন্ত্র বানাবেন না। নিজের token ফরম্যাট, hash পদ্ধতি বা লগইন-ফিল্টার লেখা শেখার জন্য ভালো, আসল অ্যাপে বিপজ্জনক। Spring Security-র কোড হাজারো প্রজেক্টে পরীক্ষিত। এই পর্বের দুটি চালানো যায় এমন উদাহরণ শুধু ভেতরের কৌশল বোঝানোর জন্য।

২ · filter chain — controller-এর আগে পাহারাদারের সারি

পর্ব ৪৮-এ Chain of Responsibility pattern দেখেছেন: একটি অনুরোধ কয়েকটি handler-এর শিকল ধরে এগোয়, আর যেকোনো handler চাইলে সেখানেই থামিয়ে দিতে পারে। Java-র ওয়েব সার্ভারে এর রূপ হলো servlet filter: Tomcat প্রতিটি request-কে DispatcherServlet-এ দেওয়ার আগে কয়েকটি filter-এর ভেতর দিয়ে পাঠায়। Spring Security পুরোটাই এই filter-এর ওপর দাঁড়িয়ে।

তিনটি স্তর আছে, আর নামগুলো প্রথমে ভারী শোনায়:

🔌 DelegatingFilterProxy

Tomcat Spring-এর bean চেনে না, চেনে শুধু servlet filter. DelegatingFilterProxy হলো Tomcat-এ বসানো একটি সাধারণ filter, যে নিজে কিছু করে না — কাজটি পাঠিয়ে দেয় Spring-এর ভেতরের একটি bean-এর কাছে। দুই জগতের মাঝে সেতু।

🧭 FilterChainProxy

সেই bean-এর নাম FilterChainProxy. এটি request-এর পথ দেখে ঠিক করে কোন SecurityFilterChain প্রযোজ্য — যেমন /api/**-এর জন্য token-ভিত্তিক একটি, /admin/**-এর জন্য লগইন পাতাওয়ালা আরেকটি। প্রথম যেটি মেলে, সেটিই চলে।

🛡️ SecurityFilterChain

একটি নির্দিষ্ট ক্রমে সাজানো security filter-এর তালিকা: CORS, CSRF, লগআউট, পরিচয় যাচাইয়ের filter, anonymous, exception অনুবাদ, আর সবশেষে অনুমতি যাচাইয়ের AuthorizationFilter. আপনি এই তালিকাটি কনফিগার করেন।

পরিচয় যাচাই সফল হলে ফলটি রাখা হয় SecurityContextHolder-এ। ডিফল্টে এটি একটি ThreadLocal: যে thread এই request সামলাচ্ছে, শুধু সেই thread দেখতে পায়। ভেতরে থাকে একটি SecurityContext, আর তার ভেতরে একটি Authentication object — তিনটি মূল তথ্যসহ: principal (কে — সাধারণত username বা ফোন নম্বর), credentials (পাসওয়ার্ড, যাচাইয়ের পরে মুছে ফেলা হয়), আর authorities (কী কী অনুমতি — যেমন ROLE_CUSTOMER). আপনার কোডের যেকোনো জায়গা থেকে এটা পড়া যায়:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String who = auth.getName();                       // যেমন "01712345678"
var roles = auth.getAuthorities();                 // যেমন [ROLE_CUSTOMER]

এবার একটি request পুরো শিকল ধরে কীভাবে এগোয়, তিনটি পরিস্থিতিতে দেখুন: token নেই, token আছে কিন্তু role ভুল, আর token ও role দুটিই ঠিক। endpoint-টি দাম বদলানোর — PUT /api/v1/products/7/price, যা শুধু ADMIN পারবেন।

  1. ClientPUTঅ্যাপ
  2. FilterChainProxyশিকল বাছাই
  3. পরিচয়authn filterকে?
  4. অনুমতিAuthorizationপারবেন?
  5. ControllerchangePrice()আপনার কোড
request ১token ছাড়া
PUT /api/v1/products/7/price
Content-Type: application/json
(Authorization header নেই)

Tomcat → DelegatingFilterProxy → FilterChainProxy
  /api/** মেলে → API-র SecurityFilterChain
request ১পরিচয়ের filter
BearerTokenAuthenticationFilter: token নেই → কিছু করল না
AnonymousAuthenticationFilter:
  SecurityContext ← anonymousUser [ROLE_ANONYMOUS]
request ১অনুমতি → 401
AuthorizationFilter: নিয়ম hasRole('ADMIN') → না
ExceptionTranslationFilter: ব্যবহারকারী anonymous
  → 401 Unauthorized
  → WWW-Authenticate: Bearer
controller চলেইনি
request ২রহিমের token
Authorization: Bearer eyJhbGciOi...
BearerTokenAuthenticationFilter:
  স্বাক্ষর ✓  মেয়াদ ✓
  SecurityContext ← rahim [ROLE_CUSTOMER]
request ২অনুমতি → 403
AuthorizationFilter: hasRole('ADMIN')?
  rahim-এর আছে শুধু ROLE_CUSTOMER → না
ExceptionTranslationFilter: পরিচয় জানা
  → 403 Forbidden
request ৩ADMIN-এর token → 200
SecurityContext ← admin [ROLE_ADMIN]
AuthorizationFilter: hasRole('ADMIN') → হ্যাঁ
  → DispatcherServlet → ProductController.changePrice(..)
200 OK  {"id":7,"price":440}
শেষেপরিষ্কার
উত্তর চলে গেল
SecurityContextHolder.clearContext()
thread pool-এ ফিরে যাওয়া thread-এ
আগের মানুষের পরিচয় থেকে যায় না
  1. শিকল বাছাই। DelegatingFilterProxy request-টি FilterChainProxy-তে পাঠায়। পথ /api/**-এর সাথে মেলে, তাই API-র SecurityFilterChain চলবে।
  2. পরিচয় খোঁজা। token নেই। এটা নিজে ভুল নয় — খোলা endpoint-এ token ছাড়াই আসার কথা। তাই anonymous filter একটি “অচেনা ব্যবহারকারী” বসায়।
  3. পরিচয় নেই → 401. নিয়ম বলে ADMIN লাগবে। ExceptionTranslationFilter দেখে ব্যবহারকারী anonymous, তাই 401 — “আগে পরিচয় দিন”। controller-এর একটি লাইনও চলেনি।
  4. দ্বিতীয় request. রহিম token পাঠালেন। filter স্বাক্ষর আর মেয়াদ যাচাই করে একটি Authentication বানিয়ে SecurityContextHolder-এ রাখে।
  5. পরিচয় আছে, অনুমতি নেই → 403. রহিম গ্রাহক, ADMIN নন। ব্যবহারকারী চেনা, তাই 403; আবার লগইন করলেও ফল বদলাবে না।
  6. সব ঠিক → 200. ADMIN-এর token-এ ROLE_ADMIN আছে; request DispatcherServlet হয়ে controller-এ পৌঁছায়।
  7. পরিষ্কার করা। উত্তরের পর SecurityContext মুছে ফেলা হয়, কারণ Tomcat একই thread পরের request-এ ব্যবহার করে।

ডেভেলপমেন্টের সময় কোন filter কোন ক্রমে চলছে দেখতে চাইলে logging.level.org.springframework.security=TRACE দিন — অ্যাপ চালুর সময় পুরো তালিকা আর প্রতিটি request-এর সিদ্ধান্ত log-এ আসবে। আসল সার্ভারে এই স্তর বন্ধ রাখুন।

৩ · starter যোগ করলেই সব বন্ধ — তারপর SecurityFilterChain দিয়ে খোলা

পর্ব ৫০-এর starter মনে আছে? pom.xml-এ একটি লাইন যোগ করুন:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

আর কিছু না লিখেই অ্যাপ চালান। Spring Boot-এর auto-configuration classpath-এ Spring Security দেখে কয়েকটি কাজ নিজে করে ফেলে:

  • সব endpoint বন্ধ: পণ্যের তালিকাও এখন লগইন ছাড়া দেখা যায় না।
  • একজন ব্যবহারকারী তৈরি: নাম user, আর প্রতিবার চালুর সময় একটি নতুন random পাসওয়ার্ড, যা log-এ ছাপা হয়।
  • লগইনের দুটি পথ চালু: ব্রাউজারের জন্য form login পাতা, আর অন্য client-এর জন্য HTTP Basic.
  • সুরক্ষা চালু: CSRF সুরক্ষা, কয়েকটি security header, আর লগইনের পর session id বদলানো।
Using generated security password: 3f1c...-....-....-............

This generated password is for development use only. Your security
configuration must be updated before running your application in production.

হুবহু লেখাটি সংস্করণভেদে একটু আলাদা হতে পারে, কিন্তু অর্থ একই: এটা শুধু ডেভেলপমেন্টের জন্য। এবার curl দিয়ে পণ্যের তালিকা চাইলে:

$ curl -i http://localhost:8080/api/v1/products
HTTP/1.1 401
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Cache-Control: no-cache, no-store, max-age=0, must-revalidate

এই ডিফল্টটি ইচ্ছা করেই কড়া: ভুলে বন্ধ থাকলে কেউ অভিযোগ করবেন, ভুলে খোলা থাকলে হয়তো কেউ টেরই পাবেন না। এবার নিজের নিয়ম লিখুন। Spring Security 6 (Spring Boot 3.x)-এ এর উপায় একটি SecurityFilterChain bean, lambda দিয়ে লেখা:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()      // দেখবেন সবাই
                .requestMatchers(HttpMethod.PUT, "/api/v1/products/*/price").hasRole("ADMIN")
                .requestMatchers("/api/v1/orders/**").hasAnyRole("CUSTOMER", "ADMIN")
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated())                                           // বাকি সব: পরিচয় লাগবে
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}
নিয়মঅর্থ
permitAll()যে কেউ, পরিচয় ছাড়াই
authenticated()পরিচয় যাচাই হলেই চলবে, role যা-ই হোক
hasRole("ADMIN")authority ROLE_ADMIN থাকতে হবে — ROLE_ অংশটি Spring নিজে যোগ করে
hasAuthority("ROLE_ADMIN")ঠিক এই নামের authority; token-এর scope-এর মতো SCOPE_orders:read-এর জন্যও
denyAll()কেউ নয় — পুরোনো endpoint সাময়িক বন্ধ রাখতে

দুটি জিনিস খেয়াল করুন। প্রথমত, নিয়মগুলো ওপর থেকে নিচে মেলানো হয়, আর প্রথম যেটি মেলে সেটিই প্রযোজ্য। তাই নির্দিষ্ট নিয়ম আগে, সাধারণ নিয়ম পরে, আর anyRequest() সবার শেষে। দ্বিতীয়ত, প্রথম নিয়মে HttpMethod.GET আছে — শুধু দেখা খোলা। এটা বাদ দিলে /api/v1/products/**-এর PUT-ও খোলা হয়ে যেত, আর পরের ADMIN-এর নিয়মটি কখনো পৌঁছাতই না (অনুশীলন ১)।

পুরোনো কোড চিনে রাখুন। ইন্টারনেটের অনেক টিউটোরিয়াল আর AI-এর উত্তরে এখনো এমন কোড দেখবেন: class SecurityConfig extends WebSecurityConfigurerAdapter, ভেতরে configure(HttpSecurity http), authorizeRequests(), antMatchers(...) আর .and() দিয়ে জোড়া। WebSecurityConfigurerAdapter Spring Security 5.7-এ deprecated হয়, আর 6.0-তে পুরোপুরি সরিয়ে দেওয়া হয়েছে — Spring Boot 3-এ এটা compile-ই হবে না। antMatchers-এর জায়গায় এখন requestMatchers, authorizeRequests-এর জায়গায় authorizeHttpRequests.

৪ · পাসওয়ার্ড রাখা — hash, salt আর ধীর হওয়ার গুণ

লগইনের জন্য সার্ভারকে পাসওয়ার্ড মিলিয়ে দেখতে হয়। কিন্তু পাসওয়ার্ড নিজে রাখা চলবে না। কারণ ধরে নিতে হবে, একদিন ডেটাবেসের কপি কারও হাতে যেতে পারে — ভুলে খোলা ব্যাকআপ, ফাঁস হওয়া পাসওয়ার্ড, ভেতরের কেউ। তখন প্রশ্ন একটাই: সেই কপি থেকে আসল পাসওয়ার্ড বের করা কত কঠিন? আর মানুষ একই পাসওয়ার্ড ইমেইল, ফেসবুক আর মোবাইল ব্যাংকিং-এ ব্যবহার করেন, তাই ক্ষতি আপনার দোকানে থেমে থাকে না।

উত্তর হলো cryptographic hash — এক দিকের রূপান্তর। পাসওয়ার্ড থেকে hash বানানো সহজ, hash থেকে পাসওয়ার্ড ফেরত বের করা কার্যত অসম্ভব। লগইনের সময় দেওয়া পাসওয়ার্ডের hash বানিয়ে জমানো hash-এর সাথে মেলানো হয়। কিন্তু সব hash এই কাজের উপযুক্ত নয়। ধাপে ধাপে দেখুন:

স্তর ০সরাসরি লেখা — খুব খারাপ
phone         password
01712345678   Dhaka@2026
01811111111   123456
ফাঁস হলে সঙ্গে সঙ্গে সবার পাসওয়ার্ড প্রকাশ্যে
স্তর ১দ্রুত hash (MD5, SHA-256) — এখনো খারাপ
01712345678   sha256 → 5e2b…a9
01811111111   sha256 → 8d96…92
01999999999   sha256 → 8d96…92   ← একই পাসওয়ার্ড, একই hash
আগে থেকে হিসাব করা তালিকায় "123456"-এর hash খুঁজে পাওয়া যায়
স্তর ২salt-সহ দ্রুত hash — অর্ধেক সমাধান
01811111111   salt=Q7x… hash=sha256(salt+pw) → 3a01…
01999999999   salt=Lm2… hash=sha256(salt+pw) → f4c8…
একই পাসওয়ার্ড, আলাদা hash; আগের তালিকা অকেজো
কিন্তু প্রতিটি অনুমান এখনো খুব সস্তা
স্তর ৩salt + ধীর hash — ঠিক
{bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMye…
         │   │  └─ salt (২২ অক্ষর) আর hash
         │   └──── cost = 10 → 2^10 রাউন্ড
         └──────── অ্যালগরিদম
একটি অনুমানে এখন মিলিসেকেন্ডের বদলে ধরুন ~১০০ ms
লগইনযাচাই
দেওয়া পাসওয়ার্ড: "Dhaka@2026"
জমানো লেখা থেকে পড়ো: অ্যালগরিদম, cost, salt
একই salt আর cost দিয়ে আবার hash
নতুন hash == জমানো hash ?  সমান-সময়ের তুলনা
→ মিলেছে → লগইন সফল
বছর পরেcost বাড়ানো
কম্পিউটার দ্রুত হয়েছে → নতুন cost = 12
পুরোনো hash: $2a$10$…   নতুন hash: $2a$12$…
সফল লগইনের মুহূর্তে আসল পাসওয়ার্ড হাতে থাকে
→ নতুন cost-এ আবার hash করে জমাও
  1. সরাসরি লেখা। ডেটাবেসের কপি যার হাতে, সব পাসওয়ার্ড তার হাতে। কোনো অবস্থাতেই চলবে না।
  2. দ্রুত hash. MD5 বা SHA-256-এ একই পাসওয়ার্ডের hash সবসময় একই, তাই আগে থেকে হিসাব করে রাখা তালিকায় (rainbow table) খুঁজলেই মেলে। আর এগুলো দ্রুত হওয়ার জন্যই বানানো — একটি GPU প্রতি সেকেন্ডে শত কোটির বেশি SHA-256 হিসাব করতে পারে।
  3. salt. প্রতিটি ব্যবহারকারীর আলাদা random byte — salt — মিশিয়ে hash করা হয়। salt গোপন নয়, hash-এর পাশেই থাকে। আগে থেকে বানানো তালিকা অকেজো, কিন্তু প্রতিটি অনুমান এখনো খুব সস্তা।
  4. ইচ্ছা করে ধীর। পাসওয়ার্ডের জন্য বানানো key derivation function — bcrypt, Argon2, PBKDF2 — একটি cost factor নেয়। আপনার সার্ভারের কাছে একটি লগইনে ১০০ ms কিছুই না, কিন্তু কোটি অনুমানের খরচ বিপুল বেড়ে যায়। Argon2 মেমরিও বেশি চায়।
  5. লগইনে যাচাই। জমানো লেখা থেকে অ্যালগরিদম, cost আর salt নিয়ে দেওয়া পাসওয়ার্ডের hash আবার বানিয়ে তুলনা। তুলনাটি সমান-সময়ের, নইলে সময় মেপে তথ্য আন্দাজ করা যায় (timing attack).
  6. cost বাড়ানো। হার্ডওয়্যার দ্রুত হলে cost বাড়ান। সফল লগইনের মুহূর্তে আসল পাসওয়ার্ড হাতে থাকে — তখনই নতুন cost-এ আবার hash করে রাখা যায়; Spring-এর DelegatingPasswordEncoder এটা সহজ করে।

এবার JDK দিয়েই একটি ধীর hash চালিয়ে দেখুন। Spring লাগবে না: JDK-তে PBKDF2 আছে (SecretKeyFactory), আর সমান-সময়ের তুলনার জন্য MessageDigest.isEqual. hash নিজে ছাপানো হয়নি, শুধু দৈর্ঘ্য আর মিলল কি না:

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.MessageDigest;
import static java.nio.charset.StandardCharsets.UTF_8;

class Main {
    static final int ITERATIONS = 600_000;                       // cost factor: প্রতিটি অনুমানের খরচ
    static byte[] hash(String password, byte[] salt) throws Exception {
        var spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, 256);
        try { return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded(); }
        finally { spec.clearPassword(); }
    }
    static boolean verify(String attempt, byte[] salt, byte[] stored) throws Exception {
        return MessageDigest.isEqual(hash(attempt, salt), stored);    // constant-time তুলনা
    }
    public static void main(String[] args) throws Exception {
        byte[] salt = "demo-salt-16byte".getBytes(UTF_8);              // শুধু ডেমো! আসলে SecureRandom
        byte[] stored = hash("Dhaka@2026", salt);                      // সাইন-আপের সময় একবার
        System.out.println("stored: salt " + salt.length + " bytes, hash " + stored.length + " bytes");
        System.out.println("login 'Dhaka@2026' -> " + verify("Dhaka@2026", salt, stored));
        System.out.println("login 'dhaka@2026' -> " + verify("dhaka@2026", salt, stored));
        byte[] salt2 = "other-salt-16byt".getBytes(UTF_8);
        System.out.println("same password, other salt, same hash? " + MessageDigest.isEqual(stored, hash("Dhaka@2026", salt2)));
    }
}

আউটপুট চার লাইন: stored: salt 16 bytes, hash 32 bytes, তারপর সঠিক পাসওয়ার্ডে true, শুধু প্রথম অক্ষর ছোট হাতের হলে false, আর শেষ লাইনে false — একই পাসওয়ার্ড, আলাদা salt, তাই আলাদা hash. প্রোগ্রামটি চলতে এক-দুই সেকেন্ড লাগতে পারে; সেটাই উদ্দেশ্য। প্রতিটি hash কলে HMAC-SHA256 ছয় লক্ষবার চলে — OWASP-এর বর্তমান নির্দেশিকায় PBKDF2-HMAC-SHA256-এর জন্য এই সংখ্যাটিই সুপারিশ করা। salt এখানে স্থির, যাতে উদাহরণ প্রতিবার একই ফল দেয়; আসল কোডে প্রতিটি ব্যবহারকারীর জন্য SecureRandom (CSPRNG) দিয়ে নতুন ১৬ byte বানিয়ে hash-এর পাশে রাখবেন। Arrays.equals প্রথম অমিলেই থামে; MessageDigest.isEqual পুরোটা দেখে, তাই সময় অমিলের অবস্থান ফাঁস করে না। আর clearPassword() কাজ শেষে মেমরির পাসওয়ার্ড মুছে দেয় — এজন্যই Java-র নিরাপত্তা API পাসওয়ার্ড নেয় char[]-এ, অপরিবর্তনীয় String-এ নয়।

Spring-এ — PasswordEncoder আর UserDetailsService

Spring Security-তে এই কাজের interface PasswordEncoder: encode(raw) সাইন-আপের সময় hash বানায়, matches(raw, stored) লগইনে মেলায়। সবচেয়ে প্রচলিত বাছাই PasswordEncoderFactories.createDelegatingPasswordEncoder(). এটি নতুন পাসওয়ার্ড bcrypt দিয়ে hash করে আর সামনে লিখে দেয় {bcrypt}. পড়ার সময় উপসর্গ দেখে ঠিক encoder বাছে ({argon2}, {pbkdf2}…), তাই অ্যালগরিদম বদলালেও পুরোনো ব্যবহারকারীরা লগইন করতে পারেন।

ব্যবহারকারীর তথ্য কোথা থেকে আসবে, Spring জানতে চায় একটি UserDetailsService-এর কাছে। দোকানের গ্রাহকরা লগইন করেন ফোন নম্বর দিয়ে:

@Bean
PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();   // নতুন hash: {bcrypt}$2a$10$...
}

@Service
public class ShopUserDetailsService implements UserDetailsService {
    private final CustomerRepository customers;
    public ShopUserDetailsService(CustomerRepository customers) { this.customers = customers; }

    @Override
    public UserDetails loadUserByUsername(String phone) {
        Customer c = customers.findByPhone(phone)
                .orElseThrow(() -> new UsernameNotFoundException("unknown user"));
        return User.withUsername(c.getPhone())
                   .password(c.getPasswordHash())        // ডেটাবেসে শুধু hash
                   .roles(c.getRole().name())            // CUSTOMER → ROLE_CUSTOMER
                   .build();
    }
}

// সাইন-আপের সময় (service-এ):
customer.setPasswordHash(passwordEncoder.encode(request.password()));

Spring এই দুটি bean দেখে নিজেই লগইনের পুরো যাচাই সাজিয়ে নেয়। ফোন নম্বর না পাওয়া গেলে আর পাসওয়ার্ড ভুল হলে Spring ডিফল্টে একই ব্যর্থতা দেখায় — “ফোন নম্বর বা পাসওয়ার্ড ভুল”। আলাদা বার্তা দিলে যে কেউ জেনে যাবে কোন নম্বরগুলো নিবন্ধিত। BCryptPasswordEncoder-এর ডিফল্ট cost ১০; প্রতি ১ বাড়ালে সময় দ্বিগুণ — নিজের সার্ভারে মেপে বেছে নিন। আর মনে রাখুন, bcrypt পাসওয়ার্ডের প্রথম ৭২ byte-ই ব্যবহার করে।

লগইনে বারবার চেষ্টা

ধীর hash ফাঁস হওয়া ডেটাবেস রক্ষা করে, কিন্তু লগইনের দরজায় কেউ একের পর এক পাসওয়ার্ড চেষ্টা করলে (brute-force attack), বা অন্য সাইট থেকে ফাঁস হওয়া ফোন-পাসওয়ার্ডের জোড়া চেষ্টা করলে (credential stuffing), সেটা আলাদা সমস্যা। এর প্রতিরক্ষা চেষ্টার সীমা (অংশ ৮) আর দ্বিতীয় একটি প্রমাণ — OTP বা authenticator অ্যাপের কোড, যাকে বলে MFA.

৫ · লগইনের পরে — session, token আর JWT

HTTP stateless: প্রতিটি request নতুন, সার্ভার আগেরটি মনে রাখে না। তাহলে একবার লগইন করার পর পরের request-এ “আমি রহিম” কীভাবে প্রমাণ হবে? প্রতিবার পাসওয়ার্ড পাঠানো ঝুঁকিপূর্ণ আর ধীর (প্রতিবার ধীর hash!)। চারটি প্রচলিত উপায়:

উপায়কীভাবেকোথায় মানানসইসাবধানতা
HTTP Basicপ্রতিটি request-এ Authorization: Basic + Base64-এ লেখা নাম:পাসওয়ার্ডভেতরের টুল, দ্রুত পরীক্ষাBase64 এনক্রিপশন নয়; HTTPS ছাড়া পাসওয়ার্ড খোলা
Form login + sessionএকবার লগইন; সার্ভার session বানিয়ে তার id একটি cookie-তে দেয়, ব্রাউজার প্রতিবার নিজে পাঠায়সার্ভারে HTML বানানো ওয়েবসাইটCSRF সুরক্ষা লাগবে; cookie HttpOnly, Secure
opaque tokenএকটি random লেখা; সার্ভার ডেটাবেসে দেখে কার tokenযেখানে তাৎক্ষণিক বাতিল জরুরিপ্রতিটি request-এ খোঁজার খরচ
JWTস্বাক্ষর করা token; ভেতরেই লেখা কে, কোন role, কখন মেয়াদ শেষমোবাইল অ্যাপের API, অনেক serviceমেয়াদের আগে বাতিল করা কঠিন

session-এ লগইনের মুহূর্তে Spring session id বদলে দেয়, যাতে লগইনের আগে কারও জানা id লগইনের পরে কাজে না লাগে (session fixation সুরক্ষা)। মোবাইল অ্যাপের API-তে সাধারণত token ব্যবহার হয়, আর token পাঠানো হয় একটি header-এ: Authorization: Bearer <token>. যার কাছে এই token, সার্ভার তাকেই মালিক ধরে — তাই নাম bearer token, বাসের টিকিটের মতো।

JWT-এর ভেতরে কী থাকে

JWT (JSON Web Token, RFC 7519) তিনটি অংশ, মাঝে বিন্দু: header.payload.signature. header বলে কোন অ্যালগরিদমে স্বাক্ষর ({"alg":"HS256"})। payload-এ কয়েকটি claim: sub (কে), exp (কখন মেয়াদ শেষ, ১৯৭০ থেকে সেকেন্ডে), iat (কখন দেওয়া), iss (কে দিয়েছে), আর নিজের claim যেমন role. প্রথম দুটি অংশ শুধু JSON, URL-এ নিরাপদ Base64URL-এ লেখা। তৃতীয় অংশ HMAC বা digital signature — প্রথম দুটি অংশের ওপর গোপন key দিয়ে বানানো স্বাক্ষর।

JWT গোপন নয়, শুধু স্বাক্ষর করা। Base64URL এনক্রিপশন নয় — যার হাতে token, সে-ই payload পড়তে পারে। তাই এতে পাসওয়ার্ড, NID নম্বর, ঠিকানা বা অন্য গোপন তথ্য রাখবেন না। স্বাক্ষর শুধু নিশ্চিত করে, লেখাটি কেউ বদলায়নি।

নিচের প্রোগ্রামে JDK-র Mac (HmacSHA256) আর Base64 দিয়ে একটি JWT-এর মতো token বানানো আর যাচাই করা হয়েছে, সাথে অংশ ২-এর 401/403/200 সিদ্ধান্ত। সময় এখানে একটি সাধারণ সংখ্যা (now), যাতে ফল প্রতিবার একই হয়:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;

class Main {
    static final byte[] KEY = "demo-only-key-read-from-env-in-real-apps".getBytes(UTF_8);
    static String b64(byte[] b) { return Base64.getUrlEncoder().withoutPadding().encodeToString(b); }
    static String sign(String data) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(KEY, "HmacSHA256"));
        return b64(mac.doFinal(data.getBytes(UTF_8)));
    }
    static String issue(String claims) throws Exception {               // লগইন সফল হলে সার্ভার দেয়
        String hp = b64("{\"alg\":\"HS256\"}".getBytes(UTF_8)) + "." + b64(claims.getBytes(UTF_8));
        return hp + "." + sign(hp);
    }
    static int check(String token, String needRole, long now) throws Exception {
        if (token == null) return 401;                                  // পরিচয় নেই
        String[] p = token.split("\\.");
        if (p.length != 3 || !MessageDigest.isEqual(sign(p[0] + "." + p[1]).getBytes(UTF_8), p[2].getBytes(UTF_8))) return 401;
        String claims = new String(Base64.getUrlDecoder().decode(p[1]), UTF_8);
        if (now >= Long.parseLong(claims.replaceAll(".*\"exp\":(\\d+).*", "$1"))) return 401;   // মেয়াদ শেষ
        return claims.contains("\"role\":\"" + needRole + "\"") ? 200 : 403;                   // অনুমতি
    }
    public static void main(String[] args) throws Exception {
        String rahim = issue("{\"sub\":\"rahim\",\"role\":\"CUSTOMER\",\"exp\":2000}");
        String admin = issue("{\"sub\":\"admin\",\"role\":\"ADMIN\",\"exp\":2000}");
        String[] r = rahim.split("\\.");
        String forged = r[0] + "." + b64("{\"sub\":\"rahim\",\"role\":\"ADMIN\",\"exp\":2000}".getBytes(UTF_8)) + "." + r[2];
        System.out.println("payload: " + new String(Base64.getUrlDecoder().decode(r[1]), UTF_8));
        System.out.println("no token  PUT /prices -> " + check(null, "ADMIN", 1000));
        System.out.println("rahim     GET /orders -> " + check(rahim, "CUSTOMER", 1000));
        System.out.println("rahim     PUT /prices -> " + check(rahim, "ADMIN", 1000));
        System.out.println("forged    PUT /prices -> " + check(forged, "ADMIN", 1000));
        System.out.println("admin     PUT /prices -> " + check(admin, "ADMIN", 1000));
        System.out.println("admin at t=2500       -> " + check(admin, "ADMIN", 2500));
    }
}

প্রথম লাইন payload: {"sub":"rahim","role":"CUSTOMER","exp":2000} — key ছাড়াই পড়া গেল, ঠিক যেমন সতর্কবার্তায় বলা হয়েছে। তারপর ক্রমে 401 (token নেই), 200 (রহিম নিজের অর্ডার দেখছেন), 403 (রহিম দাম বদলাতে চান), 401 (জাল token), 200 (ADMIN), আর 401 (সময় ২৫০০, মেয়াদ ২০০০-এ শেষ)। জাল token-এ role বদলে ADMIN করা হয়েছে, কিন্তু স্বাক্ষর পুরোনো; সার্ভার নিজের key দিয়ে হিসাব করে, মেলে না, তাই প্রত্যাখ্যান। key ছাড়া সঠিক স্বাক্ষর বানানো সম্ভব নয় — তাই key-ই সব। ক্রমও গুরুত্বপূর্ণ: আগে স্বাক্ষর, তারপর মেয়াদ, শেষে role; যাচাই না করা payload-এর কোনো কথা বিশ্বাস করা চলবে না। regex দিয়ে claim পড়া শুধু উদাহরণ ছোট রাখার জন্য; আসল কোডে প্রতিষ্ঠিত JWT লাইব্রেরি (Spring-এ Nimbus) ব্যবহার করবেন।

এবার পুরো জীবনচক্রটি ধাপে ধাপে দেখুন — লগইন থেকে মেয়াদ শেষ পর্যন্ত:

ধাপ ১লগইন
POST /api/v1/auth/login     (শুধু HTTPS-এ)
{"phone":"01712345678","password":"••••••••"}
→ UserDetailsService + PasswordEncoder.matches → ✓
ধাপ ২সার্ভার স্বাক্ষর করে
payload = {"sub":"01712345678","roles":["CUSTOMER"],
           "iat":…, "exp": iat + 15 মিনিট}
signature = HMAC-SHA256(header.payload, গোপন key)
200 {"accessToken":"eyJ…","refreshToken":"…"}
ধাপ ৩client রাখে আর পাঠায়
অ্যাপ: নিরাপদ স্টোরেজে রাখে (Android Keystore / iOS Keychain)
GET /api/v1/orders/mine
Authorization: Bearer eyJ…
ধাপ ৪সার্ভার যাচাই করে
অ্যালগরিদম প্রত্যাশিত? ✓   স্বাক্ষর ✓
exp > এখন? ✓   iss ঠিক? ✓
→ Authentication(01712345678, [ROLE_CUSTOMER])
→ 200 — ডেটাবেসে না গিয়েই পরিচয় জানা গেল
ধাপ ৫বদলানো token
payload-এ "CUSTOMER" → "ADMIN"
স্বাক্ষর আবার হিসাব → মেলে না
→ 401  WWW-Authenticate: Bearer error="invalid_token"
ধাপ ৬মেয়াদ শেষ → নতুন token
১৫ মিনিট পর: exp পার → 401
অ্যাপ: POST /api/v1/auth/refresh {refreshToken}
সার্ভার: ডেটাবেসে refresh token বৈধ? বাতিল হয়নি?
→ নতুন access token (আর নতুন refresh token)
  1. লগইন। অ্যাপ ফোন নম্বর আর পাসওয়ার্ড পাঠায়, অবশ্যই HTTPS-এ। পুরো জীবনচক্রে পাসওয়ার্ড শুধু এই একবারই যায়।
  2. স্বাক্ষর। সার্ভার payload-এ পরিচয়, role আর ছোট মেয়াদ লিখে গোপন key দিয়ে স্বাক্ষর করে। সাথে দেয় একটি দীর্ঘ মেয়াদের refresh token, যার হিসাব সার্ভারের ডেটাবেসে থাকে।
  3. রাখা আর পাঠানো। মোবাইল অ্যাপ token রাখে নিরাপদ স্টোরেজে। ব্রাউজারে মেমরিতে বা HttpOnly cookie-তে — localStorage-এ রাখলে পাতায় ঢোকানো ক্ষতিকর script (XSS) সেটা পড়ে ফেলতে পারে।
  4. যাচাই। আগে অ্যালগরিদম প্রত্যাশিতটিই কি না — token-এর header যা বলে তা অন্ধভাবে মানা যাবে না; তারপর স্বাক্ষর, মেয়াদ, কে দিয়েছে। ডেটাবেসে না গিয়েই পরিচয় জানা গেল — অনেক সার্ভার, কোনো শেয়ার করা session নেই।
  5. বদলানো token. payload বদলালে স্বাক্ষর মেলে না, ফল 401 — চালানো উদাহরণের forged লাইনের মতো।
  6. মেয়াদ শেষ। ছোট মেয়াদ চুরির ক্ষতি সীমিত রাখে। মেয়াদ শেষে অ্যাপ refresh token দিয়ে নতুন token নেয়; refresh token ডেটাবেসে আছে বলে লগআউট বা ফোন হারালে বাতিল করা যায়।

Spring-এ JWT — OAuth2 Resource Server

নিজে JWT যাচাইয়ের filter লিখবেন না। Spring Security-র OAuth2 Resource Server সমর্থন এই কাজ করে: spring-boot-starter-oauth2-resource-server যোগ করুন, token কে দেয় তা properties-এ বলুন, আর filter chain-এ এক লাইন। মোবাইল API-র জন্য পুরো কনফিগারেশন:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ApiSecurityConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers(HttpMethod.PUT, "/api/v1/products/*/price").hasRole("ADMIN")
                .anyRequest().authenticated())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable())                 // কারণ: cookie নয়, Authorization header (অংশ ৭)
            .cors(Customizer.withDefaults())
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
        return http.build();
    }
}
# token দেয় একটি আলাদা authorization server (যেমন Keycloak) — তার public key নিজে খুঁজে নেবে
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.com/realms/shop

ডিফল্টে Spring token-এর scope claim থেকে SCOPE_ উপসর্গওয়ালা authority বানায়। নিজের roles claim থেকে ROLE_ADMIN বানাতে একটি JwtAuthenticationConverter bean লাগে। আর নিজের অ্যাপই যদি token দেয়, HMAC key দিয়ে NimbusJwtDecoder.withSecretKey(key).build() একটি JwtDecoder bean বানায় — key আসবে environment variable থেকে (অংশ ৮)। অনেক service থাকলে অসমমিতিক স্বাক্ষর (RS256, ES256) ভালো: শুধু token দেওয়ার সার্ভারের কাছে private key, বাকিরা public key দিয়ে শুধু যাচাই করে, জাল করতে পারে না।

OAuth2 আর OpenID Connect — “Google দিয়ে লগইন”

অনেক অ্যাপে “Google দিয়ে চালিয়ে যান” বোতাম থাকে। এর পেছনে দুটি মান। OAuth 2.0 (RFC 6749, ২০১২) মূলত অনুমতি দেওয়ার কাঠামো: একটি অ্যাপ আপনার পাসওয়ার্ড না জেনেই আপনার পক্ষে সীমিত কাজের অনুমতি পায়। OpenID Connect (২০১৪) তার ওপর পরিচয়ের স্তর যোগ করে — একটি ID token, যা বলে “ইনি Google-এর এই ব্যবহারকারী”। সংক্ষেপে: আপনার অ্যাপ ব্যবহারকারীকে Google-এর লগইন পাতায় পাঠায়, সেখানে লগইনের পর Google একটি ক্ষণস্থায়ী code দেয়, আর আপনার সার্ভার সেই code দিয়ে সরাসরি Google থেকে token নেয় — মোবাইল আর ব্রাউজার অ্যাপে সাথে PKCE. আপনার অ্যাপ Google-এর পাসওয়ার্ড কখনো দেখে না। Spring-এ spring-boot-starter-oauth2-client আর http.oauth2Login(...) দিয়ে এটা কয়েক লাইনের কাজ।

৬ · method security আর ownership — “গ্রাহক শুধু নিজের অর্ডার দেখবেন”

URL-এর নিয়ম মোটা দাগের পাহারা: কোন পথে কোন role. কিন্তু নিয়ম অনেক সময় ব্যবসার ভেতরে থাকে, আর একই service method অনেক controller থেকে ডাকা হতে পারে। তাই দ্বিতীয় স্তর — method security. configuration class-এ @EnableMethodSecurity দিলে (Spring Security 5.6 থেকে; পুরোনো কোডে @EnableGlobalMethodSecurity) method-এর ওপর নিয়ম লেখা যায়:

@Service
public class ProductService {

    @PreAuthorize("hasRole('ADMIN')")                         // method চলার আগে পরীক্ষা
    @Transactional
    public ProductResponse changePrice(long id, BigDecimal newPrice) { ... }
}

এখন কেউ ভুল করে আরেকটি controller থেকে changePrice ডাকলেও, বা URL-এর নিয়মে ফাঁক থাকলেও, ADMIN ছাড়া কেউ দাম বদলাতে পারবেন না। Spring এটা করে AOP proxy দিয়ে — পর্ব ৫১-এর @Transactional-এর মতোই, তাই একই ফাঁদ: একই class-এর ভেতর থেকে ডাকলে পরীক্ষা হয় না।

role যথেষ্ট নয় — ownership check

রহিম আর করিম দুজনেই CUSTOMER. রহিমের অর্ডারের পাতা /api/v1/orders/1001. তিনি কৌতূহলে URL-এ লিখলেন 1002. URL-এর নিয়ম বলে “CUSTOMER যেতে পারবেন” — পাস। অর্ডারটি যদি ডেটাবেস থেকে এনে ফেরত দেওয়া হয়, রহিম করিমের নাম, ঠিকানা আর ফোন নম্বর দেখে ফেললেন। এই ভুলের নাম broken object level authorization (BOLA, পুরোনো নাম IDOR). OWASP-এর API-নিরাপত্তার আলাদা তালিকায় এটি প্রথমে। Spring Security এটা নিজে ধরতে পারে না, কারণ কোন অর্ডার কার, তা শুধু আপনার ডেটা জানে।

public interface OrderRepository extends JpaRepository<Order, Long> {
    Optional<Order> findByIdAndCustomerPhone(long id, String customerPhone);
}

@Service
public class OrderService {
    private final OrderRepository orders;
    public OrderService(OrderRepository orders) { this.orders = orders; }

    @Transactional(readOnly = true)
    public OrderResponse getMyOrder(long orderId, Authentication auth) {
        return orders.findByIdAndCustomerPhone(orderId, auth.getName())   // মালিকানা query-তেই
                .map(OrderResponse::from)
                .orElseThrow(() -> new OrderNotFoundException(orderId));  // অন্যের অর্ডার → 404
    }
}

// controller: Spring নিজেই Authentication parameter-এ বর্তমান ব্যবহারকারীকে দেয়
@GetMapping("/{id}")
public OrderResponse get(@PathVariable long id, Authentication auth) {
    return service.getMyOrder(id, auth);
}

তিনটি সিদ্ধান্ত লক্ষ করুন। মালিকানার শর্ত query-র ভেতরেই — “এই id আর এই মালিকের অর্ডার” — তাই ভুলে যাচাই বাদ পড়ার সুযোগ কম। পরিচয় আসে Authentication থেকে, URL বা JSON থেকে নয়। আর অন্যের অর্ডারে 403-এর বদলে 404, যাতে রহিম জানতেই না পারেন যে ১০০২ নম্বর অর্ডার আছে। সরল ক্ষেত্রে @PreAuthorize("#phone == authentication.name")-এর মতো expression-ও চলে।

bKash-ধরনের “টাকা পাঠান” endpoint এই নীতির সবচেয়ে স্পষ্ট উদাহরণ। POST /api/v1/payments-এর body-তে থাকে প্রাপকের নম্বর আর টাকার পরিমাণ। কিন্তু টাকা কার wallet থেকে কাটবে, তা কখনো body থেকে নেওয়া হয় না — নেওয়া হয় যাচাই করা পরিচয় থেকে (auth.getName())। body-তে কেউ "from" field পাঠালেও DTO-তে সেটা নেই, তাই উপেক্ষিত। সাথে আরও স্তর: পরিমাণের সীমা (@Positive, @Max), বড় অঙ্কে PIN দিয়ে আবার নিশ্চিত হওয়া, পর্ব ৫১-এর idempotency key, আর প্রতিটি লেনদেনের নিরীক্ষা-log.

৭ · CSRF, CORS আর security header — ব্রাউজারের তিনটি নিয়ম

এই তিনটিই ব্রাউজারের আচরণের সাথে জড়িত। মোবাইল অ্যাপ বা curl ব্রাউজার নয় — এটা মাথায় রাখলেই অনেক বিভ্রান্তি কাটে।

CSRF — ব্রাউজার যখন নিজে থেকে cookie পাঠায়

কোনো সাইটের cookie থাকলে ব্রাউজার সেই সাইটে যাওয়া প্রতিটি request-এ cookie-টি নিজে থেকে জুড়ে দেয় — request কোন পাতা থেকে শুরু হলো, তা না দেখেই। তাই দোকানে লগইন করা অবস্থায় অন্য কোনো ক্ষতিকর পাতা খুললে, সেই পাতা আপনার অজান্তে দোকানে request পাঠাতে পারে, আর তাতে আপনার session cookie যাবে। এই ঝুঁকির নাম CSRF (Cross-Site Request Forgery):

ধাপ ১লগইন করা অবস্থা
shop.example.com-এ লগইন
ব্রাউজারে cookie:  SESSION=a1b2…  (HttpOnly)
এই সাইটে যাওয়া প্রতিটি request-এ ব্রাউজার এটা নিজে জুড়ে দেয়
ধাপ ২অন্য একটি পাতা
অন্য ট্যাবে একটি অপরিচিত পাতা খোলা হলো
পাতাটি লুকিয়ে shop.example.com-এ একটি
অবস্থা-বদলানো request পাঠায়
ব্রাউজার: Cookie: SESSION=a1b2… জুড়ে দিল
ধাপ ৩CSRF token থাকলে
Spring: এই request-এ CSRF token কোথায়?
আসল পাতা প্রতিটি ফর্মে একটি গোপন token দেয়
অন্য সাইট সেই token পড়তে পারে না
→ token নেই বা ভুল → 403
ধাপ ৪token-ভিত্তিক API
মোবাইল অ্যাপ / SPA: Authorization: Bearer eyJ…
ব্রাউজার এই header নিজে থেকে কখনো জোড়ে না
অন্য পাতার request-এ পরিচয়ই থাকে না → 401
তাই এখানে CSRF token বাড়তি সুরক্ষা দেয় না
  1. cookie জমা আছে। session-ভিত্তিক লগইনে ব্রাউজারে একটি session cookie থাকে। সমস্যাটি এখানেই: পরিচয়ের প্রমাণ ব্রাউজার স্বয়ংক্রিয়ভাবে পাঠায়।
  2. অন্য পাতার request. অন্য সাইটের পাতা দোকানের উত্তর পড়তে পারে না, কিন্তু request পাঠাতে পারে — আর তাতে আপনার cookie যায়। সার্ভারের চোখে এটা আপনারই request.
  3. CSRF token. Spring Security ডিফল্টে প্রতিটি অবস্থা-বদলানো request-এ (POST, PUT, DELETE) একটি গোপন token চায়, যা শুধু আসল সাইটের পাতা জানে। অন্য সাইট সেটা পড়তে পারে না, তাই তার request 403 পায়। cookie-র SameSite বৈশিষ্ট্য দ্বিতীয় একটি স্তর যোগ করে।
  4. Authorization header. token যদি header-এ যায়, ব্রাউজার সেটা নিজে থেকে জোড়ে না — অ্যাপের কোডকে জেনে-বুঝে বসাতে হয়। অন্য পাতার request-এ token থাকেই না। তাই stateless token API-তে CSRF বন্ধ রাখা যুক্তিসংগত — কিন্তু শুধু এই কারণেই।
মূল ধারণা

CSRF বন্ধ করার প্রশ্নটি আসলে একটি প্রশ্ন: “পরিচয়ের প্রমাণ কি ব্রাউজার নিজে থেকে পাঠায়?” session cookie, বা cookie-তে রাখা JWT — হ্যাঁ, তাই CSRF সুরক্ষা চালু রাখুন। শুধু Authorization header — না, তাই বন্ধ করা চলে। “403 আসছে, CSRF বন্ধ করে দিই” — এই যুক্তিতে বন্ধ করা ভুল; আগে বুঝুন কেন 403.

CORS — কোন ওয়েবসাইট আপনার API ডাকতে পারবে

পর্ব ৫১-এ দেখেছেন, CORS ব্রাউজারের নিয়ম: shop.example.com-এর পাতা api.example.com-এর উত্তর পড়তে পারে শুধু তখনই, যখন API header দিয়ে অনুমতি দেয়। মনে রাখুন, CORS আপনার সার্ভারকে curl বা অন্য প্রোগ্রাম থেকে রক্ষা করে না — ওটা authentication-এর কাজ। Spring Security থাকলে CORS-এর কনফিগারেশন security chain-কেও জানাতে হয় (http.cors(...), ওপরের কনফিগারেশনে আছে), কারণ ব্রাউজারের preflight OPTIONS request-এ token থাকে না, আর security filter আগে চলে:

@Bean
CorsConfigurationSource corsConfigurationSource() {
    var cfg = new CorsConfiguration();
    cfg.setAllowedOrigins(List.of("https://shop.example.com"));     // নির্দিষ্ট origin, "*" নয়
    cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    cfg.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    var source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", cfg);
    return source;
}

cookie-সহ request-এর অনুমতি (setAllowCredentials(true)) দিলে origin-এ "*" দেওয়া যায় না — Spring নিজেই error দেয়, কারণ সেটা মানে পৃথিবীর যেকোনো সাইটকে আপনার ব্যবহারকারীর পরিচয় নিয়ে API ডাকতে দেওয়া।

security header — ব্রাউজারকে বাড়তি নির্দেশ

Spring Security ডিফল্টে প্রতিটি উত্তরে কয়েকটি security header বসায় (অংশ ৩-এর curl-এর উত্তরে দেখেছেন)। এগুলো বন্ধ করবেন না:

Headerকী বলে
Strict-Transport-SecurityHSTS: “এই সাইটে এরপর সবসময় HTTPS-এ এসো” — Spring শুধু HTTPS-এর উত্তরেই এটা দেয়
X-Content-Type-Options: nosniffContent-Type যা বলা আছে তা-ই মানো; ফাইলের ভেতর দেখে নিজে অনুমান কোরো না
X-Frame-Options: DENYঅন্য সাইট এই পাতা নিজের পাতার ভেতরে (frame) দেখাতে পারবে না
Cache-Control: no-store …লগইন করা মানুষের উত্তর শেয়ার করা কম্পিউটারের cache-এ রেখো না

HTML পাতা দেখানো সাইটে একটি Content-Security-Policy header যোগ করা ভালো — কোন উৎসের script চলতে পারবে, তার তালিকা। এটা সাইটভেদে আলাদা, তাই ডিফল্টে নেই; http.headers(h -> h.contentSecurityPolicy(...)) দিয়ে নিজে দিতে হয়।

৮ · কোডের বাইরের নিরাপত্তা — secret, HTTPS, log, চেষ্টার সীমা, dependency আর টেস্ট

secret কোডে নয়

JWT-এর স্বাক্ষরের key, ডেটাবেসের পাসওয়ার্ড, Google-এর client secret — এগুলো git-এ গেলে, এমনকি পরে মুছে ফেললেও, history-তে থেকে যায়। পর্ব ৫০-এ দেখেছেন, properties-এ শুধু নাম রাখুন, মান আসবে environment variable বা ক্লাউডের secret manager থেকে:

app.jwt.secret=${JWT_SECRET}                  # সার্ভারে environment variable, কোডে কখনো নয়
spring.datasource.password=${DB_PASSWORD}

HS256-এর key অন্তত ২৫৬ bit (৩২ byte) random হতে হবে — “secret123” নয়। key ফাঁস হলে মুছে দেওয়া যথেষ্ট নয়; সঙ্গে সঙ্গে নতুন key বসান (key rotation) — এতে পুরোনো সব token অবৈধ হয়ে যায়, সেটাই চাই। ডেটাবেসের ব্যবহারকারীকেও least privilege দিন: অ্যাপের ব্যবহারকারী টেবিলে পড়া-লেখা করবে, টেবিল মুছতে পারবে না।

সব জায়গায় HTTPS

HTTPS ছাড়া token, পাসওয়ার্ড আর cookie পথে খোলা লেখায় যায় — একই ক্যাফের Wi-Fi-তে থাকা কেউ পড়ে ফেলতে পারে। আসল সার্ভারে HTTP-র request HTTPS-এ ফেরত পাঠান, HSTS চালু রাখুন, cookie-তে Secure দিন। HTTPS load balancer-এ শেষ হলে Spring-কে server.forward-headers-strategy দিয়ে সেটা জানাতে হয়।

নিরাপত্তার ঘটনা log করুন — secret ছাড়া

কেউ রাত তিনটায় একটি অ্যাকাউন্টে পাঁচশোবার ভুল পাসওয়ার্ড দিলে আপনি জানতে চাইবেন। তাই log করুন: সফল ও ব্যর্থ লগইন, 403, পাসওয়ার্ড বদল, ADMIN-এর প্রতিটি দাম বদল — কে, কখন, কোন IP থেকে। Spring লগইনের event প্রকাশ করে, @EventListener দিয়ে শোনা যায়। কিন্তু log-এ কখনো নয়: পাসওয়ার্ড, token, OTP, PIN, পুরো কার্ড নম্বর; ফোন নম্বর আংশিক ঢেকে দিন। log অনেক মানুষ আর টুলের হাতে যায়, সেখানে secret গেলে সেটা আর secret থাকে না — এটাও এক ধরনের information disclosure. আর ব্যবহারকারীর লেখা log-এ বসানোর আগে নতুন লাইনের অক্ষর সরিয়ে দিন, যাতে কেউ নকল log-লাইন ঢোকাতে না পারে।

চেষ্টার সীমা

লগইন, OTP পাঠানো আর পাসওয়ার্ড ভোলার endpoint-এ rate limiting জরুরি। দুই দিক থেকে সীমা দিন: একটি অ্যাকাউন্টে (যেমন ১৫ মিনিটে ৫টি ভুল) আর একটি IP থেকে। প্রতিটি ব্যর্থতার পর অপেক্ষার সময় বাড়ান। দীর্ঘ সময় অ্যাকাউন্ট তালাবদ্ধ করার উল্টো বিপদ আছে — যে কেউ ইচ্ছা করে ভুল পাসওয়ার্ড দিয়ে আসল মালিককে আটকে রাখতে পারে; তাই সাময়িক বিরতি বা CAPTCHA প্রায়ই ভালো। সীমা পার হলে 429 Too Many Requests. Spring Security-তে এর জন্য তৈরি কিছু নেই; সাধারণত API gateway বা Bucket4j-এর মতো লাইব্রেরি এই কাজ করে।

dependency হালনাগাদ রাখুন

আপনার কোড নিখুঁত হলেও লাইব্রেরিতে ত্রুটি থাকতে পারে। পর্ব ৪৬-এ Log4Shell (২০২১) আর software supply chain-এর কথা দেখেছেন; ২০২২ সালে Spring Framework-এও “Spring4Shell” নামে পরিচিত একটি গুরুতর ত্রুটি পাওয়া যায়। তাই Spring Boot-এর patch সংস্করণ নিয়মিত হালনাগাদ করুন, Dependabot বা OWASP Dependency-Check দিয়ে জানা CVE খুঁজুন, আর অপ্রয়োজনীয় dependency বাদ দিন।

নিরাপত্তার টেস্ট — 401, 403, 200 প্রমাণ করুন

“কোড পড়ে মনে হচ্ছে ঠিক আছে” নিরাপত্তার জন্য যথেষ্ট নয়। spring-security-test যোগ করলে MockMvc-এ নকল ব্যবহারকারী বসানো যায় — @WithMockUser:

@WebMvcTest(ProductController.class)
@Import(ApiSecurityConfig.class)                      // নিজের security নিয়মগুলো টেস্টে আনুন
class PriceSecurityTest {
    @Autowired MockMvc mvc;
    @MockitoBean ProductService service;              // Spring Boot 3.4-এর আগে: @MockBean
    @MockitoBean JwtDecoder jwtDecoder;               // আসল authorization server লাগবে না

    MockHttpServletRequestBuilder changePrice() {
        return put("/api/v1/products/7/price").contentType(MediaType.APPLICATION_JSON).content("{\"price\":440}");
    }

    @Test void anyoneCanBrowse() throws Exception {
        mvc.perform(get("/api/v1/products")).andExpect(status().isOk());
    }
    @Test void anonymousGets401() throws Exception {
        mvc.perform(changePrice()).andExpect(status().isUnauthorized());
    }
    @Test @WithMockUser(roles = "CUSTOMER")
    void customerGets403() throws Exception {
        mvc.perform(changePrice()).andExpect(status().isForbidden());
    }
    @Test @WithMockUser(roles = "ADMIN")
    void adminGets200() throws Exception {
        mvc.perform(changePrice()).andExpect(status().isOk());
    }
}

প্রত্যাশিত ফল — সংস্করণভেদে আলাদা: চারটি টেস্টই পাস। কেউ পরে ভুল করে নিয়মের ক্রম বদলালে বা permitAll বসালে customerGets403 লাল হবে — ঠিক সেটাই চাই। ownership-এর জন্যও টেস্ট লিখুন: “গ্রাহক ক অন্য গ্রাহক খ-এর অর্ডার চাইলে 404”. CSRF চালু থাকা অ্যাপে অবস্থা-বদলানো request-এ .with(csrf()) যোগ করতে হয়, আর JWT-এর জন্য .with(jwt())-ও আছে।

C/C++/Python জানলে

Python-এর FastAPI-তে filter chain-এর বদলে প্রতিটি endpoint-এ Depends(...) দিয়ে পরিচয় আনা হয়; Django-তে middleware আর CSRF সুরক্ষা Spring-এর মতোই ডিফল্টে চালু। পাসওয়ার্ডের নীতি একই — salt আর ধীর hash, সমান-সময়ের তুলনা (hmac.compare_digest), আর random-এর জন্য secrets module. C আর C++-এ তৈরি কোনো ওয়েব-নিরাপত্তা framework নেই; libsodium-এর মতো লাইব্রেরি Argon2 দেয়, বাকিটা নিজে সাজাতে হয় — ভুলের সুযোগও বেশি। Python-এর দিকটা দেখুন নিরাপদ Python — input, secret আর dependency পর্বে।

পরীক্ষা ও ইন্টারভিউতে

authentication আর authorization-এর পার্থক্য? authentication — “আপনি কে?” (ব্যর্থ হলে 401); authorization — “এই কাজ কি আপনি করতে পারেন?” (ব্যর্থ হলে 403).
পাসওয়ার্ড কীভাবে রাখবেন? কখনো সরাসরি বা MD5/SHA-256 দিয়ে নয়; salt আর cost factor-সহ ধীর hash — BCrypt, Argon2 বা PBKDF2 — Spring-এ PasswordEncoder.
Spring Security-র মূল গঠন? DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain-এর filter-গুলো; ফল থাকে SecurityContextHolder-এ, Authentication object হিসেবে।
hasRole("ADMIN") আর hasAuthority("ADMIN")? প্রথমটি খোঁজে ROLE_ADMIN, দ্বিতীয়টি হুবহু ADMIN.
stateless JWT API-তে CSRF কেন প্রায়ই বন্ধ থাকে? কারণ token যায় Authorization header-এ, যা ব্রাউজার নিজে থেকে পাঠায় না; cookie-তে token রাখলে CSRF সুরক্ষা লাগবে।

৯ · AI-এর যুগে — AI-এর লেখা security কোড যাচাই

AI-কে “Spring Boot-এ JWT login বানাও” বললে কয়েক সেকেন্ডে কোড আসে, আর প্রায়ই সেটা চলে। কিন্তু নিরাপত্তার কোডে “চলে” মানে কিছুই প্রমাণ হয় না — সব দরজা খোলা রাখলেও অ্যাপ “চলে”। AI-এর লেখা security কোডে বারবার যে ভুলগুলো দেখা যায়:

AI যা করেকেন সমস্যাআপনি যা করবেন
error এড়াতে requestMatchers("/**").permitAll()পুরো API সবার জন্য খোলা — নিরাপত্তা কার্যত বন্ধডিফল্টে বন্ধ; নির্দিষ্ট পথ নির্দিষ্ট method-এ খুলুন
কারণ না বুঝে csrf.disable(), CORS-এ "*"cookie session-এ CSRF খোলা; যেকোনো সাইট API ডাকেঅংশ ৭-এর প্রশ্ন: পরিচয় কি ব্রাউজার নিজে পাঠায়?
JWT secret কোডে লেখা: "mySecretKey"git-এ গেলে যে কেউ ADMIN token বানাতে পারে৩২+ byte random key, environment variable থেকে
পাসওয়ার্ড MD5/SHA-256 দিয়ে, বা NoOpPasswordEncoderফাঁস হলে মুহূর্তে পাসওয়ার্ড বের হয়createDelegatingPasswordEncoder()
শুধু role দেখে, ownership check নেইযেকোনো গ্রাহক অন্যের অর্ডার দেখেন (BOLA)মালিকানা query-তে, পরিচয় Authentication থেকে
WebSecurityConfigurerAdapter, antMatchersSpring Security 6-এ নেই, compile হয় নাSecurityFilterChain bean, requestMatchers
নিজের হাতে JWT filter, অ্যালগরিদম যাচাই ছাড়াঅবৈধ token গ্রহণের ঝুঁকিOAuth2 Resource Server ব্যবহার করুন

AI-কে security কনফিগারেশন লিখতে বা রিভিউ করতে বলার সময় এই কাঠামো ব্যবহার করুন:

আমি Java 21, Spring Boot 3.x আর Spring Security 6 ব্যবহার করছি। একটি অনলাইন দোকানের মোবাইল API-র নিরাপত্তা কনফিগারেশন লিখে দাও। নিয়ম: ১. GET /api/v1/products/** সবার জন্য খোলা; /api/v1/orders/** শুধু লগইন করা CUSTOMER বা ADMIN; PUT /api/v1/products/{id}/price শুধু ADMIN; বাকি সব authenticated. ২. শুধু SecurityFilterChain bean আর lambda DSL (authorizeHttpRequests, requestMatchers). WebSecurityConfigurerAdapter বা antMatchers ব্যবহার করবে না। ৩. stateless JWT, OAuth2 Resource Server দিয়ে; নিজের JWT filter লিখবে না। key আসবে environment variable থেকে, কোডে নয়। ৪. পাসওয়ার্ড PasswordEncoderFactories.createDelegatingPasswordEncoder() দিয়ে। ৫. CSRF বন্ধ করলে ঠিক কোন কারণে নিরাপদ, এক লাইনে লেখো; CORS-এ শুধু https://shop.example.com. ৬. "গ্রাহক শুধু নিজের অর্ডার দেখবেন" — ownership check repository query-তে; অন্যের অর্ডারে 404. ৭. @WithMockUser আর MockMvc দিয়ে টেস্ট: anonymous → 401, CUSTOMER দাম বদলাতে চাইলে 403, ADMIN → 200, অন্যের অর্ডার → 404. ৮. কোনো API কোন সংস্করণ থেকে আছে বা অনিশ্চিত হলে স্পষ্ট বলো; বানানো method লিখবে না।

AI-এর লেখা security কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Spring Security শুরু হয়েছিল ২০০৩ সালে “Acegi Security” নামে। একটি ছোট open source প্রকল্প কীভাবে Java-জগতের প্রধান নিরাপত্তা framework হলো, আর তার নকশার কোন সিদ্ধান্তগুলো আজও টিকে আছে?

২০০০-এর দশকের শুরুতে Java-র ব্যবসায়িক অ্যাপে নিরাপত্তা মানে ছিল মূলত Java EE-র কনটেইনার-নির্ভর ব্যবস্থা — নিয়ম XML descriptor-এ, আর এক অ্যাপ সার্ভার থেকে আরেকটিতে নিলে অনেক কিছু নতুন করে সাজাতে হতো। Spring Framework তখন হালকা বিকল্প হিসেবে জনপ্রিয় হচ্ছিল, কিন্তু তার নিজের কোনো নিরাপত্তা ব্যবস্থা ছিল না।

২০০৩ সালের শেষ দিকে Ben Alex Spring-এর জন্য একটি নিরাপত্তা প্রকল্প শুরু করেন, নাম Acegi Security. প্রকল্পটি দ্রুত সম্প্রদায়ের সমর্থন পায়, পরে আনুষ্ঠানিকভাবে Spring-এর অধীনে আসে। ২০০৮ সালে সংস্করণ ২.০-এর সাথে নাম বদলে হয় Spring Security, সাথে আসে সংক্ষিপ্ত XML namespace কনফিগারেশন — কারণ Acegi-র বড় অভিযোগ ছিল, সামান্য কাজেও শত শত লাইনের XML লিখতে হতো।

যে সিদ্ধান্তগুলো টিকে গেছে, সেগুলো লক্ষণীয়। প্রথমত, servlet filter-এর ওপর ভিত্তি: অ্যাপ সার্ভারের নিজস্ব ব্যবস্থার বদলে একটি মানক জিনিসের ওপর দাঁড়ানো, তাই একই কোড যেকোনো servlet কনটেইনারে চলে। দ্বিতীয়ত, authentication আর authorization-কে আলাদা, বদলানো যায় এমন অংশে ভাগ করা — পাসওয়ার্ড ডেটাবেসে থাকুক বা LDAP-এ, নিয়ম URL-এ হোক বা method-এ, কাঠামো একই। তৃতীয়ত, SecurityContextHolder-এর মতো একটি কেন্দ্রীয় জায়গা, যেখান থেকে বর্তমান পরিচয় জানা যায়। বদলেছে মূলত কনফিগারেশনের ভাষা: XML থেকে Java config, তারপর WebSecurityConfigurerAdapter, আর শেষে SecurityFilterChain bean — inheritance-এর বদলে সাধারণ bean দিয়ে গঠন।

মূল উপলব্ধি: যে framework একটি মানক ভিত্তির ওপর দাঁড়ায় আর দায়িত্বগুলো আলাদা অংশে ভাগ রাখে, তার কনফিগারেশনের চেহারা বিশ বছরে কয়েকবার বদলালেও মূল নকশা টিকে থাকে। তাই এই পর্বের filter chain-এর ধারণা কনফিগারেশনের যেকোনো নতুন রূপেও কাজে লাগবে।

প্র ০২OWASP Top 10-এর ২০২১ সালের তালিকায় Broken Access Control প্রথম স্থানে উঠে আসে, injection নেমে যায় তৃতীয়তে। কেন ভাঙা অনুমতি এত সাধারণ, আর framework কেন এটা পুরোপুরি ঠেকাতে পারে না?

OWASP (Open Worldwide Application Security Project) একটি অলাভজনক উন্মুক্ত সম্প্রদায়, যারা ২০০৩ সাল থেকে কয়েক বছর পরপর ওয়েব অ্যাপের সবচেয়ে গুরুতর দশটি ঝুঁকির তালিকা প্রকাশ করে। বহু সংস্করণে তালিকার শীর্ষে ছিল injection. ২০২১ সালের তালিকায় ছবিটি বদলে যায়: আগে পঞ্চম স্থানে থাকা Broken Access Control উঠে আসে প্রথমে। OWASP-এর প্রকাশিত তথ্য অনুযায়ী পরীক্ষা করা অ্যাপগুলোর খুব বড় একটি অংশে কোনো না কোনো ধরনের ভাঙা অনুমতি পাওয়া গিয়েছিল। injection নেমে আসে তৃতীয়তে, আর দ্বিতীয় স্থানে আসে Cryptographic Failures — আগের “Sensitive Data Exposure”-এর নতুন, মূল কারণভিত্তিক নাম।

injection কেন নামল? কারণ framework এখানে সত্যিই জিতেছে। parameterized query, ORM, স্বয়ংক্রিয় HTML escape — আধুনিক টুলে নিরাপদ পথটিই সহজ পথ। পর্ব ৪৯-এর PreparedStatement বা Spring Data-র derived query ব্যবহার করলে আপনাকে আলাদা করে কিছু ভাবতে হয় না।

অনুমতির সমস্যা কেন থেকে গেল? কারণ অনুমতি ব্যবসার নিয়ম, আর ব্যবসার নিয়ম framework জানে না। “অর্ডার দেখবেন তার মালিক, দোকানের ম্যানেজার নিজের শাখার সব অর্ডার, আর ডেলিভারিম্যান শুধু তাঁর হাতে দেওয়া অর্ডারের ঠিকানা” — এই নিয়ম প্রতিটি অ্যাপে আলাদা। কোন object কার, সেটা আপনাকেই প্রতিটি endpoint-এ পরীক্ষা করতে হয়; একটিতে ভুলে গেলেই ফাঁক। আর সাধারণ টেস্টে একজন ব্যবহারকারী নিজের ডেটা চায়, সব সবুজ — অন্যের ডেটা চেয়ে দেখার টেস্ট কেউ না লিখলে ফাঁকটি থেকেই যায়।

API-র যুগে এটা আরও বেড়েছে: মোবাইল অ্যাপ id দিয়ে ডেটা চায়, আর যে কেউ সরাসরি API ডাকতে পারে। প্রতিকারের নীতি: ডিফল্টে নিষেধ, মালিকানার শর্ত query-র ভেতরে, পরিচয় শুধু যাচাই করা token থেকে, আর প্রতিটি “আমার …” endpoint-এ “অন্যের ডেটা চাইলে কী হয়” টেস্ট।

মূল উপলব্ধি: যে ঝুঁকি একটি টুল দিয়ে সমাধান করা যায়, সময়ের সাথে তা কমে আসে। যে ঝুঁকি আপনার ব্যবসার নিয়মের ভেতরে থাকে, সেটা কোনো framework আপনার হয়ে সমাধান করতে পারে না — সেখানে নকশা আর টেস্টই একমাত্র পাহারা।

প্র ০৩bcrypt ১৯৯৯ সালে তৈরি হয়েছিল, অথচ আজও পাসওয়ার্ডের জন্য সুপারিশ করা হয়। একটি hash ইচ্ছা করে “ধীর” বানানোর ধারণা কোথা থেকে এলো, আর adaptive cost কেন এত গুরুত্বপূর্ণ?

১৯৭৯ সালে Bell Labs-এর Robert Morris আর Ken Thompson একটি প্রবন্ধে Unix-এর পাসওয়ার্ড ব্যবস্থার বর্ণনা দেন: পাসওয়ার্ড রাখা হবে এক দিকের রূপান্তরের পর, সাথে একটি ছোট random salt, আর রূপান্তরটি ইচ্ছা করে একাধিকবার চালানো হবে। কিন্তু সেই ধীরতা ছিল সেই সময়ের হার্ডওয়্যারের মাপে বাঁধা। হার্ডওয়্যার প্রতি বছর দ্রুত হয়, কিন্তু একটি নির্দিষ্ট hash-এর খরচ স্থির থাকে — ১৯৭৯ সালে যা কঠিন, বিশ বছর পরে তা সহজ। ১৯৯৯ সালে USENIX সম্মেলনে Niels Provos আর David Mazières “A Future-Adaptable Password Scheme” প্রবন্ধে bcrypt উপস্থাপন করেন। এর মূল কথা নামেই আছে — ভবিষ্যতের সাথে মানিয়ে নেওয়া যায় এমন। Blowfish cipher-এর key তৈরির ধীর অংশটিকে ভিত্তি করে বানানো এই পদ্ধতিতে একটি cost প্যারামিটার আছে: cost এক বাড়লে কাজ দ্বিগুণ। আর cost-টি hash-এর লেখার ভেতরেই থাকে ($2a$10$…), তাই একই সিস্টেমে পুরোনো আর নতুন cost-এর hash পাশাপাশি থাকতে পারে।

কেন ধীর হওয়া এত গুরুত্বপূর্ণ? সাধারণ মানুষের পাসওয়ার্ডের সম্ভাব্য মান খুব বেশি নয় — অভিধানের শব্দ, নাম, জন্মসাল, “123456”. ডেটাবেস ফাঁস হলে আক্রমণকারী এই তালিকা ধরে একে একে চেষ্টা করে। SHA-256-এর মতো দ্রুত hash-এ একটি GPU প্রতি সেকেন্ডে শত কোটির বেশি চেষ্টা করতে পারে। একটি hash-এ ১০০ মিলিসেকেন্ড লাগলে সেটা সেকেন্ডে মাত্র কয়েকটি। আপনার সার্ভারের কাছে লগইনে এই দেরি কেউ টেরও পান না; আক্রমণকারীর কাছে এটা কাজকে কোটি গুণ ব্যয়বহুল করে।

পরের প্রজন্মও এসেছে: ২০০৯ সালে Colin Percival-এর scrypt মেমরিও বেশি চায়, আর Password Hashing Competition-এর বিজয়ী Argon2 (২০১৫) আজ OWASP-এর প্রথম পছন্দ। PBKDF2 মেমরি-কঠিন নয়, কিন্তু মানক সংস্থাগুলোর অনুমোদিত আর JDK-তে আছে।

মূল উপলব্ধি: নিরাপত্তা একটি চলমান প্রতিযোগিতা। যে নকশা নিজের খরচ বাড়ানোর সুযোগ রাখে আর সেই খরচ ডেটার ভেতরেই লিখে রাখে, সে পঁচিশ বছর পরেও প্রাসঙ্গিক থাকে — স্থির নকশা হার্ডওয়্যারের কাছে হেরে যায়।

প্র ০৪JWT মান হিসেবে প্রকাশিত হয় ২০১৫ সালে (RFC 7519). অনেক অভিজ্ঞ প্রকৌশলী বলেন “ব্রাউজারের session-এর জন্য JWT ব্যবহার করবেন না”, আবার অনেক বড় সিস্টেম JWT ছাড়া চলে না। দুই পক্ষের যুক্তি কী?

JWT এসেছে IETF-এর JOSE কার্যদলের একগুচ্ছ মানের অংশ হিসেবে — স্বাক্ষরের জন্য JWS (RFC 7515), এনক্রিপশনের জন্য JWE (RFC 7516), আর JWT (RFC 7519, মে ২০১৫), যা বলে claim-গুলো কীভাবে লেখা হবে। OAuth 2.0 আর OpenID Connect-এর ID token-এর জন্য এটা দ্রুত মানক হয়ে ওঠে। তারপর অনেকে সাধারণ ওয়েবসাইটের লগইন session-এর জন্যও JWT ব্যবহার শুরু করেন — আর সেখান থেকেই বিতর্ক।

বিপক্ষের যুক্তি। ২০১৬ সালের দিকে “Stop using JWT for sessions” শিরোনামের একটি বহুল-পঠিত ব্লগ লেখা কয়েকটি কথা তুলে ধরে। প্রথমত, বাতিল করা কঠিন: JWT মেয়াদ পর্যন্ত বৈধ; লগআউট বা চুরিতে আবার একটি সার্ভার-পার্শ্বের তালিকা লাগে, তখন stateless-এর সুবিধা অনেকটাই চলে যায়। দ্বিতীয়ত, ব্রাউজারে রাখার জায়গা — localStorage-এ XSS-এর ঝুঁকি, cookie-তে CSRF সুরক্ষা লাগে, অর্থাৎ শেষে session cookie-র মতোই। তৃতীয়ত, জটিলতা: কিছু লাইব্রেরি একসময় token-এর header-এ লেখা অ্যালগরিদম অন্ধভাবে মেনে নিত, যা থেকে গুরুতর ত্রুটি হয়েছিল; কেউ কেউ তাই PASETO-র মতো সরল বিকল্প প্রস্তাব করেছেন। সাধারণ ওয়েবসাইটে পরীক্ষিত session cookie প্রায়ই যথেষ্ট।

পক্ষের যুক্তি। যেখানে অনেক আলাদা service একই পরিচয় যাচাই করে, সেখানে JWT সত্যিই উজ্জ্বল। একটি authorization server token দেয়, আর দশটি microservice শুধু public key দিয়ে স্বাক্ষর মিলিয়ে নেয় — কেন্দ্রীয় session store-এ প্রতিটি request-এ যেতে হয় না। মোবাইল অ্যাপে cookie-র ঝামেলা নেই। আর OAuth2/OpenID Connect-এর বিশাল জগৎ — Google, Microsoft, Keycloak — এই ফরম্যাটেই কথা বলে।

ব্যবহারিক মধ্যপথ এখন প্রতিষ্ঠিত: ছোট মেয়াদের access token, সার্ভারে রাখা বাতিলযোগ্য refresh token, সার্ভারে নির্দিষ্ট করা অ্যালগরিদম, প্রতিষ্ঠিত লাইব্রেরি। ব্রাউজারভিত্তিক ওয়েবসাইটে অনেকে আবার session cookie-তেই ফিরছেন।

মূল উপলব্ধি: কোনো token ফরম্যাট নিজে নিরাপদ বা অনিরাপদ নয়; প্রশ্ন হলো সেটা কোন সমস্যার জন্য। “stateless” একটি সুবিধা, কিন্তু তার দাম “বাতিল করা কঠিন” — নকশার সময় এই দামটি জেনে-বুঝে দিন।

প্র ০৫“least privilege” নীতিটি ১৯৭৫ সালে Saltzer আর Schroeder-এর একটি প্রবন্ধে স্পষ্ট রূপ পায়। পঞ্চাশ বছর পুরোনো এই নীতিগুলো আজকের Spring অ্যাপে কোথায় কোথায় দেখা যায়?

১৯৭৫ সালে MIT-এর Jerome Saltzer আর Michael Schroeder Proceedings of the IEEE-তে “The Protection of Information in Computer Systems” প্রকাশ করেন — Multics-এর মতো বহু-ব্যবহারকারীর সিস্টেমের অভিজ্ঞতা থেকে লেখা। তাঁদের আটটি নকশা-নীতি আজও নিরাপত্তা প্রকৌশলের ভিত্তি, আর প্রায় প্রতিটি এই পর্বে কোথাও না কোথাও এসেছে।

Least privilege — প্রতিটি প্রোগ্রাম আর ব্যবহারকারী কাজের জন্য ন্যূনতম অনুমতি নিয়ে চলবে। Spring অ্যাপে: CUSTOMER role দাম বদলাতে পারে না; অ্যাপের ডেটাবেস ব্যবহারকারী টেবিল মুছতে পারে না; token-এ শুধু দরকারি scope; Docker container root হিসেবে চলে না। ভুল হলে বা কেউ ঢুকে পড়লে ক্ষতির পরিধি ছোট থাকে।

Fail-safe defaults — অনুমতির সিদ্ধান্ত হবে “অনুমতি আছে কি না” দেখে, “নিষেধ আছে কি না” দেখে নয়; ডিফল্ট উত্তর “না”। Spring Security-র “starter যোগ করলেই সব বন্ধ” আর anyRequest().authenticated() ঠিক এটাই। Complete mediation — প্রতিটি অ্যাক্সেস প্রতিবার পরীক্ষা হবে: প্রতিটি request filter chain দিয়ে যায়, আর ownership check প্রতিটি object-এ। Open design — নিরাপত্তা নকশা গোপন রাখার ওপর নির্ভর করবে না, শুধু key গোপন থাকবে; JWT-এর ফরম্যাট আর HMAC অ্যালগরিদম সবার জানা, তবু key ছাড়া জাল করা যায় না। এটা উনিশ শতকের Auguste Kerckhoffs-এর নীতির প্রতিধ্বনি।

Economy of mechanism — নিরাপত্তার যন্ত্র যত সরল, তত পরীক্ষা করা সহজ; তাই নিজের JWT filter নয়। Separation of privilege — একাধিক শর্ত মিলে তবেই অনুমতি; MFA এর আধুনিক রূপ। Psychological acceptability — নিরাপত্তা এত কষ্টকর হবে না যে মানুষ এড়িয়ে চলেন। আর least common mechanism — ভাগ করা জিনিস যত কম, ফাঁসের পথ তত কম; প্রতিটি request-এর আলাদা SecurityContext তার ছোট উদাহরণ।

মূল উপলব্ধি: প্রযুক্তি বদলায় — mainframe থেকে মোবাইল API — কিন্তু ভালো নিরাপত্তার নীতিগুলো আশ্চর্যজনকভাবে একই থাকে। নতুন কোনো framework শিখলে জিজ্ঞেস করুন: এর ডিফল্ট কি “না”, আর এটা কি প্রতিটি অ্যাক্সেস পরীক্ষা করে?

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. নিয়মের ক্রম: একজন সহকর্মী অংশ ৩-এর কনফিগারেশন এভাবে বদলেছেন — প্রথম লাইনে .requestMatchers("/api/v1/products/**").permitAll() (method ছাড়া), তার নিচে .requestMatchers(HttpMethod.PUT, "/api/v1/products/*/price").hasRole("ADMIN"). token ছাড়া PUT /api/v1/products/7/price পাঠালে কী হবে? দুটি উপায়ে ঠিক করুন।

    অনুমতির নিয়ম request-টি আটকাবে না — যে কেউ দাম বদলাতে পারবেন। কারণ নিয়ম ওপর থেকে মেলানো হয়, আর প্রথম নিয়মটি method না দেখেই /api/v1/products/**-এর সব request খুলে দেয়; ADMIN-এর নিয়মে পৌঁছানোই হয় না। (অংশ ৩-এর কনফিগারেশনে CSRF ডিফল্টে চালু, তাই CSRF token ছাড়া PUT আগেই 403 পেতে পারে — কিন্তু সেটা কাকতালীয় রক্ষা; অংশ ৫-এর মতো CSRF বন্ধ থাকা stateless API-তে request সোজা controller-এ পৌঁছায়।) ঠিক করার এক উপায়: নির্দিষ্ট নিয়মটি (PUT … price → ADMIN) ওপরে আনুন। দ্বিতীয় আর ভালো উপায়: খোলা নিয়মটিকে শুধু GET-এ সীমিত করুন — requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll(). তখন ক্রম ভুল হলেও ক্ষতি কম। আর অংশ ৮-এর customerGets403-এর মতো টেস্ট থাকলে এই বদলটি merge-এর আগেই লাল হয়ে ধরা পড়ত।

  2. cost কমিয়ে দেখুন: অংশ ৪-এর চালানো যায় এমন প্রোগ্রামে ITERATIONS ৬,০০,০০০ থেকে ১ করুন। না চালিয়ে বলুন আউটপুটের কোন লাইন বদলাবে। তারপর চালিয়ে মিলিয়ে নিন। কিছু না বদলালে এই পরিবর্তনটি কেন বিপজ্জনক?

    আউটপুটের চারটি লাইনই হুবহু একই থাকবে; শুধু প্রোগ্রাম প্রায় সঙ্গে সঙ্গে শেষ হবে। ঠিক এজন্যই এটা বিপজ্জনক: টেস্টে কোনো পার্থক্য ধরা পড়ে না, লগইন আগের মতোই কাজ করে, কিন্তু ডেটাবেস ফাঁস হলে প্রতিটি অনুমানের খরচ ছয় লক্ষ ভাগের এক ভাগ। কার্যকারিতা ঠিক থাকে, শুধু সুরক্ষা উধাও। তাই cost-এর মান কোড রিভিউতে দেখুন, আর জমানো hash-এর উপসর্গ ($2a$10$) মাঝে মাঝে পরীক্ষা করুন।

  3. token বদলানো: অংশ ৫-এর চালানো যায় এমন প্রোগ্রামে জাল token-এর payload-এ role না বদলে "exp":2000-কে "exp":9999 করুন (role CUSTOMER রেখে), আর check(forged, "CUSTOMER", 2500) ডাকুন। ফল কী হবে? আর যদি আক্রমণকারী কোনোভাবে KEY জেনে যায়?

    ফল 401. মেয়াদ বাড়ানোও payload বদলানো, তাই স্বাক্ষর মেলে না — মেয়াদের পরীক্ষা পর্যন্ত পৌঁছানোই হয় না। স্বাক্ষর পুরো payload রক্ষা করে। কিন্তু key জানা থাকলে যে কেউ যেকোনো payload-এ সঠিক স্বাক্ষর বানাতে পারবে — ADMIN, অনন্ত মেয়াদ, সব বৈধ দেখাবে। তখন একমাত্র উপায় সঙ্গে সঙ্গে নতুন key বসানো (key rotation). এজন্যই key কোডে নয়, environment variable বা secret manager-এ; আর অনেক service থাকলে অসমমিতিক স্বাক্ষর, যাতে যাচাইকারীদের হাতে জাল করার ক্ষমতাই না থাকে।

  4. নিজে লিখুন — ADMIN-ও দেখতে পারবেন: অংশ ৬-এর getMyOrder-কে এমনভাবে বদলান যাতে গ্রাহক শুধু নিজের অর্ডার দেখেন, কিন্তু ROLE_ADMIN থাকলে যেকোনো অর্ডার দেখা যায়। অন্যের অর্ডারে 404-ই থাকবে।

    একটি সম্ভাব্য সমাধান:

    @Transactional(readOnly = true)
    public OrderResponse getOrder(long orderId, Authentication auth) {
        boolean isAdmin = auth.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
        Optional<Order> order = isAdmin
                ? orders.findById(orderId)                                    // ADMIN: যেকোনো অর্ডার
                : orders.findByIdAndCustomerPhone(orderId, auth.getName());   // গ্রাহক: শুধু নিজের
        return order.map(OrderResponse::from)
                    .orElseThrow(() -> new OrderNotFoundException(orderId));   // না পেলে বা অন্যের → 404
    }

    লক্ষ করুন: authority-তে নাম উপসর্গসহ, ROLE_ADMIN. গ্রাহকের পথে মালিকানার শর্ত এখনো query-র ভেতরে। আর দুটি টেস্ট যোগ করুন: @WithMockUser(username = "01711111111", roles = "CUSTOMER") দিয়ে অন্যের অর্ডার চাইলে 404, আর roles = "ADMIN" দিয়ে 200.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমtoken-সহ একটি request Spring Boot অ্যাপে এলে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔌 Tomcat-এর filter হিসেবে DelegatingFilterProxy request পায়
  2. 🧭 FilterChainProxy পথ দেখে মানানসই SecurityFilterChain বাছে
  3. 🪪 পরিচয়ের filter token যাচাই করে SecurityContextHolder-এ Authentication রাখে
  4. 🚦 AuthorizationFilter নিয়ম মিলিয়ে অনুমতি দেয়
  5. 🌐 DispatcherServlet controller method চালায়
  6. 🧹 উত্তর যাওয়ার পর SecurityContext মুছে ফেলা হয়

একটি API-তে সঠিক token দিয়ে লগইন করা একজন সাধারণ গ্রাহক শুধু ADMIN-এর জন্য রাখা endpoint ডাকলেন। কোন ধাপটি ব্যর্থ হলো, আর উত্তর কী হবে?

  • authentication ব্যর্থ, 401
  • validation ব্যর্থ, 400
  • authorization ব্যর্থ, 403
  • routing ব্যর্থ, 404

ওয়েব অ্যাপে ব্যবহারকারীর পাসওয়ার্ড সংরক্ষণের জন্য কোনটি সবচেয়ে উপযুক্ত?

  • AES দিয়ে এনক্রিপ্ট করে রাখা
  • salt ছাড়া SHA-256 hash
  • salt-সহ MD5 hash
  • salt আর cost factor-সহ ধীর hash, যেমন BCrypt বা Argon2

HS256 দিয়ে স্বাক্ষর করা একটি JWT-এর payload অংশ সম্পর্কে কোনটি সত্য?

  • payload এনক্রিপ্ট করা, key ছাড়া পড়া যায় না
  • যে কেউ পড়তে পারে, কিন্তু key ছাড়া বদলালে স্বাক্ষর মেলে না
  • শুধু exp claim স্বাক্ষর দিয়ে সুরক্ষিত
  • পাসওয়ার্ড রাখার নিরাপদ জায়গা

Spring Security-তে hasRole("ADMIN") নিয়মটি ব্যবহারকারীর কোন authority খোঁজে?

  • ROLE_ADMIN
  • ADMIN
  • SCOPE_ADMIN
  • admin (ছোট হাতের)

কোন পরিস্থিতিতে CSRF সুরক্ষা সবচেয়ে বেশি প্রয়োজন?

  • মোবাইল অ্যাপ Authorization header-এ token পাঠায়
  • শুধু পড়ার GET endpoint, কোনো লগইন নেই
  • ব্রাউজারে session cookie দিয়ে লগইন করা ওয়েবসাইট
  • দুটি backend service-এর মধ্যে কল

Spring Boot 3 আর Spring Security 6-এ নিরাপত্তার নিয়ম লেখার প্রস্তাবিত উপায় কোনটি?

  • WebSecurityConfigurerAdapter extend করে configure() override করা
  • SecurityFilterChain bean আর authorizeHttpRequests-এর lambda DSL
  • প্রতিটি controller method-এ হাতে লেখা if-else
  • web.xml-এ security-constraint লেখা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. authentication = “আপনি কে?” → ব্যর্থ হলে 401; authorization = “এটা কি আপনি পারেন?” → ব্যর্থ হলে 403. ডিফল্টে সব বন্ধ, আর least privilege.
  2. filter chain: DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain; পরিচয় থাকে SecurityContextHolder-এ (Authentication: principal, authorities). নিয়ম লিখুন SecurityFilterChain bean-এ — নির্দিষ্ট আগে, anyRequest() শেষে।
  3. পাসওয়ার্ড: salt আর cost-সহ ধীর hash (BCrypt, Argon2, PBKDF2), createDelegatingPasswordEncoder(), সমান-সময়ের তুলনা; কখনো সরাসরি বা MD5/SHA-256 একা নয়।
  4. JWT: header.payload.signature — পড়া যায়, বদলানো যায় না; ছোট মেয়াদ, refresh token, key environment variable-এ; Spring-এ OAuth2 Resource Server.
  5. role-এর বাইরে: @PreAuthorize আর ownership check (অন্যের ডেটায় 404); CSRF শুধু তখন বন্ধ, যখন পরিচয় cookie-তে যায় না; CORS-এ নির্দিষ্ট origin; HTTPS, secret-হীন log, চেষ্টার সীমা, হালনাগাদ dependency আর 401/403/200-এর টেস্ট।

পরের পর্বে: ওয়েব অ্যাপের জগৎ থেকে একটু সরে সমস্যা সমাধানের দিকে — অ্যালগরিদম ও কনটেস্টে Java: দ্রুত ইনপুট, collection দিয়ে প্রচলিত অ্যালগরিদম, আর সময়ের হিসাব।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান