vlambda博客
学习文章列表

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Security保护应用程序

Securing Applications with Spring Security

在本章中,我们将介绍 Spring Security 的基础知识。这是 Spring Framework 的重要组成部分,所以慢慢来。我们将涵盖以下主题:

  • Introducing Spring Security
  • Defining user roles
  • Defining data transfer objects (DTOs) for users
  • Providing authentication
  • Providing authorization

Introducing Spring Security

顾名思义,Spring Security 代表了具有各种与安全相关的强大功能的框架。它提供了一个高度可定制的身份验证和访问控制框架。在现代开发中,Spring Security 是保护基于 Spring Framework 的应用程序的事实上的标准。

Spring Security 不仅为应用程序提供身份验证,还提供授权功能。 Spring Security 具有以下特性:

  • Authentication and authorization
  • Basic, digest, and form-based authentication
  • LDAP authentication
  • OpenID authentication
  • Single Sign-On implementation
  • Cross-Site Request Forgery (CSRF) implementation
  • Remember-Me through HTTP cookies
  • ACLs implementation
  • Channel Security, automatically switching between HTTP and HTTPS
  • I18N internationalization
  • JAAS, Java Authentication, and Authorization Service
  • Flow Authorization using Spring WebFlow Framework
  • WS-Security using Spring Web Services
  • Configuration through both XML and Annotations
  • WebSocket Security
  • Spring Data Integration
  • CSRF Token Argument Resolver

如您所见,Spring Security 在安全领域提供了很多东西。我们主要关注基本身份验证和 OAuth2,以便能够利用其适当的依赖关系。打开您的 build.grade 配置并扩展它:

... 
dependencies { 
    compile 'org.springframework.boot:spring-boot-starter-security' 
    ... 
} 

Defining user roles

接下来,我们将定义一些用户角色。我们将定义用户角色以及该角色可以执行的API调用。因此,我们将扮演以下角色:

  • ADMIN: Can execute all the API calls and also create, update or remove users
  • MEMBER: Represents a regular system user and can execute all API calls except user-related ones

没有角色(未登录)的用户无法执行任何 API 调用。

Defining classes for each role

我们已经确定了角色的范围,因此我们需要创建适当的类。创建一个名为 security 的新包。我们将创建的第一个类将是代表用户定义的类。创建一个名为 User 的类,并确保它扩展了 org.springframework.security.core.userdetails 包中的 UserDetails 类:

package com.journaler.api.security 
 
... 
import org.springframework.security.core.GrantedAuthority 
import org.springframework.security.core.userdetails.UserDetails 
... 
 
open class User : UserDetails { 
 
    override fun getAuthorities(): MutableCollection<out GrantedAuthority> { 
        ... 
    } 
 
    override fun isEnabled(): Boolean { 
        ... 
    } 
 
    override fun getUsername(): String { 
        ... 
    } 
 
    override fun isCredentialsNonExpired(): Boolean { 
        ... 
    } 
 
    override fun getPassword(): String { 
        ... 
    } 
 
    override fun isAccountNonExpired(): Boolean { 
        ... 
    } 
 
    override fun isAccountNonLocked(): Boolean { 
        ... 
    } 
} 

扩展 UserDetails 类提供核心用户信息。出于安全目的,Spring Security 不直接使用实现。它只是用来存储用户的信息,后来被封装成认证实现对象(从org.springframework.security.core.Authentication导入)。

这使得存储与安全无关的用户信息(例如电子邮件地址和电话号码)成为可能。

如您所见,UserDetails 类强制我们重写以下方法:

  • getAuthorities() returns the authorities granted to the user. It cannot return null!
  • isEnabled() indicates whether the user is enabled or disabled. A disabled user cannot be authenticated.
  • getUsername() returns the username used to authenticate the user. It cannot return null!
  • isCredentialsNonExpired() indicates whether the user's credentials (password) have expired or not. Expired credentials prevent authentication.
  • getPassword() returns the password used to authenticate the user.
  • isAccountNonExpired() indicates whether the user's account has expired. An expired account cannot be authenticated.
  • isAccountNonLocked() indicates whether the user is locked or unlocked. A locked user cannot be authenticated.

我们将像这样实现我们的 User 类:

package com.journaler.api.security 
 
import com.fasterxml.jackson.annotation.JsonInclude 
import com.fasterxml.jackson.annotation.JsonProperty 
import org.hibernate.annotations.CreationTimestamp 
import org.hibernate.annotations.GenericGenerator 
import org.hibernate.annotations.UpdateTimestamp 
import org.hibernate.validator.constraints.Email 
import org.hibernate.validator.constraints.NotBlank 
import org.springframework.security.core.GrantedAuthority 
import org.springframework.security.core.authority.SimpleGrantedAuthority 
import org.springframework.security.core.userdetails.UserDetails 
import java.util.* 
import javax.persistence.* 
import javax.validation.constraints.NotNull 
 
@Entity 
@Table(name = "user") 
@JsonInclude(JsonInclude.Include.NON_NULL) 
open class User( 
        @Id 
        @GeneratedValue(generator = "uuid2") 
        @GenericGenerator(name = "uuid2", strategy = "uuid2") 
        @Column(columnDefinition = "varchar(36)") 
        var id: String = "", 
 
        @Column(unique = true, nullable = false) 
        @NotNull 
        @Email 
        var email: String = "", 
 
        @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 
        @NotBlank 
        var pwd: String = "", 
 
        @NotBlank 
        var firstName: String = "", 
 
        @NotBlank 
        var lastName: String = "", 
 
        var roles: String = "", 
        var enabled: Boolean = true, 
        var accountNonExpired: Boolean = true, 
        var accountNonLocked: Boolean = true, 
        var credentialsNonExpired: Boolean = true, 
 
        @CreationTimestamp 
        var created: Date = Date(), 
 
        @UpdateTimestamp 
        var modified: Date = Date() 
) : UserDetails { 
 
    /** 
     * We need empty constructor for SecurityInitializationTest and Hibernate. 
     */ 
    constructor() : this( 
            "", "", "", "", "", "", true, true, true, true, Date(), Date() 
    ) 
 
    override fun getAuthorities(): MutableCollection<out GrantedAuthority> { 
        val authorities = mutableListOf<GrantedAuthority>() 
        roles 
                .split(",") 
                .forEach { it -> 
                    authorities.add( 
                            SimpleGrantedAuthority( 
                                    it.trim() 
                            ) 
                    ) 
                } 
        return authorities 
    } 
 
    override fun isEnabled() = enabled 
 
    override fun getUsername() = email 
 
    override fun isCredentialsNonExpired() = credentialsNonExpired 
 
    override fun getPassword() = pwd 
 
    override fun isAccountNonExpired() = accountNonExpired 
 
    override fun isAccountNonLocked() = accountNonLocked 
 
} 

有一些重要的注意事项。我们将 User 类视为一个实体,因此它将存储在数据库中。 @Email 注释告诉框架检查电子邮件以进行验证。 @NotBlank 注释将阻止我们发送无效数据。 @NotBlank 检查值不为空或为空。 Spring首先修剪该值。像这样,不可能通过空格。我们将借此机会提及两个更重要的注释:

  • @NotNull, to verify that the value is not null, disregarding the content.
  • @NotEmpty, to verify that the value is not null or empty. If it has just empty spaces, it will allow it as not empty!

构建并运行您的 Spring 应用程序,然后检查表和数据库的结构。您会注意到还有一个名为 user 的表,其结构如下:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Security保护应用程序

为了涵盖 ADMIN 角色,我们必须再引入一个类。在您创建 User 类的同一包中创建一个名为 Admin 的类:

package com.journaler.api.security 
 
import java.util.* 
import javax.persistence.DiscriminatorValue 
import javax.persistence.Entity 
 
@Entity 
@DiscriminatorValue(value = "ADMIN") 
class Admin( 
        id: String, 
        email: String, 
        pwd: String, 
        firstName: String, 
        lastName: String, 
        roles: String, 
        enabled: Boolean, 
        accountNonExpired: Boolean, 
        accountNonLocked: Boolean, 
        credentialsNonExpired: Boolean, 
        created: Date, 
        modified: Date 
) : User( 
        id, 
        email, 
        pwd, 
        firstName, 
        lastName, 
        roles, 
        enabled, 
        accountNonExpired, 
        accountNonLocked, 
        credentialsNonExpired, 
        created, 
        modified 
) { 
 
    /** 
     * We need empty constructor for SecurityInitializationTest and Hibernate. 
     */ 
    constructor() : this( 
            "", "", "", "", "", "", true, true, true, true, Date(), Date() 
    ) 
 
} 

再次运行 your application 并查看 user 表中发生的更改:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Security保护应用程序

如您所见,现在多了一个字段:dtype.

我们也必须实现一个 Member 类,所以我们涵盖了我们的 MEMBER 角色。创建一个 Member 类并确保它是这样实现的:

package com.journaler.api.security 
 
import java.util.* 
import javax.persistence.DiscriminatorValue 
import javax.persistence.Entity 
 
@Entity 
@DiscriminatorValue(value = "MEMBER") 
class Member( 
        id: String, 
        email: String, 
        pwd: String, 
        firstName: String, 
        lastName: String, 
        roles: String, 
        enabled: Boolean, 
        accountNonExpired: Boolean, 
        accountNonLocked: Boolean, 
        credentialsNonExpired: Boolean, 
        created: Date, 
        modified: Date 
) : User( 
        id, 
        email, 
        pwd, 
        firstName, 
        lastName, 
        roles, 
        enabled, 
        accountNonExpired, 
        accountNonLocked, 
        credentialsNonExpired, 
        created, 
        modified 
) { 
 
    /** 
     * We need empty constructor for SecurityInitializationTest and Hibernate. 
     */
    constructor() : this( 
            "", "", "", "", "", "", true, true, true, true, Date(), Date() 
    ) 
 
} 

Member 类与 Admin 类具有相同的实现,但它将在不同角色的上下文中使用。

Defining DTOs for the user

我们将需要 DTO 来为我们将定义的所有与用户相关的操作实现最大的灵活性。第一个 DTO 将是我们在返回系统中可用用户列表时使用的 DTO。定义一个名为 UserDetailsDTO 的新类:

package com.journaler.api.security 
 
import java.util.* 
 
data class UserDetailsDTO( 
        val id: String, 
        var email: String, 
        var firstName: String, 
        var lastName: String, 
        var roles: String, 
        var enabled: Boolean, 
        var accountNonExpired: Boolean, 
        var accountNonLocked: Boolean, 
        var credentialsNonExpired: Boolean, 
        var created: Date, 
        var modified: Date 
) 

UserDetailsDTO 是一个简单的数据类,只包含必填字段。如您所见,我们不会返回密码值。为了保存新用户,我们将定义另一个名为 UserDTO 的数据类:

package com.journaler.api.security 
 
data class UserDTO( 
        var email: String, 
        var password: String, 
        var firstName: String, 
        var lastName: String 
) 

我们将在系统中创建新用户时使用它。将仅提供这四个字段。其他字段将自动填充,或使用默认值。

为了能够将用户保存在数据库中,我们需要一个存储库。创建 UserRepository

package com.journaler.api.repository 
 
import com.journaler.api.security.User 
import org.springframework.data.repository.CrudRepository 
 
interface UserRepository : CrudRepository<User, String> { 
 
    fun findOneByEmail(email: String): User? 
 
} 

为了能够通过用户名(在我们的例子中是电子邮件地址)定位用户,我们必须有一个 findOneByEmail() 方法。

为了能够获取用户列表或创建新用户,我们需要为用户相关需求定义 API 调用。我们将有以下调用:

  • [ GET ], /users returns a list of users in the system
  • [ PUT ], /users/admin inserts a new admin user
  • [ PUT ], /users/member inserts a new member user
  • [ DELETE ], /users/{id} removes a user with ID from the system
  • [ POST ], /users updates users in the system

所有与用户相关的操作都将在 UserService 类中执行:

package com.journaler.api.service 
 
import com.journaler.api.repository.UserRepository 
import com.journaler.api.security.* 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.security.core.userdetails.UserDetailsService 
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 
import org.springframework.stereotype.Repository 
import java.util.* 
 
@Repository 
class UserService : UserDetailsService { 
 
    @Autowired 
    lateinit var repository: UserRepository 
 
    val encoder = BCryptPasswordEncoder(11) 
 
    override fun loadUserByUsername(email: String): User? { 
        return repository.findOneByEmail(email) ?: throw RuntimeException("User not found: $email") 
    } 
 
    fun saveMember(user: UserDTO): User { 
        val member = Member() 
        member.email = user.email 
        member.firstName = user.firstName 
        member.lastName = user.lastName 
        member.pwd = encoder.encode(user.password) 
        member.roles = "MEMBER" 
        return repository.save(member) 
    } 
 
    fun saveAdmin(user: UserDTO): User { 
        val admin = Admin() 
        admin.email = user.email 
        admin.firstName = user.firstName 
        admin.lastName = user.lastName 
        admin.roles = "ADMIN, MEMBER" 
        admin.pwd = encoder.encode(user.password) 
        return repository.save(admin) 
    } 
 
    fun updateUser(toSave: User): User? { 
        val user = repository.findOneByEmail(toSave.email) 
        user?.let { 
            if (!toSave.pwd.isEmpty()) { 
                user.pwd = encoder.encode(toSave.password) 
            } 
            user.firstName = toSave.firstName 
            user.lastName = toSave.lastName 
            user.accountNonExpired = toSave.accountNonExpired 
            user.accountNonLocked = toSave.accountNonLocked 
            user.credentialsNonExpired = toSave.credentialsNonExpired 
            user.modified = Date() 
            return repository.save(user) 
        } 
        return null 
    } 
 
    fun getUsers() = repository.findAll().map { it -> 
        UserDetailsDTO( 
                it.id, 
                it.email, 
                it.firstName, 
                it.lastName, 
                it.roles, 
                it.enabled, 
                it.accountNonExpired, 
                it.accountNonLocked, 
                it.credentialsNonExpired, 
                it.created, 
                it.modified 
        ) 
    } 
 
    fun deleteUser(id: String) = repository.deleteById(id) 
 
} 

userService 将执行所有 CRUD 操作。为了触发它们中的任何一个,我们需要最终将 UserServiceUserController 连接起来,这将定义我们计划的所有 API 调用:

package com.journaler.api.controller 
 
import com.journaler.api.security.User 
import com.journaler.api.security.UserDTO 
import com.journaler.api.service.UserService 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.http.MediaType 
import org.springframework.web.bind.annotation.* 
 
@RestController 
@RequestMapping("/users") 
class UserController { 
 
    @Autowired 
    lateinit var service: UserService 
 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getUsers() = service.getUsers() 
 
    @PutMapping( 
            value = "/admin", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertAdmin( 
            @RequestBody user: UserDTO 
    ) = service.saveAdmin(user) 
 
    @PutMapping( 
            value = "/member", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertMember( 
            @RequestBody user: UserDTO 
    ) = service.saveMember(user) 
 
 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteUser( 
            @PathVariable(name = "id") id: String 
    ) = service.deleteUser(id) 
 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateUser( 
            @RequestBody user: User 
    ): User? = service.updateUser(user) 
 
} 

到目前为止,我们已经准备好所有与用户相关的东西,但还没有任何东西是安全的。如果您运行您的应用程序,您将能够执行任何 API 调用而无需任何身份验证。

Securing your REST API with basic authentication

为了能够对用户进行身份验证和授权,我们将添加基本身份验证实现。默认情况下,Spring 会将 /login 处理为一个网页,这是我们的 REST API 不希望的。在 security 包中创建一个新类并将其命名为 WebSecurityEntryPoint

此类必须实现 AuthenticationEntryPoint 接口,其目的是启动身份验证方案。实现将如下所示:

package com.journaler.api.security 
 
import org.springframework.security.core.AuthenticationException 
import org.springframework.security.web.AuthenticationEntryPoint 
import org.springframework.stereotype.Component 
import javax.servlet.http.HttpServletRequest 
import javax.servlet.http.HttpServletResponse 
 
@Component 
class WebSecurityEntryPoint : AuthenticationEntryPoint { 
 
    override fun commence( 
            request: HttpServletRequest?, 
            response: HttpServletResponse?, 
            authException: AuthenticationException? 
    ) { 
        response?.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied") 
    } 
 
} 

那么,commence() 方法有什么作用呢?顾名思义,它启动了一个身份验证方案。 Spring Security 通过使用 Entry Point 的概念自动触发身份验证过程来处理身份验证。入口点是配置的必需部分,可以注入。我们的入口点实现将在触发时简单地返回 401。

我们还需要一个身份验证成功处理程序。该类将负责处理身份验证结果。创建一个名为 WebSecurityAuthSuccessHandler 的新类,并确保它扩展了 SimpleUrlAuthenticationSuccessHandler 类:

package com.journaler.api.security 
 
import org.springframework.security.core.Authentication 
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler 
import org.springframework.security.web.savedrequest.HttpSessionRequestCache 
import org.springframework.stereotype.Component 
import org.springframework.util.StringUtils 
import javax.servlet.http.HttpServletRequest 
import javax.servlet.http.HttpServletResponse 
 
@Component 
class WebSecurityAuthSuccessHandler : SimpleUrlAuthenticationSuccessHandler() { 
 
    var requestCache = HttpSessionRequestCache() 
 
    override fun onAuthenticationSuccess( 
            request: HttpServletRequest, 
            response: HttpServletResponse, 
            authentication: Authentication 
    ) { 
        val savedRequest = requestCache.getRequest(request, response) 
        if (savedRequest == null) { 
            clearAuthenticationAttributes(request) 
            return 
        } 
        val parameter = request.getParameter(targetUrlParameter) 
        val ok = isAlwaysUseDefaultTargetUrl || 
                     targetUrlParameter != null &&  
                     StringUtils.hasText(parameter) 
        if (ok) { 
            requestCache.removeRequest(request, response) 
            clearAuthenticationAttributes(request) 
            return 
        } 
        clearAuthenticationAttributes(request) 
    } 
 
} 

我们定义这样一个事实,即成功验证所需的响应应该是 200 OK。我们将注入身份验证成功处理程序实现来替换默认值。默认会执行重定向,但由于我们正在制作一个 REST API,我们不需要它,只需要成功响应。

让我们把事情联系在一起并配置 Spring Security!要配置 Spring Security,我们需要创建一个扩展 WebSecurityConfigurerAdapter 的 Spring 配置类。创建一个名为 WebSecurityConfiguration 的新类:

package com.journaler.api.security 
 
import com.journaler.api.service.UserService 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.context.annotation.Bean 
import org.springframework.context.annotation.Configuration 
import org.springframework.security.access.AccessDecisionManager 
import org.springframework.security.access.vote.AuthenticatedVoter 
import org.springframework.security.access.vote.RoleVoter 
import org.springframework.security.access.vote.UnanimousBased 
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 
import org.springframework.security.crypto.password.PasswordEncoder 
import org.springframework.security.authentication.dao.DaoAuthenticationProvider 
import org.springframework.security.config.annotation.web.builders.HttpSecurity 
import org.springframework.security.web.AuthenticationEntryPoint 
import org.springframework.security.web.access.expression.WebExpressionVoter 
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler 
import java.util.* 
 
 
@Configuration 
@EnableWebSecurity 
class WebSecurityConfiguration : WebSecurityConfigurerAdapter() { 
 
    @Autowired 
    lateinit var service: UserService 
 
    /** 
     * Will be resolved into: WebSecurityEntryPoint injected instance. 
     */ 
    @Autowired 
    lateinit var unauthorizedHandler: AuthenticationEntryPoint 
 
    @Autowired 
    lateinit var successHandler: WebSecurityAuthSuccessHandler 
 
    @Autowired 
    override fun configure(auth: AuthenticationManagerBuilder) { 
        auth.authenticationProvider(authenticationProvider()) 
    } 
 
    override fun configure(http: HttpSecurity?) { 
        http 
                ?.csrf()?.disable() 
                ?.exceptionHandling() 
                ?.authenticationEntryPoint(unauthorizedHandler) 
                ?.and() 
                ?.authorizeRequests() 
                /** 
                 * Access to Notes and Todos API calls is given to any authenticated system user. 
                 */ 
                ?.antMatchers("/notes")?.authenticated() 
                ?.antMatchers("/notes/**")?.authenticated() 
                ?.antMatchers("/todos")?.authenticated() 
                ?.antMatchers("/todos/**")?.authenticated() 
                /** 
                 * Access to User API calls is given only to Admin user. 
                 */ 
                ?.antMatchers("/users")?.hasAnyAuthority("ADMIN") 
                ?.antMatchers("/users/**")?.hasAnyAuthority("ADMIN") 
                ?.and() 
                ?.formLogin() 
                ?.successHandler(successHandler) 
                ?.failureHandler(SimpleUrlAuthenticationFailureHandler()) 
                ?.and() 
                ?.logout() 
    } 
 
    @Bean 
    fun authenticationProvider(): DaoAuthenticationProvider { 
        val authProvider = DaoAuthenticationProvider() 
        authProvider.setUserDetailsService(service) 
        authProvider.setPasswordEncoder(encoder()) 
        return authProvider 
    } 
 
    @Bean 
    fun encoder(): PasswordEncoder = BCryptPasswordEncoder(11) 
 
    @Bean 
    fun accessDecisionManager(): AccessDecisionManager { 
        val decisionVoters = Arrays.asList( 
                WebExpressionVoter(), 
                RoleVoter(), 
                AuthenticatedVoter() 
        ) 
        return UnanimousBased(decisionVoters) 
    } 
} 

让我们解释一下我们刚刚做了什么。如果从类实现的底部看,您会注意到以下方法:

  • The authenticationProvider() method will provide us with the instance of the DaoAuthenticationProvider class that will be used for authentication purposes. We assigned UserService as the mechanism to retrieve users. We also assigned a password encoder obtained by triggering the encoder() method.
  • The encoder() method will provide us with the instance of the BCryptPasswordEncoder class used to perform password encryption. The BCrypt strong hashing function will be used for encryption.
  • The accessDecisionManager() method will provide an AccessDecisionManager instance. The AccessDecisionManager abstract class is responsible for authorization. We will return a new instance of the UnanimousBased class that requires all voters to abstain or grants access. The constructor accepts a list of AccessDecisionVoter instances. Each AccessDecisionVoter represents the implementation responsible for voting on authorization decisions. We will use the following implementations:
    • WebExpressionVoter: AccessDecisionVoter implementation that handles web authorization decisions.
    • RoleVoter: AccessDecisionVoter implementation that votes if any configuration attribute starts with a prefix indicating that it is a role.
    • AuthenticatedVoter: AccessDecisionVoter implementation that votes if any configuration attribute is present: IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED, IS_AUTHENTICATED_ANONYMOUSLY.

通过使用这种选民组合,我们将能够对用户进行身份验证和授权。在您尝试我们实现的代码之前,您需要在数据库中填充一些用户。我们需要至少一个具有 ADMIN 角色的用户和至少一个具有 MEMBER 角色的用户。为此,我们将创建一个测试,该测试将实例化用户并将其插入数据库。我们不会详细介绍此测试,因为我们将在 第 9 章中介绍测试< /a>,测试

找到你的项目的测试目录(/src/test)并在名为的测试包(src/test/kotlin/com/journaler/api)下创建一个测试类SecurityInitializationTest 具有以下实现:

package com.journaler.api 
 
import com.journaler.api.security.Admin 
import com.journaler.api.security.Member 
import com.journaler.api.security.UserDTO 
import com.journaler.api.service.UserService 
import org.junit.Assert 
import org.junit.Test 
import org.junit.runner.RunWith 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.boot.test.context.SpringBootTest 
import org.springframework.test.context.junit4.SpringRunner 
 
@RunWith(SpringRunner::class) 
@SpringBootTest 
class SecurityInitializationTest { 
 
    @Autowired 
    private lateinit var userService: UserService  
    private val password = "12345" 
    private val adminEmail = "[email protected]" 
    private val memberEmail = "[email protected]"  

    @Test 
    fun initAdmin() { 
        try { 
            val admin = userService.loadUserByUsername(adminEmail) 
            if (admin is Admin) { 
                println("Admin user exists: ${admin.id}") 
            } else { 
                Assert.fail("Admin is not an admin.") 
            }
        } catch (e: RuntimeException) { 
            val toSave = UserDTO( 
                    adminEmail, 
                    password, 
                    "admin", 
                    "admin" 
            ) 
            val saved = userService.saveAdmin(toSave) 
            println("Admin user inserted: ${saved.id}") 
        } 
    }  

    @Test 
    fun initMember() { 
        try { 
            val member = userService.loadUserByUsername(memberEmail) 
            if (member is Member) { 
                println("Member user exists: ${member.id}") 
            } else { 
                Assert.fail("Member is not an member.") 
            } 
        } catch (e: RuntimeException) { 
            val toSave = UserDTO( 
                    memberEmail, 
                    password, 
                    "member", 
                    "member" 
            ) 
            val saved = userService.saveMember(toSave) 
            println("Member user inserted: ${saved.id}") 
        } 
    } 
} 

运行您的测试(右键单击 SecurityInitializationTest.kt 并单击 Run 'SecurityInitializationTest' 选项)。初始化 Spring Boot 并执行测试需要一些时间,如下图所示:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Security保护应用程序

完成后,检查数据库的内容:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Security保护应用程序

如您所见,插入了两个用户。使用测试代码中的 usernamepassword 进行身份验证。让我们尝试登录。打开 Postman 并进行身份验证:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Security保护应用程序

慢慢来,使用我们项目中引入的 Spring Security 进行 API 调用。

What else can Spring Security do?

Spring Security 是巨大的!使用 Spring Security,可以实现比我们刚才介绍的更多的事情。让我们重点介绍一些出色的 Spring Security 特性。

例如,您可以定义自己的授权提供程序和授权规则,或者,例如,只需几行代码即可轻松支持 LDAP 或 OpenID 身份验证。

使用 Spring Security,您可以对密码进行编码和验证。类必须实现 PasswordEncoder 接口:

package org.springframework.security.crypto.password; 
 
/** 
 * Service interface for encoding passwords. 
 * 
 * The preferred implementation is {@code BCryptPasswordEncoder}. 
 * 
 * @author Keith Donald 
 */ 
public interface PasswordEncoder { 
 
   /** 
    * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or 
    * greater hash combined with an 8-byte or greater randomly generated salt. 
    */ 
   String encode(CharSequence rawPassword); 
 
   /** 
    * Verify the encoded password obtained from storage matches the submitted raw 
    * password after it too is encoded. Returns true if the passwords match, false if 
    * they do not. The stored password itself is never decoded. 
    * 
    * @param rawPassword the raw password to encode and match 
    * @param encodedPassword the encoded password from storage to compare with 
    * @return true if the raw password, after encoding, matches the encoded password from 
    * storage 
    */ 
   boolean matches(CharSequence rawPassword, String encodedPassword); 
 
} 

这些是最常用的实现:

  • BaseDigestPasswordEncoder
  • BasePasswordEncoder
  • BCryptPasswordEncoder
  • LdapShaPasswordEncoder
  • Md4PasswordEncoder
  • Md5PasswordEncoder
  • MessageDigestPasswordEncoder
  • MessageDigestPasswordEncoder
  • PlaintextPasswordEncoder
  • ShaPasswordEncoder

您可能已经注意到,在我们的示例中,我们使用了 BCryptPasswordEncoder 实现。

需要注意的是,可以使用注释来执行方法级别的安全性,例如:

  • @RolesAllowed({"ROLE_MEMBER","ROLE_ADMIN"})
  • @PermitAll
  • @DenyAll

通过本章介绍的示例,我们刚刚接触了 Spring Security 的基础知识。不要害怕,因为它需要你时间来掌握它。 Spring Security 需要一些时间来学习和发现它的所有可能性。请耐心等待,尽可能多地编写代码!阅读 Spring Security 相关的文献,不要着急。您将成为 Spring Security 大师!

Summary

安全的应用程序是必须的! Spring 为我们提供了完美的工具。 Spring Security 作为一个高度可定制的身份验证和访问控制框架,是实现这一目标的绝佳选择。我们演示了如何进行基本到高级的安全设置,以及如何防止未经授权使用我们的 API 调用。在下一章中,我们将向您展示 Spring Cloud 以及如何实现分布式系统中的一些常见需求。