JWTs on Frontend Clients (GraphQL) The Ultimate Guide to handling JWTs on Frontend Clients (GraphQL) The original author used GraphQL as the server, I used SpringBoot to rewrite it.
The introduction
JWTs(JSON Web Tokens, pronounced ‘jot’) is becoming a popular authentication method. This article discusses the advantages and disadvantages of Springboot and JJWT as examples and their best practices on the Web side.
Don’t focus on the implementation technology, focus on the idea of the implementation.
Introduction: What is JWT?
See Introduction to JSON Web Tokens for more information about JWT
To implement permission verification, the server sends a JWT token back to the client after the user logs in. The JSON payload in the token contains the user’s unique information. When the client sends a request, it declares the token in the header. In this way, the server can parse the token to obtain the user information and then obtain the user permission. If the user has the permission, the server can return the information required by the user.
But why can’t the server just create JSON Paylod impersonators?
Good question! That’s why JWT also includes signatures, which are created by the server that issues the token (most likely the login endpoint), and any other server that receives the token can independently verify the token’s signature, using that signature, The server can ensure that the token’s JSON payload has not been tampered with and has a legitimate source.
Note: It can be understood that the signature is equivalent to the key. Each key opens another lock. When the server creates the token, it creates a key.
But if I have an unexpired and signed JWT and someone steals it from my client, can they use my JWT forever?
Yes! If the JWT is stolen, the hacker can always use the JWT, the JWT is independent, the API that receives the JWTs cannot get the user of the JWT, so the server has no way of knowing that it is a stolen token, which is unacceptable! As a result, JWT has an expiration time, often set to 15 minutes, so that even if stolen, it quickly becomes ineffective.
These two issues cover almost all the considerations when using JWTs: 1. JWTs should be as secure as possible; 2. In order to prevent serious consequences caused by theft, JWTs need to expire by a very short time.
This is why JWT cannot be stored in cookies or localstorage, otherwise it is not a good defense against CSRF and XSS attacks: hackers can use malicious forms or scripts to obtain JWT in Cookis or Localstorage.
So what is the structure of JWT? What does it look like?
A complete JWT would look like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHM I6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2oCopy the code
After decoding using Base64, you can find that the JWT contains three parts: header, payload, and signature.
The serialization format is as follows:
[ base64UrlEncode(header) ] . [ base64UrlEncode(payload) ] . [signature ]
JWT is not encrypted, it is base64 encoded and signed, so anyone can decode the token and use its data (payload). At this point, the SIGNATURE of the JWT is used to verify that the JWT is from a trusted source.
Here’s how JWT signs (/login) and validates (/ API) :
Well, it seems so complicated, why not use the session mechanism?
There has been a lot of discussion on the Internet about this issue, and our short (even stubborn) conclusion is that back-end developers like to use JWTs because a) microservices and b) don’t need a centralized token database.
In microservices, each microservice can independently send a request to the server to verify the validity of the token. Microservices can further parse tokens to extract relevant information without the need to have a centralized token database.
That’s why API developers like to use JWTs, so client developers need to figure out how to use it.
Basic: Login
Now that we have an overview of JWT, let’s create a simple login process that we want to achieve:
So how do we start?
This is a very simple login process, the user sends the login request using the username and password, the server issues the JWT, and returns to the client. Perhaps you logged in through the OAuth or OAuth2 step, which doesn’t matter as long as the client gets JWT after successfully logging in.
First, we build a simple login form on the client side and send the username and password to the server side. The handleSubmit handler for the login button looks like this:
async function handleSubmit() {
// ...
// fetch /login API
const response = await fetch(`${base}/login`, {
method: "POST".body: JSON.stringify({
username: username,
password: password,
}),
});
// ...
const res = await response.json();
// ...
if(res.code ! = =200) {
alert("Wrong username or password!");
return;
}
const { jwt_token } = res.data;
// Store the token
login({ jwt_token });
}
Copy the code
The Login API returns token data, which we then pass into the Login function, where we can decide what to do with the token data obtained.
So once the client gets the token, where should it be stored?
We need to store the JWT token somewhere so we can add it to the header and pass it to the server on the next request, maybe you want to use localStorage, ** don’t do that! ** Hackers using XSS attacks can easily obtain our tokens.
Maybe in a cookie?
Storing JWT in cookies is also vulnerable to XSS attacks, and as long as it can be read from the client using Javascript, it can be stolen. You might think that httpOnly cookies can help with XSS attacks, but the hacker uses CSRF to attack 😟. CSRF attacks cannot be prevented using HttpInly and certain CORS policies. Therefore, servers need appropriate CSRF defense policies if they are stored in cookies.
So how do we store tokens?
For now, we’ll store it in memory (we’ll talk more about persistence later in this article).
let inMemoryToken;
function login({ jwt_token, jwt_token_expiry }) {
inMemoryToken = {
token: jwt_token,
expiry: jwt_token_expiry,
};
}
Copy the code
We currently store the token in memory. However, when the user creates a new page, the token in memory disappears because of the refresh, which we will deal with later. I will also explain why jwt_token_expiry is needed.
Ok, now that we have the token, how do we use it?
- The token must be added to the header for each API request that requires permission verification.
- According to the
inMemoryToken
Whether the variable is empty can check whether the user is logged in; - (Optional) We can even parse the JWT on the front end to get data from the payload.
How do I check whether a user is logged in?
const jwt_token = inMemoryToken;
if(! jwt_token) {// Jump to the login page
location.href = `${base}/page/login`;
}
return jwt_token;
Copy the code
Basic: Server Settings
It’s time to write a server-side program that takes the data from the token variable and passes it to the server if the token is not empty.
If permission validation is required when a client makes a request, the Authorization attribute needs to be added to the Header with a value of Bearer
(this Header can also be added for each request using interceptors).
If a request requires Authorization validation, Springboot parses the Authorization attribute in the Header to get the JWT, and then determines whether the user has permission to act.
Here is a program that resolves Authorization using interceptors:
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) {
// If not mapped to the method directly through
if(! (objectinstanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
// Check whether permission checks are required
if (method.isAnnotationPresent(VerifyToken.class)) {
VerifyToken verifyToken = method.getAnnotation(VerifyToken.class);
if (verifyToken.required()) {
String token = httpServletRequest.getHeader("Authorization");
// Perform authentication
if (token == null) {
throw new CustomException(ResultCode.AUTH_NEED, "Please log in and perform this operation.");
}
token = token.substring(7);
// Get the username in the token
String userId = tokenService.getUserIdFromToken(token);
if (userId == null)
throw new CustomException(ResultCode.AUTH_NEED, "Please log in and perform this operation.");
/ / authentication token
try {
List<String> permissions = tokenService.getPermissions(token);
// Determine whether the user has the permission based on the user role and URL
String url = verifyToken.url();
if (permissions.contains(url)) {
return true;
}
/ / without permission
throw new CustomException();
} catch (CustomException e) {
throw new CustomException(ResultCode.METHOD_NOT_ALLOWED, "User does not have this permission");
} catch (Exception e) {
throw new CustomException(ResultCode.AUTH_NEED, "Login has expired. Please log in again."); }}}return true;
}
Copy the code
VerifyToken notes:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyToken {
boolean required(a) default true;
String url(a) default "";
}
Copy the code
Using the VerifyToken annotation, in conjunction with the interceptor, you can customize requests that require permission validation, obtain the URL of the current request, and determine whether the user has permissions.
Token Expiration Processing
Assume that the JWT token is valid for 15 minutes. If the request is sent because the token has expired, the server will reject the request (assuming the 401: Unauthorized error is returned). Therefore, we need to add expiration handling.
The procedure is roughly as follows:
if (res.code === 401) {
// or res.code === 200
if (inMemoryToken) {
await logout();
// ...}}Copy the code
You may have noticed that this leads to a rather poor user experience, requiring the user to log in again every time the token expires. Therefore App/Web needs to implement a perceptionless refresh JWT token, in fact, this is why refresh_token appears.
Basic: Log out
With JWT, we just need to delete the token in memory when logging out.
Therefore, the server does not need to provide/logout
API?
Indeed, the server does not need to provide the API, in fact, any microserver that receives the JWT will still accept it, and if the JWT is removed on the authentication server, the other microservers will not be affected and will still accept the token (because the great advantage of JWTs is that there is no centralized processing of tokens).
The deleted token is still valid. What should I do if I need to ensure that the token can no longer be used?
This is why we set the JWT duration to a very small value, and why you need to keep your tokens as secure as possible. The token is still valid after deletion, but it is only valid for a short time, so it can reduce the possibility of malicious use to some extent.
What happens when I exit from another TAB?
One way to handle this is to provide a global listening event for LocalStorage. When the logout key is updated on one TAB page, other pages trigger this event, allowing the user to log back in.
window.addEventListener("storage", syncLogout);
function syncLogout() {
if (event.key === "logout") {
if(! inMemoryToken) {// If you have not logged in, skip this step
return;
}
// ...
inMemoryToken = null; // Empty the token
location.href = `${base}/page/login`; }}Copy the code
Two things you need to do after logout:
- Delete the token
- Set localStorage
logout
key
async function logout() {
inMemoryToken = null; // Empty the token
localStorage.setItem("logout".Date.now());
}
Copy the code
No matter which TAB you log out from, the other pages will log in again.
Silent refresh
According to the current implementation, our system has two main problems:
- The token expiration time is usually set to 15 minutes. Once the token expires, the user must log in again. Ideally, we want the user to log in for a long time.
- The token is currently stored in memory and not persisted on the client side, meaning that once the user exits the login, re-entry must be logged in again.
To solve these two problems, we need to introduce the Refresh Token, which has two features:
- You can userefresh tokenAccess the Token refresh interface (suppose
/token/refresh
),Jwt tokenObtain a new token before expiration; - Can be persisted safely on the client side.
How does refresh Token work?
Refresh_token as part of authentication needs to be associated with a specific user to generate a new token. To do this, we can safely refresh the token by adding the username unique constraint to the JWT Claim information and signing it.
On the client side, before the JWT token expires, we access the /token/ Refresh interface to get a new JWT token.
How can clients safely store Refresh Tokens?
We can store refresh tokens in HTTP only cookies, which can mitigate XSS attacks, and even if a hacker builds a CSRF attack to perform a refresh interface operation, he will not be able to retrieve the new tokens returned.
To review, this is how we can best persist JWT:
Store JWT tokens in localstorage (vulnerable to XSS attacks) < Store JWT tokens in cookies with httpOnly attributes (vulnerable to CSRF, Mitigate XSS attacks) < store Refresh token to httpOnly property (immune to CSRF attacks, mitigate XSS attacks).
Note that while this approach is not yet ready for serious XSS attacks, httpOnly cookies are a recommended way to persist Refresh Tokens in combination with some common XSS defense techniques. The biggest advantage of storing JWT tokens in this way is that they are immune to CSRF attacks.
Added the refresh Token to the new login process
During the login phase, the Refresh token is returned along with the original JWT token. As shown below:
- Users click login to access the login interface.
- The server generates JWT and Refresh tokens and signs them using Secret;
refresh_token
Stored in cookies and sethttpOnly
Properties,jwt_token
å’Œjwt_token_expiry
Store in memory;- Based on the
jwt_token_expiry
You can do silent refresh.
What does silent refresh look like?
The specific steps are as follows:
- access
/token/refresh
Interface; - Server read
httpOnly
Cookie, judgerefresh_token
Do not exist, then usesecret
Check, if legal, then proceed to the third step; - The server generates a new
jwt_token
andjwt_token_expiry
, return to the client, and generate a new onerefresh_token
, the use ofSet-cookie
Settings.
So, we’ve solved the first problem.
Persistent session
For now, even if jWt_token fails, by silently refreshing the process, we can guarantee that the user will not drop off and re-log in. Let’s address the second problem: persistent sessions.
According to the current implementation scheme, if the user refreshes or closes the TAB to re-enter, he must re-log in.
We wanted to enable users to stay logged in even if they refresh or restart the browser. Since we store the JWT in memory and it disappears when the user refreshes or restarts the browser, we cannot implement this feature.
So how do we persist safely?
The answer is Refresh Token! We can safely persist Refresh tokens and use them for silent refreshing. We can use the silent refreshing mechanism to refresh JWT tokens or use this mechanism to obtain a new JWT token.
With the Refresh Token, the process of refreshing/closing the browser and re-opening it is as follows:
Error handling
If the refresh Token does not exist (the user logs in for the first time or re-logs out after the user clicks exit) or is invalid (for example, the user does not log in for a long time or the server secret changes), the token verification fails and error 401 occurs. After receiving the error, the client is redirected to the login page.
Forced out of
The original author stores refresh_token in the database, and one way to implement the forcible exit is to invalidate all the refresh token associated with the user (i.e. to delete the refresh token associated with a user in the database).
However, in my opinion, if refresh_token is stored in the database, it will lose the meaning of the existence of JWT, because JWT itself can verify its legitimacy through signature, and joining the database makes stateless JWT have state, which seems to be neither fish nor fish. Of course, maybe I haven’t studied enough :).
The sample code
According to this article, I use MySQL + SpringBoot + Thymeleaf + JJWT to implement the authorization verification.
Build table
According to the convention, create five permission verification tables: user role Permission user_role role_PERMISSION, where user and role have many-to-many relationships and role and Permission have many-to-many relationships. For the SQL statement, see jwt_test.
Adding a user:
username | password |
---|---|
normal_user | 12345 |
administrator | 12345 |
userandadmin | 12345 |
Adding a role:
id | role |
---|---|
1 | The average user |
2 | The administrator |
Add permission:
id | name | description | url |
---|---|---|---|
1 | Common user interface | The common user page is displayed | /normal |
2 | Management interface | The management page is displayed | /manage |
Assign user normal_user as a common user who has the permission to access the common page but does not have the permission to access the management page. Assign user administrator to the administrator role, which has the permission to access the management page but does not have the permission to access the common user page. Assign user userandadmin to the administrator role, which has the permission to access the common user page and the management page.
user_id | role_id |
---|---|
normal_user | 1 |
administrator | 2 |
userandadmin | 1 |
userandadmin | 2 |
role_id | permission_id |
---|---|
1 | 1 |
2 | 2 |
Client – Login
Use Thymeleaf to construct the login form, as shown below.
When the user clicks login, the following two situations occur:
- If the user logs in successfully, the server issues the login
jwt_token
andrefresh_token
And willjwt_token
Return to the client, the client saves to memory, the server willrefresh_token
Added to thehttpOnly cookies
And then based onjwt_token_expiry
Silent refresh; - If the login fails, the system prompts you to log in again.
Client request:
let inMemoryToken; // Token for authentication is stored in memory
function login({ jwt_token, jwt_token_expiry }) {
inMemoryToken = {
token: jwt_token,
expiry: jwt_token_expiry,
};
}
async function handleSubmit(username, password) {
try {
const response = await fetch(`${base}/user/login`, {
method: "POST".credentials: "include".headers: {
"Content-Type": "application/json"."Cache-Control": "no-cache",},body: JSON.stringify({
username: username,
password: password,
}),
});
if (response.ok) {
const res = await response.json();
if(res.code ! = =200) {
alert("Wrong username or password!");
return;
}
const { jwt_token, jwt_token_expiry } = res.data;
login({ jwt_token, jwt_token_expiry });
location.href = `${base}/ `; // Go to the home page
} else {
console.log(response.statusText); }}catch (e) {
console.log(e); }} $("#login").click(async() = > {const username = $("#username").val();
const password = $("#password").val();
if (username === "" || password === "") {
alert("Username and password cannot be empty!");
return;
}
await handleSubmit(username, password);
});
Copy the code
Client – Silent refresh
After entering the home page, you need to start silent refreshing to prevent users from re-logging in to refresh or close tabs, and to prevent users from logging in again due to the expiration of the Jwt_token (the reasons have been explained in detail in the previous section).
The startCountdown function defines a timer event that checks the jwt_token expiration every minute. If the jwt_token expires after one minute, the request /token/refresh interface will refresh the Jwt_token:
let interval; / / timer
function addMinutes(dt, minutes) {
return new Date(dt.getTime() + minutes * 60000);
}
function startCountdown() {
interval = setInterval(async() = > {if (inMemoryToken) {
if (addMinutes(new Date(), 1) > =new Date(inMemoryToken.expiry)) {
awaitauth(); }}else {
awaitauth(); }},60000);
}
$(function () {
auth().then(() = > {
console.log(inMemoryToken);
if (inMemoryToken) {
// Refresh succeeded
startCountdown();
} else {
alert("Login information has expired, please log in again!"); }}); });Copy the code
In the auth function, we access the /token/refresh interface for the Jwt_token refresh:
async function auth() {
if(! inMemoryToken) {const url = `${base}/token/refresh`;
try {
const response = await fetch(url, {
method: "GET"});if (response.ok) {
const res = await response.json();
if(res.code ! = =200) {
if (inMemoryToken) {
await logout();
}
// Rediret to log in
location.href = `${base}/login`;
} else {
const{ jwt_token, jwt_token_expiry } = res.data; login({ jwt_token, jwt_token_expiry }); }}else {
console.log(response.statusText); }}catch (e) {
console.log(e);
if (inMemoryToken) {
await logout();
}
// Rediret to log in
location.href = `${base}/login`; }}}Copy the code
Client – Log out
In the logout function, the exit logic is processed, including:
- Delete from memory
jwt_token
And timed events; - Set up the
localstorage
In thelogout
The value of the; - access
/user/logout
Interface Refresh interface deregistration. - The login page is displayed.
async function logout() {
inMemoryToken = null; // Empty the token
if (interval) clearInterval(interval); // Stop the timer event
localStorage.setItem("logout".Date.now());
const url = `${base}/user/logout`;
try {
const response = await fetch(url, {
method: "GET".credentials: "include"});if (response.ok) {
const res = await response.json();
console.log(res.code ! = =200 ? "Exit failed" : "Exit successful");
} else {
console.log(response.statusText); }}catch (e) {
console.log(e);
}
location.href = `${base}/login`;
}
$("#logout").click(async() = > {await logout();
});
Copy the code
When logout in localStorage is reset, syncLogout events are triggered for other tabs, causing them to be redirected to the login page as well:
window.addEventListener("storage", syncLogout);
function syncLogout(event) {
if (event.key === "logout") {
if(! inMemoryToken) {return;
}
console.log("logged out from storage!");
inMemoryToken = null; // Empty the token
if (interval) clearInterval(interval); // Stop the countdown
location.href = `${base}/login`; }}Copy the code
Client – Permission validation
When accessing a page that requires permission verification, you need to add jwt_token to the Header. The server intercepts the request to check whether the user has the permission and permits the request. Otherwise, the request cannot be executed.
The following is the main page of the system. The user has the role of administrator, but does not have the effect of ordinary user:
Implementation Function: If a User has a common role, the Normal User page is displayed. If a User has an Administrator role, the Administrator page is displayed.
$("#normal").click(async() = > {const url = `${base}/normal`;
try {
const response = await fetch(url, {
method: "GET".credentials: "include".headers: {
Authorization: `Bearer ${inMemoryToken["token"]}`,}});if (response.ok) {
const res = await response.json();
if(res.code ! = =200) {
alert(res.message);
return;
}
alert("You have successfully entered the user page!");
} else {
console.log(response.statusText); }}catch (e) {
console.log(e); }}); $("#admin").click(async() = > {const url = `${base}/manage`;
try {
const response = await fetch(url, {
method: "GET".credentials: "include".headers: {
Authorization: `Bearer ${inMemoryToken["token"]}`,}});if (response.ok) {
const res = await response.json();
if(res.code ! = =200) {
alert(res.message);
return;
}
alert("You have successfully entered the admin page!");
} else {
console.log(response.statusText); }}catch (e) {
console.log(e); }});Copy the code
Server – Generates tokens
a. jwt token
The server defines the secret key and stores the user name and user permissions in the Jwt_token payload to generate the JWt_token. The user name is the unique identifier of a user. The JWt_token can be associated with a specific user.
// The access token expires in 15 minutes
private static final long ACCESS_TOKEN_EXPIRE_TIME = 15 * 60 * 1000L;
private static final String KEY = "3EK6FD+o0+c7tzBNVfjpMkNDi2yARAAKzQlk8O2IKoxQu4nF7EdAh8s3TwpHwrdWT6R";
@Override
public Map<String, Object> getJWTToken(User user) {
Date date = new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_TIME);
// Store the user's permissions to claim
List<String> permissions = new ArrayList<>();
for (Role role : user.getRoles()) {
for (Permission permission : role.getPermissions()) {
permissions.add(permission.getUrl());
}
}
Key signKey = new SecretKeySpec(DatatypeConverter.parseBase64Binary(KEY), SignatureAlgorithm.HS256.getJcaName());
String accessToken = Jwts.builder()
.setSubject(user.getUsername())
.claim("permissions", permissions)
.setExpiration(date)
.signWith(signKey)
.compact();
Map<String, Object> map = new HashMap<>();
map.put("jwtToken", accessToken);
map.put("jwtTokenExpiry", date.getTime());
return map;
}
Copy the code
b. refresh token
The server defines secret and stores the user name in the Jwt_token payload to generate refresh_token, where the user name is the unique identifier of the user and refresh_token can be associated with a specific user.
The refresh token expires in 30 days. If the user has not logged in for a month, he/she needs to log in again
private static final long REFRESH_TOKEN_EXPIRE_TIME = 30 * 24 * 60 * 60 * 1000L;
private static final String KEY = "3EK6FD+o0+c7tzBNVfjpMkNDi2yARAAKzQlk8O2IKoxQu4nF7EdAh8s3TwpHwrdWT6R";
public Map<String, Object> getRefreshToken(String username) {
Date date = new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE_TIME);
Map<String, Object> map = new HashMap<>();
Key signKey = new SecretKeySpec(DatatypeConverter.parseBase64Binary(KEY), SignatureAlgorithm.HS256.getJcaName());
String refreshToken = Jwts.builder()
.setSubject(username)
.setExpiration(date)
.signWith(signKey)
.compact();
map.put("refreshToken", refreshToken); // Issue the refresh Token with the user ID
map.put("refreshTokenMaxAge", REFRESH_TOKEN_EXPIRE_TIME / 1000);
return map;
}
Copy the code
Server side – Silent refresh
The server defines the JWT Token refresh interface and receives the refresh Token to refresh the JWT token.
public String refresh(@CookieValue(value = "refresh_token", defaultValue = "") String refreshToken, HttpServletResponse response) {
// Validate refresh Token
if (refreshToken.isEmpty()) {
return new Result<Map<String, Object>>().success(false).message("Token shall not be empty").code(ResultCode.AUTH_NEED).toString();
}
if (tokenService.isExpire(refreshToken)) {
return new Result<Map<String, Object>>().success(false).message("Refresh token has expired").code(ResultCode.AUTH_NEED).toString();
}
/ / get the username
String username = tokenService.getUsernameFromToken(refreshToken);
if (Objects.isNull(username)) {
return new Result<Map<String, Object>>().success(false).message("Refresh token has expired").code(ResultCode.AUTH_NEED).toString();
}
// Get user according to userId
User user = userService.getByUserName(username);
// Regenerate the Access token and refresh token
Map<String, Object> accessTokenInfo = tokenService.getJWTToken(user); // Get access token
Map<String, Object> map = new HashMap<>();
map.put("jwt_token", accessTokenInfo.get("jwtToken"));
map.put("jwt_token_expiry", accessTokenInfo.get("jwtTokenExpiry"));
Map<String, Object> refreshTokenInfo = tokenService.getRefreshToken(username);
// Add refresh token to httpOnly cookie
Cookie cookie = new Cookie("refresh_token", refreshTokenInfo.get("refreshToken").toString());
cookie.setMaxAge(Integer.parseInt(refreshTokenInfo.get("refreshTokenMaxAge").toString()));
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
return new Result<Map<String, Object>>().success(true).message("Refresh succeeded").code(ResultCode.OK).data(map).toString();
}
Copy the code
Server – Log out
When the client logs out, the server needs to expire the refresh_token sent by the client.
public String logout(@CookieValue(value = "refresh_token", defaultValue = "") String refreshToken, HttpServletRequest request, HttpServletResponse response) {
if (refreshToken.isEmpty()) {
return new Result<Map<String, Object>>().success(true).message("Exit successful").code(ResultCode.OK).toString();
}
if (tokenService.isExpire(refreshToken)) {
return new Result<Map<String, Object>>().success(true).message("Exit successful").code(ResultCode.OK).toString();
}
/ / remove the token
Cookie[] cookies = request.getCookies();
Optional<Cookie> cookieOptional = Arrays.stream(cookies)
.filter(cookie1 -> "refresh_token".equals(cookie1.getName()))
.findFirst();
if (cookieOptional.isPresent()) {
Cookie cookie = cookieOptional.get();
cookie.setMaxAge(0);
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
}
return new Result<Map<String, Object>>().success(true).message("Exit successful").code(ResultCode.OK).toString();
}
Copy the code
Server side – Permission validation
The server defines an interceptor that intercepts each request that requires permission validation, parses the permissions in jWY_token, and gets the permissions for the user. If the user has the permissions, the permission is allowed or execution is not allowed. This code has been mentioned above, but is posted for completeness.
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) {
// If not mapped to the method directly through
if(! (objectinstanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
// Check whether permission checks are required
if (method.isAnnotationPresent(VerifyToken.class)) {
VerifyToken verifyToken = method.getAnnotation(VerifyToken.class);
if (verifyToken.required()) {
String token = httpServletRequest.getHeader("Authorization");
// Perform authentication
if (token == null) {
throw new CustomException(ResultCode.AUTH_NEED, "Please log in and perform this operation.");
}
token = token.substring(7);
// Get the username in the token
String userId = tokenService.getUserIdFromToken(token);
if (userId == null)
throw new CustomException(ResultCode.AUTH_NEED, "Please log in and perform this operation.");
/ / authentication token
try {
List<String> permissions = tokenService.getPermissions(token);
// Determine whether the user has the permission based on the user role and URL
String url = verifyToken.url();
if (permissions.contains(url)) {
return true;
}
/ / without permission
throw new CustomException();
} catch (CustomException e) {
throw new CustomException(ResultCode.METHOD_NOT_ALLOWED, "User does not have this permission");
} catch (Exception e) {
throw new CustomException(ResultCode.AUTH_NEED, "Login has expired. Please log in again."); }}}return true;
}
Copy the code
conclusion
If you do all of the above, your application has most of the functionality of a modern application and can avoid the security pitfalls common in JWT implementations!
If you have any questions, suggestions or feedback, please leave a comment.
Code address: springboot_jwt