注意⚠️:本文进行配置工作的方式在从 5.7.0-M2 起的 Spring Security 起,已经被不推荐使用,详见:升级指南。如果使用的版本低于 5.7.0-M2 或者想学习 Spring Security 的思想,适合阅读本文。
我们使用 Spring Security 解决认证问题(你是谁)和授权问题(你能做什么),想要对访问权限进行精细化管理,一般的做法是引入角色体系,即 RBAC 模型:通过请求传递用户凭证完成用户认证,然后根据该用户信息中具备的角色信息获取访问权限,并最终完成对 HTTP 端点的访问授权。
Spring Security 配置体系
想要使用 Spring Security 我们只需要引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
此时,访问应用内的任何一个 HTTP 端点都会跳转到一个默认的登录页,就用户认证这一场景而言,Spring Security 内部就初始化了一个默认的用户名“user”并且在应用程序启动时自动生成一个密码。通过这种方式自动生成的密码在每次启动应用时都会发生变化,并不适合面向正式的应用。 在日常开发中,继承并重写 WebSecurityConfigurerAdapter 类的 configure(HttpSecurity http) 方法可以进行一些自定义配置工作,(Spring Security 大量使用了这种继承 XXXAdaptor 类,并重写 configure() 方法的方式进行定制化配置):
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
- 通过HttpSecurity 类的 authorizeRequests() 方法对所有访问 HTTP 端点的 HttpServletRequest 进行限制;
- anyRequest().authenticated() 语句指定了对于所有请求都需要执行认证,也就是说没有通过认证的用户就无法访问任何端点;
- formLogin() 语句用于指定使用表单登录作为认证方式,也就是会弹出一个登录界面;(如果是前后端分离的方式的话,这里不应该配置 formLogin,而是放行 login 接口,自己完成登录的操作)
- httpBasic() 语句表示可以使用 HTTP 基础认证(Basic Authentication)方法来完成认证。
Spring Security 的用户认证机制
开发人员只需要进行简单的配置就可以实现用户认证的系统方法,这种简单性得益于 Spring Security 对于用户认证过程中的提炼和抽象。可以继续深入探究 Spring Security 中的用户和认证对象,以及如何基于这些对象完成定制化的用户认证方案。
Spring Security 的认证过程由一组核心对象组成,大致分为两类:用户对象和认证对象
用户对象
Spring Security 中的用户对象用来描述用户并完成对用户信息的管理,涉及UserDetails、GrantedAuthority、UserDetailsService 和 UserDetailsManager 这四个核心对象。
- UserDetails:描述 Spring Security 中的用户。
- GrantedAuthority:定义用户的操作权限,每个 UserDetails 对象有一个或多个。
- UserDetailsService:定义了对 UserDetails 的查询操作。
- UserDetailsManager:扩展 UserDetailsService,添加了创建用户、修改用户密码等功能。
认证对象
认证对象 Authentication
Authentication 对象代表认证请求本身,保存了请求访问应用程序过程中涉及的各个实体的详细信息,请求访问该应用程序的用户通常被称为主体(Principal),JDK中存在这个接口,Authentication 扩展了它。Authentication 只代表请求本身,具体的认证过程和逻辑需要专门的组建负责,即下面提到的 Authentication Provider 对象。 Authentication 对象如下图所示:
AuthenticationProvider
AP 负责具体的认证过程和逻辑
如何实现定制化用户认证方案
现在,我们已经知道 UserDetails 接口代表着用户详细信息,而负责对 UserDetails 进行各种操作的则是 UserDetailsService 接口。因此,实现定制化用户认证方案主要就是实现 UserDetails 和 UserDetailsService 这两个接口。接着,扩展 AuthenticationProvider ,提供一个自定义 AP 实现类,在 authenticate() 方法中完成用户信息的查询和密码的校验,这样就完成了用户认证工作。
最后,需要把我们定制化重写的组件注入到 Spring Security 的配置中,也是通过继承 WebSecurityConfigurerAdapter 配置类,并重写 configure(AuthenticationManagerBuilder auth) 方法:
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService springUserDetailsService;
@Autowired
private AuthenticationProvider springAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(springUserDetailsService)
.authenticationProvider(springAuthenticationProvider);
}
}
这样就完成了自定义的用户认证方案。
Spring Security 的访问授权配置
通过用户角色与权限限制访问权限
在 Spring Security 中,UserDetails 对象描述了用户,其中也包含了用户的权限信息,比如:可以给用户授予读写权限:
UserDetails user = User.withUsername("xiaohong")
.password("123456").authorities("read", "write") .build();
我们在 Spring Security 中可以通过一组方法来限制 HTTP 端点的访问权限,例如:
- hasAuthority(String),允许具有特定权限的用户进行访问;
- hasAnyAuthority(String),允许具有任一权限的用户进行访问。 这些方法同样是在在 WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 方法中添加:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.authorizeRequests().anyRequest().hasAuthority("CREATE");
}
讨论完权限,再来看角色,角色是拥有多个权限的一种数据载体,例如 “ADMIN” 角色拥有“读、写、查、删”的权限。讲到这里,你可能会认为 Spring Security 应该提供了一个独立的数据结构来承载角色的含义。但事实上,在 Spring Security 中,并没有定义类似“GrantedRole”这种专门用来定义用户角色的对象,而是复用了 GrantedAuthority 对象。事实上,以“ROLE_”为前缀的 GrantedAuthority 就代表了一种角色,因此我们可以使用如下方式初始化用户的角色。为了给开发人员提供更好的开发体验,Spring Security 提供一种简化的方法来指定用户的角色:
// 方便开发人员编码
UserDetails user = User.withUsername("xiaohong")
.password("123456")
.roles("ADMIN")
.build();
// 等价于下面的
UserDetails user = User.withUsername("xiaohong")
.password("123456")
.authoriies("ROLE_ADMIN")
.build();
和权限配置一样,Spring Security 也通过使用对应的 hasRole() 和 hasAnyRole() 方法来判断用户是否具有某个角色或某些角色:
http.authorizeRequests().anyRequest().hasRole("ADMIN");
Spring Security 还提供了其他很多有用的控制方法供开发人员进行灵活使用。作为总结,下表展示了常见的配置方法及其作用:
| 配置方法 | 作用 |
|---|---|
| anonymous() | 允许匿名访问 |
| authenticated() | 允许认证用户访问 |
| denyAll() | 无条件禁止一切访问 |
| hasAnyAuthority(String) | 允许具有任一权限的用户进行访问 |
| hasAnyRole(String) | 允许具有任一角色的用户进行访问 |
| hasAuthority(String) | 允许具有特定权限的用户进行访问 |
| hasIpAddress(String) | 允许来自特定 IP 地址的用户进行访问 |
| hasRole(String) | 允许具有特定角色的用户进行访问 |
| permitAll() | 无条件允许一切访问 |
使用配置方法控制访问权限
MVC 匹配器
这段代码是自解释的,通过请求方法、请求 URL 来控制访问权限。
http.authorizeRequests()
.mvcMatchers(HttpMethod.POST, "/hello").authenticated()
.mvcMatchers(HttpMethod.GET, "/hello").permitAll()
.anyRequest().denyAll();
MVC 匹配器是最常用的,其余的还有 Ant 匹配器和正则匹配器,大家可以自行去了解。
Spring Security 实现 Oauth2
首先,不了解 Oauth2 的可以先了解一下:理解 Oauth2
Oauth2 中有客户端、授权服务器和资源服务器这三类概念
对应到微服务系统中,服务提供者充当的角色就是资源服务器,而服务消费者就是客户端。所以每个服务本身既可以是客户端,也可以作为资源服务器,或者两者兼之。当客户端拿到 Token 之后,该 Token 就能在各个服务之间进行传递。在微服务系统中,Oauth2 的密码模式也是更合适的一种方式。
构建授权服务器
需要为授权服务器创建一个单独的 SB 应用,引入 OAuth2 的依赖:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
并在项目入口处使用 @EnableAuthorizationServer 注解,这个注解的作用在于为微服务运行环境提供一个基于 OAuth2 协议的授权服务,该授权服务会暴露一系列基于 RESTful 风格的端点(例如 /oauth/authorize 和 /oauth/token)供 OAuth2 授权流程使用。
在 OAuth2 密码模式下,整个流程:
首先应该区分客户端信息和用户信息的区别,客户端自身应该在授权服务器这里是有记录的,然后拿着他的 clientId 和 clientSecret 以及 用户的用户名和密码来进行授权。例如,我们用XXX应用的 GitHub 登录,XXX应用应该是具备自己的 clientId 和 clientSecret 的。
设置客户端信息
客户端信息包括:clientId , clientSecret , scope , authorizedGrantTypes
首先 clientId 是一个必备属性,用来唯一标识客户的 Id,而 clientSecret 代表客户端安全码。这里的 Scope 用来限制客户端的访问范围,如果这个属性为空,客户端就拥有全部的访问范围。常见的设置方式可以是 webclient 或 mobileclient,分别代表 Web 端和移动端。
最后,authorizedGrantTypes 代表客户端可以使用的授权模式,可选的范围包括代表授权码模式的 authorization_code、代表隐式授权模式 implicit、代表密码模式的 password 以及代表客户端凭据模式的 client_credentials。这个属性在设置上也可以添加 refresh_token,通过刷新操作获取以上授权模式下产生的新 Token。
和实现认证过程类似,Spring Security 也提供了 AuthorizationServerConfigurerAdapter 这个配置适配器类来简化配置类的使用方式。我们可以通过继承该类并覆写其中的 configure(ClientDetailsServiceConfigurer clients) 方法进行配置。使用 AuthorizationServerConfigurerAdapter 进行客户端信息配置的基本代码结构如下:
@Configuration
public class SpringAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("spring")
.secret("{noop}spring_secret")
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
}
因为我们指定了授权模式为密码模式,而密码模式包含认证环节。所以针对 AuthorizationServerEndpointsConfigurer 配置类需要指定一个认证管理器 AuthenticationManager,用于对用户名和密码进行认证。同样因为我们指定了基于密码的授权模式,所以需要指定一个自定义的 UserDetailsService 来替换全局的实现。
@Configuration
public class SpringAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService);
}
}
设置用户认证
其中 AuthenticationManager 和 UserDetailsService 两个 Bean 都是为了给上面 AuthorizationServerConfigurerAdapter 的配置使用
现在,OAuth2 授权服务器已经构建完毕,启动这个授权服务器,我们就可以获取 Token。我们在构建 OAuth2 服务器时已经提到授权服务器中会暴露一批端点供 HTTP 请求进行访问,而获取 Token 的端点就是http://localhost:8080/oauth/token。在使用该端点时,我们需要提供前面配置的客户端信息和用户信息。在“Authorization”请求头中指定认证类型为“Basic Auth”,然后设置客户端名称和客户端安全码分别为“spring”和“spring_secret”。接下来我们在请求体中指定针对授权模式的专用配置信息。首先是用于指定授权模式的 grant_type 属性,以及用于指定客户端访问范围的 scope 属性,这里分别设置为 “password”和“webclient”。既然设置了密码模式,所以也需要指定用户名和密码用于识别用户身份。
请求后就会得到 Token 相关的一系列信息。
在资源服务器集成 OAuth2 授权
Spring Security 框架为此提供了专门的 @EnableResourceServer 注解。通过在 Bootstrap 类中添加 @EnableResourceServer 注解,相当于声明该服务中的所有内容都是受保护的资源。一旦我们在微服务中添加了 @EnableResourceServer 注解,该服务就会对所有的 HTTP 请求进行验证以确定 Header 部分中是否包含 Token 信息。如果没有 Token 信息,就会直接限制访问;如果有 Token 信息,则通过访问 OAuth2 服务器进行 Token 的验证。我们需要在各个微服务和 OAuth2 授权服务器之间建立起一种交互关系。我们可以在配置文件中添加如下所示的 security.oauth2.resource.userInfoUri 配置项来实现这一目标:
security:
oauth2:
resource:
userInfoUri: http://localhost:8080/userinfo
这个 HTTP 端点不在授权服务器自动暴露的范围内,因此我们需要在授权服务器中自行定义,返回 user 字段和 authorities 字段:
那么引入 OAuth2 后,我们就可以使用上面所说的访问授权配置来进行各个 HTTP 端点的权限控制了,比如限制某类 HTTP 端点的角色访问。但注意,这里在资源服务器中我们继承的配置类是 ResourceServerConfigurerAdapter
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity.authorizeRequests()
.antMatchers(HttpMethod.PUT, "/user/**")
.hasRole("ADMIN")
.anyRequest()
.authenticated();
}
}