codestory

Sicherer Spring Boot RESTful Service mit Auth0 JWT

  1. Restful Spring Boot & JWT
  2. Das Projekt Spring Boot erstellen
  3. Model, DAO & REST API
  4. Security & Login Filter
  5. Die Applikation mit dem Browser prüfen
  6. Die Applikation mit  RestTemplate prüfen

1. Restful Spring Boot & JWT

Angenommen, Sie haben ein RESTful API , das auf Spring Boot geschrieben wird. Die Client (die andere Applikation) kann auf Ihr RESTful API rufen und das Ergebnis erhalten
Allerdings können nicht alle RESTful API wegen ihrer Sentivitität public sein. Deshalb müssen Sie sie sichern. Es gibt einige Technik für Sie um Ihr RESTful API zu sichern:
  • RESTful API mit Basic Authentication sichern.
  • RESTful API mit JWT (JSON Web Token) sichern.
Es gibt einige Technik, die ich hier nicht listen. Aber grundsätzlich wenn Client auf einem gesicherten RESTful API rufen möchten, soll es ein Bedarf mit der beigefügten Informationen schicken (es kann username/password sein).
Mehr sehen
  • Tìm hiểu về JWT (JSON Web Token)
Der Aufruf vom Client auf einem public REST API ist einfach. Es ist gleich wie die folgende Illustration:
Falls REST API durch Basic Authentication gesichert wird, muss Client das String "username:password" mit dem Algorithmus Base64 verschlüsseln um ein Array byte zu erhalten und das Array auf Request Headers in jedem Aufruf auf REST API beifügen.
Für mit Auth0 gesicherten REST API ist der Aufruf ein bisschen mehr kompliziert.
  • Der Schritt 1: Sie müssen request (die Anforderung) zur Anmeldung mit username/password schicken und eine Rückantwort bekommen, die ein auf Response Header beigefügten "Authorization String" ist.
  • Der Schritt 2: Nachdem "Authorization String" verfügbar ist, füg es auf Request Header bei um nach REST API aufzurufen.

2. Das Projekt Spring Boot erstellen

Auf Eclipse erstellen Sie das Projekt Spring Boot.
Deklarieren Sie die Bibliotheke Auth0 um das Projekt zu verwenden.
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.auth0/auth0 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>auth0</artifactId>
    <version>1.5.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.auth0/auth0-spring-security-api -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>auth0-spring-security-api</artifactId>
    <version>1.0.0</version>
</dependency>
Die volle Inhalt der File pom.xml:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
      http://maven.apache.org/xsd/maven-4.0.0.xsd">
      
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>SpringBootJWT</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringBootJWT</name>
    <description>Spring Boot + Rest + JWT</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.auth0/auth0 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>auth0</artifactId>
            <version>1.5.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.auth0/auth0-spring-security-api -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>auth0-spring-security-api</artifactId>
            <version>1.0.0</version>
        </dependency>
        

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
SpringBootJwtApplication.java
package org.o7planning.sbjwt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootJwtApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootJwtApplication.class, args);
    }
    
}

3. Model, DAO & REST API

Employee.java
package org.o7planning.sbjwt.model;

public class Employee {

    private String empNo;
    private String empName;
    private String position;

    public Employee() {

    }

    public Employee(String empNo, String empName, String position) {
        this.empNo = empNo;
        this.empName = empName;
        this.position = position;
    }

    public String getEmpNo() {
        return empNo;
    }

    public void setEmpNo(String empNo) {
        this.empNo = empNo;
    }

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

}
EmployeeDAO.java
package org.o7planning.sbjwt.dao;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.o7planning.sbjwt.model.Employee;
import org.springframework.stereotype.Repository;

@Repository
public class EmployeeDAO {

    private static final Map<String, Employee> empMap = new HashMap<String, Employee>();

    static {
        initEmps();
    }

    private static void initEmps() {
        Employee emp1 = new Employee("E01", "Smith", "Clerk");
        Employee emp2 = new Employee("E02", "Allen", "Salesman");
        Employee emp3 = new Employee("E03", "Jones", "Manager");

        empMap.put(emp1.getEmpNo(), emp1);
        empMap.put(emp2.getEmpNo(), emp2);
        empMap.put(emp3.getEmpNo(), emp3);
    }

    public Employee getEmployee(String empNo) {
        return empMap.get(empNo);
    }

    public Employee addEmployee(Employee emp) {
        empMap.put(emp.getEmpNo(), emp);
        return emp;
    }

    public Employee updateEmployee(Employee emp) {
        empMap.put(emp.getEmpNo(), emp);
        return emp;
    }

    public void deleteEmployee(String empNo) {
        empMap.remove(empNo);
    }

    public List<Employee> getAllEmployees() {
        Collection<Employee> c = empMap.values();
        List<Employee> list = new ArrayList<Employee>();
        list.addAll(c);
        return list;
    }

}
MainRESTController.java
package org.o7planning.sbjwt.controller;

import java.util.List;

import org.o7planning.sbjwt.dao.EmployeeDAO;
import org.o7planning.sbjwt.model.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MainRESTController {

    @Autowired
    private EmployeeDAO employeeDAO;

    @RequestMapping("/")
    @ResponseBody
    public String welcome() {
        return "Welcome to Spring Boot + REST + JWT Example.";
    }

    @RequestMapping("/test")
    @ResponseBody
    public String test() {
        return "{greeting: 'Hello'}";
    }

    // URL:
    // http://localhost:8080/employees
    @RequestMapping(value = "/employees", //
            method = RequestMethod.GET, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public List<Employee> getEmployees() {
        List<Employee> list = employeeDAO.getAllEmployees();
        return list;
    }

    // URL:
    // http://localhost:8080/employee/{empNo}
    @RequestMapping(value = "/employee/{empNo}", //
            method = RequestMethod.GET, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public Employee getEmployee(@PathVariable("empNo") String empNo) {
        return employeeDAO.getEmployee(empNo);
    }

    // URL:
    // http://localhost:8080/employee

    @RequestMapping(value = "/employee", //
            method = RequestMethod.POST, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public Employee addEmployee(@RequestBody Employee emp) {

        System.out.println("(Service Side) Creating employee: " + emp.getEmpNo());

        return employeeDAO.addEmployee(emp);
    }

    // URL:
    // http://localhost:8080/employee
    @RequestMapping(value = "/employee", //
            method = RequestMethod.PUT, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public Employee updateEmployee(@RequestBody Employee emp) {

        System.out.println("(Service Side) Editing employee: " + emp.getEmpNo());

        return employeeDAO.updateEmployee(emp);
    }

    // URL:
    // http://localhost:8080/employee/{empNo}
    @RequestMapping(value = "/employee/{empNo}", //
            method = RequestMethod.DELETE, //
            produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public void deleteEmployee(@PathVariable("empNo") String empNo) {

        System.out.println("(Service Side) Deleting employee: " + empNo);

        employeeDAO.deleteEmployee(empNo);
    }

}

4. Security & Login Filter

Die Applikation hat die Anmeldung-Funktion. Client kann eine Anmeldungsanforderung mit der Methode POST senden. Deshalb ist die Erstellung einer Anmeldungsseite nicht nötig. Stattdessen haben wir einen Filter . Wenn ein request mit einem Pfad /login wird es durch Filter behandelt.
Die request , die zum Controller gehen möchten, müssen sie diese Filter überwinden :
In dieser Unterricht werde ich für die Einfachheit 2 User (Benutzer) in die Speicherung erstellen. Client kann mit einem der 2 folgenden User anmelden:
  • tom/123
  • jerry/123
WebSecurityConfig.java
package org.o7planning.sbjwt.config;

import org.o7planning.sbjwt.filter.JWTAuthenticationFilter;
import org.o7planning.sbjwt.filter.JWTLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                // No need authentication.
                .antMatchers("/").permitAll() //
                .antMatchers(HttpMethod.POST, "/login").permitAll() //
                .antMatchers(HttpMethod.GET, "/login").permitAll() // For Test on Browser
                // Need authentication.
                .anyRequest().authenticated()
                //
                .and()
                //
                // Add Filter 1 - JWTLoginFilter
                //
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                        UsernamePasswordAuthenticationFilter.class)
                //
                // Add Filter 2 - JWTAuthenticationFilter
                //
                .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        return bCryptPasswordEncoder;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        String password = "123";

        String encrytedPassword = this.passwordEncoder().encode(password);
        System.out.println("Encoded password of 123=" + encrytedPassword);

        InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> //
        mngConfig = auth.inMemoryAuthentication();

        // Defines 2 users, stored in memory.
        // ** Spring BOOT >= 2.x (Spring Security 5.x)
        // Spring auto add ROLE_
        UserDetails u1 = User.withUsername("tom").password(encrytedPassword).roles("USER").build();
        UserDetails u2 = User.withUsername("jerry").password(encrytedPassword).roles("USER").build();

        mngConfig.withUser(u1);
        mngConfig.withUser(u2);

        // If Spring BOOT < 2.x (Spring Security 4.x)):
        // Spring auto add ROLE_
        // mngConfig.withUser("tom").password("123").roles("USER");
        // mngConfig.withUser("jerry").password("123").roles("USER");

    }
    
}
Wenn ein Request mit dem Pfad /login nach Server geschickt, wird es durch JWTLoginFilter behandelt. Die Klasse wird username/password prüfen. Wenn gültig, wird ein Authorization String in Response Header zur Rückgabe für Client beigefügt.
JWTLoginFilter.java
package org.o7planning.sbjwt.filter;

import java.io.IOException;
import java.util.Collections;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.o7planning.sbjwt.service.TokenAuthenticationService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        System.out.printf("JWTLoginFilter.attemptAuthentication: username/password= %s,%s", username, password);
        System.out.println();

        return getAuthenticationManager()
                .authenticate(new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList()));
    }

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

        System.out.println("JWTLoginFilter.successfulAuthentication:");

        // Write Authorization to Headers of Response.
        TokenAuthenticationService.addAuthentication(response, authResult.getName());

        String authorizationString = response.getHeader("Authorization");

        System.out.println("Authorization String=" + authorizationString);
    }

}
Die Klasse TokenAuthenticationService ist eine Utility Klasse, die "Authorization string" in Response Header schreibt um für Client zurückzugeben. Das Authorization String funktioniert in einer kurzer Zeit (10 Tage). Das heißt , dass Client nur ein mal anmeldet (login) und "das Authorization String " hat und kann es in die obengenannte Zeitraum verwenden. Wenn "das Authorization String" ist ausgelaufen, mussClient wieder login um das neue Authorization String zu schaffen
TokenAuthenticationService.java
package org.o7planning.sbjwt.service;

import java.util.Collections;
import java.util.Date;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class TokenAuthenticationService {
    
    static final long EXPIRATIONTIME = 864_000_000; // 10 days
    
    static final String SECRET = "ThisIsASecret";
    
    static final String TOKEN_PREFIX = "Bearer";
    
    static final String HEADER_STRING = "Authorization";

    public static void addAuthentication(HttpServletResponse res, String username) {
        String JWT = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, SECRET).compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
    }

    public static Authentication getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            String user = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody()
                    .getSubject();

            return user != null ? new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()) : null;
        }
        return null;
    }
    
}
Um REST API aufrufen zu können, werden die Request (Anforderungen)"Authorization string" auf Request Header beigefügt. Die Klasse JWTAuthenticationFilter wird "Authorization string" prüfen, wenn gültig, wird die Anforderung (Request) bestätigt. Er kann zum Controller weiter gehen.
JWTAuthenticationFilter.java
package org.o7planning.sbjwt.filter;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.o7planning.sbjwt.service.TokenAuthenticationService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

public class JWTAuthenticationFilter extends GenericFilterBean {
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        
        System.out.println("JWTAuthenticationFilter.doFilter");
        
        Authentication authentication = TokenAuthenticationService
                .getAuthentication((HttpServletRequest) servletRequest);
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        filterChain.doFilter(servletRequest, servletResponse);
    }
    
}

5. Die Applikation mit dem Browser prüfen

Sie können den Browser benutzen um die Funktion Login zu prüfen und die Operation der Klasse JWTLoginFilter sehen. OK!, Greifen Sie in den folgenden Pfad auf dem Browser zu
Sehen Sie die Information, die aufs Fenster Console von Eclipse geschrieben werden:
JWTLoginFilter.attemptAuthentication: username/password= tom,123
JWTLoginFilter.successfulAuthentication:
Authorization String=Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b20iLCJleHAiOjE1MjA3OTQyNjF9.GP0p5Aaj0HNMShNEAPbieHPeYxeGJ_-lB8ahHr6dJvrs_pAoSgGNCj8bNzRYpi4H7cJ1xQ_DZwV1bMw6ihK2Mw
JWTAuthenticationFilter.doFilter
JWTAuthenticationFilter.doFilter

6. Die Applikation mit  RestTemplate prüfen

Ein Beispiel: die Verwendung der Klasse RestTemplate (Spring REST Client) ruft auf ein von Auth0 gesichertes REST API auf:
JWTClientExample.java
package org.o7planning.sbjwt.restclient;
 
import java.util.Arrays;
import java.util.List;
 
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
 
public class JWTClientExample {
 
    static final String URL_LOGIN = "http://localhost:8080/login";
    static final String URL_EMPLOYEES = "http://localhost:8080/employees";
 
    // POST Login
    // @return "Authorization string".
    private static String postLogin(String username, String password) {
 
        // Request Header
        HttpHeaders headers = new HttpHeaders();
 
        // Request Body
        MultiValueMap<String, String> parametersMap = new LinkedMultiValueMap<String, String>();
        parametersMap.add("username", username);
        parametersMap.add("password", password);
 
        // Request Entity
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parametersMap, headers);
 
        // RestTemplate
        RestTemplate restTemplate = new RestTemplate();
 
        // POST Login
        ResponseEntity<String> response = restTemplate.exchange(URL_LOGIN, //
                HttpMethod.POST, requestEntity, String.class);
 
        HttpHeaders responseHeaders = response.getHeaders();
 
        List<String> list = responseHeaders.get("Authorization");
        return list == null || list.isEmpty() ? null : list.get(0);
    }
 
    private static void callRESTApi(String restUrl, String authorizationString) {
        // HttpHeaders
        HttpHeaders headers = new HttpHeaders();
 
        //
        // Authorization string (JWT)
        //
        headers.set("Authorization", authorizationString);
        //
        headers.setAccept(Arrays.asList(new MediaType[] { MediaType.APPLICATION_JSON }));
 
        // Request to return JSON format
        headers.setContentType(MediaType.APPLICATION_JSON);
 
        // HttpEntity<String>: To get result as String.
        HttpEntity<String> entity = new HttpEntity<String>(headers);
 
        // RestTemplate
        RestTemplate restTemplate = new RestTemplate();
 
        // Send request with GET method, and Headers.
        ResponseEntity<String> response = restTemplate.exchange(URL_EMPLOYEES, //
                HttpMethod.GET, entity, String.class);
 
        String result = response.getBody();
 
        System.out.println(result);
    }
 
    public static void main(String[] args) {
        String username = "tom";
        String password = "123";
 
        String authorizationString = postLogin(username, password);
 
        System.out.println("Authorization String=" + authorizationString);
 
        // Call REST API:
        callRESTApi(URL_EMPLOYEES, authorizationString);
    }
 
}

Anleitungen Spring Boot

Show More