前言

Web API 接口服务场景里,用户的认证和鉴权是很常见的需求,Spring Security 据说是这个领域里事实上的标准,实践下来整体涉及上确实有不少可圈可点之处,也在一定程度上印证了小伙们经常提到的 “太复杂了” 的说法也是很有道理的。

本文以一个简单的 SpringBoot Web 应用为例,重点介绍以下内容:

  • 演示 Spring Security 接口认证和鉴权的配置方法;
  • 以内存和数据库为例,介绍认证和鉴权数据的存储和读取机制;
  • 若干模块的自定义实现,包括:认证过滤器、认证或鉴权失败处理器等。

SpringBoot 示例

创建 SpringBoot 示例,用于演示 Spring Security 在 SpringBoot 环境下的应用,简要介绍四部分内容:pom.xml、application.yml、IndexController 和 HelloController。

SpringBoot pom.xml

  ...
<artifactId>boot-example</artifactId> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>

boot-example 是用于演示的 SpringBoot 项目子模块(Module)。

注: 依赖项的版本已在项目 pom.xml dependencyManagement 中声明。

SpringBoot application.yml

spring:
application:
name: example server:
port: 9999 logging:
level:
root: info

SpringBoot 应用名称为 example,实例端口为 9999

SpringBoot IndexController

@RestController
@RequestMapping("/")
public class IndexController {
@GetMapping
public String index() {
return "index";
}
}

IndexController 实现一个接口:/。

SpringBoot HelloController

@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/world")
public String world() {
return "hello world";
} @GetMapping("/name")
public String name() {
return "hello name";
}
}

HelloController 实现两个接口:/hello/world 和 /hello/name。

编译启动 SpringBoot 应用,通过浏览器请求接口,请求路径和响应结果:

http://localhost:9999
index http://localhost:9999/hello/world
hello world http://localhost:9999/hello/name
hello name

SpringBoot 示例准备完成。

SpringBoot 集成 Spring Security

SpringBoot 集成 Spring Security 仅需要在 pom.xml 中添加相应的依赖:spring-boot-starter-security,如下:

  <dependencies>
... <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>

编译启动应用,相对于普通的 SpringBoot 应用,我们可以在命令行终端看到特别的两行日志:

2022-01-09 16:05:57.437  INFO 87581 --- [           main] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: 3ef27867-e938-4fa4-b5da-5015f0deab7b

2022-01-09 16:05:57.525  INFO 87581 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@11e355ca, org.springframework.security.web.context.SecurityContextPersistenceFilter@5114b7c7, org.springframework.security.web.header.HeaderWriterFilter@24534cb0, org.springframework.security.web.csrf.CsrfFilter@77c233af, org.springframework.security.web.authentication.logout.LogoutFilter@5853ca50, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@6d074b14, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@3206174f, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@70d63e05, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5115f590, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@767f6ee7, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@7b6c6e70, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@e11ecfa, org.springframework.security.web.session.SessionManagementFilter@106d77da, org.springframework.security.web.access.ExceptionTranslationFilter@7b66322e, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3e5fd2b1]

表示 Spring Security 已在 SpringBoot 应用中生效。默认情况下,Spring Security 自动化地帮助我们完成以下三件事件:

  1. 开启 FormLogin 登录认证模式;

    我们使用浏览器请求接口 /:

    http://localhost:9999/

    会发现请求会被重定向至页面 /login:

    http://localhost:9999/login

    提示使用用户名和密码登录:

  2. 生成用于登录的用户名和密码;

    用户名为 user,密码会输出到应用的启动日志:

    Using generated security password: 3ef27867-e938-4fa4-b5da-5015f0deab7b

    每一次应用启动,密码都会重新随机生成。

  3. 注册用于认证和鉴权的过滤器;

    Spring Security 本质就是通过 过滤器过滤器(链) 实现的,每一个接口请求都会按顺序经过这些过滤器的“过滤”,每个过滤器承担的各自的职责,组合起来共同完成认证和鉴权。

    根据配置的不同,注册的过滤器也会有所不同,默认情况下,加载的过滤器列表可以参考启动日志:

    WebAsyncManagerIntegrationFilter
    SecurityContextPersistenceFilter
    HeaderWriterFilter
    CsrfFilter
    LogoutFilter
    UsernamePasswordAuthenticationFilter
    DefaultLoginPageGeneratingFilter
    DefaultLogoutPageGeneratingFilter
    BasicAuthenticationFilter
    RequestCacheAwareFilter
    SecurityContextHolderAwareRequestFilter
    AnonymousAuthenticationFilter
    SessionManagementFilter
    ExceptionTranslationFilter
    FilterSecurityInterceptor

使用 Spring Security 默认为我们生成的用户名和密码进行登录(Sign in),成功之后会自动重定向至 / :

index

之后我们就可以通过浏览器正常请求 /hello/world 和 /hello/name。

默认情况下,Spring Security 仅支持基于 FormLogin 方式的认证,只能使用固定的用户名和随机生成的密码,且不支持鉴权。如果想要使用更丰富的安全特性:

  • 其他认证方式,如:HttpBasic
  • 自定义用户名和密码
  • 鉴权

    则需要我们自定义配置 Spring Security。自定义配置可以通过两种方式实现:
  • Java Configuration:使用 Java 代码的方式配置
  • Security NameSpace Configuration:使用 XML 文件的方式配置

本文以 Java Configuration 的方式为例进行介绍,需要我们提供一个继承自 WebSecurityConfigurerAdapter 配置类,然后通过重写若干方法进而实现自定义配置。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception { }
}

SecurityConfig 使用 @Configuration 注解(配置类),继承自 WebSecurityConfigurerAdapter,本文通过重写 configure 方法实现自定义配置。

需要注意:WebSecurityConfigurerAdapter 中有多个名称为 configure 的重载方法,这里使用的是参数类型为 HttpSecurity 的方法。

注: Spring Security 默认自动化配置参考 Spring Boot Auto Configuration

Spring Security 使用 HttpBasic 认证

  protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize
.anyRequest()
.authenticated())
.httpBasic();
}

http.authorizeHttpRequests()

用以指定哪些请求需要什么样的认证或授权,这里使用 anyRequest()authenticated() 表示所有的请求均需要认证。

http.authorizeHttpRequests()

表示我们使用 HttpBasic 认证。

编译启动应用,会发现终端仍会输出密码:

Using generated security password: e2c77467-8c46-4fe1-ab32-eb87558b8c0e

因为,我们仅仅改变的是认证方式。

为方便演示,我们使用 CURL 直接请求接口:

curl http://localhost:9999

{
"timestamp": "2022-01-10T02:47:20.820+00:00",
"status": 401,
"error": "Unauthorized",
"path": "/"
}

会提示我们 Unauthorized,即:没有认证。

我们按照 HttpBasic 要求添加请求头部参数 Authorization,它的值:

Basic Base64(user:e2c77467-8c46-4fe1-ab32-eb87558b8c0e)

即:

Basic dXNlcjplMmM3NzQ2Ny04YzQ2LTRmZTEtYWIzMi1lYjg3NTU4YjhjMGU=

再次请求接口:

curl -H "Authorization: Basic dXNlcjplMmM3NzQ2Ny04YzQ2LTRmZTEtYWIzMi1lYjg3NTU4YjhjMGU=" http://localhost:9999

index

认证成功,接口正常响应。

Spring Security 自定义用户名和密码

使用默认用户名和随机密码的方式不够灵活,大部分场景都需要我们支持多个用户,且分别为他们设置相应的密码,这就涉及到两个问题:

  • 用户名和密码如何读取(查询)
  • 用户名和密码如何存储(增加/删除/修改)

对于 读取,Spring Security 设计了 UserDetailsService 接口:

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername

实现按照用户名(username)从某个存储介质中加载相对应的用户信息(UserDetails)。

username

用户名,客户端发送请求时写入的用于用户名。

UserDetails

用户信息,包括用户名、密码、权限等相关信息。

注意:用户信息不只用户名和用户密码。

对于 存储,Spring Security 设计了 UserDetailsManager 接口:

public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user); void updateUser(UserDetails user); void deleteUser(String username); void changePassword(String oldPassword, String newPassword); boolean userExists(String username);
}

createUser

创建用户信息

updateUser

修改用户信息

deleteUser

删除用户信息

changePassword

修改当前用户的密码

userExists

检查用户是否存在

注意UserDetailsManager 继承自 UserDetailsService

也就是说,我们可以通过提供一个已实现接口 UserDetailsManager* 的类,并重写其中的若干方法,基于某种存储介质,定义用户名、密码等信息的存储和读取逻辑;然后将这个类的实例以 Bean 的形式注入 Spring Security,就可以实现用户名和密码的自定义。

实际上,Spring Security 仅关心如何 读取存储 可以由业务系统自行实现;相当于,只实现接口 UserDetailsService 即可。

Spring Security 已经为我们预置了两种常见的存储介质实现:

InMemoryUserDetailsManagerJdbcUserDetailsManager 均实现接口 UserDetailsManager,本质就是对于 UserDetailsCRUD。我们先介绍 UserDetails,然后再分别介绍基于内存和数据库的实现。

UserDetails

UserDetails 是用户信息的抽象接口:

public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }

getUsername

获取用户名。

getPassword

获取密码。

getAuthorities

获取权限,可以简单理解为角色名称(字符串),用于实现接口基于角色的授权访问,详情见后文。

其他

获取用户是否可用,或用户/密码是否过期或锁定。

Spring Security 提供了一个 UserDetails 的实现类 User,用于用户信息的实例表示。另外,User 提供 Builder 模式的对象构建方式。

UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();

username

设置用户名称。

password

设置密码,Spring Security 不建议使用明文字符串存储密码,密码格式:

{id}encodedPassword

其中,id 为加密算法标识,encodedPassword 为密码加密后的字符串。这里以加密算法 bcrypt 为例,详细内容可参考 Password Storage

roles

设置角色,支持多个。

UserDetails 实例创建完成之后,就可以使用 UserDetailsManager 的具体实现进行存储和读取。

In Memory

InMemoryUserDetailsManager 是 Spring Security 为我们提供的基于内存实现的 UserDetailsManager

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
... @Bean
public UserDetailsManager users() {
UserDetails user = User.builder()
.username("userA")
.password("{bcrypt}$2a$10$CrPsv1X3hM" +
".giwVZyNsrKuaRvpJZyGQycJg78xT7Dm68K4DWN/lxS")
.roles("USER")
.build(); InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user); return manager;
}
}
  1. 创建用户信息实例 user,用户名为 userA,密码为 123456(使用 Bcrypt 算法加密);认证并需要角色参与,但 roles 必须被设置,这里指定为 USER;
  2. 创建 InMemoryUserDetailsManager 实例 manager;
  3. 使用 createUser 方法 将 user 存储至 manager;相当于把用户信息存储至内存介质中;
  4. 返回 manager;

使用 @BeanInMemoryUserDetailsManager 实例注入 Spring Security。

创建 InMemoryUserDetailsManager 实例之后,并不是必须立即调用 createUser 添加用户信息,也可以在业务系统的其它地方获取已注入的 InMemoryUserDetailsManager 动态存储 UserDetails 实例。

编译启动应用,使用我们自己创建的用户名和密码(userA/123456)访问接口:

curl -H "Authorization: Basic dXNlckE6MTIzNDU2" http://localhost:9999

index

基于内存介质自定义的用户名和密码已生效,接口正常响应。

JDBC

JdbcUserDetailsManager 是 Spring Security 为我们提供的基于数据库实现的 UserDetailsManager,相较于 InMemoryUserDetailsManager 使用略复杂,需要我们创建数据表,并准备好数据库连接需要的数据源(DataSource), JdbcUserDetailsManager 实例的创建依赖于数据源。

JdbcUserDetailsManager 可以与业务系统共用一个数据库数据源实例,本文不讨论数据源的相关配置。

MySQL 为例,创建数据表语句:

create table users(
username varchar(50) not null primary key,
password varchar(500) not null,
enabled boolean not null
); create table authorities (
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
); create unique index ix_auth_username on authorities (username,authority);

其他数据库语句可参考 User Schema

JdbcUserDetailsManager 实例的创建与注入,除

  • 获取已注入的数据源实例 dataSource;
  • 创建实例时需要传入数据源实例 dataSource;

之外,整体流程与 InMemoryUserDetailsManager 类似,不再赘述。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...... @Autowired
private DataSource dataSource; @Bean
public UserDetailsManager users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$CrPsv1X3hM" +
".giwVZyNsrKuaRvpJZyGQycJg78xT7Dm68K4DWN/lxS")
.roles("USER")
.build(); JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
manager.createUser(user); return manager;
}
}

在业务系统中获取已注入的 JdbcUserDetailsManager 实例,可以动态存储 UserDetails 实例。

编译启动应用,使用我们自己创建的用户名和密码(userA/123456)访问接口:

curl -H "Authorization: Basic dXNlckE6MTIzNDU2" http://localhost:9999

index

基于数据库介质自定义的用户名和密码已生效,接口正常响应。

Spring Security 鉴权

Spring Security 可以提供基于角色的权限控制:

  • 不同的用户可以属于不同的角色
  • 不同的角色可以访问不同的接口

假设,存在两个角色 USER(普通用户) 和 ADMIN(管理员),

角色 USER 可以访问接口 /hello/name,

角色 ADMIN 可以访问接口 /hello/world,

所有用户认证后可以访问接口 /。

我们需要按上述需求重新设置 HttpSecurity

  protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize
.mvcMatchers("/hello/name").hasRole("USER")
.mvcMatchers("/hello/world").hasRole("ADMIN")
.anyRequest().authenticated())
.httpBasic();
}

mvcMatchers("/hello/name").hasRole("USER")

设置角色 USER 可以访问接口 /hello/name。

mvcMatchers("/hello/world").hasRole("ADMIN")

设置角色 ADMIN 可以访问接口 /hello/world。

anyRequest().authenticated()

设置其他接口认证后即可访问。

mvcMatchers 支持使用通配符。

创建属于角色 USER 和 ADMIN 的用户:

用户名:userA,密码:123456,角色:USER

用户名:userB,密码:abcdef,角色:ADMIN

  @Bean
public UserDetailsManager users() {
UserDetails userA = User.builder()
.username("userA")
.password("{bcrypt}$2a$10$CrPsv1X3hM.giwVZyNsrKuaRvpJZyGQycJg78xT7Dm68K4DWN/lxS")
.roles("USER")
.build(); UserDetails userB = User.builder()
.username("userB")
.password("{bcrypt}$2a$10$PES8fUdtRrQ9OxLqf4CofOfcXBLQ3lkY2TSIcs1E9A0z2wECmZigG")
.roles("ADMIN")
.build(); JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource); manager.createUser(userA);
manager.createUser(userB); return manager;
}

对于用户 userA

使用用户 userA 的用户名和密码访问接口 /:

curl -H "Authorization: Basic dXNlckE6MTIzNDU2" http://localhost:9999

index

认证通过,可正常访问。

使用用户 userA 的用户名和密码访问接口 /hello/name:

curl -H "Authorization: Basic dXNlckE6MTIzNDU2" http://localhost:9999/hello/name

hello name

认证通过,鉴权通过,可正常访问。

使用用户 userA 的用户名和密码访问接口 /hello/world:

curl -H "Authorization: Basic dXNlckE6MTIzNDU2" http://localhost:9999/hello/world

{
"timestamp": "2022-01-10T13:11:18.032+00:00",
"status": 403,
"error": "Forbidden",
"path": "/hello/world"
}

认证通过,用户 userA 不属于角色 ADMIN,禁止访问。

使用用户 userA 的用户名和密码访问接口 /:

curl -H "Authorization: Basic dXNlckE6MTIzNDU2" http://localhost:9999

index

认证通过,可正常访问。

对于用户 userB

使用用户 userB 的用户名和密码访问接口 /:

curl -H "Authorization: Basic dXNlckI6YWJjZGVm" http://localhost:9999

index

认证通过,可正常访问。

使用用户 userB 的用户名和密码访问接口 /hello/world:

curl -H "Authorization: Basic dXNlckI6YWJjZGVm" http://localhost:9999/hello/world

hello world

认证通过,鉴权通过,可正常访问。

使用用户 userB 的用户名和密码访问接口 /hello/name:

curl -H "Authorization: Basic dXNlckI6YWJjZGVm" http://localhost:9999/hello/name

{
"timestamp": "2022-01-10T13:18:29.461+00:00",
"status": 403,
"error": "Forbidden",
"path": "/hello/name"
}

认证通过,用户 userB 不属于角色 USER,禁止访问。

这里可能会有一点奇怪,一般情况下我们会认为 管理员 应该拥有 普通用户 的全部权限,即普通用户 可以访问接口 /hello/name,那么 管理员 应该也是可以访问接口 /hello/name 的。如何实现呢?

方式一,设置用户 userB 同时拥有角色 USER 和 ADMIN;

    UserDetails userB = User.builder()
.username("userB")
.password("{bcrypt}$2a$10$PES8fUdtRrQ9OxLqf4CofOfcXBLQ3lkY2TSIcs1E9A0z2wECmZigG")
.roles("USER", "ADMIN")
.build();

这种方式有点不够“优雅”。

方式二,设置角色 ADMIN 包含 USER;

Spring Security 有一个 Hierarchical Roles 的特性,可以支持角色之间的 包含 操作。

使用这个特性要特别注意两个地方:

  1. authorizeRequests
  @Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize ->
authorize
.mvcMatchers("/hello/name").hasRole("USER")
.mvcMatchers("/hello/world").hasRole("ADMIN")
.mvcMatchers("/").authenticated())
.httpBasic();
}

前文使用的是 HttpSecurity.authorizeHttpRequests 方法,此处需要变更为 HttpSecurity.authorizeRequests 方法。

  1. RoleHierarchy
  @Bean
RoleHierarchy hierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); return hierarchy;
}

使用 RoleHierarchy 以 Bean 的方式定义角色之间的 层级关系;其中,“ROLE_” 是 Spring Security 要求的固定前缀。

编译启动应用,使用用户 userB 的用户名和密码访问接口 /hello/name:

curl -H "Authorization: Basic dXNlckI6YWJjZGVm" http://localhost:9999/hello/name

hello name

认证通过,鉴权通过,可正常访问。

如果开启 Spring Security 的 debug 日志级别,访问接口时可以看到如下的日志输出:

From the roles [ROLE_ADMIN] one can reach [ROLE_USER, ROLE_ADMIN] in zero or more steps.

可以看出,Spring Security 可以从角色 ADMIN 推导出用户实际拥有 USER 和 ADMIN 两个角色。

特别说明

Hierarchical Roles 文档中的示例有明显错误:

@Bean
AccessDecisionVoter hierarchyVoter() {
RoleHierarchy hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER\n" +
"ROLE_USER > ROLE_GUEST");
return new RoleHierarcyVoter(hierarchy);
}

接口 RoleHierarchy 中并不存在方法 setHierarchy。前文所述 authorizeRequestsRoleHierarchy 结合使用的方法是结合网络搜索和自身实践得出的,仅供参考。

另外,authorizeHttpRequestsRoleHierarchy 结合是没有效果的,authorizeRequestsauthorizeHttpRequests 两者之间的区别可以分别参考 Authorize HttpServletRequests with AuthorizationFilterAuthorize HttpServletRequest with FilterSecurityInterceptor

鉴权的前提需要认证通过;认证不通过的状态码为401,鉴权不通过的状态码为403,两者是不同的。

Spring Security 异常处理器

Spring Security 异常主要分为两种:认证失败异常和鉴权失败异常,发生异常时会分别使用相应的默认异常处理器进行处理,即:认证失败异常处理器和鉴权失败异常处理器。

使用的认证或鉴权实现机制不同,可能使用的默认异常处理器也不相同。

认证失败异常处理器

Spring Security 认证失败异常处理器:

public interface AuthenticationEntryPoint {
void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException;
}

如前文所述,认证失败时,Spring Security 使用默认的认证失败处理器实现返回:

{
"timestamp": "2022-01-10T02:47:20.820+00:00",
"status": 401,
"error": "Unauthorized",
"path": "/"
}

如果想要自定义返回内容,则可以通过自定义认证失败处理器实现:

  AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, authException) -> response
.getWriter()
.print("401");
} @Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint());
}

authenticationEntryPoint() 会创建返回一个自定义的 AuthenticationEntryPoint 实例;其中,使用 HttpServletResponse.getWriter().print() 写入我们想要返回的内容:401。

httpBasic().authenticationEntryPoint(authenticationEntryPoint()) 使用我们自定义的 AuthenticationEntryPoint 替换 HttpBasic 默认的 BasicAuthenticationEntryPoint

编译启动应用,使用不正确的用户名和密码访问接口 /:

curl -H "Authorization: Basic error" http://localhost:9999

401

认证不通过,使用我们自定义的内容 401 返回。

鉴权失败异常处理器

Spring Security 鉴权失败异常处理器:

public interface AccessDeniedHandler {
void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException;
}

如前文所述,认证失败时,Spring Security 使用默认的认证失败处理器实现返回:

{
"timestamp": "2022-01-10T13:18:29.461+00:00",
"status": 403,
"error": "Forbidden",
"path": "/hello/name"
}

如果想要自定义返回内容,则可以通过自定义鉴权失败处理器实现:

  AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> response
.getWriter()
.print("403");
} @Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
}

自定义鉴权失败处理器与认证失败处理器过程类似,不再赘述。

编译启动应用,使用用户 userA 的用户名和密码访问接口 /hello/world:

curl -H "Authorization: Basic dXNlckE6MTIzNDU2" http://localhost:9999/hello/world

403

鉴权不通过,使用我们自定义的内容 403 返回。

特别注意

exceptionHandling() 也是有一个 authenticationEntryPoint() 方法的;对于 HttpBasic 而言,使用 exceptionHandling().authenticationEntryPoint() 设置自定义认证失败处理器是不生效的,具体原因需要大家自行研究。

Spring Security 自定义认证

前文介绍两种认证方式:FormLoginHttpBasic,Spring Security 还提供其他若干种认证方式,详情可参考 Authentication Mechanisms

如果我们想实现自己的认证方式,也是比较简单的。Spring Security 本质就是 过滤器,我们可以实现自己的认证过滤器,然后加入到 Spring Security 中即可。

  Filter preAuthenticatedFilter() {
return (servletRequest, servletResponse, filterChain) -> {
...
UserDetails user = User
.builder()
.username("xxx")
.password("xxx")
.roles("USER")
.build(); UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(
user,
user.getPassword(),
user.getAuthorities()); SecurityContext context =
SecurityContextHolder.createEmptyContext();
context.setAuthentication(token); SecurityContextHolder.setContext(context); filterChain.doFilter(servletRequest, servletResponse);
};
}

认证过滤器核心实现流程:

  1. 利用 Http 请求(servletRequest)中的信息完成自定义认证过程(省略),可能的情况:

    • 检查请求中的用户名和密码是否匹配
    • 检查请求中的 Token 是否有效
    • 其他

      如果认证成功,则继续下一步;认证失败,则可以抛出异常,或者跳过后续步骤;
  2. 从 Http 请求中提取 username(用户名),使用已注入的 UserDetailsService 实例,加载 UserDetails(用户信息)(省略);

    简单起见,模拟创建一个用户信息实例 user;因为到这一步时,用户已是认证成功的,用户名和密码可以随意设置,实际只有角色是必须的,我们设置已认证用户的角色为 USER

  3. 创建用户认证标识;

    Spring Security 内部是依靠 Authentication.isAuthenticated() 来判断用户是否已认证过的,UsernamePasswordAuthenticationTokenAuthentication 的一种具体实现,需要注意创建实例时使用的构造方法和参数,构造方法内部会调用 Authentication.setAuthenticated(true)

  4. 创建并设置环境上下文 SecurityContext;

    环境上下文中保存着用户认证标识:context.setAuthentication(token)

特别注意

除去抛出异常的情况外,filterChain.doFilter(servletRequest, servletResponse); 是必须保证被执行的。

理解认证过滤器涉及的概念会比较多,详情参考 Servlet Authentication Architecture

认证过滤器创建完成之后,就可以加入到 Spring Security 中:

  @Override
protected void configure(HttpSecurity http) throws Exception {
http
......
.addFilterBefore(preAuthenticatedFilter(),
ExceptionTranslationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
}

Spring Security 根据我们配置的不同,会为我们自动按照一定的次序组装一条 过滤器链,通过这条链上的若干过滤器完成认证鉴权的。我们需要把自定义的认证过滤器加到这个链的合适位置,这是选取的位置是在 ExceptionTranslationFilter 的前面。

过滤器链的顺序可以参考 Security Filters

ExceptionTranslationFilter 的作用可以参考 Handling Security Exceptions

特别注意

使用自定义认证过滤器时,自定义认证失败异常处理器和鉴权失败异常处理器的设置方法。

编译启动应用,我们会发现可以在不填入任何认证信息的情况下直接访问接口 / 和 /hello/name,因为模拟用户已认证且角色为 USER;访问接口 /hello/world 时会出现提示 403。

结语

Spring Security 自身包含的内容很多,官方文档也不能很好的讲述清楚每个功能特性的使用方法,很多时候需要我们自己根据文档、示例、源码以及他人的分享,尽可能多的实践,逐步加深理解。

Spring Security 接口认证鉴权入门实践指南的更多相关文章

  1. Spring Boot整合实战Spring Security JWT权限鉴权系统

    目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以.像以前做项目的安全认证基于 session 的登录拦截,属于后端全栈式的开发的模式 ...

  2. 基于Springboot集成security、oauth2实现认证鉴权、资源管理

    1.Oauth2简介 OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAu ...

  3. 认证鉴权与API权限控制在微服务架构中的设计与实现(四)

    引言: 本文系<认证鉴权与API权限控制在微服务架构中的设计与实现>系列的完结篇,前面三篇已经将认证鉴权与API权限控制的流程和主要细节讲解完.本文比较长,对这个系列进行收尾,主要内容包括 ...

  4. 最简单易懂的Spring Security 身份认证流程讲解

    最简单易懂的Spring Security 身份认证流程讲解 导言 相信大伙对Spring Security这个框架又爱又恨,爱它的强大,恨它的繁琐,其实这是一个误区,Spring Security确 ...

  5. Spring Cloud实战 | 第九篇:Spring Cloud整合Spring Security OAuth2认证服务器统一认证自定义异常处理

    本文完整代码下载点击 一. 前言 相信了解过我或者看过我之前的系列文章应该多少知道点我写这些文章包括创建 有来商城youlai-mall 这个项目的目的,想给那些真的想提升自己或者迷茫的人(包括自己- ...

  6. 0.Python 爬虫之Scrapy入门实践指南(Scrapy基础知识)

    目录 0.0.Scrapy基础 0.1.Scrapy 框架图 0.2.Scrapy主要包括了以下组件: 0.3.Scrapy简单示例如下: 0.4.Scrapy运行流程如下: 0.5.还有什么? 0. ...

  7. Spring Security 入门(1-5)Spring Security - 匿名认证

    匿名认证 对于匿名访问的用户,Spring Security 支持为其建立一个匿名的 AnonymousAuthenticationToken 存放在 SecurityContextHolder 中, ...

  8. SpringBoot Spring Security 核心组件 认证流程 用户权限信息获取详细讲解

    前言 Spring Security 是一个安全框架, 可以简单地认为 Spring Security 是放在用户和 Spring 应用之间的一个安全屏障, 每一个 web 请求都先要经过 Sprin ...

  9. Mongodb 认证鉴权那点事

    [TOC] 一.Mongodb 的权限管理 认识权限管理,说明主要概念及关系 与大多数数据库一样,Mongodb同样提供了一套权限管理机制. 为了体验Mongodb 的权限管理,我们找一台已经安装好的 ...

随机推荐

  1. Git忽略提交规则 .gitignore文件

    在使用Git的过程中,我们喜欢有的文件比如日志,临时文件,编译的中间文件等不要提交到代码仓库,这时就要设置相应的忽略规则,来忽略这些文件的提交.简单来说一个场景:在你使用git add .的时候,遇到 ...

  2. 车载以太网第二弹 | 测试之实锤-物理层PMA测试实践

    前言 本期先从物理层"PMA测试"开始,下图1为"PMA测试"的测试结果汇总图.其中,为了验证以太网通信对线缆的敏感度,特选取两组不同特性线缆进行测试对比,果然 ...

  3. 01-gevent完成多任务

    gevent完成多任务 一.原理 gevent实现多任务并不是依靠多进程或是线程,执行的时候只有一个线程,在遇到堵塞的时候去寻找可以执行的代码.本质上是一种协程. 二.代码实现 import geve ...

  4. Table.SplitColumn拆分…Split…(Power Query 之 M 语言)

    数据源: 一列若干行数据. 目标: 根据特定条件拆分 操作过程: 选取"品名"列>[主页](或[转换])>[拆分列] 选取"品名"列>[主页] ...

  5. Spring5 概述及Spring IOC学习

    Spring Framework 5 1. Spring框架 1.1 Spring框架概述 1.2 主要内容 Spring框架是一个开源的JavaEE的应用程序 主要核心是 IOC(控制反转)和AOP ...

  6. CF70B Text Messaging 题解

    Content 有一个短信软件最多只能够上传长度为 \(n\) 的消息.现在你有一段话,但不一定能够一次发出.这段话由若干句话组成,以 ..? 或者 ! 为结尾.你不能够将一句话拆开来发,但是如果容量 ...

  7. CF637B Chat Order 题解

    Content 有 \(n\) 个字符串,每次出现这个单词就把这个单词放到队列的队首(若已经出现就把原队列里面的那个单词提到队首),求最后的队列由队首到队尾的元素依次是多少. 数据范围:\(1\leq ...

  8. Python3 day6面向对象

    http://www.cnblogs.com/alex3714/articles/5188179.html ====================生活中==================== 世界 ...

  9. ABP VNext框架中Winform终端的开发和客户端授权信息的处理

    在ABP VNext框架中,即使在它提供的所有案例中,都没有涉及到Winform程序的案例介绍,不过微服务解决方案中提供了一个控制台的程序供了解其IDS4的调用和处理,由于我开发过很多Winform项 ...

  10. PlatformIO手工升级stcgal到1.6版本

    PlatformIO自带的stcgal版本为1.4, 这个版本只支持到STC15, 不支持STC8. 在使用PlatformIO内建的upload写入STC8A8K64S4A12时, 会提示不识别的协 ...