در این بخش به مباحث پایه ای امنیت مثل Cookie, Login و Authentication در REST Api توسط Spring Security 5 میپردازیم
معماری Spring Security کلا بر اساس Servlet Filter ها طراحی شده پس قبل از لایه Spring MVC میتواند درخواست های Http را از نظر امنیتی بررسی کند
موقعی که از Spring Boot استفاده میکنیم راحترین راه برای ثبت و فعال کردن Spring Security استفاده از EnableWebSecurity@ در کلاس Config است :
@Configuration @EnableWebSecurity public class SecurityJavaConfig extends WebSecurityConfigurerAdapter { // ... }
و اگر از Spring Boot استفاده نکنیم برای فعال کردن Spring Security ارث بری کردن از کلاس AbstractSecurityWebApplicationInitializer و در Constractor آن کلاس Config را پاس میدهیم :
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { public SecurityWebApplicationInitializer() { super(SecurityJavaConfig.class); } }
و یا در web.xml انرا تعریف میکنیم :
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
* باید به این نکته توجه داشت که نام filter را springSecurityFilterChain بگذاریم تا Bean مورد نظر توسط Context شناسایی و ساخته شود
*همچنین باید دقت داشت که برای هر فیلتر تعریف شده دقیقا همان کلاس ساخته نمیشود، یک DelegatingFilterProxy وجود دارد که متد های فیلتر ها را به Bean های داخلی مورد نظر واگذار میکند و آن Bean میتواند از انعطاف پذیری و چرخه عمر Context اسپرینگ هم استفاده کند
الگوی اعمال فیلتر در URL به صورت پیش فرض */ است و تمامی URL ها را در بر میگیرد ولی ما میتوانیم URL مورد نظر را برای Service مورد نظر انتخاب کنیم
اعمال تنظیمات Authentication در جاوا :
از کلاس Configuration@ که از WebSecurityConfigurerAdapter ارث بری کرده بود میتوان متد های مورد نظر را Override کرده و تنظیمات دلخواه را اعمال کنیم و role هایی را برای کاربران تعریف کنیم :
@Configuration @EnableWebSecurity public class SecurityJavaConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password(encoder().encode("adminPass")).roles("ADMIN") .and() .withUser("user").password(encoder().encode("userPass")).roles("USER"); } @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } }
* توجه داشته باشید که کد بالا برای تست میباشد و برای Authentication نام کاربری، رمز عبور و role ها hardcode شده اند که از روش in-memory استفاده شده در آینده این قسمت بصورت کاربردی تر و جامع تر پرداخته خواهد شد
و همینطور تنظیمات کلی تر برای Api را ست کنیم :
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .exceptionHandling() .authenticationEntryPoint(restAuthenticationEntryPoint) .and() .authorizeRequests() .antMatchers("/api/foos").authenticated() .antMatchers("/api/admin/**").hasRole("ADMIN") .and() .formLogin() .successHandler(mySuccessHandler) .failureHandler(myFailureHandler) .and() .logout(); }
متغییر http برای ست کردن تنظیمات امنیتی با استفاده از متد های منعطفی که دارد استفاده میشود مثلا با متد antMatchers مشخص کردیم که کاربران احراز هویت شده میتوانند به /api/foos دسترسی داشته باشند و همینطور مشخص کردیم که کاربرانی که ADMIN هستند میتوانند به **/api/admin دسترسی داشته باشند (Authorization)
مفهوم Entry Point :
بطور معمول در یک برنامه وب، کاربران وقتی به آدرسی درخواست میدهند که نیاز احراز هویت دارد و انها login نکرده باشند اتوماتیک به صفحه Login هدایت میشوند اما این امر در REST service کاربردی ندارد و اگر کاربر احراز هویت نشده درخواست را ارسال کند که نیاز به احراز هویت داشته باشد باید با کد 401 (UNAUTHORIZED) روبرو شود و عملیات درخواستی آن انجام نشود
Spring Security این عملیات را بطور اتوماتیک توسط مفهوم Entry Point برای وب پیاده میکند و تنها نیاز دارد که ما با متد authenticationEntryPoint انرا تنظیم کنیم ولی در REST ما خود باید آنرا پیاده سازی کنیم و کد وضعیت 401 را ارسال کنیم :
@Component public final class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } }
فیلتر Form Login :
در REST Api راه های زیادی برای احراز هویت وجود دارد یکی از آنها که بصورت پیش فرض وجود دارد استفاده از فیلتر Form Login است که در مسیر org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter وجود دارد، این فیلتر دو متد اضافی برای ست کردن Handler های دلخواه دارد که successHandler برای موقعی که احراز هویت موفقیت آمیز بود و failurHandler برای موقعی که احراز هویت نا موفق بود قابل استفاده است
در اینجا باید اشاره کنیم که استفاده از EnableWebSecurity@ باید میشود ما درگیر پیاده سازی ها و تنظیمات کمتری شویم
کد وضعیت احراز هویت در REST :
احراز هویت موفقیت آمیز باید کد وضعیت 200 را به جای 301 که در وب استفاده میکنیم ارسال کند و این از طریق استفاده از authentication handler ای که میسازیم میسر است :
public class MySavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private RequestCache requestCache = new HttpSessionRequestCache(); @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest == null) { clearAuthenticationAttributes(request); return; } String targetUrlParam = getTargetUrlParameter(); if (isAlwaysUseDefaultTargetUrl() || (targetUrlParam != null && StringUtils.hasText(request.getParameter(targetUrlParam)))) { requestCache.removeRequest(request, response); clearAuthenticationAttributes(request); return; } clearAuthenticationAttributes(request); } public void setRequestCache(RequestCache requestCache) { this.requestCache = requestCache; } }
و اگر احراز هویت نا موفق بود باید کد 401 را به جای 302 ارسال کنیم و همانند روش بالا انرا تعریف میکنیم ولی از کلاس SimpleUrlAuthenticationFailureHandler استفاده میکنیم البته استاندارد پیاده سازی شده در این کلاس برای ارسال کد 401 کافی است و نیازی به تعریف و ارث بری کلاس جدید برای اینکار نیست
نقش cookie در REST service :
از کوکی میتوان برای نگهداری وضعیت احراز هویت بعد از login استفاده کرد و بعد از آن همیشه با ارسال کوکی سرویس متوجه خواهد شد که کاربر احراز هویت شده است
فرض کنیم آدرس login/ برای احراز هویت در نظر گرفته شده و ما میخواهیم با نام کاربری و رمز عبور احراز هویت کنیم :
curl -i -X POST -d username=user -d password=userPass http://localhost:8080/spring-security-rest/login
درخواست بالا یک کوکی در پاسخ خواهد داشت پس بهتر است آنرا در قالب یک فایل ذخیره کنیم، بیاییم دوباره درخواست بالا را با امکان ذخیره کوکی ارسال کنیم :
curl -i -X POST -d username=user -d password=userPass -c /opt/cookies.txt http://localhost:8080/spring-security-rest/login
حالا در درخواست های بعدی کافی است کوکی از فایل خوانده و ارسال شود و درخواست های دیگری را همراه با وضعیت احراز هویت ارسال کنیم :
curl -i --header "Accept:application/json" -X GET -b /opt/cookies.txt http://localhost:8080/spring-security-rest/api/foos
پاسخ چیزی شبیه به این خواهد بود :
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 24 Jul 2013 20:31:13 GMT [{"id":0,"name":"JbidXc"}]
تنظیمات امنیتی در قالب xml :
ما میتوانیم تنظیمات امنیتی که در بالا ست کردیم را در قالب xml هم ست کنیم :
<http entry-point-ref="restAuthenticationEntryPoint"> <intercept-url pattern="/api/admin/**" access="ROLE_ADMIN"/> <form-login authentication-success-handler-ref="mySuccessHandler" authentication-failure-handler-ref="myFailureHandler" /> <logout /> </http> <beans:bean id="mySuccessHandler" class="org.rest.security.MySavedRequestAwareAuthenticationSuccessHandler"/> <beans:bean id="myFailureHandler" class= "org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"/> <authentication-manager alias="authenticationManager"> <authentication-provider> <user-service> <user name="admin" password="adminPass" authorities="ROLE_ADMIN"/> <user name="user" password="userPass" authorities="ROLE_USER"/> </user-service> </authentication-provider> </authentication-manager>