本篇介紹如何建立Spring Boot Security專案並以夾帶form-data的api登入。
我原本的需求是想從Postman發送帶有帳密的form-data給Spring Security來登入,但總是跳回Spring Security預設的登入頁而無法以API登入,或一直返回UNAUTHORIZED
,花了一天的時間才搞定,特別記錄一下。
範例環境:
- macOS High Sierra
- Java 1.8
- Eclipse for Java EE 2019-06 (4.12.0)
- Spring Boot 2.1.8.RELEASE
- Eclipse Gradle Buildship plug-in
首先建立Spring Boot的Gradle專案並引入Spring Web及Spring Security。
Spring Boot專案的建立參考使用Eclipse STS建立Spring Boot應用程式專案,建立的時候type選擇Gradle。
專案建好後的build.gradle
內容如下。
build.gradle
plugins {
id 'org.springframework.boot' version '2.1.8.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.abc'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
在application.properties
設定context path路徑為/demo
。
application.properties
server.servlet.context-path=/demo
Spring Boot的進入點SpringBootApplication
類別如下。
SpringSecurityFormApiLoginApplication
package com.abc.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringSecurityFormApiLoginApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityFormApiLoginApplication.class, args);
}
}
建立Spring Security的配置類別DemoWebSecurityConfig
如下,必須掛上@EnableWebSecurity
並繼承WebSecurityConfigurerAdapter
。
userDetailsService()
方法建立一個UserDetailsService
Bean類來產生測試用的使用者帳號密碼資料。建立兩個使用者帳密分別為user123/user123
與admin123/admin123
。
覆寫WebSecurityConfigurerAdapter.configure(HttpSecurity http)
,裡面設定Spring Security的HttpSecurity
安全配置。要關閉CSRF否則無法由API傳送form-data通過登入認證。
DemoWebSecurityConfig
package com.abc.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.User.UserBuilder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@EnableWebSecurity
public class DemoWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user123")
.password("user123")
.roles("USER").build());
manager.createUser(users.username("admin123")
.password("admin123")
.roles("USER", "ADMIN")
.build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler( new DemoAuthenticationSuccessHandler() )
.failureHandler( new DemoAuthenticationFailureHandler() )
.and()
.exceptionHandling()
.authenticationEntryPoint(new DemoAuthenticationEntryPoint())
.and()
.csrf()
.ignoringAntMatchers("/login");
}
}
建立請求返回結果的資料傳輸物件類別(DTO)DemoResponse
,用來裝載回應的資料等,是個簡單的POJO。
DemoResponse
package com.abc.demo.dto;
import org.springframework.http.HttpStatus;
public class DemoResponse {
private Integer status;
private String message;
public DemoResponse() {
this.status = HttpStatus.OK.value();
this.message = "success";
}
public DemoResponse(Integer status, String message) {
this.status = status;
this.message = message;
}
// getters and setters...
}
建立驗證成功處理器DemoAuthenticationSuccessHandler
,實作Spring Security的AuthenticationSuccessHandler
介面,並覆寫onAuthenticationSuccess()
方法。
onAuthenticationSuccess()
方法在驗證成功後會被呼叫,在此可透過傳入的HttpServletResponse
物件來決定返回的資料形式,例如返回JSON字串,夾帶成功訊息等。
DemoAuthenticationSuccessHandler
package com.abc.demo.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import com.abc.demo.dto.DemoResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DemoAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.getOutputStream().println(new ObjectMapper().writeValueAsString(new DemoResponse()));
}
}
建立驗證失敗處理器DemoAuthenticationFailureHandler
,實作Spring Security的AuthenticationFailureHandler
介面,並覆寫onAuthenticationFailure()
方法。
onAuthenticationFailure()
方法在驗證失敗後會被呼叫,在此可透過傳入的HttpServletResponse
物件來決定返回的資料形式,例如返回JSON字串,夾帶失敗訊息等。
DemoAuthenticationFailureHandler
package com.abc.demo.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import com.abc.demo.dto.DemoResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DemoAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
response.setStatus(HttpStatus.FORBIDDEN.value());
DemoResponse data = new DemoResponse(HttpStatus.FORBIDDEN.value(),exception.getMessage());
response.getOutputStream().println(new ObjectMapper().writeValueAsString(data));
}
}
建立未通過驗證請求資源時的返回設定DemoAuthenticationEntryPoint
,實作Spring Security的DemoAuthenticationEntryPoint
介面,並覆寫commence()
方法。
commence()
方法在未通過驗證時請求資源時會被呼叫,在此可透過傳入的HttpServletResponse
物件來決定返回的資料形式,例如返回JSON字串,夾帶未通過認證訊息。
DemoAuthenticationEntryPoint
package com.abc.demo.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import com.abc.demo.dto.DemoResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DemoAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
DemoResponse data = new DemoResponse(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.name());
response.getOutputStream().println(new ObjectMapper().writeValueAsString(data));
}
}
建立一個Controller類DemoController
,此類的api為被保護的資源路徑,必須成功登下後,也就是通過Spring Security的驗證後才可以存取的資源。
DemoController
package com.abc.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/")
public String index() {
return "Welcome to the home page!";
}
}
範例專案結構目錄如下。
完成以上後即可啟動專案來用Postman測試。
開啟Postman,在uri欄位輸入http://localhost:8080/demo/login
,HTTP method為POST
在Body的form-data輸入下面兩個key/value。
KEY | VALUE |
---|---|
username | user123 |
password | user123 |
完成以上後按Send送出,返回結果如下代表通過驗證。
{"status":200,"message":"success"}
通過驗證後可以用GET來傳送http://localhost:8080/demo/
,返回結果如下。
Welcome to the home page!
真心覺得每次Spring Security碰到RESTful API都很麻煩。
- What is the reason to disable csrf in spring boot web application?
- When to use CSRF protection
- Spring Security 取得 CsrfToken
- Spring Boot Security two form api login範例
- Spring Boot + Spring Security + Spring LDAP + Gradle 驗證範例
- Spring Security LDAP form api login auth認證
- Spring Boot Security two form api login with two UserDetailsService
非常實用
回覆刪除小弟剛接觸很多不懂
請問大大可以把此專案打包zip給小弟RUN嗎?
非常感恩
@KJ sorry,專案我寫完範例就砍了,所以沒有囉
回覆刪除Matt 大大
回覆刪除新手小弟請問我網頁打不開都出錯
不知道是哪裡有問題?
網頁錯誤
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Dec 01 21:47:13 CST 2019
There was an unexpected error (type=Internal Server Error, status=500).
No serializer found for class com.example.demo.dto.DemoResponse and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
然後有出現Exception如下
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.example.demo.dto.DemoResponse and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.9.10.1.jar:2.9.10.1]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191) ~[jackson-databind-2.9.10.1.jar:2.9.10.1]
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:313) ~[jackson-databind-2.9.10.1.jar:2.9.10.1]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:71) ~[jackson-databind-2.9.10.1.jar:2.9.10.1]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:33) ~[jackson-databind-2.9.10.1.jar:2.9.10.1]
....
希望可以指點一下小弟~感激不盡
@KJ DemoResponse 的getter和setter要寫
回覆刪除抱歉 Matt 大大
回覆刪除我剛接觸 Spring Boot
不太懂getter和setter要怎寫?
感謝大大!
@KJ 在DemoResponse類別中撰寫兩個屬性status與message的getters settters ,加入以下。
回覆刪除public void setStatus(Integer status) {
this.status = status;
}
public Integer getStatus() {
return this.status;
}
public String setMessage(String message) {
this.message = message;
}
public void getMessage() {
return this.message;
}
另外getter setter和Spring Boot無關,這是Java基本的東西,了解一下什麼叫做封裝(encapsulation)。
感恩
回覆刪除我再試試看