Pisałem ostatnio aplikację webową, która wymagała bardzo prostego uwierzytelniania. Grails – jak się okazuje – ma wtyczkę JSecurity, dzięki której można realizować proste uwierzytelnianie, ale i bardziej skomplikowane operacje autoryzacji. Wtyczka JSecurity dla Grails jest wyposażona w fajny QuickStart, który to już po instalacji pozwala na całkiem kompleksowe zabezpieczenie aplikacji. Mi jednak potrzebny był ekstremalnie prosty mechanizm, na szczęście wtyczka zaoferowała coś w sam raz.

Poniższy wpis jest oparty właśnie na wspomnianym wyżej QuickStarcie, jednak został uproszczony i ograniczony jedynie do mechanizmu autentykacji uwierzytelnienia, nie zaś autoryzacji (kontroli dostępu do zasobów) użytkowników.

Dostęp do aplikacji z JSecurity jest realizowany za pomocą przestrzeni (ang. realms), można skonstruować mechanizm autentykacji uwierzytelnienia (i autoryzacji) na różne sposoby, np. za pomocą katalogów LDAP, loginów systemowych, usług sieciowych itd. My wykorzystamy najbardziej tradycyjny mechanizm: sprawdzania użytkowników zapisanych w bazie danych. JSecurity realizuje mechanizm weryfikacji dostępu do poszczególnych zasobów systemu za pomocą przynależności użytkowników do ról, jednak mój przykład będzie znacznie prostszy – wystarczy, że użytkownik będzie istniał w bazie danych, a już będzie miał dostęp do zasobów systemu.

Oto jak wzbogacić naszą aplikację o wymaganie autentykacji uwierzytelniania użytkowników, kompletnie nie zajmując się kwestiami autoryzacji dostępu do zasobów (wszyscy zalogowani użytkownicy mają dostęp):

1. Instalujemy wtyczkę

grails install-plugin jsecurity

2. Tworzymy klasę dziedzinową użytkownika:

class User {
    String username
    String password
 
    static mapping = {
        table "users"
    }
 
    public String toString() {
        username
    }
}

Używam tutaj mapowania na inną niż domyślna nazwę tabeli (domyślnie byłaby „user”, a ja dałem „users”) – taką kiedyś mi wpojono konwencję, by tabele miały nazwy w liczbie mnogiej. Z kolei metoda toString() przyda się nam nieco później.

Sama tabelka nie wystarczy, trzeba jeszcze użytkownika zapisać w bazie. Możemy to zrobić używając poniższego fragmentu kodu:

new User(username: "jasiu", password: new Sha1Hash("sekret").toHex()).save()

Jak widzisz, hasło jest kodowane za pomocą algorytmu Sha1 (kodowanie to wspierane jest przez JSecurity), nie obędzie się więc bez:

import org.jsecurity.crypto.hash.Sha1Hash

3.  Dodajemy do katalogu grails-app/conf/ pliczek SecurityFilters.groovy o zawartości:

class SecurityFilters {
    def filters = {
        auth(controller: "*", action: "*") {
            before = {
                accessControl { true }
            }
        }
    }
}

Ten filtr spowoduje, że wszystkie żądania (gwiazdki oznaczają dowolny kontroler, i dowolną akcję) zostaną poddane działaniu naszego filtra, a ten, przed wykonaniem żądania, wymusi na nim sprawdzenie praw dostępu.

4. W katalogu realms/ tworzymy klasę MyDbRealm, o postaci:

import org.jsecurity.authc.AccountException
import org.jsecurity.authc.IncorrectCredentialsException
import org.jsecurity.authc.UnknownAccountException
import org.jsecurity.authc.SimpleAccount
 
class MyDbRealm {
    static authTokenClass = org.jsecurity.authc.UsernamePasswordToken
 
    def authenticate(authToken) {
        def username = authToken.username;
        def user = User.findByUsername(username);
        if (!user) {
            throw new UnknownAccountException("Użytkownik ${username} nie istnieje w bazie danych")
        }
 
        def account = new SimpleAccount(username, user.password, "MyDbRealm")
        if (!credentialMatcher.doCredentialsMatch(authToken, account)) {
            log.info 'Nieprawidłowe hasło dla użytkownika ${user.username}'
            throw new IncorrectCredentialsException("Nieprawidłowe hasło dla użytkownika ${user.username}")
        }
        return user;
    }
}

5. Tworzymy kontroler naszego mechanizmu uwierzytelniania AuthController (zapisujemy go oczywiście w katalogu controllers/ naszej aplikacji):

import org.jsecurity.authc.AuthenticationException
import org.jsecurity.authc.UsernamePasswordToken
import org.jsecurity.SecurityUtils
 
class AuthController {
    def jsecSecurityManager
 
    def index = { redirect(action: 'login', params: params) }
 
    def login = {
        return [ username: params.username, targetUri: params.targetUri ]
    }
 
    def signIn = {
        def authToken = new UsernamePasswordToken(params.username, params.password)
 
        try{
            this.jsecSecurityManager.login(authToken)
            def targetUri = params.targetUri ?: "/"
            redirect(uri: targetUri)
        }
        catch (AuthenticationException ex){
            log.info "Błąd logowania użytkownika: '${params.username}'."
            flash.message = "Logowanie nieudane"
 
            def m = [ username: params.username ]
            if (params.targetUri) {
                m['targetUri'] = params.targetUri
            }
 
            redirect(action: 'login', params: m)
        }
    }
 
    def signOut = {
        SecurityUtils.subject?.logout()
        redirect(uri: '/')
    }
}

6. Tworzymy okienko logowania (views/auth/login.gsp):

 
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta name="layout" content="main" />
  <title>Login</title>
</head>
<body>
  <g:if test="${flash.message}">
    <div class="message">${flash.message}</div>
  </g:if>
  <g:form action="signIn">
    <input type="hidden" name="targetUri" value="${targetUri}" />
    <table>
      <tbody>
        <tr>
          <td>Username:</td>
          <td><input type="text" name="username" value="${username}" /></td>
        </tr>
        <tr>
          <td>Password:</td>
          <td><input type="password" name="password" value="" /></td>
        </tr>
        <tr>
          <td />
          <td><input type="submit" value="Zaloguj" /></td>
        </tr>
      </tbody>
    </table>
  </g:form>
</body>
</html>

7. Modyfikujemy nasz główny layout tak, by pokazał czy i jako kto jesteśmy zalogowani:

<jsec:isLoggedIn>
    <div>Witaj <jsec:principal/>! (<g:link controller="auth" action="signOut">Wyloguj</g:link>)</div>
</jsec:isLoggedIn>

I tu właśnie przydała nam się metoda toString() klasy User, albowiem znacznik jsec:principial zwróci nam właśnie wartość obiektu, który przedstawia zalogowanego aktualnie użytkownika (to, co zwróciła metoda authenticate() klasy MyDbRealm), a ten reprezentowany jest właśnie przez toString().
Tag jsec:isLoggedIn wraz z zawartością umieszczamy np. nad użyciem znacznika g:layoutBody. Dla chcących potestować inne tagi dostarczane przez wtyczkę polecam lekturę ich listy.

Gotowe, aplikacja zabezpieczona, a my możemy spać spokojnie :)

I na koniec, dwa linki, jako uzupełnienie tematu zabezpieczania aplikacji Grails z JSecurity: