3.1.2 创建自定义的安全配置
覆盖自动配置很简单,就当自动配置不存在,直接显式地写一段配置。这段显式配置的形式不限,Spring 支持的 XML 和 Groovy 形式配置都可以。
在编写显式配置时,我们会专注于 Java 形式的配置。在 Spring Security 的场景下,这意味着写一个扩展了 WebSecurityConfigurerAdapter
的配置类。代码清单 3-1 中的 SecurityConfig
就是我们需要的东西。
代码清单 3-1 覆盖自动配置的显式安全配置
package readinglist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.
builders.AuthenticationManagerBuilder;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.
UsernameNotFoundException;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ReaderRepository readerRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").access("hasRole('READER')") ←---要求登录者有 READER 角色
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/login") ←---设置登录表单的路径
.failureUrl("/login?error=true");
}
@Override
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(new UserDetailsService() { ←---定义自定义 UserDetailsService
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return readerRepository.findOne(username);
}
});
}
}
SecurityConfig
是个非常基础的 Spring Security 配置,尽管如此,它还是完成了不少安全定制工作。通过这个自定义的安全配置类,我们让 Spring Boot 跳过了安全自动配置,转而使用我们的安全配置。
扩展了 WebSecurityConfigurerAdapter
的配置类可以覆盖两个不同的 configure()
方法。在 SecurityConfig
里,第一个 configure()
方法指明,“ /
”( ReadingListController
的方法映射到了该路径)的请求只有经过身份认证且拥有 READER 角色的用户才能访问。其他的所有请求路径向所有用户开放了访问权限。这里还将登录页和登录失败页(带有一个 error
属性)指定到了/login。
Spring Security 为身份认证提供了众多选项,后端可以是 JDBC(Java Database Connectivity)、LDAP 和内存用户存储。在这个应用程序中,我们会通过 JPA 用数据库来存储用户信息。第二个 configure()
方法设置了一个自定义的 UserDetailsService
,这个服务可以是任意实现了 UserDetailsService
的类,用于查找指定用户名的用户。代码清单 3-2 提供了一个匿名内部类实现,简单地调用了注入 ReaderRepository
(这是一个 Spring Data JPA 仓库接口)的 findOne()
方法。
代码清单 3-2 用来持久化读者信息的仓库接口
package readinglist;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ReaderRepository
extends JpaRepository<Reader, String> { ←---通过 JPA 持久化读者
}
和 BookRepository
类似,你无需自己实现 ReaderRepository
。这是因为它扩展了 JpaRepository
,Spring Data JPA 会在运行时自动创建它的实现。这为你提供了 18 个操作 Reader
实体的方法。
说到 Reader
实体, Reader
类(如代码清单 3-3 所示)就是最后一块拼图了,它就是一个简单的 JPA 实体,其中有几个字段用来存储用户名、密码和用户全名。
代码清单 3-3 定义 Reader
的 JPA 实体
package readinglist;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Entity
public class Reader implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
private String username; (以下三行)Reader 字段
private String fullname;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullname() {
return fullname;
}
public void setFullname(String fullname) {
this.fullname = fullname;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
// UserDetails methods
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { ←---授予 READER 权限
return Arrays.asList(new SimpleGrantedAuthority("READER"));
}
@Override
public boolean isAccountNonExpired() { ←---不过期,不加锁,不禁用
return true;
}
@Override
public boolean isAccountNonLocked() { ←---不过期,不加锁,不禁用
return true;
}
@Override
public boolean isCredentialsNonExpired() { ←---不过期,不加锁,不禁用
return true;
}
@Override
public boolean isEnabled() { ←---不过期,不加锁,不禁用
return true;
}
}
如你所见, Reader
用了 @Entity
注解,所以这是一个 JPA 实体。此外,它的 username
字段上有 @Id
注解,表明这是实体的 ID。这个选择无可厚非,因为 username
应该能唯一标识一个 Reader
。
你应该还注意到 Reader
实现了 UserDetails
接口以及其中的方法,这样 Reader
就能代表 Spring Security 里的用户了。 getAuthorities()
方法被覆盖过了,始终会为用户授予 READER 权限。 isAccountNonExpired()
、 isAccountNonLocked()
、 isCredentialsNonExpired()
和 isEnabled()
方法都返回 true
,这样读者账户就不会过期,不会被锁定,也不会被撤销。
重新构建并重启应用程序后,你应该就能以读者身份登录应用程序了。
保持简单 在一个大型应用程序里,赋予用户的授权本身也可能是实体,它们被维护在独立的数据表里。同样,表示一个账户是否为非过期、非锁定且可用的布尔值也是数据库里的字段。但是,出于演示考虑,我决定让这些细节保持简单,以免分散我们的注意力,影响正在讨论的话题 - 我说的是覆盖 Spring Boot 自动配置。
在安全配置方面,我们还能做更多事情 1 ,但此刻这样就足够了,上面的例子足以演示如何覆盖 Spring Boot 提供的安全自动配置。
1 想要深入了解 Spring Security,可以参考《Spring 实战(第 4 版)》中的第 9 章和第 14 章。
再重申一次,想要覆盖 Spring Boot 的自动配置,你所要做的仅仅是编写一个显式的配置。Spring Boot 会发现你的配置,随后降低自动配置的优先级,以你的配置为准。想弄明白这是如何实现的,让我们揭开 Spring Boot 自动配置的神秘面纱,看看它是如何运作的,以及它是怎么允许自己被覆盖的。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论