Spring Basic Authentication for REST-APIs mit Datenbank für Nutzer und Rechte

Dieser Blogeintrag behandelt die Basic Authentication mittels Spring Security innerhalb einer Spring Boot App. Zur Konfiguration wird Java Configuration genutzt, dies lässt sich aber auch problemlos mit einer XML-Konfiguration umsetzen. Das Resultat ist eine geschützte Rest-API, die ihre Nutzer und deren Rollen in einer Datenbank speichert und zur Basic Authentication heranzieht.

In diesem Eintrag gehen wir die nötigen Arbeitsschritte durch.

  1. Grundlegendes
  2. Einfache in-Memory Authentifizierung
  3. Datenbank und Entity Design
  4. Konfiguration für Datenbank Authentifizierung
  5. Controllerverknüpfung mit Rechten

Grundlegendes

Zu allererst muss in der pom.xml die zugehörige Abhängigkeit eingefügt werden:

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

Wir brauchen natürlich auch einen einfachen Rest-Service den wir ansprechen können. Da es hierzu schon viele gute Tutorials gibt, verweisen wir an dieser Stelle auf die offizielle Dokumentation über „Building a RESTful Web Service„.

In Memory

Java Configuration wird benutzt um die Spring Boot App entsprechend zu konfigurieren. Folgender Code führt dazu, dass jegliche HTTP-Requests eine Authentifizierung benötigen. Dies kann man nach Wünschen auch nur auf bestimmte Teile der API anwenden. Mit Verkettungen von antmatchers (z.B. antmatchers(„/secured/**)) kann man bestimmt Teile der URL ansprechen und vor Zugriffen schützen.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// protect all resources
		http.authorizeRequests().anyRequest().fullyAuthenticated();
		// protect with http basic authentication
		http.httpBasic();
		http.csrf().disable();
	}
}

Die Authentifizierung wird dann in der Klasse WebSecurityConfiguration, die wiederum von der Klasse GlobalAuthenticationConfigurerAdapter erbt, durchgeführt.

@Configuration
class WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter {
	@Override
	public void init(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication().withUser("itdevcons").password("pass123").roles("ADMIN");
		auth.inMemoryAuthentication().withUser("testuser").password("pass123").roles("USER");
		

	}
}

Man könnte auch in der SecurityConfig eine entsprechende Methode generieren, jedoch bietet sich die Teilung in zwei Klassen nach dem Seperation of Concerns Prinzip an. Wenn alle Schritte bis hierher durchgeführt wurden, dann sollte jede Anfrage auf den Rest-Service mit einer Aufforderung zur Authentifizierung einhergehen und nur erfolgreich sein, wenn einer der beiden User mit entsprechendem Passwort eingegeben wurde.

Datenbankanbindung

Meist möchte man seine Nutzerverwaltung allerdings flexibel gestalten und über eine Datenbank steuern. Ansonsten müsste man immer bei Änderung von Benutzern oder von Passwörtern den Code ändern und den Service neustarten.

Je nach Datenbank muss die pom.xml abgeändert werden und die Zugangsdaten in Spring konfiguriert werden (siehe dazu auch die Spring Dokumentation zur Konfiguration von Datenbanken). Für MySQL sieht die pom.xml z.B. wie folgt aus:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

Für die User brauchen wir eine JPA-Entity und Tabelle in der Datenbank mit entsprechenden Feldern.

@Entity
public class User {
	@GeneratedValue
	@Id
	private int id;
	private String username;
	private String password;
	private String role;

	protected User() {
		super();
	}

	public User(String username, String password, String role) {
		super();
		this.username = username;
		this.password = password;
		this.role = role;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getRole() {
		return role;
	}

	public void setRole(String role) {
		this.role = role;
	}

	public int getId() {
		return id;
	}
}

In der Datenbank speichern wir das Passwort selbstverständlich verschlüsselt. Außerdem brauchen wir natürlich noch Rollen. In unserem Beispiel hat ein User eine bestimmte Rolle und mit einer Rolle sind wiederum verschiedene Rechte verbunden:

@Entity
public class RolesAccess {
	@Id
	private String role;
	private boolean readAccess;
	private boolean writeAccess;
    public String getRole() {
		return role;
	}

	public void setRole(String role) {
		this.role = role;
	}
    public boolean isReadAccess() {
		return readAccess;
	}

	public void setReadAccess(boolean readAccess) {
		this.readAccess = readAccess;
	}
    public boolean isWriteAccess() {
		return writeAccess;
	}

	public void setWriteAccess(boolean writeAccess) {
		this.writeAccess = writeAccess;
	}
}

Für die User und Roles müssen noch Repositories angelegt werden. Wir nutzen hier die Klasse JPARepository.

public interface UserRepository extends JpaRepository<User, Integer> {
	User findByUsername(String username);
}
public interface RolesAccessRepository extends JpaRepository<RolesAccess, String> {
	RolesAccess findByRole(String role);
}

Damit die Authentifizierung über die Datenbank funktioniert, müssen wir noch das Interface UserDetailsService implementieren. Die Methode loadUserByUsername  muss immer überschrieben werden. Es wird dann geschaut, ob es einen User mit dem Namen gibt und die Rolle des Nutzers zu einer AuthorityList hinzugefügt.

@Service
public class CustomUserDetailsService implements UserDetailsService {
	@Autowired
	UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUsername(username);

		if (user == null) {
			return null;
		}
		List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole());

		String password = user.getPassword();

		SecurityConfig.setUserId(user.getId());

		return new org.springframework.security.core.userdetails.User(username, password, auth);
	}
}

Je nach System, kann ein Nutzer natürlich verschiedene Rollen haben. Dies muss dann in der Entity und in der Datenbank mit abgebildet werden. Zur Veranschaulichung beschränken wir uns hier auf eine Rolle pro Benutzer.

Für die Verschlüsselung von Anwendungsseite ergänzen wir die WebsecurityConfiguration um einen BCryptPasswordEncoder.

@Configuration
class WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter {
	@Autowired
	UserRepository accountRepository;

	@Autowired
	CustomUserDetailsService custom;

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

	@Override
	public void init(AuthenticationManagerBuilder auth) throws Exception {
		// use our CustomUserDetailsService to authentificate user
		auth.userDetailsService(custom).passwordEncoder(passwordEncoder());

	}
}

Um für Testzwecke Passwörter direkt verschlüsselt in der Datenbank zu speichern empfiehlt sich folgendes Online Tool: https://www.dailycred.com/article/bcrypt-calculator

Verknüpfung von Controller und Rechten

Die SecurityConfig muss abgeändert werden. Zuerst gibt es den Aufzählungstyp AccessType , damit wir die verschiedenen Rechte unterscheiden können. hasPermissions()  kann dann von den Controllern aufgerufen zu werden um zu prüfen, ob ein Benutzer die nötigen Rechte hat.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	RolesAccessRepository rolesAccessRepository;

	public enum AccessType {
		READ_ACCESS, WRITE_ACCESS;
	}

	private static HashMap<String, RolesAccess> roleMapping = new HashMap<>();
	private static int userId;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests().anyRequest().fullyAuthenticated();
		http.httpBasic();
		http.csrf().disable();

		// initialize the role permission mapping
		rolesAccessRepository.findAll().forEach(role -> {
			roleMapping.put(role.getRole(), role);
		});
	}

	public static boolean hasPermission(AccessType accessType) {
		UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

		return userDetails.getAuthorities().stream().anyMatch(auth -> {
			RolesAccess role = roleMapping.get(auth.getAuthority());
			switch (accessType) {
			case READ_ACCESS:
				return role.isReadAccess();
			case WRITE_ACCESS:
				return role.isWriteAccess();
			}
			return false;
		});
	}

	public static int getUserId() {
		return userId;
	}

	public static void setUserId(int userId) {
		SecurityConfig.userId = userId;
	}
}

Innerhalb eines Controllers kann dann z.B. über folgendenden Code eine SecurityException  geworfen werden.

if (!SecurityConfig.hasPermission(AccessType.READ_EVERY_INCIDENT)
	&& !(SecurityConfig.hasPermission(AccessType.READ_OWN_INCIDENT)
	&& incident.getUserId() == SecurityConfig.getUserId())) 
{
	    throw new SecurityException("Insufficient authorization");
}

Ausblick

Prinzipiell wurde in diesem Beitrag die Einrichtung einer Basic Authentication innerhalb von Spring durchgesprochen. Die Codesamples sind nicht immer vollständig, sollten aber alle wichtigen Schritte der Implementierung abbilden. Das verwendete Beispiel lässt sich natürlich auf unterschiedliche Art und Weise erweitern. Hier noch drei Erweiterungsmöglichkeiten zur Anregung:

  1. Implementierung eines umfangreichen Rechtesystem
  2. Die Möglichkeit einem Benutzer mehrere Rollen zuzuweisen
  3. Umstellung von BasicAuth auf OAuth2