Spring Boot JWT Security Guide | Spring Boot JWT ආරක්ෂාව - SC Blog

ආයුබෝවන් කට්ටියටම! කොහොමද ඉතින්? අද අපේ SC Blog එකෙන් කතා කරන්න යන්නේ ඔයාලා හැමෝටම නියමෙටම වැදගත් වෙන මාතෘකාවක් ගැන. මේ වෙද්දි අපි දන්නවා, ඔන්ලයින් ලෝකය දවසින් දවස දියුණු වෙනවා වගේම, අපේ දත්ත වල ආරක්ෂාව කියන එකත් ඊට සමාන්තරව ගොඩක් වැදගත් වෙනවා කියලා. විශේෂයෙන්ම, අපි හදන API වල ආරක්ෂාව ගැන සැලකිලිමත් වෙන එක අත්යාවශ්ය දෙයක් වෙලා.
දැන් කෙනෙක්ට හිතෙන්න පුළුවන්, "අපෝ මේක ඉතින් හැමදාම කතා කරන දෙයක්නේ" කියලා. ඒත් අපි අද බලමු Spring Boot API එකක් JSON Web Tokens (JWT) භාවිතයෙන් ආරක්ෂා කරගන්නේ කොහොමද කියලා. මේක "stateless" විදිහට, ඒ කියන්නේ session එකක් නැතුව ආරක්ෂාව සපයන නිසා, ලොකු පරිමාණයේ (scalable) applications වලට පට්ට විසඳුමක්. අපි මේ ලිපියෙන් JWT කියන්නේ මොකක්ද, Spring Boot වලට මේක කොච්චර ගැලපෙනවද, සහ පියවරෙන් පියවර මේක implement කරන්නේ කොහොමද කියලා විස්තරාත්මකව කතා කරනවා. එහෙනම්, අපි පටන් ගමුද?
JWT කියන්නේ මොකක්ද? (What is JWT?)
සරලව කිව්වොත්, JWT කියන්නේ සංකේතනය (digitally signed) කරපු, ආරක්ෂිතව තොරතුරු සම්ප්රේෂණය කරන්න පුළුවන් compact, URL-safe token එකක්. මේක සාමාන්යයෙන් authentication සහ authorization වලට තමයි භාවිත කරන්නේ. අපි හිතමු ඔයා system එකකට login වෙනවා කියලා. සාමාන්ය ක්රම වලදී session එකක් maintain කරනවා. ඒත් JWT වලදී එහෙම නෑ. User කෙනෙක් සාර්ථකව login වුණාම server එකෙන් JWT එකක් generate කරලා client එකට යවනවා. ඊට පස්සේ client එක, protected resources වලට access කරන්න ඕන හැම වෙලාවකදීම මේ JWT එක request header එකේ යවනවා. Server එක මේ JWT එක validate කරලා user ට අවශ්ය access එක දෙනවා.
JWT එකක් කොටස් තුනකින් හැදිලා තියෙනවා:
- Header: මේකේ තියෙන්නේ token එකේ වර්ගය (උදා: JWT) සහ signing algorithm එක (උදා: HMAC SHA256 or RSA) වගේ දේවල්.
- Payload: මේක තමයි token එකේ core data එක. මේකට "claims" කියලා කියනවා. Claims කියන්නේ user ID, role, expiration time වගේ දේවල්. මේවා private, public හෝ registered claims වෙන්න පුළුවන්.
- Signature: මේක තමයි JWT එකේ ආරක්ෂාව සහතික කරන්නේ. Header එක, Payload එක සහ server එක දන්න secret key එකක් (හෝ private key එකක්) පාවිච්චි කරලා මේ signature එක හදනවා. මේ නිසා, token එකේ තොරතුරු වෙනස් කරන්න හැදුවොත් server එකට ඒක අඳුරගන්න පුළුවන්.
මේ කොටස් තුන dot (.
) එකකින් වෙන් කරලා base64-URL encoded විදිහට තමයි එකට සම්බන්ධ වෙන්නේ. ඒ කියන්නේ aaaaa.bbbbb.ccccc
වගේ format එකකින්.
Spring Boot වලට JWT ඇයි? (Why JWT for Spring Boot?)
Spring Boot කියන්නේ Microservices සහ RESTful APIs හදන්න කදිම Framework එකක්. මෙතනදී traditional session-based authentication වලට වඩා JWT ගොඩක් වාසි සපයනවා:
- Statelessness: JWT නිසා server එකට user session state එකක් තියාගන්න ඕන වෙන්නේ නෑ. Request එකක් ආවම token එක validate කරලා අවශ්ය දේ කරනවා. මේක scalable applications වලට, විශේෂයෙන්ම load balancers සහ multiple server instances තියෙන architectures වලට ගොඩක් වැදගත්.
- Scalability: Stateless නිසා, ඕනෑම server instance එකකට ඕනෑම request එකක් handle කරන්න පුළුවන්. ඒ කියන්නේ system එක load එකට අනුව පහසුවෙන් expand කරන්න පුළුවන්.
- Mobile & Cross-domain Friendly: Mobile applications සහ different domains වල තියෙන Frontends එක්ක වැඩ කරද්දී JWT ගොඩක් පහසුයි. Token එක HTTP header එක හරහා ඕනෑම තැනකට යවන්න පුළුවන්.
- Performance: Session lookups නැති නිසා, සමහර වෙලාවට performance එක වැඩි වෙන්න පුළුවන්.
පියවරෙන් පියවර JWT යොදමු (Implementing JWT Step-by-Step)
හරි, දැන් අපි බලමු කොහොමද Spring Boot Project එකකට JWT authentication එක එකතු කරගන්නේ කියලා. මේක පොඩ්ඩක් සංකීර්ණ වගේ පෙනුනත්, හරි විදිහට පියවරෙන් පියවර කළොත් ලේසියි.
1. අවශ්ය Dependencies එකතු කරගමු (Add Dependencies)
ඔයාගේ pom.xml
file එකට පහත dependencies ටික එකතු කරගන්න. අපි මෙතනදී Spring Security සහ JWT සඳහා io.jsonwebtoken පුස්තකාලය භාවිත කරනවා.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2. JWT Utility Class එක හදමු (Create JWT Utility)
මේ class එක තමයි JWT tokens generate කරන්න, validate කරන්න, සහ token එකෙන් user details extract කරන්න පාවිච්චි කරන්නේ. මේකේ Secret Key එක application.properties
එකෙන් ගන්නවා නම් වඩා හොඳයි.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}") // Define this in application.properties
private String SECRET_KEY;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
}
application.properties
එකට jwt.secret=some_very_long_base64_encoded_secret_key
වගේ එකක් දාන්න මතක තියාගන්න. මේක generate කරන්න පුළුවන් Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded()
වගේ දෙයක් use කරලා.
3. Custom UserDetailsService එකක් හදමු (Implement Custom UserDetailsService)
Spring Security වලදී user details load කරන්න UserDetailsService
interface එක භාවිත කරනවා. අපිට පුළුවන් අපේ database එකෙන් user details load කරන්න මේක customize කරන්න.
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// Here you would fetch user from your database
// For demonstration, let's use a dummy user
if ("foo".equals(userName)) {
return new User("foo", "$2a$10$wT0o3q.eL1Lz.fJ9f2.o.S4v.q.C9n.Q.g.B.i.V.g.Q.Z.k.W.X.Y.Z", new ArrayList<>()); // "password" encoded
} else {
throw new UsernameNotFoundException("User not found: " + userName);
}
}
}
මතක තියාගන්න, password එක encode කරන්න ඕනේ. BCryptPasswordEncoder
එක මේකට පාවිච්චි කරන්න පුළුවන්.
4. Security Configuration එක හදමු (Configure Spring Security)
මේක තමයි project එකේ ආරක්ෂාව manage කරන තැන. අපි JWT Filter එකක් සහ authentication manager එකක් define කරනවා.
import com.example.demo.filter.JwtRequestFilter; // Assume this filter exists
import com.example.demo.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
public class SecurityConfigurer {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/authenticate").permitAll() // Allow this endpoint without authentication
.anyRequest().authenticated() // All other requests need authentication
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider());
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(myUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
මෙහිදී අපි /authenticate
endpoint එකට authentication නැතුව access කරන්න ඉඩ දෙනවා. අනිත් හැම request එකකටම authentication අවශ්යයි.
5. JWT Request Filter එක හදමු (Create JWT Request Filter)
මේ filter එක තමයි එන හැම request එකකම JWT එකක් තියෙනවද කියලා බලලා, තියෙනවා නම් validate කරලා, Spring Security context එක update කරන්නේ.
import com.example.demo.service.MyUserDetailsService;
import com.example.demo.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
6. Authentication Endpoint එක හදමු (Create Authentication Endpoint)
අන්තිමට, user ට username සහ password දීලා JWT එකක් ඉල්ලගන්න පුළුවන් Controller එකක් හදමු.
import com.example.demo.model.AuthenticationRequest; // Custom class for request body
import com.example.demo.model.AuthenticationResponse; // Custom class for response
import com.example.demo.service.MyUserDetailsService;
import com.example.demo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtTokenUtil;
@Autowired
private MyUserDetailsService userDetailsService;
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
}
catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
}
AuthenticationRequest
සහ AuthenticationResponse
කියන්නේ සරල POJO classes දෙකක්. (User, password) සහ (jwt token) තියාගන්න.
ප්රායෝගික උපදෙස් සහ හොඳම ක්රියාකාරකම් (Practical Tips and Best Practices)
- Secret Key ආරක්ෂිතව තියාගන්න: JWT Secret Key එක කවදාවත් publicly expose කරන්න එපා. ඒක Environment variables, Spring Cloud Config Server වගේ ආරක්ෂිත තැනක තියාගන්න.
- Token Expiration: Tokens වලට කෙටි කාලයක expiration time එකක් දෙන්න. (උදා: පැය 1-2). මේක security එක වැඩි කරනවා.
- Refresh Tokens: කෙටි කාලීන access tokens සමග දිගු කාලීන refresh tokens භාවිතා කරන්න. Access token එක expire වුණාම refresh token එකෙන් අලුත් access token එකක් ගන්න පුළුවන්.
- Token Revocation/Blacklisting: User කෙනෙක් logout වුණාම හෝ token එක compromise වුණොත්, ඒ token එක revoke කරන්න (blacklist කරන්න) යාන්ත්රණයක් තියාගන්න එක වැදගත්. මේක Redis වගේ fast-access store එකකින් කරන්න පුළුවන්.
- HTTPS භාවිත කරන්න: JWT Tokens, network එක හරහා plain text විදිහට යවන්න එපා. හැමවිටම HTTPS (SSL/TLS) භාවිත කරන්න.
- Sensitive Data Payload එකේ තියන්න එපා: Token Payload එක base64 encoded මිසක් encrypted නැති නිසා, sensitive data (e.g., credit card numbers) ඒකේ තියන්න එපා.
අවසාන වශයෙන් (Conclusion)
Spring Boot application එකක් JWT භාවිතයෙන් ආරක්ෂා කරන එක, නූතන web applications වලට අත්යවශ්ය දෙයක්. මේකෙන් අපිට stateless, scalable, සහ maintain කරන්න පහසු security solution එකක් ලැබෙනවා. අපි මේ ලිපියෙන් JWT වල මූලික සංකල්ප වලින් පටන් අරන්, Spring Boot project එකක කොහොමද JWT authentication එක implement කරන්නේ කියලා පියවරෙන් පියවර කතා කළා.
ඔයාලා මේ concepts තේරුම් අරන්, ඔයාලගේ project වලට මේක implement කරන්න උත්සාහ කරන්න. මොකද, අතේ කරලා බලන එක තමයි හොඳම ඉගෙනීමේ ක්රමය. ඔයාලට මේ ගැන ගැටලු තියෙනවා නම්, නැත්නම් වෙනත් Spring Boot security aspects ගැන දැනගන්න ඕන නම්, පහතින් comment එකක් දාන්න. අපි ඒ ගැනත් කතා කරමු. එහෙනම්, තවත් අලුත් ලිපියකින් හමුවෙනකම්, ජයවේවා!