JWT Authentication Tutorial: An example using Spring Boot--转

本文通过Spring Boot实现JWT认证,包括Ajax认证及JWT Token认证流程。文章详细介绍各组件的实现原理及如何处理认证请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文地址:http://www.svlada.com/jwt-token-authentication-with-spring-boot/

Table of contents:

  1. Introduction
  2. PRE-requisites
  3. Ajax authentication
  4. JWT Authentication

Introduction

This article will guide you on how you can implement JWT authentication with Spring Boot.

We will cover the following two scenarios:

  1. Ajax Authentication
  2. JWT Token Authentication

PRE-requisites

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.

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).

Overall project structure is shown below:

+---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

Ajax authentication

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.

In the first part of this tutorial Ajax authentication is implemented by following standard patterns found in the Spring Security framework.

Following is the list of components that we'll implement:

  1. AjaxLoginProcessingFilter
  2. AjaxAuthenticationProvider
  3. AjaxAwareAuthenticationSuccessHandler
  4. AjaxAwareAuthenticationFailureHandler
  5. RestAuthenticationEntryPoint
  6. WebSecurityConfig

Before we get to the details of the implementation, let's look at the request/response authentication flow.

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:

  1. HTTP status "200 OK"
  2. Signed JWT Access and Refresh tokens are included in the response body

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:

  1. Authentication is performed by verifying JWT Access Token signature. If signature proves to be valid, access to requested API resource is granted.
  2. Authorization is done by looking up privileges in the scope attribute of JWT Access token.

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  
AjaxLoginProcessingFilter

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); } } 
AjaxAuthenticationProvider

Responsibility of the AjaxAuthenticationProvider class is to:

  1. Verify user credentials against database, LDAP or some other system which holds the user data
  2. If username and password do not match the record in the database authentication exception is thrown
  3. Create UserContext and populate it with user data you need (in our case just username and user privileges)
  4. Upon successful authentication delegate creation of JWT Token to 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)); } } 
AjaxAwareAuthenticationSuccessHandler

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() .
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值