Spring MVC Security konfiguracija

Na samom startu svake Spring MVC web aplikacije susrećemo se sa konfiguracijom i pisanjem bean-ova koji ćemo kasnije koristiti.
Jedna od stvari koje ta konfiguracija sadrži su i security bean-ovi.
Dobra stvar Spring-a je da možemo da imamo definisanu sigurnost aplikacije na nivou bean-ova, bez pisanja dodatnog koda u Java klasama. Kad kažem sigurnost, mislim na login i autorizaciju korisnika za korišćenje aplikacije koja takvu funkcionalnost zahteva, naravno.

Spring MVC aplikacija ća automatski instancirati sve klase koje se nalaze u bean-ovima, tako da one ostaju spremne za korišćenje u Javi, što je, ispostavilo se, dosta praktično, i uveliko smanjuje broj napisanih linija koda.

Osnovna struktura aplikacije može izgledati npr. ovako:

1. servlet-context.xml – Osnovni konfiguracioni fajl

Prvo sto moramo da kreiramo svakako je servlet-context.xml.
On, u jednostavnoj varijanti potrebnoj za ovu priču, može izgledati ovako:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc 
                        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
                        http://www.springframework.org/schema/beans 
                        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/context 
                        http://www.springframework.org/schema/context/spring-context-3.0.xsd
                        ">
                        
	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	<context:component-scan base-package="com.ghcpro" />

	<!-- Configures the @Controller programming model -->
	<mvc:annotation-driven />

	<!-- Forwards requests to the "/" resource to the "welcome" view -->
	<mvc:view-controller path="/" view-name="home" />
	<!-- Handles HTTP GET requests for /resources/** by efficiently serving 
		up static resources in the ${webappRoot}/resources directory -->
	<mvc:resources mapping="/resources/**" location="/resources/" />

	<!-- MySQL Configuration -->
	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost:3306/db_name" />
		<property name="username" value="root" />
		<property name="password" value="" />
	</bean>
    
    <!-- Transaction manager -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

Obavezan je DataSource bean, njega će Spring koristiti da bi se povezao na bazu i pročitao podatke o korisnicima.

	
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<property name="driverClassName" value="com.mysql.jdbc.Driver" />
	<property name="url" value="jdbc:mysql://localhost:3306/db_name" />
	<property name="username" value="root" />
	<property name="password" value="" />
</bean>

Ostalo u servlet-context.xml fajlu su manje ili više standarne bean-ovi Spring aplikacije. Vi ćete svakako svoj proširiti i prilagoditi svojim potrebama.

2. security-context.xml – Konfiguracija Spring Security-a

Kada smo kreirali servlet-context.xml, biće nam potreban još jedan, security-context.xml.

Generalno, sve ovo može da bude definisano i u jednom xml-u, al radi bolje preglednosti, ipak ćemo ih razdvojiti u posebne fajlove.

Ovako izgleda security-context.xml

<beans:beans xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">

	<http use-expressions="true" auto-config="true" >
	    <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"  />
		<intercept-url pattern="/login*" filters="none" />
		<intercept-url pattern="/resources/**" filters="none" />
		<intercept-url pattern="/**" access="hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')" />
		<form-login login-processing-url="/j_spring_security_check" always-use-default-target="true" login-page="/login" default-target-url="/" authentication-failure-url="/loginfailed" />
		<logout logout-url="/logout" logout-success-url="/" />
		<access-denied-handler error-page="/access-denied"/>
	</http>

	<authentication-manager>
		<authentication-provider>
			<jdbc-user-service data-source-ref="dataSource"
				users-by-username-query="select email as username, password, active as enabled from users where email=?"
				authorities-by-username-query="select u.email as username, concat('ROLE_', u.role) as authority from users u where u.email=?" />
		</authentication-provider>
	</authentication-manager>

</beans:beans>

O ovom xml-u definišemo prava pristupa delovima aplikacije, kao i inicijalnu autorizaciju korisnika.

Obratite pažnju na nekoliko stvari:

  1. Akcije u Spring VMC kontrolerima treba da odgovaraju definiciji u security bean-u
    <form-login login-processing-url="/j_spring_security_check" always-use-default-target="true" login-page="/login" default-target-url="/" authentication-failure-url="/loginfailed" />
    <logout logout-url="/logout" logout-success-url="/" />
    <access-denied-handler error-page="/access-denied"/>
    

    što podrazumeva da će vaš npr. HomeController imati definisane akcije “login”, “loginfailed”, “access-denied” i “logout” url-ove, kako bi Spring znao da pravilno redirektuje određene zahteve. Naravno, prilagođavate po svojoj potrebi, ovo napominjem da bih ispratio konfiguraciju iz primera.

  2. Role moraju početi sa “ROLE_”. Dakle, mi ovde u primerima imamo ROLE_USER i ROLE_ADMIN (koje ćete koristiti ukoliko želite da razdvojite delove aplikacije na npr. deo za korisnike i deo za administratore). Za početak je ovo sasvim dovoljno. U ovom primeru vidimo kreiranje select upita da bismo dobili rezultat koji će Spring “razumeti”:
    concat('ROLE_', u.role) as authority
  3. SQL select upiti moraju imati naziv kolona kakav Spring očekuje, Ukoliko to nije slučaj, koristite alias-e kolona, kao ja u ovom primeru.

VAŽNO!

O ovom primeru je aplikacija konfigurisana tako da ne dozvoljava pristup bilo kom delu sajta ukoliko se korisnik nije prijavio, pored toga, deli aplikaciju na korisnički i administratorski deo. Ukoliko to nije potreba, izmenite “intercept-url” delove po želji.
Na primer, da biste dozvolili pristup kompletnom sajtu promenite intercept-url pravila u npr.

<intercept-url pattern="/**" filters="none" />

Prilagodite ostala “intercept-url” pravila po želji.
I obratite pažnju na redosled definisanja pravila! Spring će prestati da upoređuje pravila sa datim URL-um čim naiđe na prvo pravilo koji odgovara URL-u.

3. Login forma

I na kraju nam ostaje da napravimo login formu koja će se povezati sa Spring security-em.

Osnovna login forma izgleda ovako:

<c:if test="${not empty error}">
		<div class="error">
			Login failed. Please check your credentials and try again.<br /> 
			System error: ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
		</div>
	</c:if>

	<form name='f' action="<c:url value='/j_spring_security_check' />" method='POST'>
		<table>
			<tr>
				<td>Email:</td>
				<td><input type='text' name='j_username' value=''>
				</td>
			</tr>
			<tr>
				<td>Password:</td>
				<td><input type='password' name='j_password' />
				</td>
			</tr>
			<tr>
				<td colspan="2" align="right"><input class="button blue" name="submit" type="submit" value="Login" />
				</td>
			</tr>
		</table>
	</form>

I Out-of-Box Spring Security je spreman.

Nadam se da će nekome ovo biti od pomoći.

Advertisements

Kreiranje objekta sa RowMapper-om

Ideja je da kreiramo jedostavan, precizan i uniforman način za mapiranje/čitanje objekata iz baze, a da pritom ne koristimo gotove framework-e.

Idealan način, bar za mene, su RowMapper klase.

Ovo nije ništa novo, RowMapper-i se koriste u Javi kao podrazumevani, bar koliko sam ja uspeo da primetim.

Ono sto mi nije jasno, zašto ih ja nisam koristio ranije? Definitivno skraćuju vreme pisanja, osiguravaju precizan kod, klase su male i fokusirane, baš sve kako treba.

Za početak nam treba interface koji ćemo koristiti kako bismo “predali” određeni RowMapper klasi za čitanje podataka iz baze.

using System.Data;

namespace GHC.Data
{
    /// <summary>
    /// Map data reader values to a new instance of type T
    /// </summary>
    public interface IRowMapper
    {
        T Map(IDataRecord record);
    }
}

Konkretne stvari

Da bih najbolje objasnio o čemu se radi, uzeću za primer objekat City. Dakle, cilj je jasan, čitamo podatke o gradovima iz baze.

Evo ga model:

using System;

namespace GHC.Models
{
    public class City
    {
        public Guid ID { get; set; }
        public string Name { get; set; }
        public string PostalCode { get; set; }
        public override string ToString()
        {
            return this.PostalCode + " " + this.Name;
        }
    }
}

Za svaki model ćemo kreirati odgovarajuću RowMapper klasu koja će implementirati prethodno kreirani IRowMapper interfejs.

Možda Vam izgleda kao preterano, ali taj kod svakako pišete, samo što ga u ovom slučaju držimo u posebnim klasama. Da ponovim, zadržaćemo male, fokusirane klase, automatski smanjujemo mogućnost greške, “gubljenja” po kodu i pisanje čuvenog “špageti koda”

Ovako izgleda RowMapper klasa za model City:

using GHC.Data;
using GHC.Models;

namespace GHC.RowMappers
{
    public class CityRowMapper : IRowMapper
    {
        public City Map(System.Data.IDataRecord record)
        {
            return new City()
            {
                ID = record.GetGuid(0),
                Name = record["Name"].ToString(),
                PostalCode = record["PostalCode"].ToString(),
                State = new State() { ID = record.GetGuid(3) }
            };
        }
    }
}

Ono što se vidi i što stvari pojednostavljuje, je to sto implementacija IRowMapper-a po definiciji vraća zadati tip, tako da kasnije nema nikakvih dodatnih konvertovanja i suvišnog koda.

Connector – a gde su ti podaci?

Došli smo do još konkretnijih stvari.

Kreiramo klasu Connector koja će nam služiti za povezivanje sa bazom i vraćanje podataka preko RowMapper-a

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using Microsoft.SqlServer.Management.Smo;

namespace GHC.Data
{
    public class Connector
    {
		private string _connectionString;

		public string Server { get; set; }
		public string Database { get; set; }
		public string Username { get; set; }
		public string Password { get; set; }

		public string ConnectionString
		{
			get
			{
				if (string.IsNullOrEmpty(_connectionString))
				{
					_connectionString = CreateConnectionString(this.Server, this.Database, this.Username, this.Password, null, 0);
				}

				return _connectionString;
			}
		}

		private string CreateConnectionString(string server, string database, string username, string password, int connectionTimeout)
		{
			SqlConnectionStringBuilder cb = new SqlConnectionStringBuilder();
			cb.DataSource = server;
			cb.InitialCatalog = database;
			cb.MultipleActiveResultSets = true;
			cb.UserID = username;
			cb.Password = password;
			cb.Pooling = true;

			if (connectionTimeout >= 0)
			{
				cb.ConnectTimeout = connectionTimeout;
			}

			return cb.ToString();
		}

		///<summary>
		/// Execute reader and return first instance of object created with IRowMapper type to instantiate
        	/// </summary>
		/// Sql Query to retreive object from database
	        /// IRowMapper used to map object type from reader
        	/// List of T
        	public T Get<T>(string query, IRowMapper rowMapper)
        	{
            		object obj = null;

			using (SqlConnection Connection = new SqlConnection(ConnectionString))
			{
				try
				{
					Connection.Open();

					using (SqlCommand command = Connection.CreateCommand())
					{
						command.CommandText = query;

						SqlDataReader reader = command.ExecuteReader();

						e.Reader = reader;

						reader.Read();

						if (reader.HasRows)
						{
							obj = rowMapper.Map(reader);
						}

						reader.Close();
					}
				}
				catch (SqlException ex)
				{
					throw ex;
				}
				catch(Exception)
				{
					throw;
				}
			}

            		return (T)obj;
        	}

		///<summary>
		/// Execute reader and create list of provided type using IRowMapper interface
		/// </summary>
		/// Type of object to create
		/// Sql Query
		/// IRowMapper to map provided object
		/// List of provided object type
		public List<T> GetList<T>(string query, IRowMapper<T> rowMapper)
		{
			List list = new List();

			using (SqlConnection Connection = new SqlConnection(ConnectionString))
			{
				try
				{
					Connection.Open();

					using (SqlCommand command = Connection.CreateCommand())
					{
						command.CommandText = query;

						SqlDataReader reader = command.ExecuteReader();

						while (reader.Read())
						{
							list.Add((T)rowMapper.Map(reader));
						}

						reader.Close();
					}
				}
				catch (SqlException ex)
				{
					throw ex;
				}
				catch(Exception)
				{
					throw;
				}
			}

			return list;
		}
	}
}

U klasi Connector nema ništa komplikovano. To je ConnectionString koji nam svakako treba, koristimo DataReader jer je neuporedivo brži od popunjavanja npr. DataSet-ova, i mapiramo dati rezultat na neki RowMapper.
Ovde vidimo dve metode, jednu koja vraća jednu instancu datog tipa, i drugu koja vraca listu datih tipova.

public T Get<T>(string query, IRowMapper<T> rowMapper)
public List<T> GetList<T>(string query, IRowMapper<T> rowMapper)

Praktično, prva metoda će “direktno” vratiti zadati tip T, dok će druga vratiti listu tipa T, šta nam više treba u životu?

A evo i kako.

Podrazumevam malo lepše pisanje upita, ali za potrebe primera, biće sasvim dovoljni.

using System.Collections.Generic;
using GHC.Models;
using GHC.RowMappers;

namespace GHC.Services
{
    public class CityService
    {
        private Connector Connector
        {
            get
            {
                // Kreiranje Connector klase cete izdvojiti negde, naravno, ispisao sam ga ovde, tek toliko da imamo neki kompletan kod
                Connector connector = new Connector();
                connector.Server = "server";
                connector.Database = "database";
                connector.Username = "username";
                connector.Password = "password";

                return connector;
            }
        }

        public City GetCity()
        {
            return Connector.Get<City>("SELECT ID, Name, PostalCode FROM City WHERE CityID='00000000-0000-0000-0000-000000000001'", new CityRowMapper());
        }

        public List<City> GetCities()
        {
            return Connector.GetList<City>("SELECT ID, Name, PostalCode FROM City", new CityRowMapper());
        }
    }
}

I eto, sad možete da zaboravite na nepotrebna prepakivanja SQL result set-ova na modele gde zatrebaju.
Sa ovako kreiranim RowMapper-om za model, lista ili objekat je kasnije popunjen u jednoj liniji koda.

WPF – prevod aplikacije

Ovde smo da rešimo dilemu oko prevoda WPF aplikacije.

Tu su delovi koda i objašnjenja, nadam se da će nekome koristiti. Trudiću se da sve ispričam tako da bude razumljivo, obećavam.

Na internetu se dosta pominje prevod WPF aplikacije koristeći “Resources” fajlove, ali to nije ono što sam hteo.
Potreba je bila da postoje ekternalizovani fajlovi sa prevodima, tako da se lako dodaje novi i menja postojeći prevod.
Odluka je da za ovu aplikaciju koristimo XML format. Jednostavan i direktan, lak za pregled i izmenu.

1. XML fajl sa prevodom

Dakle, prvo što će nam trebati je XML fajl sa prevodom, snimite ga u neki folder gde ce ga aplikacija pronaći. Ja nekako volim da koristim “Lang” folder, deluje mi logično…
Evo ga primer XML-a, skraćena verzija radi lakšeg razumevanja.

<?xml version="1.0" encoding="utf-8"?>
<Language name="Srpski" code="sr">
  <Buttons>
    <Ok Text="Ok"/>
    <Save Text="Snimi"/>
    <Cancel Text="Odustani"/>
    <Close Text="Zatvori"/>
    <New Text="Novi" />
    <Edit Text="Izmeni" />
    <Delete Text="Obriši" />
    <Refresh Text="Osveži" />
    <Print Text="Štampa" />
  </Buttons>
</Language>

Vrlo jednostavno, priznaćete.
Sve one koje mrzi da koriste XML, format prevoda može biti bilo kakav, s’tim da ćete u tom slučaju morati da pišete sopstveni parser tog fajla. Ne komplikovati stvari bez preke potrebe.
Ja sam za neke ranije aplikacije koristio INI format, sto se pokazalo kao zaista isplativo, ali sam se ovde ipak odlučio za XML. Zašto, ne sećam se tačno… Ima tu nekih razloga, al nije tema ove priče…

2. Čitanje fajla sa prevodom

Sledeće što će Vam trebati je neki “parser” prethodno kreiranog XML-a, tj. klasa koja će znati da prepozna “ključeve” i vrati vrednost zadatog “ključa”.

Kreirajte novu klasu i dajte joj naziv TranslationService.

Evo ga kod:

using System;
using System.Xml;
using System.IO;

namespace GHC.Utilities
{
    public class TranslationService
    {
        private static string _language = "sr";
        public static string Language { get { return _language; } set { _language = value; } }

        private static XmlDocument doc;
        private static XmlDocument Document
        {
            get
            {
                if (doc == null)
                {
                    doc = new XmlDocument();

                    string langPath = Path.Combine("Lang", Language + ".flgx");
                    if (File.Exists(langPath))
                    {
                        doc.Load(langPath);
                    }
                }

                return doc;
            }
        }

        public static String GetTranslation(string key)
        {
            try
            {
                string xpath = "Language/" + key.Replace(".", "/");
                XmlNode nod = Document.SelectSingleNode(xpath);
                if (nod != null)
                {
                    return nod.Attributes[0].Value.Replace(@"\n", Environment.NewLine);
                }
                else
                {
                    //Ako nije pronadjen zadati kljuc, vrati isti da vidimo sta fali od prevoda
                    return key;
                }
            }
            catch
            {
                //Necemo da dozvolimo da aplikacija pukne ako nesto nije u redu, ili hocemo, vas izbor
                return key;
            }
        }
    }
}

Podrazumevani jezik je Srpski. Ukoliko želite da koristite neki drugi prevod/fajl, promenite “Language” property pre prvog poziva GetTranslation(string key) metode, i eto ga!
Zašto pre prvog poziva? Ako pogledate kod, videćete da se XmlDocument kreira jednom, prvi put kada se pozove, sa zadatim Language-om… Da usput malo razmišljamo i o optimizaciji, jel tako…

Dakle, sva pamet ove klase je u traženju xml nod-a po zadatoj putanji.

string xpath = "Language/" + key.Replace(".", "/");
XmlNode nod = Document.SelectSingleNode(xpath);

Odakle sad ova tačka i kakav je sad ovo string.Replace(“.”, “/”)? Objasniću na kraju, deluje elegantno kasnije u XAML-u.

Sve što ćete trebati da uradite je da pošaljete željenu putanju do ključa u samom XAML-u, dolazimo do toga.

3. XAML – MarkupExtension

MarkupExtension je veoma zanimljiv, ako ga niste koristili do sada, svideće Vam se, sigurno, bar meni jeste. To je ono sto ustvari daje mogućnost da svoje transformacije zadržite na nivou XAML-a, bez preteranih egzibicija u samom kodu.

Negde u kod dodajte novu klasu, mi smo je nazvali TranslationExtension koja nasledjuje baznu MarkupExtension klasu.

using System;
using System.Windows.Markup;

namespace GHC.UI.Extensions
{
    public class TranslationExtension : MarkupExtension
    {
        private string _key;

        public TranslationExtension() { }
        public TranslationExtension(string key) {
            this._key = key;
        }

        [ConstructorArgument("Key")]
        public string Key { get { return _key; } set { _key = value; } }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return GHC.Utilities.TranslationService.GetTranslation(Key);
        }
    }
}

Ne bih da mnogo širim priču dalje oko MarkupExtension-a, sve će biti jasno kada se vidi primer XAML-a

4. Konačno – XAML

I to je to, samo je ostalo da “pozovemo” naš servis za prevod iz XAML-a.

Prvo što nam treba je xmlns našeg servisa. Negde u Window tag dodajte ovo

xmlns:ext="clr-namespace:GHC.Extensions;"

Ovim smo dodali namespace gde se nalazi TranslationService u klasu, slično kao kada bismo koristili “using GHC.Extensions;” u kodu.

Na primer:


 <button name="btnOk"></button>
 <button name="btnCancel"></button>

I to je to!

Sada je jasna ona priča odakle tačka u parseru od malopre. TranslationService menja tačku sa slash-om da bi dobio ispravan xpath, odakle povlači vrednost noda, i vraća prevedeni tekst.

Sve što treba da uradite je da dodate poziv na Translation iz svog XAML-a, i prevod je tu.

Content="{ext:Translation Buttons.Save}"

Nadam se da sam bio od pomoći.