{getToc} $title={Table of Contents}
In postman tool Header Authorization, we got jwt token authoriztion key after successfully reequested.
That's it. Happy Coding :)
Spring Security & JWT Authentication |
Introduction
This is the blog about how to create spring boot maven project and implementing Spring Security with JJWT Web Token as the authentication token.
I'm going to setup spring boot maven project from scratch. And adding the
neccessary dependencies as needed step by step and implement code line by
using postman request testing for each step.
Using Java, Spring, Other Libs & JJWT:
- java: 11
- spring boot: V2.7.3
- spring-web: 5.3.22
- spring-security-web: 5.7.3
- jjwt: V0.11.5
- loombox: 1.18.24
Simple Project Classes & Packages Structured
Spring Boot MVN Project Class&Packages Structured |
What Are JWTs?
JWTs are an encoded representation of a JSON object. The JSON object
consists of zero or more name/value pairs, where the names are strings and
the values are arbitrary JSON values. JWT is useful to send such information
in the clear (for example in an URL) while it can still be trusted to be
unreadable (i.e. encrypted), unmodifiable (i.e. signed) and url-safe (i.e.
Base64 encoded).
Java JWT: JSON Web Token for Java and Android
JJWT aims to be the easiest to use and understand library for creating and
verifying JSON Web Tokens (JWTs) on the JVM and Android.
JJWT is a pure Java implementation based exclusively on the
JWT,
JWS,
JWE,
JWK
and
JWA
RFC specifications and open source under the terms of the
Apache 2.0 License.
The library was created by
Les Hazlewood
and is supported and maintained by a
community
of contributors.
This Project Dependencies
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.3</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>spring-boot-security-jwt-auth</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-security-jwt-auth</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </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> <!-- or jjwt-gson if Gson is preferred --> <version>0.11.5</version> <scope>runtime</scope> </dependency> <!-- Uncomment this next dependency if you are using JDK 10 or earlier and you also want to use RSASSA-PSS (PS256, PS384, PS512) algorithms. JDK 11 or later does not require it for those algorithms: <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> <scope>runtime</scope> </dependency> --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
Understanding JJWT Dependencies
Notice the above dependency declarations all have only one compile-time
dependency and the rest are declared as runtime dependencies.
This is because JJWT is designed so you only depend on the APIs that are
explicitly designed for you to use in your applications and all other
internal implementation details - that can change without warning - are
relegated to runtime-only dependencies. This is an extremely important point
if you want to ensure stable JJWT usage and upgrades over time:
JJWT guarantees semantic versioning compatibility for all of its artifacts
except the jjwt-impl .jar. No such guarantee is made for the jjwt-impl .jar
and internal changes in that .jar can happen at any time. Never add the
jjwt-impl .jar to your project with compile scope - always declare it with
runtime scope.
This is done to benefit you: great care goes into curating the jjwt-api .jar
and ensuring it contains what you need and remains backwards compatible as
much as is possible so you can depend on that safely with compile scope. The
runtime jjwt-impl .jar strategy affords the JJWT developers the flexibility
to change the internal packages and implementations whenever and however
necessary. This helps us implement features, fix bugs, and ship new releases
to you more quickly and efficiently.
Security Configuration
In SecurityConfiguration class, I have custom user & password storage
(UserDetailsService) by using spring security UserDetailsService.
@EnableWebSecurity @EnableMethodSecurity public class SecurityConfiguration { @Autowired @Lazy private LoginFilter loginFilter; @Autowired private JwtAuthFilter jwtAuthFilter; @Bean UserDetailsService userDetailsService() { var userDt = new InMemoryUserDetailsManager(); userDt.createUser(User.builder().username("user").password("{noop}user").roles("USER").build()); userDt.createUser(User.builder().username("admin").password("{noop}admin").roles("USER", "ADMIN").build()); return userDt; } @Bean AuthenticationManager authenticationManager(UserDetailsService userDetailsService) { var daoAuth = new DaoAuthenticationProvider(); daoAuth.setUserDetailsService(userDetailsService); return new ProviderManager(daoAuth); } /* Expose AuthenticationManager bean */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable(); http.authorizeHttpRequests().anyRequest().authenticated(); http.addFilterAt(loginFilter, BasicAuthenticationFilter.class); http.addFilterAfter(jwtAuthFilter, BasicAuthenticationFilter.class); http.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS); /* Custom error message */ http.exceptionHandling() .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().print(accessDeniedException.getMessage()); }).authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().print(authException.getMessage()); }); return http.build(); } }
I have used In-memory data-store
@Lazy to avoid cyclic dependencies error.
Prefix which specifies hashing algorithm. Example:
* {noop}: use raw password
* {bcrypt}: use Bcrypt algorithm
Expose AuthenticationManager bean to create LoginFilter (user, pass
authentication).
@Bean AuthenticationManager authenticationManager(UserDetailsService userDetailsService) { var daoAuth = new DaoAuthenticationProvider(); daoAuth.setUserDetailsService(userDetailsService); return new ProviderManager(daoAuth); }
Due to WebSecurityConfigurerAdapter is deprecated since version 5.7, I
have created the Component-based security configuration using
SecurityFilterChain.
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable(); http.authorizeHttpRequests().anyRequest().authenticated(); http.addFilterAt(loginFilter, BasicAuthenticationFilter.class); http.addFilterAfter(jwtAuthFilter, BasicAuthenticationFilter.class); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); /* Custom error message */ http.exceptionHandling() .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().print(accessDeniedException.getMessage());/* Define your own message */ }).authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().print(authException.getMessage());/* Define your own message */ }); return http.build(); }
The SessionCreationPolicy.STATELESS is mean Token-based
authentication does not require session.
I have created the Custom error message exceptionHandling.
For the simple message, I use lambda instead of create new class and implement
AccessDeniedHandler.
You can Define your own message.
Before using JJWT Token, I have add filter by using simeple username &
password login:
http.addFilterAt(loginFilter, BasicAuthenticationFilter.class)
In SecurityConfiguration, I have using spring @Autowired to
inject LoginFilter class, and using @Lazy to avoid cyclic
dependencies error.
@Autowired @Lazy private LoginFilter loginFilter;
I have injected JwtAuthFilter in order to add
JJWT BasicAuthenticationFilter filter authorization.
@Autowired private JwtAuthFilter jwtAuthFilter;
Login Filter
I prefer Filter to handle login action, you can create rest controller with
login end-point instead.
public class LoginFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { var username = request.getHeader("username"); var password = request.getHeader("password"); var authenticated = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password) ); //response.setHeader(HttpHeaders.AUTHORIZATION, username); /* Add jwt-token for login */ response.setHeader(HttpHeaders.AUTHORIZATION, generateJwtAuthToken(authenticated)); } private String generateJwtAuthToken(Authentication authentication) { var user = (User) authentication.getPrincipal(); var roles = user.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return jwtAuthHelper.generateJwtAuthToken(user.getUsername(), Map.of("roles", roles)); } @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { var reqMethod = request.getMethod(); var reqUri = request.getRequestURI(); var isLogin = HttpMethod.POST.matches(reqMethod) && reqUri.startsWith("/login"); return !isLogin; } }
I have extended OncePerRequestFilter: Filter base class that
aims to guarantee a single execution per requestdispatch, on any servlet
container. It provides a doFilterInternalmethod with HttpServletRequest
and HttpServletResponse arguments.
Overided the shouldNotFilter method to make sure this filter only
with login request.
Jwt Auth Helper
@Component public class JwtAuthHelper { private final Key key; public JwtAuthHelper(@Value("${jwt.secret.key}") String secretKey) { key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); } public String generateJwtAuthToken(String subject, Map<String, Object> claims) { return Jwts.builder().setSubject(subject).addClaims(claims) .setExpiration(Date.from(Instant.now().plus(5, ChronoUnit.MINUTES))).signWith(key).compact(); } public Map<String, Object> parseClaims(String token) { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); } }
I have set the Expired in 5mns since login.
SecretKey Formats
If you want to sign a JWS using HMAC-SHA algorithms and you have a secret key
String or encoded byte array, you will need to convert it into a SecretKey
instance to use as the signWith method argument.
I have used online random tools to generate random string for secret key.
I have set the key property in application.properties file.
jwt.secret.key=Iwr4oPxIR3tnGMDP0p8z8BXxWxk1ahdJHA3BpbESz2hpS1xLWalwMRMKSJESfLnt
A raw (non-encoded) string (e.g. a password String):
key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
It is always incorrect to call secretString.getBytes() (without providing
a charset).
However, raw password strings like this, e.g. correcthorsebatterystaple
should be avoided whenever possible because they can inevitably result in
weak or susceptible keys. Secure-random keys are almost always stronger.
If you are able, prefer creating a
new secure-random secret key
instead.
Jwt Auth Helper
To create Jwt Filter (jwt authentication).
@Component @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { private final JwtAuthHelper jwtAuthHelper; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { var getToken = getToken(request); var claims = jwtAuthHelper.parseClaims((String) getToken); SecurityContextHolder.getContext().setAuthentication(createJwtAuthentication(claims)); filterChain.doFilter(request, response); } private Authentication createJwtAuthentication(Map<String, Object> claims) { var roles = Arrays.stream(claims.get("roles").toString().split(",")).map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return new UsernamePasswordAuthenticationToken(claims.get(Claims.SUBJECT), null, roles); } private Object getToken(HttpServletRequest request) { return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)) .filter(auth -> auth.startsWith("Bearer ")).map(auth -> auth.replace("Bearer ", "")) .orElseThrow(() -> new BadCredentialsException("Invalid token.")); } @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { return request.getRequestURI().startsWith("/login"); } }
The same as in Login Filter class, I have
extended OncePerRequestFilter: Filter base class that aims
to guarantee a single execution per requestdispatch, on any servlet
container. It provides a doFilterInternalmethod with
HttpServletRequest and HttpServletResponse arguments.
Overided the shouldNotFilter method to make sure this
filter only with login request.
Claims
Claims are a JWT's 'body' and contain the information that the JWT creator
wishes to present to the JWT recipient(s).
Set authenticated user to context if token is valid (no exception when
parsing).
SecurityContextHolder.getContext().setAuthentication(createJwtAuthentication(claims));
filterChain.doFilter(request, response);
Login Controller
In LoginController class, I have created with 3 end-points:
- Welcom home page.
- Simple user page.
- And admin page.
@RestController @Slf4j public class LoginController { @GetMapping("/") public String homePage() { return "Home page."; } @GetMapping("/user") @PreAuthorize("hasRole('USER')") public String userPage(Authentication authentication) { log.info("Username={}, roles={}", authentication.getPrincipal(), authentication.getAuthorities()); return "User page."; } @GetMapping("/admin") @PreAuthorize("hasRole('ADMIN')") public String adminPage(Authentication authentication) { log.info("Username={}, roles={}", authentication.getPrincipal(), authentication.getAuthorities()); return "Admin page"; } }
- Enable annotation (ex: @PreAuthorize) to protect method.
- Using Authentication to access authentication object Spring auto
inject Authenticated user from SecurityContext
@PreAuthorize("hasRole('USER')"): using Spring-EL expression.
Postman Request
I'm using postman tool this case.
This request is using Admin user to get Jwt Token authorization key.
Request jwt token by Admin user |
In postman tool Header Authorization, we got jwt token authoriztion key after successfully reequested.
eyJhbGciOiJIUzUxMiJ9.
eyJzdWIiOiJhZG1pbiIsInJvbGVzIjoiUk9MRV9BRE1JTixST0xFX1VTRVIiLCJleHAiOjE2NjE0MTcwNjR9.
f6ANIOdcc9Qn_4NEhJYyI_fzKz13tuRfDINpzYwstWbL61vVFdAVTaJzPMHazIm4cFsGNlOTD4wfdToMLg0nRQ
Now Let's use then admin authorizatin key to access admin page.
Access Admin page by admin authorizaton jwt key |
Let's see log after request access via postman, the log which we have already
create in controller.
access page logging |
Let's check the access token data in JSON Web Tokens - jwt.io
And when you use simple user token key to access admin page, then it will
response with access denied.
access denied |
That's it. Happy Coding :)
Conclusion
Hope this article was helpful.
Git source code : https://github.com/Yuth-Set/Spring-Boot-Security-Jwt-Authenticatoin