The tutorial is Part 2 of the series: Angular Spring Boot JWT Authentication example | Angular 6 + Spring Security + MySQL Full Stack. Today we’re gonna build a SpringBoot Security RestAPIs that can interact with MySQL database.
– Part 1: Overview and Architecture.
– Part 3: Build Angular Frontend
Related Posts:
– Spring Boot + Angular 6 example | Spring Data JPA + REST + MySQL CRUD example
JWT Authentication with SpringBoot Security RestAPIs
Demo
Overview
Look back to the diagram for Spring Security/JWT classes that are separated into 3 layers:
– HTTP
– Spring Security
– REST API
For more details about this Architecture, please visit:
Spring Security – JWT Authentication Architecture | Spring Boot
Generate/Validate Token
We use a class named JwtProvider
. It gets username
from Authentication
object, then builds JWT Token with username
, Date()
object, secretKey
. JwtProvider
can also be used to validate JWT Token:
class JwtProvider { @Value("${ozenero.app.jwtSecret}") private String jwtSecret; @Value("${ozenero.app.jwtExpiration}") private int jwtExpiration; public String generateJwtToken(Authentication authentication) { UserPrinciple userPrincipal = (UserPrinciple) authentication.getPrincipal(); return Jwts.builder() .setSubject((userPrincipal.getUsername())) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()).getTime() + jwtExpiration*1000)) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } public boolean validateJwtToken(String authToken) { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); return ...; } public String getUserNameFromJwtToken(String token) { return Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody().getSubject(); } }
Filter the Request
We add our JwtAuthTokenFilter
(that extends Spring OncePerRequestFilter
abstract class) to the chain of filters.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { ... http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } }
JwtAuthTokenFilter
validates the Token using JwtProvider
:
class JwtAuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtProvider tokenProvider; @Override protected void doFilterInternal(...) { String jwt = getJwt(request); if (jwt!=null && tokenProvider.validateJwtToken(jwt)) { ... } filterChain.doFilter(request, response); } }
Now we have 2 cases:
– Login/SignUp: RestAPI with non-protected APIs -> authenticate Login Request with AuthenticationManager
, if error occurs, handle AuthenticationException
with AuthenticationEntryPoint
.
– With protected Resources:
+ jwt
token is null/invalid -> if Authenticated Error occurs, handle AuthenticationException
with AuthenticationEntryPoint
.
+ jwt
token is valid -> from token, get User information, then create AuthenticationToken
.
Create AuthenticationToken from Token
JwtAuthTokenFilter
extracts username/password from the received token using JwtProvider
, then based on the extracted data, JwtAuthTokenFilter
:
– creates a AuthenticationToken
(that implements Authentication
)
– uses the AuthenticationToken
as Authentication
object and stores it in the SecurityContext
for future filter uses (e.g: Authorization filters).
In this tutorial, we use UsernamePasswordAuthenticationToken
:
// extract user information String username = tokenProvider.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); // create AuthenticationToken UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
Store Authentication object in SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder
is the most fundamental object where we store details of the present security context of the application (includes details of the principal). Spring Security uses an Authentication
object to represent this information and we can query this Authentication
object from anywhere in our application:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // currently authenticated user Object principal = authentication.getPrincipal();
getContext()
returns an instance of SecurityContext
interface that holds the Authentication
and possibly request-specific security information.
Authenticate with AuthenticationProvider
These are some authentication providers that Spring Framework provides, in this example, we use DaoAuthenticationProvider
. This Provider works well with form-based logins or HTTP Basic authentication which submits a simple username/password authentication request.
It authenticates the User simply by comparing the password submitted in a UsernamePasswordAuthenticationToken
against the one loaded by the UserDetailsService
(as a DAO):
@Autowired AuthenticationManager authenticationManager; ... Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password) );
Configuring this provider is simple with AuthenticationManagerBuilder
:
class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceImpl userDetailsService; @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
For more details about AuthenticationManager
& AuthenticationProvider
, please visit:
Delegate AuthenticationToken for AuthenticationManagager.
Retrieve User details with UserDetailsService
We can obtain a principal from the Authentication
object. This principal can be cast into a UserDetails
object to lookup the username, password and GrantedAuthority
s.
Therefore, after authenticating is successful, we can simply get UserDetails
from Authentication
object:
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); // userDetails.getUsername() // userDetails.getPassword() // userDetails.getAuthorities()
DaoAuthenticationProvider
also uses UserDetailsService
for getting UserDetails
object. This is the common approach in which we only pass a String-based ‘username’ argument and returns a UserDetails
:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
It is simple to implement UserDetailsService
and easy for us to retrieve authentication information using a persistence strategy:
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserRepository userRepository; @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username).orElseThrow( () -> new UsernameNotFoundException("User Not Found with -> username or email : " + username)); return UserPrinciple.build(user); // UserPrinciple implements UserDetails } }
Protect Resources with HTTPSecurity & Method Security Expressions
Configure HTTPSecurity
To help Spring Security know when we want to require all users to be authenticated, which Exception Handler to be chosen, which filter and when we want it to work. We implement WebSecurityConfigurerAdapter
and provide a configuration in the configure(HttpSecurity http)
method:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() ...; http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } }
Method Security Expressions
Spring Security provides some annotations for pre and post-invocation authorization checks, filtering of submitted collection arguments or return values: @PreAuthorize
, @PreFilter
, @PostAuthorize
and @PostFilter
.
To enable Method Security Expressions, we use @EnableGlobalMethodSecurity
annotation:
@EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }
In the code below, we use the most useful annotation @PreAuthorize
to decide whether a method can actually be invoked or not:
@RestController public class TestRestAPIs { @GetMapping("/api/test/user") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") public String userAccess() { return ">>> User Contents!"; } @GetMapping("/api/test/pm") @PreAuthorize("hasRole('PM') or hasRole('ADMIN')") public String projectManagementAccess() { return ">>> Project Management Board"; } @GetMapping("/api/test/admin") @PreAuthorize("hasRole('ADMIN')") public String adminAccess() { return ">>> Admin Contents"; } }
Handle AuthenticationException – AuthenticationEntryPoint
If the user requests a secure HTTP resource without being authenticated, AuthenticationEntryPoint
will be called. At this time, an AuthenticationException
is thrown, commence()
method on the entry point is triggered:
@Component public class JwtAuthEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { logger.error("Unauthorized error. Message - {}", e.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized"); } }
Spring Boot server for JWT Authentication Overview
Goal
The diagram below show how our system handles User Registration and User Login processes:
– We expose 2 RestAPIs to signup and signin:
/api/auth/signup
-> sign up/api/auth/signin
-> sign in
– We expose 3 RestAPIs to test protected resources:
@GetMapping("/api/test/user") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") public String userAccess() { return ">>> User Contents!"; } @GetMapping("/api/test/pm") @PreAuthorize("hasRole('PM') or hasRole('ADMIN')") public String projectManagementAccess() { return ">>> Board Management Project"; } @GetMapping("/api/test/admin") @PreAuthorize("hasRole('ADMIN')") public String adminAccess() { return ">>> Admin Contents"; }
- Access Successfully ->
- Unauthorized ->
Technologies
– Spring Boot 2
– jjwt – 0.9.0
– Spring Security
– Spring JPA
– MySQL
Project Structure
– model
package defines 2 entities User
& Role
that have many-to-many relationship:
– repository
package contains interfaces that use Hibernate JPA to store/retrieve data from MySQL database.
– controller
package defines RestAPIs for user signup/signin and testing protected resources that is secured with JWT.
– message
package defines payload data transferred from user agents (Browser/RestClient…) to RestAPIs and message back.
– security
package is the main part of the project that implements JWT security.
Practice
Create SpringBoot project
Dependency for the Project:
org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime io.jsonwebtoken jjwt 0.9.0
Create Models
– User
model includes 5 attributes:
- id
- name
- username
- password
model/User.java
package com.ozenero.jwtauthentication.model; import java.util.HashSet; import java.util.Set; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import org.hibernate.annotations.NaturalId; @Entity @Table(name = "users", uniqueConstraints = { @UniqueConstraint(columnNames = { "username" }), @UniqueConstraint(columnNames = { "email" }) }) public class User{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank @Size(min=3, max = 50) private String name; @NotBlank @Size(min=3, max = 50) private String username; @NaturalId @NotBlank @Size(max = 50) @Email private String email; @NotBlank @Size(min=6, max = 100) private String password; @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) private Setroles = new HashSet<>(); public User() {} public User(String name, String username, String email, String password) { this.name = name; this.username = username; this.email = email; this.password = password; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Set getRoles() { return roles; } public void setRoles(Set roles) { this.roles = roles; } }
– Role
model with 2 attributes:
- id
- rolename
model/Role.java
package com.ozenero.jwtauthentication.model; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import org.hibernate.annotations.NaturalId; @Entity @Table(name = "roles") public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Enumerated(EnumType.STRING) @NaturalId @Column(length = 60) private RoleName name; public Role() {} public Role(RoleName name) { this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public RoleName getName() { return name; } public void setName(RoleName name) { this.name = name; } }
model/RoleName.java
package com.ozenero.jwtauthentication.model; public enum RoleName { ROLE_USER, ROLE_PM, ROLE_ADMIN }
Implement Repository
repository/UserRepository.java
package com.ozenero.jwtauthentication.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.ozenero.jwtauthentication.model.User; @Repository public interface UserRepository extends JpaRepository{ Optional findByUsername(String username); Boolean existsByUsername(String username); Boolean existsByEmail(String email); }
repository/UserRepository.java
package com.ozenero.jwtauthentication.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.ozenero.jwtauthentication.model.Role; import com.ozenero.jwtauthentication.model.RoleName; @Repository public interface RoleRepository extends JpaRepository{ Optional findByName(RoleName roleName); }
For more details about Spring JPA with MySQL, please visit:
How to use Spring JPA MySQL | Spring Boot
Implement JWT Security
security/WebSecurityConfig.java
package com.ozenero.jwtauthentication.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 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.authentication.UsernamePasswordAuthenticationFilter; import com.ozenero.jwtauthentication.security.jwt.JwtAuthEntryPoint; import com.ozenero.jwtauthentication.security.jwt.JwtAuthTokenFilter; import com.ozenero.jwtauthentication.security.services.UserDetailsServiceImpl; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( prePostEnabled = true ) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceImpl userDetailsService; @Autowired private JwtAuthEntryPoint unauthorizedHandler; @Bean public JwtAuthTokenFilter authenticationJwtTokenFilter() { return new JwtAuthTokenFilter(); } @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } }
Create UserDetails Service
– UserDetailsServiceImpl
implements UserDetailsService
and overrides loadUserByUsername()
method.
loadUserByUsername
method finds a record from users
database tables to build a UserDetails
object for authentication.
security/services/UserDetailsServiceImpl.java
package com.ozenero.jwtauthentication.security.services; import com.ozenero.jwtauthentication.model.User; import com.ozenero.jwtauthentication.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; 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 org.springframework.transaction.annotation.Transactional; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserRepository userRepository; @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username).orElseThrow( () -> new UsernameNotFoundException("User Not Found with -> username or email : " + username)); return UserPrinciple.build(user); } }
– UserPrinciple
will implement UserDetails
.
UserPrinciple
is not used directly by Spring Security for security purposes.
It simply stores user information which is later encapsulated into Authentication
objects. This allows non-security related user information (such as email addresses, telephone numbers etc) to be stored.
security/services/UserPrinciple.java
package com.ozenero.jwtauthentication.security.services; import com.ozenero.jwtauthentication.model.User; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public class UserPrinciple implements UserDetails { private static final long serialVersionUID = 1L; private Long id; private String name; private String username; private String email; @JsonIgnore private String password; private Collection extends GrantedAuthority> authorities; public UserPrinciple(Long id, String name, String username, String email, String password, Collection extends GrantedAuthority> authorities) { this.id = id; this.name = name; this.username = username; this.email = email; this.password = password; this.authorities = authorities; } public static UserPrinciple build(User user) { Listauthorities = user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName().name()) ).collect(Collectors.toList()); return new UserPrinciple( user.getId(), user.getName(), user.getUsername(), user.getEmail(), user.getPassword(), authorities ); } public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public Collection extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserPrinciple user = (UserPrinciple) o; return Objects.equals(id, user.id); } }
JWT Authentication Classes
– JwtAuthTokenFilter
extends OncePerRequestFilter
.
org.springframework.web.filter.OncePerRequestFilter
-> Executes once per request. This is a filter base class that is used to guarantee a single execution per request dispatch. It provides a doFilterInternal
method with HttpServletRequest
and HttpServletResponse
arguments.
Inside JwtAuthTokenFilter
class, the doFilterInternal
method will:
- get
JWT
token from header - validate
JWT
- parse
username
from validatedJWT
- load data from
users
table, then build anauthentication
object - set the
authentication
object to Security Context
security/jwt/JwtAuthTokenFilter.java
package com.ozenero.jwtauthentication.security.jwt; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.web.filter.OncePerRequestFilter; import com.ozenero.jwtauthentication.security.services.UserDetailsServiceImpl; public class JwtAuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtProvider tokenProvider; @Autowired private UserDetailsServiceImpl userDetailsService; private static final Logger logger = LoggerFactory.getLogger(JwtAuthTokenFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwt(request); if (jwt != null && tokenProvider.validateJwtToken(jwt)) { String username = tokenProvider.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("Can NOT set user authentication -> Message: {}", e); } filterChain.doFilter(request, response); } private String getJwt(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { return authHeader.replace("Bearer ", ""); } return null; } }
– JwtAuthEntryPoint
is used to handle Error exception when having unauthorized
requests.
security/jwt/JwtAuthEntryPoint.java
package com.ozenero.jwtauthentication.security.jwt; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; @Component public class JwtAuthEntryPoint implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { logger.error("Unauthorized error. Message - {}", e.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized"); } }
– JwtProvider
is an util class -> it implements useful functions:
- generate a
JWT
token - valiate a
JWT
token - parse
username
fromJWT
token
security/jwt/JwtProvider.java
package com.ozenero.jwtauthentication.security.jwt; import io.jsonwebtoken.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import com.ozenero.jwtauthentication.security.services.UserPrinciple; import java.util.Date; @Component public class JwtProvider { private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class); @Value("${ozenero.app.jwtSecret}") private String jwtSecret; @Value("${ozenero.app.jwtExpiration}") private int jwtExpiration; public String generateJwtToken(Authentication authentication) { UserPrinciple userPrincipal = (UserPrinciple) authentication.getPrincipal(); return Jwts.builder() .setSubject((userPrincipal.getUsername())) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()).getTime() + jwtExpiration*1000)) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } public boolean validateJwtToken(String authToken) { try { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); return true; } catch (SignatureException e) { logger.error("Invalid JWT signature -> Message: {} ", e); } catch (MalformedJwtException e) { logger.error("Invalid JWT token -> Message: {}", e); } catch (ExpiredJwtException e) { logger.error("Expired JWT token -> Message: {}", e); } catch (UnsupportedJwtException e) { logger.error("Unsupported JWT token -> Message: {}", e); } catch (IllegalArgumentException e) { logger.error("JWT claims string is empty -> Message: {}", e); } return false; } public String getUserNameFromJwtToken(String token) { return Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody().getSubject(); } }
Implement RestControllers
Create Payload Message
– LoginForm
with username
& password
.
message/request/LoginForm.java
package com.ozenero.jwtauthentication.message.request; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; public class LoginForm { @NotBlank @Size(min=3, max = 60) private String username; @NotBlank @Size(min = 6, max = 40) private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
– SignUpForm
includes:
- name
- username
- role
- password
message/request/SignUpForm.java
package com.ozenero.jwtauthentication.message.request; import java.util.Set; import javax.validation.constraints.*; public class SignUpForm { @NotBlank @Size(min = 3, max = 50) private String name; @NotBlank @Size(min = 3, max = 50) private String username; @NotBlank @Size(max = 60) @Email private String email; private Setrole; @NotBlank @Size(min = 6, max = 40) private String password; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Set getRole() { return this.role; } public void setRole(Set role) { this.role = role; } }
– JwtResponse
object will be returned by SpringBoot server once an authentication is successful, it contains:
- JWT Token
- Schema Type of Token
- Username
- Array of User’s Authorities
message/response/JwtResponse.java
package com.ozenero.jwtauthentication.message.response; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; public class JwtResponse { private String token; private String type = "Bearer"; private String username; private Collection extends GrantedAuthority> authorities; public JwtResponse(String accessToken, String username, Collection extends GrantedAuthority> authorities) { this.token = accessToken; this.username = username; this.authorities = authorities; } public String getAccessToken() { return token; } public void setAccessToken(String accessToken) { this.token = accessToken; } public String getTokenType() { return type; } public void setTokenType(String tokenType) { this.type = tokenType; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Collection extends GrantedAuthority> getAuthorities() { return authorities; } }
– ResponseMessage
object is just a message object.
message/response/ResponseMessage.java
package com.ozenero.jwtauthentication.message.response; public class ResponseMessage { private String message; public ResponseMessage(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
RestAPIs Controller
– AuthRestAPIs
defines 2 APIs:
/api/auth/signup
: sign up
-> check username/email is already in use.
-> createUser
object
-> store to database/api/auth/signin
: sign in
-> attempt to authenticate withAuthenticationManager
bean.
-> addauthentication
object toSecurityContextHolder
-> GenerateJWT
token, then returnJWT
to client
controller/AuthRestAPIs.java
package com.ozenero.jwtauthentication.controller; import java.util.HashSet; import java.util.Set; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.ozenero.jwtauthentication.message.request.LoginForm; import com.ozenero.jwtauthentication.message.request.SignUpForm; import com.ozenero.jwtauthentication.message.response.JwtResponse; import com.ozenero.jwtauthentication.message.response.ResponseMessage; import com.ozenero.jwtauthentication.model.Role; import com.ozenero.jwtauthentication.model.RoleName; import com.ozenero.jwtauthentication.model.User; import com.ozenero.jwtauthentication.repository.RoleRepository; import com.ozenero.jwtauthentication.repository.UserRepository; import com.ozenero.jwtauthentication.security.jwt.JwtProvider; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api/auth") public class AuthRestAPIs { @Autowired AuthenticationManager authenticationManager; @Autowired UserRepository userRepository; @Autowired RoleRepository roleRepository; @Autowired PasswordEncoder encoder; @Autowired JwtProvider jwtProvider; @PostMapping("/signin") public ResponseEntity> authenticateUser(@Valid @RequestBody LoginForm loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtProvider.generateJwtToken(authentication); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getUsername(), userDetails.getAuthorities())); } @PostMapping("/signup") public ResponseEntity> registerUser(@Valid @RequestBody SignUpForm signUpRequest) { if (userRepository.existsByUsername(signUpRequest.getUsername())) { return new ResponseEntity<>(new ResponseMessage("Fail -> Username is already taken!"), HttpStatus.BAD_REQUEST); } if (userRepository.existsByEmail(signUpRequest.getEmail())) { return new ResponseEntity<>(new ResponseMessage("Fail -> Email is already in use!"), HttpStatus.BAD_REQUEST); } // Creating user's account User user = new User(signUpRequest.getName(), signUpRequest.getUsername(), signUpRequest.getEmail(), encoder.encode(signUpRequest.getPassword())); SetstrRoles = signUpRequest.getRole(); Set roles = new HashSet<>(); strRoles.forEach(role -> { switch (role) { case "admin": Role adminRole = roleRepository.findByName(RoleName.ROLE_ADMIN) .orElseThrow(() -> new RuntimeException("Fail! -> Cause: User Role not find.")); roles.add(adminRole); break; case "pm": Role pmRole = roleRepository.findByName(RoleName.ROLE_PM) .orElseThrow(() -> new RuntimeException("Fail! -> Cause: User Role not find.")); roles.add(pmRole); break; default: Role userRole = roleRepository.findByName(RoleName.ROLE_USER) .orElseThrow(() -> new RuntimeException("Fail! -> Cause: User Role not find.")); roles.add(userRole); } }); user.setRoles(roles); userRepository.save(user); return new ResponseEntity<>(new ResponseMessage("User registered successfully!"), HttpStatus.OK); } }
– TestRestAPIs
define 3 RestAPIs:
/api/test/user
-> access by users hasUSER_ROLE
orADMIN_ROLE
/api/test/pm
-> access by users hasUSER_PM
orADMIN_ROLE
/api/test/admin
-> access by users hasADMIN_ROLE
controller/TestRestAPIs.java
package com.ozenero.jwtauthentication.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @CrossOrigin(origins = "*", maxAge = 3600) @RestController public class TestRestAPIs { @GetMapping("/api/test/user") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") public String userAccess() { return ">>> User Contents!"; } @GetMapping("/api/test/pm") @PreAuthorize("hasRole('PM') or hasRole('ADMIN')") public String projectManagementAccess() { return ">>> Project Management Board"; } @GetMapping("/api/test/admin") @PreAuthorize("hasRole('ADMIN')") public String adminAccess() { return ">>> Admin Contents"; } }
Configure Spring Datasource, JPA and define App Properties
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false spring.datasource.username=root spring.datasource.password=123456 spring.jpa.generate-ddl=true # App Properties ozenero.app.jwtSecret=jwtGrokonezSecretKey ozenero.app.jwtExpiration=86400
Run & Check Results
Start SpringBoot
– Start Springboot server by commandline mvn spring-boot:run
– Check database tables ->
– Insert data to roles
table ->
INSERT INTO roles(name) VALUES('ROLE_USER'); INSERT INTO roles(name) VALUES('ROLE_PM'); INSERT INTO roles(name) VALUES('ROLE_ADMIN');
SignUp
Sign-Up 3 users:
- Jack has
ROLE_USER
role - Adam has
ROLE_PM
&ROLE_USER
roles - Thomas has
ROLE_ADMIN
role
– Check database’s tables ->
SignIn and Access Protected Resources
– Jack can access api/test/user
url, can NOT access others.
-> Sign In:
-> Access Protected Resources:
– Thomas can access all URLs.
-> Sign In:
-> Access Protected Resources:
– Adam can access api/test/user
and api/test/pm
url.
Can NOT access /api/test/admin
url.