JWT introduction
What is JWT
JWT is the abbreviation of JSON web token, which is based on theJSON
(RFC 7519). A concise, self-contained method is defined for communication between two partiesJSON
The formal security of the object. Because of the existence of a digital signature, this information is trusted and can be used by JWTHMAC
Algorithm orRSA
The public and private secret key pair of.
JWT request process
- Users use account number and password to initiate post request;
- The server uses the private key to create a JWT;
- The server returns the JWT to the browser;
- The browser sends the JWT string in the request header like the server;
- The server verifies the JWT;
- Returns the response resource to the browser.
Main application scenarios of JWT
Authentication in this scenario, once the user has completed the login, JWT is included in each subsequent request, which can be used to verify the user’s identity and to verify the access rights of routes, services and resources. Because of its low cost, it can be easily transmitted in different domain name systems, so it is widely used in single sign on (SSO). Information exchange between the two sides of communication using JWT to encode the data is a very safe way, because its information is signed, it can ensure that the information sent by the sender is not forged.
JWT data structure
JWT is composed of three pieces of information.
Together, it forms the JWT string.
And: the header of jwtheader: signature.
Header
The header part is a JSON object, which describes the metadata of JWT. It usually looks like the following.
{
"alg": "HS256",
"typ": "JWT"
}
In the code above,alg
Attribute represents the algorithm of the signature. The default is HMAC sha256 (written as hs256);typ
Attribute indicates the type of the token. JWT token is written asJWT
。
Finally, convert the above JSON object into a string using the base64 URL algorithm.
Payload
The payload part is also a JSON object, which is used to store the valid information that needs to be passed. Valid information consists of three parts:
- Declaration registered in the standard
- Public statement
- Private statement
Statement registered in the standard (recommended but not mandatory)
- ISS (issuer): issuer
- Exp (expiration time): the expiration time must be greater than the issuing time
- Subject: subject
- Aud (audience): audience
- NBF (not before): effective time
- IAT (issued at): issuing time
- JTI (JWT ID): number, unique identification of JWT, mainly used as one-time
token
To avoid replay attacks.
Public statement:
Any information can be added to the public statement. Generally, the relevant information of the user or other necessary information required by the business can be added. However, it is not recommended to add sensitive information as this part can be decrypted on the client side.
Private statement:
Private statements are defined by both providers and consumers. It is generally not recommended to store sensitive information becausebase64
It is symmetric decoding, which means that this part of information can be classified as plaintext information.
The JSON object is also converted to a string using the base64 URL algorithm.
Signature
The signature part is the signature of the first two parts to prevent data tampering.
First, you need to specify a secret. This key is only known to the server and cannot be disclosed to users. Then, the signature algorithm specified in the header (HMAC sha256 by default) is used to generate the signature according to the following formula.
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
After the signature is calculated, the header, payload and signature are combined into a string, and the “dot” is used between each part(.
)Separated, it can be returned to the user.
Base64URL
As mentioned earlier, the algorithm for header and payload serialization is base64url. This algorithm is basically similar to Base64 algorithm, but with some small differences.
As a token, JWT may be put into the URL in some cases (for exampleapi.example.com/?token=xxx
)。 Base64 has three characters+
、 /
and=
, which has a special meaning in the URL, so it should be replaced:=
Omitted+
replace with-
,/
replace with_
。 This is the base64 URL algorithm.
How to use JWT
After receiving the JWT returned by the server, the client needs to save it locally. After that, every time the client communicates with the server, it will bring this JWT. The general practice is to put the header information of the HTTP requestAuthorization
Field.
Authorization: Bearer <token>
In this way, in each request, the server can get JWT in the request header for parsing and authentication.
Features of JWT
- JWT is not encrypted by default, but can also be encrypted. After the original token is generated, it can be encrypted again with the key.
- When JWT is not encrypted, secret data cannot be written to JWT.
- JWT can be used not only for authentication, but also for authentication. Effective use of JWT can reduce the number of times the server queries the database.
- The biggest drawback of JWT is that because the server does not save the session state, it is unable to revoke a token or change the permissions of a token during use. That is, once the JWT is issued, it will remain valid until it expires, unless the server deploys additional logic.
- JWT itself contains authentication information, once leaked, anyone can get all rights of the token. In order to reduce embezzlement, the validity period of JWT should be set short. For some important permissions, users should be authenticated again.
- In order to reduce the embezzlement, JWT should not use the HTTP protocol, but use the HTTPS protocol.
Simple encapsulation based on nimbus Jose JWT
Nimbus Jose JWT is the most popular JWT open source library. Based on Apache 2.0 open source protocol, it supports all standard JWs and JWe algorithms. Nimbus Jose JWT supports the use of symmetric encryption (HMAC) and asymmetric encryption (RSA) algorithms to generate and parse JWT tokens.
Next, we encapsulate nimbus Jose JWT to support the following functions:
- Support the use of HMAC and RSA algorithms to generate and parse JWT tokens
- It supports private information as payload directly, and standard information + private information as payload. Built in support of the latter.
- It provides tool class and extensible interface for user-defined extension development.
Add dependency in POM
First of all, we are in pom.xml The nimbus Jose JWT dependency is introduced.
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.20</version>
</dependency>
JwtConfig
This class is used for unified management of related parameter configurations.
public class JwtConfig {
//JWT default key in HTTP header
private String tokenName = JwtUtils.DEFAULT_TOKEN_NAME;
//HMAC key, used to support HMAC algorithm
private String hmacKey;
//JKS key path, used to support RSA algorithm
private String jksFileName;
//JKS key cipher is used to support RSA algorithm
private String jksPassword;
//Certificate password to support RSA algorithm
private String certPassword;
//JWT standard information: issuer - ISS
private String issuer;
//JWT standard information: subject - sub
private String subject;
//JWT standard information: audience aud
private String audience;
//JWT standard information: effective time - NBF, how long will it take effect in the future
private long notBeforeIn;
//JWT standard information: effective time - NBF, which time is effective
private long notBeforeAt;
//JWT standard information: expiration time - exp, how long will it expire in the future
private long expiredIn;
//JWT standard information: expiration time - exp, which time does it expire
private long expiredAt;
}
hmacKey
Field is used to support HMAC algorithm. As long as the field is not empty, this value is used as HMAC’s key to sign and verify JWT.
jksFileName
、jksPassword
、certPassword
Three fields are used to support RSA algorithm. The program will read the certificate file as RSA key to sign and verify JWT.
Several other fields are used to set the standard information that needs to be carried in the payload.
JwtService
Jwtservice is an interface to provide JWT signature and verification. The built-in hmacjwtserviceimpl provides the implementation of HMAC algorithm and rsajwtserviceimpl provides the implementation of RSA algorithm. The two algorithms are different in the way of obtaining the key, which is also proposed as the interface method. Later, if you want to customize the implementation, you only need to write a specific implementation class.
public interface JwtService {
/**
*Get key
*
* @return
*/
Object genKey();
/**
*Sign the message
*
* @param payload
* @return
*/
String sign(String payload);
/**
*Verify and return information
*
* @param token
* @return
*/
String verify(String token);
}
public class HMACJwtServiceImpl implements JwtService {
private JwtConfig jwtConfig;
public HMACJwtServiceImpl(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
@Override
public String genKey() {
String key = jwtConfig.getHmacKey();
if (JwtUtils.isEmpty(key)) {
throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, new NullPointerException("HMAC need a key"));
}
return key;
}
@Override
public String sign(String info) {
return JwtUtils.signClaimByHMAC(info, genKey(), jwtConfig);
}
@Override
public String verify(String token) {
return JwtUtils.verifyClaimByHMAC(token, genKey(), jwtConfig);
}
}
public class RSAJwtServiceImpl implements JwtService {
private JwtConfig jwtConfig;
private RSAKey rsaKey;
public RSAJwtServiceImpl(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
private InputStream getCertInputStream() throws IOException {
//Read the certificate path in the configuration file
String jksFile = jwtConfig.getJksFileName();
if (jksFile.contains("://")) {
//Read from local file
return new FileInputStream(new File(jksFile));
} else {
//Read from classpath
return getClass().getClassLoader().getResourceAsStream(jwtConfig.getJksFileName());
}
}
@Override
public RSAKey genKey() {
if (rsaKey != null) {
return rsaKey;
}
InputStream is = null;
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
is = getCertInputStream();
keyStore.load(is, jwtConfig.getJksPassword().toCharArray());
Enumeration<String> aliases = keyStore.aliases();
String alias = null;
while (aliases.hasMoreElements()) {
alias = aliases.nextElement();
}
RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey(alias, jwtConfig.getCertPassword().toCharArray());
Certificate certificate = keyStore.getCertificate(alias);
RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();
rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
return rsaKey;
} catch (IOException | CertificateException | UnrecoverableKeyException
| NoSuchAlgorithmException | KeyStoreException e) {
e.printStackTrace();
throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
public String sign(String payload) {
return JwtUtils.signClaimByRSA(payload, genKey(), jwtConfig);
}
@Override
public String verify(String token) {
return JwtUtils.verifyClaimByRSA(token, genKey(), jwtConfig);
}
}
JwtUtils
The implementation class of jwtservice is relatively concise, because the main methods are provided in jwtutils. The following is the signature and verification implementation of the two algorithms when payload contains only private information. These methods can be used to implement their own extensions.
/**
*Use HMAC algorithm to sign information (payload contains only private information)
*
* @param info
* @param key
* @return
*/
public static String signDirectByHMAC(String info, String key) {
try {
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT)
.build();
//Build a payload
Payload payload = new Payload(info);
//Combine the head with the load
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
//Create a key
JWSSigner jwsSigner = new MACSigner(key);
//Signature
jwsObject.sign(jwsSigner);
//Generating token
return jwsObject.serialize();
} catch (JOSEException e) {
e.printStackTrace();
throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
}
}
/**
*Signature information using RSA algorithm (payload contains only private information)
*
* @param info
* @param rsaKey
* @return
*/
public static String signDirectByRSA(String info, RSAKey rsaKey) {
try {
JWSSigner signer = new RSASSASigner(rsaKey);
JWSObject jwsObject = new JWSObject(
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(),
new Payload(info)
);
//Encrypt
jwsObject.sign(signer);
return jwsObject.serialize();
} catch (JOSEException e) {
e.printStackTrace();
throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
}
}
/**
*Using HMAC algorithm to verify token (payload contains only private information)
*
* @param token
* @param key
* @return
*/
public static String verifyDirectByHMAC(String token, String key) {
try {
JWSObject jwsObject = JWSObject.parse(token);
//Create an unlock key
JWSVerifier jwsVerifier = new MACVerifier(key);
if (jwsObject.verify(jwsVerifier)) {
return jwsObject.getPayload().toString();
}
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
} catch (JOSEException | ParseException e) {
e.printStackTrace();
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
}
}
/**
*Using RSA algorithm to verify token (payload contains only private information)
*
* @param token
* @param rsaKey
* @return
*/
public static String verifyDirectByRSA(String token, RSAKey rsaKey) {
try {
RSAKey publicRSAKey = rsaKey.toPublicJWK();
JWSObject jwsObject = JWSObject.parse(token);
JWSVerifier jwsVerifier = new RSASSAVerifier(publicRSAKey);
//Validation data
if (jwsObject.verify(jwsVerifier)) {
return jwsObject.getPayload().toString();
}
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
} catch (JOSEException | ParseException e) {
e.printStackTrace();
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
}
}
JwtException
By defining a unified exception class, we can shield nimbus Jose JWT and other exceptions such as loading certificate error. When other projects integrate our packaged library, we can easily handle the exception.
In different stages of jwtservice implementation, we encapsulate different jwtexception subclasses to facilitate external processing according to needs. If the exception is keygenerateexception, it will be handled as a server processing error; if the exception is tokenverifyexception, it will be handled as token validation failure without permission.
JwtContext
JWT is used for user authentication. After token verification is completed, the program needs to obtain the current login user information. Jwtcontext provides a method to save information through thread local variables.
public class JwtContext {
private static final String KEY_TOKEN = "token";
private static final String KEY_PAYLOAD = "payload";
private static ThreadLocal<Map<Object, Object>> context = new ThreadLocal<>();
private JwtContext() {}
public static void set(Object key, Object value) {
Map<Object, Object> locals = context.get();
if (locals == null) {
locals = new HashMap<>();
context.set(locals);
}
locals.put(key, value);
}
public static Object get(Object key) {
Map<Object, Object> locals = context.get();
if (locals != null) {
return locals.get(key);
}
return null;
}
public static void remove(Object key) {
Map<Object, Object> locals = context.get();
if (locals != null) {
locals.remove(key);
if (locals.isEmpty()) {
context.remove();
}
}
}
public static void removeAll() {
Map<Object, Object> locals = context.get();
if (locals != null) {
locals.clear();
}
context.remove();
}
public static void setToken(String token) {
set(KEY_TOKEN, token);
}
public static String getToken() {
return (String) get(KEY_TOKEN);
}
public static void setPayload(Object payload) {
set(KEY_PAYLOAD, payload);
}
public static Object getPayload() {
return get(KEY_PAYLOAD);
}
}
@AuthRequired
In project practice, not all methods in controller must pass token, and @ authrequired annotation is used to distinguish whether methods need to verify token.
/**
*The method applied to the controller to identify whether to intercept for JWT verification
*/
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AuthRequired {
boolean required() default true;
}
Spring boot integrated JWT instance
With the above packaged library, we integrate JWT into the springboot project. After creating the spring boot project, we write the following main classes.
JwtDemoInterceptor
In the spring boot project, requests and responses can be intercepted by customizing the implementation class of handlerinterceptor. We create a new jwtdemointerceptor class to intercept.
public class JwtDemoInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(JwtDemoInterceptor.class);
private static final String PREFIX_BEARER = "Bearer ";
@Autowired
private JwtConfig jwtConfig;
@Autowired
private JwtService jwtService;
/**
*Preprocessing callback method to realize the preprocessing of the processor (such as checking login). The third parameter is the responding processor, and the controller is defined
*Return value:
*True means to continue the process (such as calling the next interceptor or or processor);
*False means that the process is interrupted (such as login check failure), and other interceptors or processors will not be called. In this case, we need to generate a response through response.
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//If it is not mapped to a method, it passes through the
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//Check whether there is @ authrequired annotation. If yes and required() is false, skip it
if (method.isAnnotationPresent(AuthRequired.class)) {
AuthRequired authRequired = method.getAnnotation(AuthRequired.class);
if (!authRequired.required()) {
return true;
}
}
String token = request.getHeader(jwtConfig.getTokenName());
logger.info("token: {}", token);
if (StringUtils.isEmpty(token) || token.trim().equals(PREFIX_BEARER.trim())) {
return true;
}
token = token.replace(PREFIX_BEARER, "");
String payload = jwtService.verify(token);
//Local token in thread
JwtContext.setToken(token);
JwtContext.setPayload(payload);
return true;
}
/**
*The postprocessing callback method implements the post-processing of the processor (but before rendering the view). At this time, we can process the model data or view through modelandview (model and view object), and modelandview may be null.
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
*When the call back is finished, we can also monitor the output time of some call backs, such as when the call back is finished
*However, only the aftercompletion of the interceptor whose prehandle returns true in the processor execution chain is called.
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
JwtContext.removeAll();
}
}
preHandle
、postHandle
、afterCompletion
The specific functions of the three methods can be seen in the comments on the code.
preHandle
The logic in this code is as follows:
- Intercept methods annotated by @ authrequired, as long as they are not
required = false
The token will be checked. - The token is parsed from the request and verified. If an exception is validated, an exception is thrown in the method.
- If token verification is passed, relevant information will be set in thread local variables for subsequent programs to obtain processing.
afterCompletion
This code cleans up the thread variables.
InterceptorConfig
Define interceptorconfig. Through the @ configuration annotation, spring will load the class and complete the assembly.
addInterceptors
Method, and intercept all requests.
jwtDemoConfig
Method is injected with jwtconfig and hmackey is set.
jwtDemoService
Method will generate a specific jwtservice according to the injected jwtconfig configuration. Here is hmacjwtserviceimpl.
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtDemoInterceptor()).addPathPatterns("/**");
}
@Bean
public JwtDemoInterceptor jwtDemoInterceptor() {
return new JwtDemoInterceptor();
}
@Bean
public JwtConfig jwtDemoConfig() {
JwtConfig jwtConfig = new JwtConfig();
jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");
return jwtConfig;
}
@Bean
public JwtService jwtDemoService() {
return JwtUtils.obtainJwtService(jwtDemoConfig());
}
}
Write test controller
@RestController
public class UserController {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private JwtService jwtService;
@GetMapping("/sign")
@AuthRequired(required = false)
public String sign() throws JsonProcessingException {
UserDTO userDTO = new UserDTO();
userDTO.setName("fatfoo");
userDTO.setPassword("112233");
userDTO.setSex(0);
String payload = objectMapper.writeValueAsString(userDTO);
return jwtService.sign(payload);
}
@GetMapping("/verify")
public UserDTO verify() throws IOException {
String payload = (String) JwtContext.getPayload();
return objectMapper.readValue(payload, UserDTO.class);
}
}
sign
Method to sign the user information and return token@AuthRequired(required = false)
The interceptor will not intercept it.
verify
Method after the token passes the validation, the parsed information is obtained and returned.
Test with postman
Access the sign interface and return the signature token.
Add token information to header, request verify interface, and return user information.
Test RSA algorithm implementation
Above, we only set the hmackey parameter of jwtconfig, using HMAC algorithm for signature and verification. In this section, we demonstrate the implementation of RSA algorithm for signature and verification.
Generate signature file
Using the keytool tool of Java can easily generate certificate file.
➜ resources git:(master) ✗ keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
Enter keystore password:
Keystore password too short - must be at least 6 characters
Enter keystore password: ronjwt
Enter the new password again: ronjwt
What's your first and last name?
[Unknown]: ron
What is the name of your organizational unit?
[Unknown]: ron
What is the name of your organization?
[Unknown]: ron
What is the name of your city or area?
[Unknown]: Xiamen
What is the name of your province?
[Unknown]: Fujian
What is the two letter country code for the unit?
[Unknown]: CN
Is CN = Ron, Ou = Ron, o = Ron, l = Xiaomen, St = Fujian, C = CN correct?
[no]: Yes
Enter the key password of < JWT >
(if the password is the same as that of the keystore, press enter)
Warning:
The JKS keystore uses a private format. "Keytool - importkeystore - srckeystore" is recommended jwt.jks -destkeystore jwt.jks -Deststoretype pkcs12 "to the industry standard format pkcs12.
After the file is generated, copy it to the resource directory of the project.
Set jwtconfig parameter
Modify thejwtDemoConfig
Method, which are jksfilename, jkspassword and certpassword.
@Bean
public JwtConfig jwtDemoConfig() {
JwtConfig jwtConfig = new JwtConfig();
// jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");
jwtConfig.setJksFileName("jwt.jks");
jwtConfig.setJksPassword("ronjwt");
jwtConfig.setCertPassword("ronjwt");
return jwtConfig;
}
Do not set the hmackey parameter, otherwise hmacjwtserviceimpl will be loaded. becauseJwtUtils#obtainJwtService
The method is implemented as follows:
/**
*Gets the factory method of the built-in jwtservice.
*
*HMAC algorithm is preferred
*
* @param jwtConfig
* @return
*/
public static JwtService obtainJwtService(JwtConfig jwtConfig) {
if (!JwtUtils.isEmpty(jwtConfig.getHmacKey())) {
return new HMACJwtServiceImpl(jwtConfig);
}
return new RSAJwtServiceImpl(jwtConfig);
}
In this way, we can test the signature and verification of RSA algorithm. Run the program and use the postman test to see the difference.
– End –
This article is only the first part of integrating JWT with spring boot. In the future, we will continue to encapsulate this library, build spring boot starter, and customize @ enable annotation to facilitate the introduction in the project.
Please pay attention to my official account: Java (ID:craft4j)The first time to obtain knowledge dynamics.
If you are interested in the full source code of the project, you can reply in the official account.jwt
To get.