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; 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; }
19.4.2010 at 10:59
1) Odstavec „Tento článek“ je uveden dvakrát
2) šlo by zvejřenit zazipované zdrojáky?
19.4.2010 at 19:41
ad 1) Opraveno, dekuji za upozornění.
ad 2) S uveřejněním zdrojových souborů je problém. Máme to zaintegrované do aplikace a musel bych zde zveřejnit zdrojové kódy celé aplikace. Všechen potřebný kód jsem snad ale uvedl v článku. Akorat nastavení Spring Security zde chybí. U čeho konrétně potřebujete vidět zdrojové soubory? Něco Vám z uvedených příkladů nefunguje?