Java >> Java Tutorial >  >> Tag >> Spring

Verwendung von Spring Security in Webanwendungen – Teil VIII

In diesem Beitrag zeigen wir, wie Sie Spring Boot Security zum Anmelden, zur Autorisierung basierend auf der Benutzerrolle, zum Abmelden und zur Fehlerbehandlung verwenden.

Wir werden den folgenden Anwendungsfall besprechen

  1. Ein Benutzer greift auf eine Homepage für eine Anwendung zu.
  2. Ein Benutzer gibt Anmeldeinformationen ein
  3. Wenn die Anmeldeinformationen korrekt sind, erstellen wir eine Sitzung und überprüfen die Benutzerrolle. Benutzer mit der USER-Rolle sehen die Benutzerprofilseite. Benutzer mit der ADMIN-Rolle sehen die Seite mit der Benutzerliste.
  4. Falsche Anmeldedaten, der Benutzer sieht erneut den Anmeldebildschirm, um die Anmeldedaten einzugeben.
  5. Ein Benutzer klickt auf Abmelden, die Sitzung wird gelöscht und der Benutzer wird zur Anmeldeseite weitergeleitet.
  6. Wenn ein Benutzer (egal welcher Rolle) versucht, sich nach dem Abmelden anzumelden, sollte der Benutzer auf die entsprechende Seite umgeleitet werden
  7. In einem Szenario, in dem ein Benutzer weder USER noch ADMIN ist, wird er auf die Fehlerseite umgeleitet
  8. Handhabung des CSRF-Tokens

Um diesen Beitrag vollständig zu verstehen, stellen Sie sicher, dass Sie meine anderen Beiträge zur Spring Boot-Serie gelesen haben.

  1. Spring Boot-REST-CRUD-API
  2. Swagger-Dokumentation
  3. Benutzeroberfläche mit AngularJS

Datenbankänderungen

Da es sich bei diesem Beitrag um die Autorisierung für Benutzer handelt, müssen wir einige Datenbankänderungen vornehmen. Wir werden ein paar Tabellen und entsprechende Modellklassen in unserer REST-API-Modifikation hinzufügen.

  • Tabellenrolle
  • Tabelle user_role

create table role (id int(11) auto_increment primary key not null, role varchar(255) )

create table user_role (user_id int(11) primary key not null, role_id int(11) primary key not null))

user_role Die Tabelle hilft dabei, eine Viele-zu-Viele-Beziehung zwischen dem Benutzer und der Rollentabelle aufrechtzuerhalten. Wir haben nur zwei Rollen für Demozwecke, USER und ADMIN.

Eine weitere Änderung, die wir in der Tabelle user vorgenommen haben, besteht darin, dass wir ein Feld namens password_hash hinzugefügt haben, um das vom Benutzer/Administrator festgelegte Passwort für die Anmeldung eines Benutzers zu speichern. Wir speichern einen Hash-Passwortwert des ursprünglichen Passworts, das der Benutzer festlegen wird.

Abhängigkeiten

Da wir Spring-Security für Authentifizierungs- und Autorisierungszwecke verwenden werden, fügen wir die Abhängigkeit für Spring-Security wie folgt hinzu:

<dependency>   

<groupId>org.springframework.boot</groupId>   

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

Controller und Webschicht

Abgesehen von den erwähnten Änderungen werden wir diesen Beitrag von oben nach unten und nicht von unten nach oben demonstrieren.

Für die Webschicht definieren wir also einen neuen Controller LoginController und modifizieren Sie unseren bestehenden MainController .

package com.betterjavacode.benefits.controller;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.betterjavacode.benefits.entities.User;
import com.betterjavacode.benefits.interfaces.UserManager;

/**
*
* @author Yogesh Mali
*
*/
@Controller
public class LoginController {

public static final Logger LOGGER = LogManager.getLogger(LoginController.class);

@Autowired
UserManager userManager;

/**
*
* @param model
* @return
*/
@RequestMapping(value = "/user", method = RequestMethod.GET)
public String userpage(Model model) 
{
  LOGGER.info(" Enter >> userpage() ");
  Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  String name = auth.getName();
  User user = userManager.findUserByEmail(name);
  model.addAttribute("name", user.getFirstname());
  model.addAttribute("userid", user.getId());
  LOGGER.info(" Exit << userpage() ");
  return "user";
}

/**
*
* @return
*/
@RequestMapping(value = { "/login" })
public String login() {
  return "login";
}

/**
*
* @return
*/
@RequestMapping(value = "/403", method = RequestMethod.GET)
public String Error403() {
  return "403";
}
}

Wie in diesem Controller gezeigt, haben wir eine Benutzerseite, eine Anmeldeseite und eine Fehlerseite (403) definiert. Ein Benutzer mit der Rolle BENUTZER oder ADMIN oder beiden kann auf eine Benutzerseite zugreifen, auf der das Profil des angemeldeten Benutzers angezeigt wird.

Jeder Benutzer sieht unabhängig von seiner Rolle die Anmeldeseite zur Authentifizierung. Wenn während der Authentifizierung oder Autorisierung Fehler auftreten, wird dem Benutzer eine Fehlerseite angezeigt, auf der die Zugriffsverweigerungsseite (403) angezeigt wird.

Der Quellcode für die Anmeldeseite lautet wie folgt:

<!DOCTYPE html><!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head> <title>Benefits Application</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /> <link rel="stylesheet" type="text/css" th:href="@{/css/login.css}" />  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script></head>
<body>
<div class="container"> <form th:action="@{/login}" method="POST" class="form-signin">
<h3 class="form-signin-heading" th:text="Welcome"></h3>
<input type="text" id="email" name="username"  th:placeholder="Email" class="form-control" style="width:350px"/>
<input type="password"  th:placeholder="Password" id="password" name="password" class="form-control" style="width:350px"/>
<div align="center" th:if="${param.error}">
<p style="font-size: 20; color: #FF1C19;">Email or Password invalid, please verify</p>

</div>
<button class="btn btn-lg btn-primary btn-block" name="Submit" value="Login" type="Submit" th:text="Login" style="width:350px"></button> </form></div>
</body></html>

Diese Anmeldeseite zeigt ein einfaches Formular zur Eingabe von Benutzername (E-Mail) und Passwort und verarbeitet diese Authentifizierung mit der Spring-Security-Datenbank-Authentifizierungsmethode.

@RequestMapping(value = "/home", method = RequestMethod.GET)
public String homepage(Model model) 
{
  LOGGER.info(" Enter >> homepage() ");
  Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  String name = auth.getName();
  User user = userManager.findUserByEmail(name);
  model.addAttribute("name", user.getFirstname());
  LOGGER.info(" Exit << homepage() ");
  return "index";
}

Bei Änderungen in MainController geht es um einen authentifizierten Benutzer und die Übergabe des Vornamens dieses Benutzers an das Modell zur Anzeige auf der HTML-Seite. UserManager in der Dienstschicht wurde verbessert, um einen Benutzer basierend auf dem Benutzernamen (der E-Mail ist) zurückzugeben. Wir haben auch eine E-Mail hinzugefügt, die als Einschränkung in der Datenbank eindeutig sein muss.

Die Benutzerseite für einen Benutzer mit der Rolle USER ist nichts anderes als eine Benutzerprofilinformation, die er jederzeit bearbeiten und aktualisieren kann.

<html ng-app="benefitApp"><html ng-app="benefitApp"><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Benefit Application</title><script>document.write('<base href="' + document.location + '" />');</script> <link rel="stylesheet" href="/css/bootstrap.css" /><script src="https://code.angularjs.org/1.6.1/angular.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-route.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-resource.js"></script><script type="text/javascript" src="./js/app.js"></script></head><body ng-controller="UserCtrl">Hello
<p th:text="${name}"></p>

<div>
<ul class="menu">
<li><a th:href="@{'userProfile/' + ${userid}}">Profile</a></li>
</ul>
<div ng-view="ng-view"></div>
</div>
<div class="input-group">
<div class="controls">    <a ng-click="logout()" class="btn btn-small">Logout</a></div>
</div>
</body></html>

Authentifizierung

Jetzt haben wir die Anwendung mit allen erforderlichen Backend-Details zum Hinzufügen des Authentifizierungsteils bereit. Denken Sie daran, dass wir spring-security für die Authentifizierung und Autorisierung einer Anwendung verwenden.

package com.betterjavacode.benefits;

import javax.sql.DataSource;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@ComponentScan("com.betterjavacode.benefits.services")
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

public static final Logger LOGGER = LogManager.getLogger(SecurityConfig.class);

@Autowired
private SimpleAuthenticationSuccessHandler loginSuccess;

@Autowired
private LogoutSuccess logoutSuccess;

@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;

@Autowired
private DataSource dataSource;

@Value("${spring.queries.users-query}")
private String usersQuery;

@Value("${spring.queries.roles-query}")
private String rolesQuery;

@Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
LOGGER.info(" Enter >> configureGlobal() ");
auth.jdbcAuthentication()
.usersByUsernameQuery("select email,password_hash,enabled from user where email=?")
.authoritiesByUsernameQuery("select u.email,r.role from user u inner join user_role ur on(u.id=ur.user_id) inner join role r on(r.id=ur.role_id) where u.email=?")
.dataSource(dataSource)
.passwordEncoder(bCryptPasswordEncoder);
LOGGER.info(" Exit << configureGlobal() ");
}

/**
* Handle Login - Authentication and Redirection
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/home")
.hasAuthority("ADMIN")
.antMatchers("/user")
.hasAnyAuthority("USER", "ADMIN")
.and()
.formLogin()
.loginPage("/login")
.successHandler(loginSuccess)
.permitAll()
.and()
.logout()
.logoutSuccessHandler(logoutSuccess)
.deleteCookies("JSESSIONID")
.invalidateHttpSession(false)
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/403");

}

/**
* Exclude resources from user-access
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**");
}
}

Was passiert in diesem Code?

  • Wenn ein Benutzer mit der Rolle ADMIN oder USER entweder /home- oder /user-Seiten aufruft, muss sich der Benutzer anmelden.
  • Sobald der Benutzer Anmeldeinformationen eingibt, erfolgt die Überprüfung der Anmeldeinformationen anhand des von spring-security bereitgestellten JDBC-Datenbankauthentifizierungsmechanismus.
  • Wenn ein Benutzer der Rolle USER versucht, auf die ADMIN-Homepage zuzugreifen, sieht der Benutzer eine Fehlerseite 403. Authentication Success Handler handhabt die Umleitungsstrategie.
  • Wenn der Benutzer auf der Seite, auf der er sich befindet, auf die Schaltfläche LOGOUT klickt, wird die Sitzung gelöscht und der Benutzer meldet sich von der Anwendung ab. Der Benutzer sieht die Anmeldeseite. Alle Cookies werden gelöscht. Logout Success Handler übernimmt die Umleitung.

Änderungen im AngularJS User Interface Controller

Wie in user.html gezeigt Seite, sobald der Benutzer mit der Rolle USER angemeldet ist, sieht er die URL für seine Profilinformationen. Wenn ein Benutzer auf diese URL klickt, sieht der Benutzer seine Profilinformationen. Diese Seite hat einen Controller namens UserCtrl  die im Grunde die Abmeldung auf dieser Einstiegsseite übernimmt. Das Benutzerprofil wird auf userprofile.html angezeigt Seite mit singleusercontroller. Dieser Winkel-JS-Controller verarbeitet die Aktualisierung von Benutzerprofilinformationen oder die Abmeldung. Das Github-Repository enthält den Rest des Codes.

Umgang mit CSRF-Token

Es gibt zwei Möglichkeiten, wie wir das Cross-Site Request Forgery-Token in der Spring-Anwendung handhaben können. Die erste Möglichkeit besteht darin, diese Token-Generierung zu deaktivieren. Dies ist kein empfohlener Ansatz, da dies Ihre Anwendung möglichen CSRF-Sicherheitsangriffen für Hacker aussetzt. Wenn Sie dies nur zu Demozwecken tun, können Sie dies in SecurityConfig.java deaktivieren, indem Sie http.csrf().disable(). aufrufen

Wie Spring betont, sollte eine Anfrage, die über Browser kommt, CSRF-Schutz enthalten.

Wir werden Spring Security verwenden, um das CSRF-Token auf der Serverseite und nicht auf der Clientseite zu handhaben. Also fügen wir jeder Anfrage, die an den Server kommt, ein CSRF-Token hinzu und verifizieren sie dann. Angular JS überprüft das Cookie für das CSRF-Token, bevor ein Benutzer eine Anfrage stellen kann.

Fügen Sie eine CSRF-Filterklasse hinzu

Wir werden einen Filter hinzufügen, der die Einstellung des CSRF-Tokens in einem Cookie handhabt. Angular JS erwartet einen Cookie-Namen als XSRF-TOKEN. Diese Klasse sieht wie folgt aus:

public class CSRFHeaderFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException 
{
  CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
  if (csrf != null) 
  {
    Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
    String token = csrf.getToken();
    if (cookie == null || token != null && !token.equals(cookie.getValue())) 
    {
      cookie = new Cookie("XSRF-TOKEN", token);
      cookie.setPath("/");
      response.addCookie(cookie);
    }
  }
  filterChain.doFilter(request, response);
}

}

Jetzt aktivieren wir das csrf-Token in SecurityConfig wie unten gezeigt

.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(new CSRFHeaderFilter(), CsrfFilter.class);

Was ist csrfTokenRepository?

Wir weisen spring-security an, CSRF-Token in dem Format zu erwarten, in dem Angular es zurücksenden möchte, einen Header namens X-XSRF-TOKEN anstelle des standardmäßigen X-CSRF-TOKEN. Mit diesen Änderungen müssen wir auf Client-Seite nichts unternehmen.

private CsrfTokenRepository csrfTokenRepository() 
{
  HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
  repository.setHeaderName("X-XSRF-TOKEN");
  return repository;
}

Demo

In diesem Beitrag haben wir gezeigt, wie man Spring Security für die Authentifizierung und Autorisierung verwendet. Jetzt zeigen wir, wie die Anwendung ausgeführt wird. Sobald die Anwendung erstellt und von Eclipse aus ausgeführt wurde, greifen Sie auf die Seite https://localhost:8443/home zu , sehen wir den folgenden Bildschirm:

Es wird derselbe Bildschirm angezeigt, wenn Sie auf https://localhost:8443/user zugreifen . Wenn wir nun die Anmeldeinformationen eines Admin-Benutzers eingeben, sehen wir den folgenden Bildschirm:

Der Benutzerbildschirm sieht wie folgt aus:

Wenn Sie auf Abmelden klicken, wird der Benutzer abgemeldet und der Anmeldebildschirm erneut angezeigt. Auf diese Weise haben wir gezeigt, wie wir Spring Security für die Authentifizierung und Autorisierung verwenden können. Code dafür ist im Github-Repository verfügbar.

Referenzen

  1. Spring Boot-Sicherheit
  2. Anmeldeseite Angular JS und Spring Security


Java-Tag