網頁

2019/9/25

Spring Boot Security form api login範例

本篇介紹如何建立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()方法建立一個UserDetailsServiceBean類來產生測試用的使用者帳號密碼資料。建立兩個使用者帳密分別為user123/user123admin123/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。

KEYVALUE
usernameuser123
passworduser123



完成以上後按Send送出,返回結果如下代表通過驗證。

{"status":200,"message":"success"}

通過驗證後可以用GET來傳送http://localhost:8080/demo/,返回結果如下。

Welcome to the home page!

真心覺得每次Spring Security碰到RESTful API都很麻煩。


7 則留言:

  1. 非常實用
    小弟剛接觸很多不懂
    請問大大可以把此專案打包zip給小弟RUN嗎?
    非常感恩

    回覆刪除
  2. @KJ sorry,專案我寫完範例就砍了,所以沒有囉

    回覆刪除
  3. 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]

    ....


    希望可以指點一下小弟~感激不盡

    回覆刪除
  4. @KJ DemoResponse 的getter和setter要寫

    回覆刪除
  5. 抱歉 Matt 大大

    我剛接觸 Spring Boot

    不太懂getter和setter要怎寫?

    感謝大大!

    回覆刪除
  6. @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)。

    回覆刪除