birbmap/docs/thesis/content/birdmap-backend.tex

226 lines
16 KiB
TeX
Raw Normal View History

2020-12-01 09:44:11 +01:00
%----------------------------------------------------------------------------
2020-12-01 16:27:26 +01:00
\chapter{Szerver oldal}
2020-12-01 09:44:11 +01:00
\label{chapt:birdmap-backend}
%----------------------------------------------------------------------------
2020-12-01 14:21:26 +01:00
Ebben a fejezetben bemutatom a szerveroldal architektúráját, felépítését. Ismertetem a különböző szoftver komponensek feladatát.
%----------------------------------------------------------------------------
\section{Architektúra}
%----------------------------------------------------------------------------
2020-12-09 17:35:26 +01:00
A szerveroldal fejlesztésénél a három rétegú architektúrát alkalmaztam, melynek lényege, hogy az alkalmazást logikailag három elkülönülő részre bontjuk:
2020-12-01 14:21:26 +01:00
\begin{itemize}
2020-12-08 22:39:18 +01:00
\item \textbf{Adatelérési réteg}. Ez a rész felel a tárolt entitások modell definícióiért, illetve azoknak a kiolvasásáért, tárolásáért egy adatbázisból vagy fájlrendszerből.
2020-12-09 17:35:26 +01:00
\item \textbf{Megjelenítési réteg}. Ezen réteg feladata a kliensoldal közvetlen kiszolgálása. Bármilyen irányú kommunikáció a kliensek felé ezen a rétegen keresztül történik.
\item \textbf{Üzleti logikai réteg}. Minden, ami nem a közvetlen kommunikációért, megjelenítésért vagy adat elérésért, tárolásért felel, az ide kerül.
2020-12-01 14:21:26 +01:00
A fenti két réteg között helyezkedik el és feladata a különböző folyamatok értékelése és futtatása, valamint az adatok feldolgozása.
\end{itemize}
Az ASP.NET Core beépítetten támogatja a dependency injection-t, mely a \verb+Startup+ osztály \verb+ConfigureServices+ metódusával konfigurálható.
Én minden rétegbe tettem egy ilyen \verb+Startup+ osztályt, hogy azok feleljenek a saját szolgáltatásaik konfigurálásáért és regisztrálásáért.
%----------------------------------------------------------------------------
2020-12-08 22:39:18 +01:00
\section{Adatelérési réteg}
2020-12-01 14:21:26 +01:00
%----------------------------------------------------------------------------
Az adatelérést az Entity Framework Core segítségével oldottam meg. Telepítettem egy MSSQL adatbázis szervert a számítógépemre, melynek csatlakozási paramétereivel
a \verb+Startup+ osztályban felkonfigurálom az EF Core által nyújtott \verb+DbContext+ saját leszármazott változatát.
Így csak az entitások elkészítése és azok alapértelmezett értékeinek az adatbázisba való feltöltése marad hátra.
%----------------------------------------------------------------------------
\subsection{Entitások}
%----------------------------------------------------------------------------
Mivel az adatok nagy részét külső szolgáltatások fogják nyújtani, így lokálisan összesen két entitás létrehozására volt szükség.
Az egyik a \verb+User+, mely az alkalmazás felhasználóinak adatait tárolja.
A másik a \verb+Service+, mely a külső szolgáltatások adatainak tárolását szolgálja, amelyeket azért tárolok az adatbázisban és nem mondjuk a konfigurációs fájlban,
mert szerettem volna, hogyha a kezelőfelületen lehetne őket szerkeszteni, törölni.
2020-12-06 15:41:59 +01:00
\begin{lstlisting}[style=csharp, caption=A User és a Service modell]
2020-12-01 14:21:26 +01:00
public record User
{
public int Id { get; set; }
public string Name { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public Roles Role { get; set; }
public bool IsFromConfig { get; set; }
}
public record Service
{
public int Id { get; set; }
public string Name { get; set; }
2020-12-06 15:41:59 +01:00
public Uri Url { get; set; }
2020-12-01 14:21:26 +01:00
public bool IsFromConfig { get; set; }
}
\end{lstlisting}
Az alkalmazás használata szempontjából a felhasználók két csoportba oszlanak.
Vannak adminisztrátor és sima felhasználók, utóbbi csak az adatok olvasására, míg előbb azok módosítására is jogosult.
2020-12-01 16:27:26 +01:00
A \verb+Role+ mező ennek a megkülönböztetsnek a jelzője.
2020-12-01 14:21:26 +01:00
%----------------------------------------------------------------------------
\subsection{Seedelés}
2020-12-03 16:37:13 +01:00
\label{subsect:seeding}
2020-12-01 14:21:26 +01:00
%----------------------------------------------------------------------------
Az alkalmazás konfigurációs fájljából meg lehet adni alapértelmezett felhasználókat és szolgáltatásokat.
Ezeknek megkülönböztetésére szolgál az entitások \verb+IsFromConfig+ mezője.
A szerver indítása legelején, megvizsgálja, hogy létezik-e az adatbázis és ha igen kitöröl minden olyan entitást ahol az \verb+IsFromConfig+ mező igaz.
Majd hozzáadja az újonnan beolvasott értékeket.
%----------------------------------------------------------------------------
\section{Üzleti logikai réteg}
%----------------------------------------------------------------------------
2020-12-07 23:19:09 +01:00
Ebben a rétegben található meg a szerver legtöbb szolgáltatása. It vannak implementálva a Birbnetes Command and Control és Input komponensekkel kommunikáló szolgáltatások is,
melyeket azok OpenAPI leírói alapján az NSwag Studio \cite{nswag-studio} alkalmazással generáltam. Az OpenAPI a klienseken kívül definiálja még az azok által használt modelleket is.
2020-12-01 16:27:26 +01:00
A Command and Control által használt \verb+Device+ modell tartalmazza annak egyedi azonosítóját, státuszát, koordinátáit és a használt szenzorok listáját,
melyeknek szintén van egy modellje \verb+Sensor+ néven. Ennek szintén van azonosítója és státusza. Az Input szolgáltatásnak is van saját modellje,
amely a hangüzenetek metaadatait reprezentálja. Többek között tartalmazza a kihelyezett eszköz egyedi azonosítóját és a hangüzenet keltének dátumát.
2020-12-01 14:21:26 +01:00
Ugyan itt található meg a \verb+User+ és \verb+Service+ entitások létrehozásáért, olvasásáért, szerkesztéséért és törléséért felelős szolgáltatások is.
2020-12-09 17:35:26 +01:00
Valamint itt található még az autentikációért felelős szolgáltatás is. A felhasználók jelszavainak tárolására a HMAC (Hash-based Message Authentication Code) algoritmust,
2020-12-07 23:19:09 +01:00
pontosabban annak a \verb+HMACSHA512+ \cite{hmacsha512} C\# implementációját használtam.
2020-12-01 14:21:26 +01:00
2020-12-01 16:27:26 +01:00
Minden jelszóhoz generálok egy egyedi kulcsot és azzal egy hash-t, majd ezeket tárolom a \verb+User+ modell \verb+PasswordSalt+ és \verb+PasswordHash+ mezőiben.
Amikor egy felhasználó be akar jelentkezni először megvizsgálom, hogy egyáltalán létezik-e az adatbázisban az adott nevű felhasználó,
ha igen, akkor a megadott jelszóból az imént említett folyamattal generált kulcsot és hash-t összehasonlítom az adatbázisban tárolttal.
2020-12-01 14:21:26 +01:00
2020-12-09 17:35:26 +01:00
Azért hasznos ily módon, és nem mondjuk egyszerű szöveges formában tárolni a felhasználók jelszavát, mert így a felhasználón kívül senki sem tudja, hogy mi volt az eredeti jelszava,
az algoritmus egyirányú volta miatt\footnotemark. Ha véletlenül rossz kezekbe kerülne az adatbázis tartalma, akkor sem fognak tudni bejeletkezni a felhasználók adataival.
2020-12-06 15:41:59 +01:00
\footnotetext{Generálni egyszerű és gyors. Visszafejteni közel lehetetlen.}
%----------------------------------------------------------------------------
\subsection{Kommunikációs Szolgáltatások}
%----------------------------------------------------------------------------
A kliensoldal frissítésére több megoldás is létezik. Például bizonyos időközönként lehetne kéréseket indítani a szerver felé a friss adatok megszerzéséért.
Egy másik megoldás a SignalR használata, amellyel a klienseket eseményvezérelten lehet értesíteni, megvalósítja a kétoldalú kommunikációt.
Így a kliensek csak akkor indítanak kéréseket amikor az adat tényleg változott. Ezzel a technológiával oldottam meg például, hogy az eszközök állapotainak változására
frissüljön a felület.
2020-12-07 23:19:09 +01:00
Egy másik szerveroldalon használt szolgáltatás a Birbnetes MQTT kommunikációért felelős szolgáltatás,
mely felregisztrál a \ref{subsect:birdnetes-ai-service}-as alfejezetben bemutatott AI Service által publikált üzenetekre.
2020-12-09 17:35:26 +01:00
Ezekben az üzenetekben található a hanganyagok egyedi azonosítója, illetve azok seregélytől való származásának valószínűsége.
Ha a szolgáltatás kap egy ilyen üzenetet akkor lekérdezi a \ref{subsect:birdnetes-input-service}-es alfejezetben bemutatott Input Service-től
a hanganyag azonosítójához tartozó metaadatokat.
2020-12-09 17:35:26 +01:00
Ezekből felhasználva a kihelyezett eszköz azonosítóját, a hanganyag beérkezésének dátumát és az említett valószínűséget új üzenetek készülnek, melyeket egy pufferben tárolódnak.
Ezt a folyamatot a \ref{fig:birdmap-mqtt-service}-es ábra szemlélteti.
\begin{figure}[!ht]
\centering
\includegraphics[width=150mm, keepaspectratio]{figures/mqtt-communication-sequence.png}
\caption{A Birdmap MQTT szolgáltatásának szekvenciája}
\label{fig:birdmap-mqtt-service}
\end{figure}
A puffer tartalmát másodperces gyakorisággal elküldöm a klienseknek a SignalR segítségével.
2020-12-09 17:35:26 +01:00
Azért van szükség a puffer használatára, mert az MQTT-n érkezett üzenetek gyakorisága akár milliszekundum nagyságrendű is lehet.
Míg a szerver képes is az üzeneteket feldolgozni, ha ezeket rögtön tovább küldeném a kliensek felé, azok nem biztos, hogy képesek lennének rá.
2020-12-01 14:21:26 +01:00
%----------------------------------------------------------------------------
\section{Megjelenítési réteg}
%----------------------------------------------------------------------------
2020-12-02 21:41:01 +01:00
A fejezet elején említett \verb+Startup+ osztály ebben a rétegben található, itt kerülnek az egyes szolgáltatások regisztrálásra.
2020-12-03 16:37:13 +01:00
Itt történik a \ref{subsect:seeding} fejezetben leírt adatbázis seedelése is.
2020-12-02 21:41:01 +01:00
Többek között a naplózás is itt kerül inicializálásra, mely az NLog saját konfigurációs fájljával történik.
Meg lehet adni különböző szűrőket és kimeneteket, amellyel szelektálni lehet, hogy az egyes naplózott események hova kerüljenek.
2020-12-09 17:35:26 +01:00
Például az MQTT szolgáltatás napló bejegyzéseit a \ref{lst:nlog-config} lista alapján szűrtem.
Minden \verb+Debug+ szinttől nagyobb és \verb+Error+ szinttől kisebb bejegyzés, mely tartalmazza az \verb+Mqtt+ kulcsszót az \verb+mqttFile+ azonosítójú fájlba kerül.
2020-12-02 21:41:01 +01:00
2020-12-06 15:41:59 +01:00
\begin{lstlisting}[style=xml, caption=Az NLog.config fájl egy részlete, label=lst:nlog-config]
2020-12-02 21:41:01 +01:00
<targets>
...
2020-12-06 15:41:59 +01:00
<target xsi:type="File" name="mqttFile" fileName="..." layout="..." />
2020-12-02 21:41:01 +01:00
...
</targets>
<rules>
...
2020-12-06 15:41:59 +01:00
<logger name="*.*Mqtt*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile"
final="true"/>
2020-12-02 21:41:01 +01:00
...
</rules>
\end{lstlisting}
2020-12-03 16:37:13 +01:00
A \verb+Startup+ osztály másik metódusa a \verb+Configure+, mellyel a HTTP kérések csővezetéke konfigurálható.
Azaz, hogy egy kérés-t milyen sorrendben dolgozzák fel a regisztrált szolgáltatások.
A szerveroldali kivételkezelésre szánt szolgáltatás, az \verb+ExceptionHandlerMiddleware+ is itt van használva,
amely elkap minden kivételt, amit a csővezeték további részei dobtak és JSON formátumban visszaadja azokat a kliensnek.
2020-12-06 15:41:59 +01:00
%----------------------------------------------------------------------------
\subsection{Swagger}
\label{subsect:backend-swagger}
%----------------------------------------------------------------------------
2020-12-07 23:19:09 +01:00
Az NSwag \cite{nswag} szoftvercsomag segítségével regisztrálok egy szolgáltatást,
mely a szerveroldalon található kontrollereket felhasználva generál egy OpenAPI specifikációt és annak egy Swagger UI \cite{swagger-ui} felületet,
2020-12-03 16:37:13 +01:00
ahol a végpontok kipróbálhatóak, tesztelhetőek kliensoldal nélkül is.
\begin{figure}[!ht]
\centering
\includegraphics[width=150mm, keepaspectratio]{figures/swagger-ui.png}
\caption{Az alkalmazásom Swagger felülete}
\label{fig:swagger-ui}
\end{figure}
2020-12-01 16:27:26 +01:00
%----------------------------------------------------------------------------
\subsection{Kontrollerek}
%----------------------------------------------------------------------------
2020-12-03 16:37:13 +01:00
A kontrollerek határozzák meg, hogy a szerveroldalon milyen végpontokat, milyen paraméterekkel lehet meghívni, ahhoz milyen jogosultságok kellenek.
2020-12-06 15:41:59 +01:00
\begin{lstlisting}[style=csharp, caption=Az eszköz kontroller és annak "online" végpontja, label=lst:devices-controller]
2020-12-03 16:37:13 +01:00
[Authorize(Roles = "User, Admin")]
[ApiController]
[Route("api/[controller]")]
public class DevicesController : ControllerBase
{
[Authorize(Roles = "Admin")]
[HttpPost, Route("online")]
public async Task<IActionResult> Onlineall()
{
...
}
...
}
\end{lstlisting}
2020-12-09 17:35:26 +01:00
A jogosultságok kezelését a JSON Web Token-ekkel oldottam meg. A felhasználó bejelentkezéskor kap egy ilyen token-t,
2020-12-06 15:41:59 +01:00
amelyben tárolom a hozzá tartozó szerepet. A \ref{lst:devices-controller}-as listában látszik, hogy hogyan használom ezeket a szerepeket.
A \verb+DevicesController+ végpontjait alapértelmezetten \verb+User+ és \verb+Admin+ jogosultságú felhasználó hívhatja, az "api/devices/online" végpontot azonban csak \verb+Admin+ jogosultságú.
2020-12-09 17:35:26 +01:00
Hasonlóképpen oldottam meg ezt a többi kontrollernél is. A \verb+User+ felhasználók csak olyan végpontokat hívhat, mely kizárólag az állapotok olvasásával jár.
2020-12-06 15:41:59 +01:00
Az \verb+Admin+ felhasználók hívhatnak bármilyen végpontot.
A szerveroldalon négy különböző kontroller található, melyek mindegyikének alapvető feladata az üzleti logikát megvalósító szolgáltatások használata, a működés naplózás,
2020-12-09 17:35:26 +01:00
illetve az imént említett végpontok autorizálása és kiszolgálása. Ezeken kívül a kontrollerek speciális feladata a következő:
2020-12-06 15:41:59 +01:00
\begin{itemize}
\item Az \textbf{AuthController} felel a felhasználók bejelentkezésének lebonyolításáért, a JSON Web Token elkészítéséért. Az \verb+[Authorize]+ helyett itt az \verb+[AllowAnonymous]+ attribútum van használva, mellyel azt lehet jelezni, hogy a végpont bejelentkezés nélkül is hívható.
2020-12-07 23:19:09 +01:00
\item A \textbf{ServiceController} felel az alkalmazás által használt külső szolgáltatások állapotának lekérdezhetőségéért. Ilyenek például a Birbnetes rendszer vagy az MQTT szolgáltatás állapota.
2020-12-06 15:41:59 +01:00
\item A \textbf{DevicesController} felel a Command and Control mikroszolgáltatással való kommunikáció megvalósításáért, illetve a SignalR használatáért. Ha egy felhasználó valamelyik végpontot használva változtat valamelyik eszköz állapotán, akkor a kontroller jelez erről a klienseknek.
\item A \textbf{LogController} felel azért, hogy az \verb+Admin+ jogosultságú felhasználók letölthessék a szerveroldalon készült naplófájlokat.
\end{itemize}
Az adatbázisból érkező adatok gyakran túl sok vagy túl kevés információt tartalmaznak ahhoz, hogy kiolvasás után rögtön elküldjem a kliensoldalnak.
2020-12-09 17:35:26 +01:00
Például amikor a felhasználó bejelentkezik a kiolvasott \verb+User+ objektum tartalmazza annak jelszavát (hash-elt formában), viszont nem tartalmazza az autorizációhoz használt token adatait.
2020-12-06 15:41:59 +01:00
Ennek a megoldására adatátviteli objektumokat hoztam létre, melyek csak azokat a mezőket tartalmazzák amelyekre a felhasználónak szüksége van.
Az adatbázisból kiolvasott objektum hasznos részeit és egyéb használni kívánt információt átmásolom az átviteli objektumba. Majd ezt küldöm el a kliensoldal felé.
2020-12-07 23:19:09 +01:00
Hogy az adatok másolását ne kézzel kelljen csinálnom, az AutoMapper \cite{automapper} szoftvercsomagot alkalmaztam, melynek használata rendkívül egyszerű.
2020-12-06 15:41:59 +01:00
Meg lehet adni profilokat, ahol két objektum közötti leképzéseket lehet felvenni. A szoftvercsomag automatikusan átmásolja az azonos nevű mezőket az egyik objektumból a másikba,
de meg lehet adni egyedi leképzéseket is.
\pagebreak
\begin{lstlisting}[style=csharp, caption=Egy példa az AutoMapper használatára.]
// Creating maps.
CreateMap<User, AuthenticateResponse>()
.ForMember(m => m.Username, opt => opt.MapFrom(m => m.Name))
.ForMember(m => m.UserRole, opt => opt.MapFrom(m => m.Role))
.ReverseMap();
CreateMap<Service, ServiceRequest>()
.ReverseMap();
// Using maps.
IMapper mapper = GetMapper();
User user = GetUserFromDb();
AuthenticateResponse response = mapper.Map<AuthenticateResponse>(user);
\end{lstlisting}