3 Token Based Authentication
3 Token-Based Authentication
Goal
- Brief introduction of token-based authentication.
- Implement user signin functions.
Token-Based vs Session-Based Authentication
sequenceDiagram
participant Browser
participant Server
rect rgb(191, 223, 255)
note right of Browser: Session-Based Auth
Browser->>Server: POST /authenticate username=_&password=_
Server-->>Browser: HTTP 200 OK Set-Cookie: session=...
Browser->>Server: GET /api/user Cookie: session=...
Server-->>Browser: HTTP 200 OK {name: "foo"}
end
Session-Based Authentication
How it works
- A user enters their login credentials (username + password).
- The server verifies the credentials are correct and creates a session which is then stored in a database.
- The client-side stores the session ID returned from the server.
- On subsequent requests, the session ID is verified against the database and if valid the request is processed.
- Once a user logs out of the app, the session is destroyed on the server-side.
sequenceDiagram
participant Browser
participant Server
rect rgb(220, 220, 220)
note right of Browser: Token-Based Auth
Browser->>Server: POST /authenticate username=_&password=_
Server-->>Browser: HTTP 200 OK {token: "JWT..."}
Browser->>Server: GET /api/user Authorization: Bearer ...JWT
Server-->>Browser: HTTP 200 OK {name: "foo"}
end
Token-Based Authentication
How it works
- A user enters their login credentials (=username + password).
- The server verifies the credentials are correct and creates an encrypted and signed token with a private key ( { username: “abcd”, exp: “2021/1/1/10:00” }, private key => token).
- The client-side stores the token returned from the server.
- On subsequent requests, the token is decoded with the same private key and if valid the request is processed.
- Once a user logs out, the token is destroyed client-side, no interaction with the server is necessary.
Advantages of Token-Based Authentication
Stateless, Scalable and Decoupled
- Stateless: The back-end does not need to keep a record of tokens.
- Self-contained, containing all the data required to check its validity. No DB lookup is needed.
Mobile Friendly
- Native mobile platforms and cookies do not mix well.
Disadvantages of Token-Based Authentication
- The size of a token is usually larger than a session id.
Session-Based Authentication 和 Token-Based Authentication 的主要区别如下:
Session-Based Authentication
- 会话创建:用户登录后,服务器会在服务器端创建一个会话(session),并在响应中通过 Set-Cookie 头将会话 ID 发送给客户端。
- 会话存储:服务器在内存或数据库中保存会话信息,客户端每次请求时都会发送这个会话 ID。
- 会话验证:服务器通过会话 ID 查找并验证用户的会话。
- 状态管理:服务器负责管理会话状态,因此扩展性较差,因为需要在服务器上维护所有用户的会话信息。
Token-Based Authentication
- 令牌生成:用户登录后,服务器生成一个 JWT(JSON Web Token),并在响应中返回给客户端。
- 令牌存储:客户端(通常是浏览器)将这个令牌存储在本地(如 localStorage 或 sessionStorage)。
- 令牌验证:客户端每次请求时都会在 Authorization 头中发送这个令牌,服务器验证令牌的有效性。
- 无状态管理:服务器不存储任何会话信息,仅验证令牌,因此扩展性较好,因为服务器无需保存用户的状态信息。
主要区别
- 存储方式: - Session-Based:会话信息存储在服务器上。 - Token-Based:令牌存储在客户端上。
- 验证方式: - Session-Based:服务器验证会话 ID。 - Token-Based:服务器验证令牌的签名和有效性。
- 扩展性: - Session-Based:服务器需要存储和管理所有会话信息,扩展性较差。 - Token-Based:服务器不存储会话信息,仅验证令牌,扩展性较好。
- 安全性: - Session-Based:容易受到 CSRF 攻击,但可以通过使用 SameSite 属性和 CSRF 令牌来防护。 - Token-Based:更容易受到 XSS 攻击,因为令牌存储在客户端的脚本可访问的位置,但可以通过使用 HttpOnly 属性来减轻风险。
Implement Authentication with JSON Web Token(JWT)
Download JWT Library
- Open your
staybooking
project and navigate to thepom.xml
file. Add the following dependency.
<dependencies>
<!-- ... existing dependencies -->
<!-- Only insert the following, do not change or touch other lines -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Only insert the above, do not change or touch other lines -->
</dependencies>
- Save your changes in pom.xml and run Maven Install to download the java libraries. Make sure you see the BUILD SUCCESS information from the console output.
Enable Spring Security AuthenticationManager
- Need to configure the
AuthenticationManager
provided by Spring Security under theSecurityConfig.class
to readuser/authority
data from theMySQL
database for authentication.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import javax.sql.DataSource;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
...
}
@Bean
public PasswordEncoder passwordEncoder() {
...
}
//https://en.wikipedia.org/wiki/SQL_injection
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.passwordEncoder(passwordEncoder())
.usersByUsernameQuery("SELECT username, password, enabled FROM user WHERE username = ?")
.authoritiesByUsernameQuery("SELECT username, authority FROM authority WHERE username = ?");
}
}
- In addition to configuring the datasource for
AuthenticationManage
, we also need to expose it as a bean so that we can use it in our authentication service. In the session-based authentication, we don't do it becasue we can usehttp.loginForm()
provided by Spring Security.
import org.springframework.security.authentication.AuthenticationManager;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
...
}
@Bean
public PasswordEncoder passwordEncoder() {
...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
...
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Explanation
- PasswordEncoder Bean: Configures the
BCryptPasswordEncoder
.- SecurityFilterChain Bean: Configures the HTTP security, allowing POST requests to
/register/*
and requiring authentication for other requests.- AuthenticationManager Bean: Configures the
AuthenticationManager
with JDBC authentication, password encoding, and SQL queries for user details and authorities.This setup avoids using
WebSecurityConfigurerAdapter
and properly configures Spring Security with modern practices.
Create Token Class and JWT Related Utility Functions
- Create a new Token class under
com.eve.staybooking.model
package. We don’t need to mark the class as@Entity
since we don’t store token information in database.
package com.eve.staybooking.model;
public class Token {
private final String token;
public Token(String token) {
this.token = token;
}
public String getToken() {
return token;
}
}
- Create a new package called
com.eve.staybooking.util
and add a new classJwtUtil
to it.
- Open the application.properties file and add a new variable named
jwt.secret
. We’ll use the value of thejwt.secret
for JWT generation. You can use any string as the value.
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.datasource.url = jdbc:mysql://localhost:3306/staybooking?createDatabaseIfNotExist=true&serverTimezone=UTC
spring.datasource.username = root
spring.datasource.password = Eve123456
jwt.secret=secret
- Go back to the
JwtUtil.class
, add a private field secretKey.
package com.eve.staybooking.util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
}
- Add a method to generate the JWT and return the encrypted result of it.
package com.eve.staybooking.util;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
public String generateToken(String subject) {
return Jwts.builder()
.setClaims(new HashMap<>())
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
}
-
Next to the
generateToken()
method, add methods to decrypt a JWT from the encrypted value.import io.jsonwebtoken.Claims; @Component public class JwtUtil { @Value("${jwt.secret}") private String secret; public String generateToken(String subject) { ... } private Claims extractClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } public String extractUsername(String token) { return extractClaims(token).getSubject(); } public Date extractExpiration(String token) { return extractClaims(token).getExpiration(); } public Boolean validateToken(String token) { return extractExpiration(token).after(new Date()); } }
Create Authentication Service
- Go to
com.eve.staybooking.service
package and create a new classAuthenticationService
.
package com.eve.staybooking.service;
import org.springframework.stereotype.Service;
@Service
public class AuthenticationService {
}
- Add the
AuthenticationManager
,AuthorityRepository
, andJwtUtil
as the private field and create a constructor for initialization.
package com.eve.staybooking.service;
import com.eve.staybooking.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Service;
@Service
public class AuthenticationService {
private AuthenticationManager authenticationManager;
private JwtUtil jwtUtil;
@Autowired
public AuthenticationService(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
}
- Similar to the RegisterService, we should throw a
UserNotExist
exception when the given user credential is invalid. So go to the com.eve.staybooking.exception package and create aUserNotExistException.class
.
package com.eve.staybooking.exception;
public class UserNotExistException extends RuntimeException{
public UserNotExistException(String message) {
super(message);
}
}
- Go back to the
AuthenticationService
and add theauthenticate
method to check the user credential and return the Token if everything is OK.
package com.eve.staybooking.service;
import com.eve.staybooking.exception.UserNotExistException;
import com.eve.staybooking.model.Token;
import com.eve.staybooking.model.User;
import com.eve.staybooking.model.UserRole;
import com.eve.staybooking.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
@Service
public class AuthenticationService {
private AuthenticationManager authenticationManager;
private JwtUtil jwtUtil;
@Autowired
public AuthenticationService(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
public Token authenticate(User user, UserRole role) throws UserNotExistException {
Authentication auth = null;
try {
auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
} catch (Exception e) {
throw new UserNotExistException("User does not exist");
}
if (auth == null || !auth.isAuthenticated() || !auth.getAuthorities().contains(new SimpleGrantedAuthority(role.name()))) {
throw new UserNotExistException("User Doesn't Exist");
}
return new Token(jwtUtil.generateToken(user.getUsername()));
}
}
Create Authentication Controller
- Go to
com.eve.staybooking.controller
package and create a new classAuthenticationController
.
package com.eve.staybooking.controller;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthenticationController {
}
- Add
AuthenticationService
as a private field and implement two separate authentication APIs for host and guest.
package com.eve.staybooking.controller;
import com.eve.staybooking.model.Token;
import com.eve.staybooking.model.User;
import com.eve.staybooking.model.UserRole;
import com.eve.staybooking.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthenticationController {
private AuthenticationService authenticationService;
@Autowired
public AuthenticationController(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@PostMapping("/authenticate/guest")
public Token authenticateGuest(@RequestBody User user) {
return authenticationService.authenticate(user, UserRole.ROLE_GUEST);
}
@PostMapping("/authenticate/host")
public Token authenticateHost(@RequestBody User user) {
return authenticationService.authenticate(user, UserRole.ROLE_HOST);
}
}
- Go to CustomExceptionHandler and add a new exception handler for UserNotExistException.
package com.eve.staybooking.controller;
import com.eve.staybooking.exception.UserAlreadyExistException;
import com.eve.staybooking.exception.UserNotExistException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(UserAlreadyExistException.class)
public final ResponseEntity<String> handleUserAlreadyExistExceptions(Exception ex, WebRequest request){
return new ResponseEntity<>(ex.getMessage(), HttpStatus.CONFLICT);
}
@ExceptionHandler(UserNotExistException.class)
public final ResponseEntity<String> handleUserNotExistExceptions(Exception ex, WebRequest request){
return new ResponseEntity<>(ex.getMessage(), HttpStatus.UNAUTHORIZED);
}
}
- Update SecurityConfig to allow
/authentcation/guest
and/authenticate/host
URLs.
Test
- Save all your changes and start your project. Make sure there’s no error in the log.
- Use the sample request in the Postman collection to verify both guest and host authentication are OK. You should see the token string in the response.
java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter