Spring Session امکان ساده ای برای مدیریت session در سمت سرور میباشد که محدودیت های Http Session را مرتفع میسازد. این راه حل قابلیت به اشتراک گذاشتن Session ها در بین سرویس های یک cloud بدون اینکه وابسته به یک نود خاص باشند را امکانپذیر میکند علاوه بر این امکان استفاده از چندین Session در مرورگر و header را فراهم میکند
در این بخش به بررسی session و احراز هویت کاربران با استفاده از Spring Session در وب و با استفاده از JDBC , Gemfire , Redis و یا MongoDB دیتای session را ذخیره و بازیابی خواهیم کرد
ابتدا وابستگی های مورد نیاز را به pom اضافه میکنیم :
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.0.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> </dependency> </dependencies>
بعد تنظیمات ساده ای برای Redis server در Application.properties ست میکنیم :
spring.redis.host=localhost spring.redis.port=6379
اضافه کردن کلاس Configuration@ برای Spring Session :
@Configuration @EnableRedisHttpSession public class SessionConfig extends AbstractHttpSessionApplicationInitializer { }
تنظیمات پروژه بدون Spring Boot :
اول اضافه کردن وابستگی ها :
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> <version>1.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.5.0.RELEASE</version> </dependency>
حال ساخت کلاس Configuration@ بدون Spring Boot :
@Configuration @EnableRedisHttpSession public class SessionConfig extends AbstractHttpSessionApplicationInitializer { @Bean public JedisConnectionFactory connectionFactory() { return new JedisConnectionFactory(); } }
* همانطور که میبینید تفاوت استفاده از Spring Boot و استفاده نکردن ان بسیار جزیی است و فرقش تنها Spring Boot برای ما JedisConnectionFactory Bean را میسازد ولی وقتی Spring Boot استفاده نکردیم ما خود JedisConnectionFactory را میسازیم
تنظیمات Application :
اضافه کردن REST Controller :
@RestController public class SessionController { @RequestMapping("/") public String helloAdmin() { return "hello admin"; } }
اضافه کردن تنظیمات امنیتی :
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin").password("password").roles("ADMIN"); } @Override protected void configure(HttpSecurity http) throws Exception { http .httpBasic().and() .authorizeRequests() .antMatchers("/").hasRole("ADMIN") .anyRequest().authenticated(); } }
در تنظیمات امنیتی ما یک کاربر داریم و Authentication را از نوع Basic قرار دادیم
تست :
ابتدا سرور Redis را آماده میکنیم و دیتای احتمالی ذخیره شده را حذف میکنیم
public class SessionControllerTest { private Jedis jedis; private TestRestTemplate testRestTemplate; private TestRestTemplate testRestTemplateWithAuth; private String testUrl = "http://localhost:8080/"; @Before public void clearRedisData() { testRestTemplate = new TestRestTemplate(); testRestTemplateWithAuth = new TestRestTemplate("admin", "password", null); jedis = new Jedis("localhost", 6379); jedis.flushAll(); } }
بعد سرور Redis را تست میکنیم که آیا دیتایی در ان وجود دارد یا خیر :
@Test public void testRedisIsEmpty() { Set<String> result = jedis.keys("*"); assertEquals(0, result.size()); }
حالا تست میکنیم که آیا هنگامی که یک کاربر unauthenticate درخواست غیر مجازی را ارسال میکند توسط Spring Security کد 401 ارسال میشود یا خیر :
@Test public void testUnauthenticatedCantAccess() { ResponseEntity<String> result = testRestTemplate.getForEntity(testUrl, String.class); assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode()); }
حالا تست نهایی را برای احراز هویت و دریافت Authentication Token انجام میدهیم :
- ابتدا صحت احراز هویت Admin را بررسی میکنیم که آیا موفقیت آمیز بوده است یا خیر
- بعد مقدار session را از Response Header دریافت میکنیم و از آن برای احراز هویت ثانویه استفاده میکنیم و اگر معتبر بود کلیه دیتا های Redis را پاک میکنیم
- در آخر یک درخواست دیگر ارسال میکنیم تا تایید کنیم که کاربر ما logout شده است و دیگر احراز هویت و session آن معتبر نیست
@Test public void testRedisControlsSession() { ResponseEntity<String> result = testRestTemplateWithAuth.getForEntity(testUrl, String.class); assertEquals("hello admin", result.getBody()); //login worked Set<String> redisResult = jedis.keys("*"); assertTrue(redisResult.size() > 0); //redis is populated with session data String sessionCookie = result.getHeaders().get("Set-Cookie").get(0).split(";")[0]; HttpHeaders headers = new HttpHeaders(); headers.add("Cookie", sessionCookie); HttpEntity<String> httpEntity = new HttpEntity<>(headers); result = testRestTemplate.exchange(testUrl, HttpMethod.GET, httpEntity, String.class); assertEquals("hello admin", result.getBody()); //access with session works worked jedis.flushAll(); //clear all keys in redis result = testRestTemplate.exchange(testUrl, HttpMethod.GET, httpEntity, String.class); assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode()); //access denied after sessions are removed in redis }