Spring Security ~AuthenticationToken

Wadcharapong Naklam
4 min readJul 15, 2024

--

Photo by Amol Tyagi on Unsplash

จากบทความที่แล้วผมเล่าถึง Filter และการทำ Jwt authentication ด้วย Filter ใครที่ยังไม่ได้อ่าน สามารถเข้าไปอ่านได้ที่ spring-security-jwt

บทความนี้จะเป็นเรื่องต่อเนื่องในส่วนที่ Spring รู้ได้ยังไงว่า Authenticated ผ่านแล้ว มาเริ่มกันเลย ~~~

ก่อนอื่นเราต้องรู้จักกับของ 5 อย่างที่สำคัญของ AutheticationToken นั้นก็คือ

  • Principal -> เป็นของที่เราเอาไว้ระบุตัวตนเช่น name , email, id
  • GrantedAuthorities -> เป็นของที่เอาไว้บอกสิทธิ์การเข้าถึงเช่น Roles
  • isAuthenticated -> คือ Flag ที่เอาไว้บอกว่า Authenticated แล้ว
  • Detail -> เป็นที่เก็บข้อมูลเพิ่มเติม คำอธิบายเพิ่มเติม
  • Credentials -> password

เมื่อเรารู้จัก AutheticationToken แล้ว เราจะมาเริ่ม implement กันเลยเริ่มจากสร้าง JwtAuthenticationToken.java

package com.example.demofilter;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

private final UserProfile principal;
private final Object credentials;

public JwtAuthenticationToken(UserProfile userProfile, Object credentials) {
super(AuthorityUtils.createAuthorityList(userProfile.getRoles()));
this.principal = userProfile;
this.credentials = credentials;
}

@Override
public Object getCredentials() {
return this.credentials;
}

@Override
public UserProfile getPrincipal() {
return this.principal;
}

@Override
public boolean isAuthenticated() {
return true;
}

}

เราจะ extends AbstractAuthenticationToken เพื่อใช้ implement AuthenticationToken ของเราเอง

  • ให้ Principal เป็นการเก็บข้อมูล UserProfile
  • ให้ GrantedAuthorities เก็บข้อมูล userProfile.getRoles()
  • isAuthenticated() จะให้เป็น True เสมอเพื่อเป็นการบอกว่า Authenticated ผ่านแล้ว

จากนั้นเราจะกลับไปแก้ไข JwtAuthorizationFilter.java เปลี่ยนมาใช้ JwtAuthenticationToken ที่เราสร้างขึ้นมาใหม่แทน

package com.example.demofilter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

public class JwtAuthorizationFilter extends OncePerRequestFilter {

final JwtService jwtService;

public JwtAuthorizationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}

@Override
public void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String token = req.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
chain.doFilter(req, res);
return;
}
token = token.substring(7); // split Bearer
Claims claims = jwtService.getClaims(token);
Integer userId = (Integer) claims.get("I_USER");
String firstName = (String) claims.get("F_NAME");
String lastName = (String) claims.get("L_NAME");
String email = (String) claims.get("U_EMAIL");
List<String> roles = (List<String>) claims.get("U_ROLES");
UserProfile userProfile = new UserProfile(Long.valueOf(userId), email, firstName, lastName, roles);

JwtAuthenticationToken authentication = new JwtAuthenticationToken(userProfile, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
}

Class UserProfile

package com.example.demofilter;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class UserProfile {
private Long id;
private String firstName;
private String lastName;
private String email;
private List<String> roles;

public UserProfile(Long id, String email, String firstName, String lastName, List<String> roles) {
this.id = id;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.roles = roles;
}
}

จากนั้นเราจะมาดูผลลัพธ์กัน แต่ก่อนจะไปดูเราจะต้องเพิ่ม config เข้าไปที่ application.properties

logging.level.org.springframework.security= trace

config นี้จะช่วยให้ log ของ org.springframework.security เรียงเป็นบรรทัดให้ดูง่าย ๆ นั้นเอง (อย่าลืมเอาออกก่อนขึ้น Production นะครับ) เสร็จแล้วก็เริ่ม run โปรเจคขึ้นมาเลยกันเลย

curl  'localhost:8080/public/token'  
เพื่อขอ Token
curl 'localhost:8080/private' \
--header 'Authorization: Bearer {youToken}' \
จะได้ response -> Hello World private !!!!!
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@571c7838, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@37adeb7f, org.springframework.security.web.context.SecurityContextHolderFilter@1f2f0109, org.springframework.security.web.header.HeaderWriterFilter@73a0f2b, org.springframework.web.filter.CorsFilter@700bbe71, org.springframework.security.web.csrf.CsrfFilter@152c4495, org.springframework.security.web.authentication.logout.LogoutFilter@27443560, com.example.demofilter.JwtAuthorizationFilter@23df58c5, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@687e6293, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@771a7d53, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@17b6293e, org.springframework.security.web.access.ExceptionTranslationFilter@32eae6f2, org.springframework.security.web.access.intercept.AuthorizationFilter@50a13c2f]] (1/1)
2024-07-12T02:24:58.535+07:00 DEBUG 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Securing GET /private
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/13)
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/13)
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/13)
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/13)
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking CorsFilter (5/13)
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (6/13)
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2024-07-12T02:24:58.535+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (7/13)
2024-07-12T02:24:58.536+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST]
2024-07-12T02:24:58.536+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking JwtAuthorizationFilter (8/13)
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] w.c.HttpSessionSecurityContextRepository : Did not find SecurityContext in HttpSession 4F58A6CAF8282D5E6B283FA1B087D6B0 using the SPRING_SECURITY_CONTEXT session attribute
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (9/13)
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.s.w.s.HttpSessionRequestCache : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (10/13)
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (11/13)
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (12/13)
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (13/13)
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] estMatcherDelegatingAuthorizationManager : Authorizing GET /private
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] estMatcherDelegatingAuthorizationManager : Checking authorization on GET /private using org.springframework.security.authorization.AuthenticatedAuthorizationManager@469820d7
2024-07-12T02:24:58.538+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.s.w.a.AnonymousAuthenticationFilter : Did not set SecurityContextHolder since already authenticated JwtAuthenticationToken [Principal=UserProfile(id=1, firstName=Menkung, lastName=Iris, email=menkung@email.test.x, roles=[USER]), Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[USER]]
2024-07-12T02:24:58.538+07:00 DEBUG 33786 --- [demofilter] [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Secured GET /private
2024-07-12T02:24:58.539+07:00 TRACE 33786 --- [demofilter] [nio-8080-exec-7] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]

ซึ่งแน่นอนว่าเราจะสามารถเข้าส่วน private API ได้ แต่ส่วนที่เราอยากดูจะเป็น log ซึ่งเราจะเห็นการทำงานของ Filter เป็นลำดับและเห็นข้อมูล Authenticated ที่บรรทัดที่ 3 นับจากล่างสุด

Did not set SecurityContextHolder since already authenticated JwtAuthenticationToken 
[
Principal=UserProfile(id=1, firstName=Menkung, lastName=Iris, email=menkung@email.test.x, roles=[USER]),
Credentials=[PROTECTED],
Authenticated=true,
Details=null,
Granted Authorities=[USER]
]

เพียงเท่านี้เราจะการ Authenticated ด้วยของที่เรากำหนดเองจาก JwtAuthenticationToken แล้วครับ เรายังสามารถ Customize เพิ่มเติมตามความเหมาะสมของแต่ละโปรเจคได้เลยครับ

--

--