Tento článek bude popisovat ajaxovou alternativu ke klasickému přihlašování odesláním požadavku na adresu „/j_spring_security_check“ pomocí Spring Security. Použiji k tomu ve Springu velmi oblíbenou knihovnu DWR a javascriptový framework jQuery. Integraci Springu, DWR a jQuery zde nebudu popisovat, protože o tom je již na našem blogu pěkný článek a budu tedy z něho přímo vycházet.

Přihlašovací formulář bude podobný jako pro klasické přihlašování.


<table>
<tr>
<th>Login:</th><td><input type="text" id="j_username" /></td>
</tr>
<tr>
<th>Heslo:</th><td><input type="password" id="j_password" /></td>
</tr>
<tr>
<th>Zapamatovat:</th><td><input type="checkbox" id="_spring_security_remember_me" /></td>
</tr>
<tr>
<td></td>
<td><input id="login-button" type="button" value="Přihlásit"/></td>
</tr>
</table>

Vytvoříme si třídu, která bude reprezentovat výsledek DWR požadavku na přihlášení. Tato třída bude obsahovat atribut pro chybovou zprávu a remember-me cookie.


import javax.servlet.http.Cookie;

/**
* Pomocna trida slouzici k prenaseni informaci o neuspesnem prihlaseni a remember-me cookie.
*/
public class LoginDTO {

private String message;
private Cookie cookie;

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
public Cookie getCookie() {
return cookie;
}

public void setCookie(Cookie cookie) {
this.cookie = cookie;
}

}

Při klasické přihlašování odesláním požadavku na adresu „/j_spring_security_check“ se Spring sám postará o vytvoření remember-me cookie, tedy pokud v požadavku odešleme i parametr „_spring_security_remember_me“ (například zaškrtnutím příslušného checkboxu pro zapamatování přihlášení). Pokud ovšem pro přihlašování využijeme DWR, musíme se o vytvoření remember-me cookie postarat sami. Pro tento účel si vytvoříme třídu RememberMeCookieGenerator, která vygeneruje remember-me cookie přesně stejným postupem, jaký používá Spring Security, aby při dalším přístupu na stránku naší aplikace Spring tuto cookie rozeznal a uživatele automaticky přihlásil.


import javax.servlet.http.Cookie;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;

/**
* Trida slouzici k vygenerovani remember-me cookie pro prihlaseni mimo spring security, napr.
* pomoci DWR. Generovani teto cookie je presne stejne jako ve spring security, cimz je zaruceno, ze
* pri pruchodu tridou rememberMeProcessingFilter je uzivatel touto cookie autentizovan a prihlasen.
*/
public class RememberMeCookieGenerator {

private static final String DELIMITER = ":";
/**
* Nazev remember-me cookie.
*/
private String cookieName = "SPRING_SECURITY_REMEMBER_ME_COOKIE";
/**
* Klic, ktery se pridava do hashovaci funkce pro generovani cookie. Musi byt stejny jako v
* rememberMeAuthenticationProvider a rememberMeServices.
*/
private String key;
private String applicationContextPath;

/**
* Vygeneruje remeber-me cookie presne stejne jako ve spring security.
*
* @param successfulAuthentication uspesne autentizovany uzivatel
*/
public Cookie generateRemembermeCookie(Authentication successfulAuthentication) {
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// delka platnosti rememberme tokenu, 2 tydny
int tokenValiditySeconds = AbstractRememberMeServices.TWO_WEEKS_S;

long expiryTime = System.currentTimeMillis();
// SEC-949
expiryTime += 1000L* tokenValiditySeconds;
String signatureValue = makeTokenSignature(expiryTime, username, password);
String cookieValue = encodeCookie(new String[] {username, Long.toString(expiryTime), signatureValue});
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setMaxAge(tokenValiditySeconds);
cookie.setSecure(false);
if (applicationContextPath == null) {
cookie.setPath("/");
} else {
cookie.setPath(applicationContextPath);
}
return cookie;
}

/**
* Calculates the digital signature to be put in the cookie. Default value is
* MD5 ("username:tokenExpiryTime:password:key")
*/
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
return DigestUtils.md5Hex(username + ":" + tokenExpiryTime + ":" + password + ":" + key);
}

protected String retrieveUserName(Authentication authentication) {
if (isInstanceOfUserDetails(authentication)) {
return ((UserDetails) authentication.getPrincipal()).getUsername();
}
else {
return authentication.getPrincipal().toString();
}
}

protected String retrievePassword(Authentication authentication) {
if (isInstanceOfUserDetails(authentication)) {
return ((UserDetails) authentication.getPrincipal()).getPassword();
}
else {
if (authentication.getCredentials() == null) {
return null;
}
return authentication.getCredentials().toString();
}
}

private boolean isInstanceOfUserDetails(Authentication authentication) {
return authentication.getPrincipal() instanceof UserDetails;
}

/**
* Inverse operation of decodeCookie.
*
* @param cookieTokens the tokens to be encoded.
* @return base64 encoding of the tokens concatenated with the ":" delimiter.
*/
protected String encodeCookie(String[] cookieTokens) {
StringBuffer sb = new StringBuffer();
for(int i=0; i < cookieTokens.length; i++) {
sb.append(cookieTokens[i]);

if (i < cookieTokens.length - 1) {
sb.append(DELIMITER);
}
}

String value = sb.toString();

sb = new StringBuffer(new String(Base64.encodeBase64(value.getBytes())));

while (sb.charAt(sb.length() - 1) == '=') {
sb.deleteCharAt(sb.length() - 1);
}

return sb.toString();
}

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getApplicationContextPath() {
return applicationContextPath;
}

public void setApplicationContextPath(String applicationContextPath) {
this.applicationContextPath = applicationContextPath;
}

}

Tuto třídu nadefinujeme jako bean v našem aplikačním kontextu pro Spring Security. Atribut key je klíč, který musí stejný jako u beanu RememberMeAuthenticationProvider a atribut applicationContextPath je cesta ke kontextu naší aplikace, tedy to, co vratí metoda getContextPath() třídy HttpServletRequest. Jelikož my při ajaxovém přihlašování nemáme k dispozici instanci třídy HttpServletRequest, tak kontext naší aplikace musíme zadat takto ručně.


<bean id="rememberMeCookieGenerator" class="info.hraci.spring.security.RememberMeCookieGenerator">
<property name="key" value="nejaky_klic"/>
<property name="applicationContextPath" value="${application_context_path}" />
<bean>

Nyní si vytvoříme třídu, která bude obsluhovat DWR požadavek na přihlášení. Všimněte si, že používám anotaci @RemoteProxy, pro oznámení DWR, že tento bean bude přístupný z klienta (viz. výše zmiňovaný článek). Nejprve se zkontroluje, zda uživatel zadal login i heslo a pokud ne, tak uloží příslušnout hlášku do atributu message našeho návratového objektu LoginDTO a vrátí jej. Text příslušné hlášky je uložený v properties souboru pro daný jazyk a načítá se pomocí beanu messageSource, který spravuje zdroj zpráv. Pokud nevyžadujeme lokalizaci, tak můžeme do atributu message rovnou ukládat řetězec s příslušnou hláškou. Pokud uživatel zadal vše v pořádku, tak vytvoříme autentizační token a přihlásíme uživatele. Na závěr vygenerujeme remember-me cookie, pokud uživatel zašrtnul daný checkbox „Zamapatovat“ a vložíme ji do návratového objektu.


import javax.servlet.http.Cookie;
import org.apache.commons.lang.StringUtils;
import org.directwebremoting.annotations.RemoteProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

/**
* Trida slouzici k prihlasovani pomoci DWR.
*/
@Service
@RemoteProxy(name = "LoginDWRController")
public class LoginDwrController {

@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private MessageSource messageSource;
@Autowired
private RememberMeCookieGenerator rememberMeCookieGenerator;

/**
* Prihlasi uzivatele na zaklade jeho uzivatelskeho jmena a hesla.
* Pokud bude prihlaseni neuspesne, tak vrati chybovou hlasku.
*/
public LoginDTO login(String username, String password, boolean rememberMe) {
String message = "success";
LoginDTO result = new LoginDTO();
if (StringUtils.isBlank(username) &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp; StringUtils.isBlank(password)) {
message = messageSource.getMessage("error.login.username.required", null, LocaleContextHolder.getLocale());
message += "\n" + messageSource.getMessage("error.login.password.required", null, LocaleContextHolder.getLocale());
result.setMessage(message);
return result;
} else if (StringUtils.isBlank(username)) {
message = messageSource.getMessage("error.login.username.required", null, LocaleContextHolder.getLocale());
result.setMessage(message);
return result;
} else if (StringUtils.isBlank(password)) {
message = messageSource.getMessage("error.login.password.required", null, LocaleContextHolder.getLocale());
result.setMessage(message);
return result;
}
try {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);

if (rememberMe) {
// nastavi remember-me cookie
Cookie cookie = rememberMeCookieGenerator.generateRemembermeCookie(authentication);
result.setCookie(cookie);
}
} catch (Exception ae) {
message = messageSource.getMessage("login.error", null, LocaleContextHolder.getLocale());
}
result.setMessage(message);
return result;
}

}

Aplikační kontext pro nastavení DWR bude vypadat následovně. Musíme zde definovat konvertor pro objekty LoginDTO a Cookie a musíme uvést balíček „naseAplikace.dwr“, který bude scanován na DWR anotace. Bude to tedy ten balíček, kde máme uloženou třídu LoginDwrController.


< ?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:context="https://www.springframework.org/schema/context"
xmlns:dwr="https://www.directwebremoting.org/schema/spring-dwr"
xmlns:aop="https://www.springframework.org/schema/aop"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
https://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
https://www.directwebremoting.org/schema/spring-dwr
https://www.directwebremoting.org/schema/spring-dwr-3.0.xsd
https://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd"
default-autowire="byName">

<dwr :configuration>
<dwr :convert type="bean" class="naseAplikace.dwr.dto.LoginDTO" />
<dwr :convert type="bean" class="javax.servlet.http.Cookie" />
</dwr>
<dwr :annotation-config/>
<dwr :url-mapping/>
<dwr :controller id="dwrController" debug="true" />

<context :component-scan base-package="naseAplikace.dwr" />

</beans>

Nyní máme veškerou funkcionalitu v javě hotovou a pustíme se do javascriptu. Vytvoříme funkci, která se zavolá po kliku na přihlašovací tlačítko a spustí metodu login LoginDWRControlleru. Pokud tato metoda nevrátí žádnou chybu a uživatel chtěl přihlášení zapamatovat, tak se do prohlížeče vloží vygenerovaná remember-me cookie a pokud máme správně nastavený RememberMeAuthenticationFilter v nastavení Spring Security, tak bude uživatel při každém přístupu na stránku přihlášen na základě této cookie.


<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
<script src="<c:url value='/dwr/interface/LoginDWRController.js' />" type="text/javascript">
<script type="text/javascript">

$(function() {

// prihlasovani
$("#login-button").click(function(e) {
e.preventDefault();
LoginDWRController.login($("#j_username").val(), $("#j_password").val(), $("#_spring_security_remember_me").attr("checked"), function(data){
if (data.message == 'success') {
if (data.cookie != null) {
setRememberMeCookie(data.cookie);
}
// akce po uspesnem prihlaseni, napr. schovani prihlasovaciho formulare a zobrazeni informaci o prihlasenem uzivateli
} else {
alert(data.message);
}
});
});

});

/**
* Sestavi remember-me cookie podle cookie vytvorene v java aplikaci.
*
* @param javaCookie cookie sestavena v aplikaci
*/
function setRememberMeCookie(javaCookie) {
var nowMiliseconds = new Date().getTime();
var expireDate = new Date(nowMiliseconds + (javaCookie.maxAge * 1000));
document.cookie = javaCookie.name + "=" + escape(javaCookie.value) + ";expires=" + expireDate.toGMTString() + ";path=" + javaCookie.path;
}