JWT Token in einer Spring Boot Application verarbeiten

Erwartete Lesezeit: 5 minuten

Was ist ein JWT Token

Ein JSON Web Token ist ein auf JSON basierendes Token, welches einen verifizierten Austausch von Informationen (auch Claims genannt) gewährleistet. (Siehe auch RFC7519)

Standardmäßig besteht ein JWT Token aus drei Teilen , den Header, den Payload und der Signatur.
Der Header eines normalen JWT Tokens enthält einmal den verwendeten Verschlüsselungsalgorithmus und den Typ , der bei einem JWT immer „JWT“ ist.
Beispiel

{
  "alg": "HS256",
  "typ": "JWT"
}

Der Payload enthält den Claim der übertragen werden soll. Fogende Claims sind hierbei laut Spezifikation reserviert

FeldBezeichnungErklärung
issIssuerDer Aussteller des Token
subSubjectInformation für welches Subject dieser Claim gilt.
Hier kann z.B. ein Username hinterlegt werden
audAudienceDie Domäne des Token
expExpiration TimeDer Zeitpunkt ab dem das Token ungültig wird
nbfNot BeforeDer Zeitpunkt ab dem das Token gültig wird
iatIssued AtAusstellungszeitpunkt
jtiJWT IDEine eindeutige Id um ggf,
Mehrfachverwendungen zu verhindern

Ein Token kann neben diesen Feldern auch noch weitere Informationen beinhalten, z.B. Benutzer Rollen und Rechte.

Die Signatur wird nach einem im RFC 7515 genormten Verfahren berechnet.

var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
var hash = HMACSHA256(encodedString, secret);

Der finale Aufbau des Token sieht dann zum Schluß wie folgt aus :

jwt = base64(header) + "." + base64(payload) + "." + base64(hash) 

Beispiel :

Header :
{
  "alg": "HS256",
  "typ": "JWT"
}

Paylod :
{
    "iss": "Marco Oderkerk",
    "iat": 1554633924,
    "exp": 1586169924,
    "aud": "Oderkerk.de",
    "sub": "test@example.com",
    "Role": "Manager"
}

Das finale Token sieht dann wie folgt aus :

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJNYXJjbyBPZGVya2VyayIsImlhdCI6MTU1NDYzMzkyNCwiZXhwIjoxNTg2MTY5OTI0LCJhdWQiOiJPZGVya2Vyay5kZSIsInN1YiI6InRlc3RAZXhhbXBsZS5jb20iLCJSb2xlIjoiTWFuYWdlciJ9.
QUDfgZyBpsdufWWwPdn7RiME3kqb1P2RH2eGBj0S0ZU

Wie können wir das Token nun in Spring Boot nutzen ?

Mein Beispiel basiert auf einer einfachen Spring Boot Rest Api Anwendung.
Um sicherzustellen, dass ein Anwender Aktionen ausführen darf wird die Anmeldung und die Berechtigungen über das Token gesteuert.

Hierzu sind im Payload des Token auch die Authorities hinterlegt worden.

Um nicht in jeden Request eine Berechtigungsprüfung einbauen zu müssen, bauen wir uns einen JWT Filter der von OncePerRequestFilter erbt.

Ablauf der Prüfung:
1. Schritt: Wir holen und den Header „Authorization“ der den JWT Token enthält.
2. Schritt: Wenn der Header gefunden wurde, wird der Token extrahiert, und der Prefix, in unserem Fall ‚Bearer‘ entfernt, sodass nur noch der Token verarbeitet wird
3. Schritt: Extrahieren der Claims auf dem Token mit Hilfe des JWT Secrets
4. Schritt: Ermitteln des Usernamen aus dem Claim „Subject“
5. Schritt: Extrahieren der Authorities
6. Schritt: Erstellen eines UsernamePasswordAuthenticationToken
7. Schritt: Setzen der Authentication im Security Context

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {

		String header = request.getHeader(jwtConfig.getHeader());

		if (header == null || !header.startsWith(jwtConfig.getPrefix())) {
			chain.doFilter(request, response);
			return;
		}

		String token = header.replace(jwtConfig.getPrefix(), "");
		if (logger.isDebugEnabled())
			logger.debug("token found : {}", token.substring(1, 10));
		try {
			Claims claims = Jwts.parser().setSigningKey(jwtConfig.getSecret().getBytes()).parseClaimsJws(token)
					.getBody();
			String username = claims.getSubject();
			if (logger.isDebugEnabled())
				logger.debug("username found : {}", username);

			if (username != null) {
				String[] tempAuthorities = ((String) claims.get("authorities")).split(",");
				List<String> authorities = new ArrayList<String>();
				for (String tauth : tempAuthorities) {
					authorities.add(tauth);
				}
				UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null,
						authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
				if (logger.isDebugEnabled())
					logger.debug("UsernamePasswordAuthenticationToken found : {}", auth.getAuthorities());
				SecurityContextHolder.getContext().setAuthentication(auth);
			}

		} catch (Exception e) {
			logger.error("Exception occured Type={} , Message = {}", e.getClass().toGenericString(), e.getMessage());
			SecurityContextHolder.clearContext();
		}

		chain.doFilter(request, response);
	}

Damit haben wir alle Informationen aus dem Token in der Anwendung.
Als nächstes bauen wir uns eine Security Config die vom WebSecurityConfigurerAdapter erbt. Hier hinterlegen wir für jede URL der Anwendung , welche Berechtigung benötigt wird.

@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable()

				.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

				.exceptionHandling()
				.authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED)).and()
				// Add a filter to validate the tokens with every request
				.addFilterAfter(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
				// authorization requests config
				.authorizeRequests()			
				.antMatchers(HttpMethod.GET, "/").permitAll().antMatchers(HttpMethod.GET, "/csrf/**").permitAll()
				.antMatchers(HttpMethod.GET, "/sw**").permitAll().antMatchers(HttpMethod.GET, "/webjars/**").permitAll()
				.antMatchers(HttpMethod.GET, "/v2/api-docs/**").permitAll().antMatchers("/swagger-resources/**")
				.permitAll().antMatchers("/uploadFile").hasAuthority("fileupload")
				.antMatchers(HttpMethod.GET, "/downloadFile/**").hasAuthority("filedownload")
				// Any other request must be authenticated
				.anyRequest().authenticated()
		// .anyRequest().authenticated()
		;
	}

Nun werden automatisch die Authorities die benötigt werden gegen den SecurityContext abgeglichen. Je nach Ergebnis wird die Verarbeitung weitergeführt oder ein Http401 (Wenn der Token ungültig ist) bzw. ein Http403 (Wenn die Berechtigung fehlt) zurückgegeben.

Weitere Varianten

Wenn man die Berechtigungen nicht Token speichern möchte , kann man einfach im JWT Filter eine SQL oder LDAP Abfrage einbauen, die dann die Authorities liest.
Wenn man nur mit Benutzerrollen arbeiten möchte , dann man entweder im Token die Rollen hinterlegen oder die Rollen per SQL der LDAP Abfrage ermitteln. In der Security Config kann man dann einfach statt mit hasAuthority mit hasRole arbeiten.

Ein komplettes Codebeispiel ist unter
https://github.com/MOderkerk/FileTransferManager
zu finden


Quellen