We introduced the first two chapters. Spring Boot Security Quick Start and Spring Boot JWT Quick Start This chapter uses JWT and Spring Boot Security components as a front-end and back-end separate authentication system. The code examples in this chapter are from Spring Boot Security + JWT Hello World Example.
This chapter does not use thymeleaf, but uses pure html and rest api directly.
- spring boot security
- jsonwebtoken
- jquery 1.11 +
Source code download of this project
1 New Spring Boot Maven Demonstration Project
- File > New > Project, select Spring Initializr as shown below and click Next for the next step
- Fill in GroupId (package name) and Artifact (project name). Click Next
groupId=com.fishpro
artifactId=securityjwt - Choose to rely on Spring Web Starter to tick ahead.
- The project name is set to spring-boot-study-security jwt.
2 Dependency Introducing Pom.xml
This paper introduces
- Spring Boot Security
- jsonwebtoken
<?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 http://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.1.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.fishpro</groupId> <artifactId>securityjwt</artifactId> <version>0.0.1-SNAPSHOT</version> <name>securityjwt</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</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>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3 Profile application
The port and jwt keys are configured
server: port: 8086 jwt: #jwt's secret key secret: javainuse
4. Establish a normal Hello Controller
package com.fishpro.securityjwt.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloWorldController { @RequestMapping({ "/hello" }) public String firstPage() { return "Hello World"; } }
At this point, access to localhost:8086/hello is normal because there is no permission requirement at this time
5 Establishing Jwt Request and Return Entities
5.1 JwtRequest request class
package com.fishpro.securityjwt.dto; import java.io.Serializable; public class JwtRequest implements Serializable { private static final long serialVersionUID = 5926468583005150707L; private String username; private String password; //need default constructor for JSON Parsing public JwtRequest() { } public JwtRequest(String username, String password) { this.setUsername(username); this.setPassword(password); } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } }
5.2 JwtResponse return class
package com.fishpro.securityjwt.dto; import java.io.Serializable; public class JwtResponse implements Serializable { private static final long serialVersionUID = -8091879091924046844L; private final String jwttoken; public JwtResponse(String jwttoken) { this.jwttoken = jwttoken; } public String getToken() { return this.jwttoken; } }
5.3 JwtUtil Operational Class
package com.fishpro.securityjwt.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; /** * jwt library * */ @Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -2550185165626007488L; public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; @Value("${jwt.secret}") private String secret; //retrieve username from jwt token public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } //retrieve expiration date from jwt token public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } //for retrieveing any information from token we will need the secret key private Claims getAllClaimsFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } //check if the token has expired private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } //generate token for user public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } //while creating the token - //1. Define claims of the token, like Issuer, Expiration, Subject, and the ID //2. Sign the JWT using the HS512 algorithm and secret key. //3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) // compaction of the JWT to a URL-safe string private String doGenerateToken(Map<String, Object> claims, String subject) { return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)) .signWith(SignatureAlgorithm.HS512, secret).compact(); } //validate token public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
5.4 JwtUserDetailsService
package com.fishpro.securityjwt.config; 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.security.provisioning.InMemoryUserDetailsManager; import org.springframework.stereotype.Service; import java.util.ArrayList; @Service public class JwtUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if ("javainuse".equals(username)) { return new User("javainuse", "$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6", new ArrayList<>()); } else { throw new UsernameNotFoundException("User not found with username: " + username); } } }
6 Redefining Authentication Entry Point page unauthorized unified return
Used to resolve anonymous user access to privileged resources when the exception
package com.fishpro.securityjwt.config; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Serializable; /** * AuthenticationEntryPoint Used to resolve anonymous user access to privileged resources when the exception * AccessDeineHandler Used to resolve exceptions when authenticated users access unauthorized resources * */ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -7858869558953243875L; //Send Unauthorized when an error occurs @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } }
7 JwtRequestFilter filter for validation of Jwt
package com.fishpro.securityjwt.config; import com.fishpro.securityjwt.util.JwtTokenUtil; import io.jsonwebtoken.ExpiredJwtException; 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.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Filters for Spring Boot Security * OncePerRequestFilter A request passes through a filter only once, without requiring repetitive execution * */ @Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private JwtUserDetailsService jwtUserDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); String username = null; String jwtToken = null; // JWT Token gets the Bearer of the request header // only the Token if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { jwtToken = requestTokenHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { System.out.println("JWT Token has expired"); } } else { logger.warn("JWT Token does not begin with Bearer String"); } // Verification if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username); // JWT validation is managed using Spring Security if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // After setting the Authentication in the context, we specify // that the current user is authenticated. So it passes the // Spring Security Configurations successfully. SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); } }
8 Defines the routes used to validate Jwt Token
package com.fishpro.securityjwt.config; import com.fishpro.securityjwt.dto.JwtRequest; import com.fishpro.securityjwt.dto.JwtResponse; import com.fishpro.securityjwt.util.JwtTokenUtil; 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.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.bind.annotation.*; /** * Used to verify that jwt returns to client jwt (json web token) * */ @RestController @CrossOrigin public class JwtAuthenticationController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private JwtUserDetailsService userDetailsService; /** * Get the username password from the client and use the secret key to encrypt it into json web token * */ @RequestMapping(value = "/authenticate", method = RequestMethod.POST) public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception { authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); final UserDetails userDetails = userDetailsService .loadUserByUsername(authenticationRequest.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return ResponseEntity.ok(new JwtResponse(token)); } /** * Get the username password from the client and use the secret key to encrypt it into json web token * */ private void authenticate(String username, String password) throws Exception { try { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (DisabledException e) { throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { throw new Exception("INVALID_CREDENTIALS", e); } } }
9 Define Web Security Config
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private UserDetailsService jwtUserDetailsService; @Autowired private JwtRequestFilter jwtRequestFilter; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { // configure AuthenticationManager so that it knows from where to load // user for matching credentials // Use BCryptPasswordEncoder auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // This example does not require the use of CSRF httpSecurity.csrf().disable() // Authentication pages do not require permissions .authorizeRequests().antMatchers("/authenticate").permitAll(). //Other pages anyRequest().authenticated().and(). //Login Page Simulator Client formLogin().loginPage("/login.html").permitAll().and(). // store user's state. exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement() //Do not use session .sessionCreationPolicy(SessionCreationPolicy.STATELESS); //Verify that the request is correct httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } }
10 login.html mock client
Note that when using ajax here, be sure to fill in the parameter contentType: "application/json;charset=UTF-8"
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Use jwt Login page</title> </head> <body> <div> <input id="userName" name="userName" value=""> </div> <div> <input id="password" name="password" value=""> </div> <div> <input type="button" id="btnSave" value="Sign in"> </div> <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script> <script> $(function() { $("#btnSave").click(function () { var username=$("#userName").val(); var password=$("#password").val(); $.ajax({ cache: true, type: "POST", url: "/authenticate", contentType: "application/json;charset=UTF-8", data:JSON.stringify({"username":username ,"password" : password}), dataType: "json", async: false, error: function (request) { console.log("Connection error"); }, success: function (data) { //save token localStorage.setItem("token",data.token); } }); }); }); </script> </body> </html>