原文地址:http://www.svlada.com/jwt-token-authentication-with-spring-boot/html
This article will guide you on how you can implement JWT authentication with Spring Boot.java
We will cover the following two scenarios:ios
Please check out the sample code/project from the following GitHub repository: https://github.com/svlada/springboot-security-jwt before going further reading the article.git
This project is using H2 in-memory database to store sample user information. To make things easier I have created data fixtures and configured Spring Boot to automatically load them on the application startup (/jwt-demo/src/main/resources/data.sql
).github
Overall project structure is shown below:web
+---main | +---java | | \---com | | \---svlada | | +---common | | +---entity | | +---profile | | | \---endpoint | | +---security | | | +---auth | | | | +---ajax | | | | \---jwt | | | | +---extractor | | | | \---verifier | | | +---config | | | +---endpoint | | | +---exceptions | | | \---model | | | \---token | | \---user | | +---repository | | \---service | \---resources | +---static | \---templates
When we talk about Ajax authentication we usually refer to process where user is supplying credentials through JSON payload that is sent as a part of XMLHttpRequest.ajax
In the first part of this tutorial Ajax authentication is implemented by following standard patterns found in the Spring Security framework.spring
Following is the list of components that we'll implement:sql
AjaxLoginProcessingFilter
AjaxAuthenticationProvider
AjaxAwareAuthenticationSuccessHandler
AjaxAwareAuthenticationFailureHandler
RestAuthenticationEntryPoint
WebSecurityConfig
Before we get to the details of the implementation, let's look at the request/response authentication flow.json
Ajax authentication request example
The Authentication API allows user to pass in credentials in order to receive authentication token.
In our example, client initiates authentication process by invoking Authentication API endpoint (/api/auth/login
).
Raw HTTP request:
POST /api/auth/login HTTP/1.1 Host: localhost:9966 X-Requested-With: XMLHttpRequest Content-Type: application/json Cache-Control: no-cache { "username": "svlada@gmail.com", "password": "test1234" }
CURL:
curl -X POST -H "X-Requested-With: XMLHttpRequest" -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{ "username": "svlada@gmail.com", "password": "test1234" }' "http://localhost:9966/api/auth/login"
Ajax authentication response example
If client supplied credentials are valid, Authentication API will respond with the HTTP response including the following details:
JWT Access token - used to authenticate against protected API resources. It must be set in X-Authorization
header.
JWT Refresh token - used to acquire new Access Token. Token refresh is handled by the following API endpoint: /api/auth/token
.
Raw HTTP Response:
{ "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMDMzMzA4LCJleHAiOjE0NzIwMzQyMDh9.41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ", "refreshToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfUkVGUkVTSF9UT0tFTiJdLCJpc3MiOiJodHRwOi8vc3ZsYWRhLmNvbSIsImp0aSI6IjkwYWZlNzhjLTFkMmUtNDg2OS1hNzdlLTFkNzU0YjYwZTBjZSIsImlhdCI6MTQ3MjAzMzMwOCwiZXhwIjoxNDcyMDM2OTA4fQ.SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg" }
JWT Access Token
JWT Access token can be used for authentication and authorization:
Decoded JWT Access token has three parts: Header, Claims and Signature as shown below:
Header
{ "alg": "HS512" }
Claims
{ "sub": "svlada@gmail.com", "scopes": [ "ROLE_ADMIN", "ROLE_PREMIUM_MEMBER" ], "iss": "http://svlada.com", "iat": 1472033308, "exp": 1472034208 }
Signature (base64 encoded)
41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ
JWT Refresh Token
Refresh token is long-lived token used to request new Access tokens. It's expiration time is greater than expiration time of Access token.
In this tutorial we'll use jti
claim to maintain list of blacklisted or revoked tokens. JWT ID(jti
) claim is defined by RFC7519 with purpose to uniquely identify individual Refresh token.
Decoded Refresh token has three parts: Header, Claims and Signature as shown below:
Header
{ "alg": "HS512" }
Claims
{ "sub": "svlada@gmail.com", "scopes": [ "ROLE_REFRESH_TOKEN" ], "iss": "http://svlada.com", "jti": "90afe78c-1d2e-4869-a77e-1d754b60e0ce", "iat": 1472033308, "exp": 1472036908 }
Signature (base64 encoded)
SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg
First step is to extend AbstractAuthenticationProcessingFilter
in order to provide custom processing of Ajax authentication requests.
De-serialization and basic validation of the incoming JSON payload is done in the AjaxLoginProcessingFilter#attemptAuthentication
method. Upon successful validation of the JSON payload authentication logic is delegated to AjaxAuthenticationProvider class.
In case of a successful authentication AjaxLoginProcessingFilter#successfulAuthentication
method is invoked.
In case of failure authentication AjaxLoginProcessingFilter#unsuccessfulAuthentication
method is invoked.
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class); private final AuthenticationSuccessHandler successHandler; private final AuthenticationFailureHandler failureHandler; private final ObjectMapper objectMapper; public AjaxLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { super(defaultProcessUrl); this.successHandler = successHandler; this.failureHandler = failureHandler; this.objectMapper = mapper; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) { if(logger.isDebugEnabled()) { logger.debug("Authentication method not supported. Request method: " + request.getMethod()); } throw new AuthMethodNotSupportedException("Authentication method not supported"); } LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class); if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) { throw new AuthenticationServiceException("Username or Password not provided"); } UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); return this.getAuthenticationManager().authenticate(token); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { successHandler.onAuthenticationSuccess(request, response, authResult); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); failureHandler.onAuthenticationFailure(request, response, failed); } }
Responsibility of the AjaxAuthenticationProvider class is to:
username
and password
do not match the record in the database authentication exception is thrownusername
and user privileges
)AjaxAwareAuthenticationSuccessHandler
@Component
public class AjaxAuthenticationProvider implements AuthenticationProvider { private final BCryptPasswordEncoder encoder; private final DatabaseUserService userService; @Autowired public AjaxAuthenticationProvider(final DatabaseUserService userService, final BCryptPasswordEncoder encoder) { this.userService = userService; this.encoder = encoder; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.notNull(authentication, "No authentication data provided"); String username = (String) authentication.getPrincipal(); String password = (String) authentication.getCredentials(); User user = userService.getByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); if (!encoder.matches(password, user.getPassword())) { throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); } if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned"); List<GrantedAuthority> authorities = user.getRoles().stream() .map(authority -> new SimpleGrantedAuthority(authority.getRole().authority())) .collect(Collectors.toList()); UserContext userContext = UserContext.create(user.getUsername(), authorities); return new UsernamePasswordAuthenticationToken(userContext, null, userContext.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); } }
We'll implement AuthenticationSuccessHandler interface that is called when client has been successfully authenticated.
AjaxAwareAuthenticationSuccessHandler class is our custom implementation of AuthenticationSuccessHandler interface. Responsibility of this class is to add JSON payload containing JWT Access and Refresh tokens into the HTTP response body.
@Component
public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final ObjectMapper mapper; private final JwtTokenFactory tokenFactory; @Autowired public AjaxAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory) { this.mapper = mapper; this.tokenFactory = tokenFactory; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { UserContext userContext = (UserContext) authentication.getPrincipal(); JwtToken accessToken = tokenFactory.createAccessJwtToken(userContext); JwtToken refreshToken = tokenFactory.createRefreshToken(userContext); Map<String, String> tokenMap = new HashMap<String, String>(); tokenMap.put("token", accessToken.getToken()); tokenMap.put("refreshToken", refreshToken.getToken()); response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); mapper.writeValue(response.getWriter(), tokenMap); clearAuthenticationAttributes(request); } /** * Removes temporary authentication-related data which may have been stored * in the session during the authentication process.. * */ protected final void clearAuthenticationAttributes(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return; } session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); } }
Let's focus for a moment on how JWT Access token is created. In this tutorial we are using Java JWT library created by Stormpath.
Make sure that JJWT
dependency is included in your pom.xml
.
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jjwt.version}</version> </dependency>
We have created factory class (JwtTokenFactory
) to decouple token creation logic.
Method JwtTokenFactory#createAccessJwtToken
creates signed JWT Access token.
Method JwtTokenFactory#createRefreshToken
creates signed JWT Refresh token.
@Component
public class JwtTokenFactory { private final JwtSettings settings; @Autowired public JwtTokenFactory(JwtSettings settings) { this.settings = settings; } /** * Factory method for issuing new JWT Tokens. * * @param username * @param roles * @return */ public AccessJwtToken createAccessJwtToken(UserContext userContext) { if (StringUtils.isBlank(userContext.getUsername())) throw new IllegalArgumentException("Cannot create JWT Token without username"); if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty()) throw new IllegalArgumentException("User doesn't have any privileges"); Claims claims = Jwts.claims().setSubject(userContext.getUsername()); claims.put("scopes", userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList())); DateTime currentTime = new DateTime(); String token = Jwts.builder() .setClaims(claims) .setIssuer(settings.getTokenIssuer()) .setIssuedAt(currentTime.toDate()) .setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate()) .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) .compact(); return new AccessJwtToken(token, claims); } public JwtToken createRefreshToken(UserContext userContext) { if (StringUtils.isBlank(userContext.getUsername())) { throw new IllegalArgumentException("Cannot create JWT Token without username"); } DateTime currentTime = new DateTime(); Claims claims = Jwts.claims().setSubject(userContext.getUsername()); claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN.authority())); String token = Jwts.builder() .