龙空技术网

手把手教你如何使用SpringSecurity在API网关中的认证功能

java领路人 132

前言:

此时我们对“netsession类”大约比较看重,各位老铁们都想要知道一些“netsession类”的相关资讯。那么小编同时在网络上收集了一些对于“netsession类””的相关文章,希望咱们能喜欢,各位老铁们快快来了解一下吧!

Spring Security是Spring提供的安全框架,提供认证和授权等安全管理的功能,与所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足自定义要求。本节重点介绍SpringSecurity在API网关中的认证功能的使用。

在Spring Boot 2.0之后,Spring Web应用分为传统阻塞式的Spring MVC和非阻塞式的基于Reactive的Spring WebFlux,SpringSecurity对于这两种方式均提供了支持,如图5.24所示。

不过Spring并没有单独提供Spring Security Reactive的工程,所以无论哪种技术栈,都需要添加相同的Spring Security的依赖,在build.gradle中添加如下配置。

implementation "org.springframework.boot:spring-boot-starter-security'

接下来的用法就不一样了,如果使用的是Spring MVC或Zuul1.x,那么身份认证请参考以下阻塞式的用法。

1. 阻塞式

首先,我们需要创建一个安全配置类,如SecurityConfig,代码如下。

在上述代码中,需要继承WebSecurityConfigurerAdapter抽象类,然后重写configure方法,方法中我们可以根据不同的路径定义不同的安全规则,代码设置了路径是/login的请求为permitAll,表示不做任何限制,路径为/users/ **的接口必须是权限为admin的用户才能访问,路径为/goods/ **的接口只需认证通过即可访问,不用关心权限。

这时Spring Security已经配置完成,我们的应用已经受到了保护,再次访问这些受限的接口将返回403。然后需要实现Spring Security提供的AuthenticationManager接口来定义认证的规则,如使用用户名密码登录,代码如下。

简单定义一个User类,代码如下。

通过Authentication方法可以自定义任何用户认证的规则,为了演示方便,直接硬编码用户名和密码必须是zhanggang和123456,在真实系统中应该通过查询用户数据库或者调用认证服务来完成用户名和密码的校验工作。当用户名和密码校验成功后,就可以返回一个Authentication对象,并且设置Authentication的状态为true,这个对 象 最 终 会 被 Spring Security 存 储 在 Session 中 , 我们使用的AuthenticationToken代码如下。

在上述代码中,可以通过继承AbstractAuthenticationToken类来快速定义一个认证类型,我们可以将用户信息(如User对象)作为Principal属性存储在Authentication中,这样方便通过Session获取用户信息。需要注意的是,我们需要在构造器中设置用户的权限信息,否则无法设置该Authentication为true。

接下来只需开发登录和注销的接口即可,首先使用之前预留的开发权限的路径/login来开发一个登录接口,代码如下。

这里可以定义一个值对象用来接收请求的参数(用户名密码),代码如下。

然后将之前写好的认证管理服务UserAuthenticationManager注入Controller中,并调用Authenticate方法执行身份认证,然后调用SecurityContextHolder.getContext().setAuthentication将认证结果对象设置到Spring Security的上下文中,最终Authentication对象将保存在Session中。

同样,我们可以使用SecurityContextHolder实现注销的操作,代码如下。

通 过 setAuthentication(null) 的方式 ,可以清空 SpringSecurity上下文中的认证信息,达到注销的目的。

如5.4.3节中所说的,这种方式在分布式系统中会存在状态不一致的问题,可以通过继承Redis来实现Session的同步,做法很简单,Spring提供了Session管理的工具框架,可以在项目中添加springboot-starter-data-redis和spring-session-data-redis的依赖,即在build.gradle文件中添加如下dependencies。

然后在application.yml配置文件中加入Redis和Session的相关配置,内容如下。

上述配置先设置了Spring Data Redis的连接方式,然后设置Spring管理Session的存储类型是使用Redis进行存储,需要在之前的SecurityConfig 中 添 加 @EnableRedisHttpSession 注 解 来 开 启 存 储Session到Redis的过滤器,代码如下。

这样关于Session和Redis的集成就已经完成,我们可以验证一下效果,如登录后重启服务器,只要Session没有超时(默认是半小时),再次请求服务,认证应该依然生效,不需要再次登录。

需要注意的是,由于使用了Redis进行Session的存储,我们自定义的Authentication中的Principle必须实现Serializable接口,否则调用Redis存储时会报如下错误。

org.springframework.data.redis.serializer.SerializationException: Cannot deserialize; ...

例如,我们使用自定义的AuthenticationToken,并将User作为Principle属性存储到Session中,所以User类需要实现Serializable接口,代码如下。

2. 非阻塞式

如果使用WebFlux或Spring Cloud Gateway,那么我们可以使用非阻塞式的方式来实现用户身份认证,整体的实现过程和阻塞式一样,只是在代码的形式上略有区别。

首先,我们要有一个SecurityConfig来配置安全规则,不过不需要任何继承,只需添加@EnableWebFluxSecurity注解即可,然后需要配置SecurityWebFilterChain的Bean,代码如下。

规则和之前阻塞式的配置类似 , 只不过这里使用的是ServerHttpSecurity作为参数进行的配置。例如,我们使用的是用户名密码的Basic认证方式,那么需要开启httpBasic,然后添加一个userDetailsService来实现用户的查询,代码如下。

这里使用匿名内部类的方式实现ReactiveUserDetailsService,当然,也可以单独定义一个类来实现ReactiveUserDetailsService,不过要添加@Service注解来声明定义的类初始化为Spring的Bean。这里只是简单实现了一个假的用户信息返回,在真实场景下一般会调用后端用户服务或认证服务来完成这一步。

需要注意的是,ReactiveUserDetailsService的接口方法需要返回 一 个 Mono 的 UserDetails 对 象 , 所 以 我 们 的 User 需 要 实 现UserDetails,代码如下。

关于用户的属性先全部设置为true,构建一个基础的非阻塞式的认证规则,只要请求头中包含key为Authorization的信息,Spring就会自动解析信息的值进行认证,Authorization头信息的规则是Basic+空格+base64编码的“用户名:密码”,内容如下。

Basic emhhbmdnYW5nOjEyMzQ1Ng=-

当然,在大多数情况下我们不会在每次请求时都使用用户名密码来进行用户认证,那么在非阻塞式(WebFlux)下如何利用SpringSecurity去集成Redis,而不用每次认证都使用用户名密码?

下 面 介 绍 比 较 实 用 的 用 法 , 就 像 阻 塞 式 一 样 , 可 以 使 用Redis+Session的方式,首先需要集成SpringBoot的data redis和session data redis框架,内容如下。

implementation 'org.springframework.session:spring-session-data-redis'

implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'

同样,在application.yml文件中需要加入Redis和Session的相关配置,内容如下.

在SecurityConfig配置类上加入@EnableRedisWebSession注解来开启Redis存储Session的功能,然后开放一个login的请求路径用于登录接口的调用,删除之前定义的userDetailsService方法,代码如下。

再定义一个WebConfig配置类来增加一个WebFlux的登录接口,代码如下。

上述代码是传统的定义一个WebFlux接口的方式,当然Spring 2.x也支持直接使用Spring MVC的@RestController、@RequestMapping等注解来定义一个Controller,这里使用静态方法POST定义了一个PostMapping为login的接口,请求会被路由到LoginController中的login方法上,所以我们需要定义一个LoginController并实现一个login方法,代码如下。

上述代码可能有点复杂,要完全理解需要有一定的WebFlux基础,下面一起来解读这些代码的作用。

首 先 , login 方 法 的参数类型 ServerRequest 和 返 回 类 型ServerResponse是WebFlux接口的固定格式,除非直接使用Spring MVC的注解来定义Controller,否则可以不用关心这两个类型,WebFlux会自动进行兼容转换。通过ServerRequest我们可以获取请求Body和Session等信息,类似HttpServletRequest的功能,不过获取的是一个响应式对象,即Mono或Flux对象,我们将请求的用户名密码转换成UsernamePasswordAuthenticationToken对象,然后调用内部的认证方法Authenticate(Authentication)。

Authenticate方法很简单,就是之前的userDetailsService方法。这里不再解释,比较关键的一步是zipWith,在认证成功后需要将认证信息保存到Session中,但无论Session还是Authentication都是响应式的对象,不能像阻塞式那样直接去setAttribute,两个响应式的对象进行 打包 ,然 后 可 以 对 它 们 进 行 操 作 , 我们需要将Authentication 作 为 Session 的 Attribute 存 储 在 Session 中 , 而Session会被自动保存到Redis中。

最后,我们可以自定义一个Token返回客户端,用于后续的请求认证,这里的createToken方法使用Session的ID加Authentication的credentials组合方式来生成一个Token,这里代码中使用的Token生成规则和之前我们做Basic认证时的Authorization头信息的规则一样。需要注意的是,Authentication的credentials并不是用户的密码,它是一个无意义的值,如UUID、时间戳等,这里使用的是UUID,在登录成功后我们自定义的AuthenticationToken构造器中被创建,代码如下。

登录成功后会返回一个Token给客户端,例如:

只要客户端拥有这个Token,在后续的请求中都可以使用该Token来进行身份认证,下面只需定义一个认证方法即可。除了可以重写userDetailsService方法,Spring Security还提供了通过自定义AuthenticationManager类来实现自定义认证规则的方式,创建一个UserAuthenticationManager类并实现ReactiveAuthenticationManager接口,然后将这个Manager声明为Spring的Bean,代码如下。

在上述代码中引入了ReactiveSessionRepository来帮助我们从Redis中获取Session信息,Authentication方法会自动在我们配置需要拦截的请求时执行,并且会自动将请求头中的Authorization信息( Basic+ 空 格 +Base64 编 码 的 session ID: 随 机 字 符 串 ) 解 析 为Authentication对象,并作为方法参数传入该方法,默认的解析规则和Basic认证的消息头规则相同,例如:

客户端只要在Header中附加key为“Authorization”的上述内容,就能复用这个过滤器的解析方式,不需要关心过滤器本身数据的解 析 和 传 递 等 逻 辑 , 通 过 authentication.getPrincipal() 获 取Session ID,通过authentication.getCredentials()获取在登录时创建的随机字符串,同样的,sessionRepository.findById得到的是一个响应式的对象Mono<Session>,然后去Redis中查询对应的Session并校验随机字符串,就能获取用户的认证状态和身份信息了。

登录和认证功能完成,只要Session没有过期,用户就可以一直保持登录状态,当然,用户还可以主动注销,这里给大家介绍一个简单的注销方式,直接在SecurityConfig中添加如下配置。

如果不配置logoutUrl,默认的注销路径也是“/logout”,当我们访问logout时,Spring会自动清除Redis中的Session信息,从而达到注销用户的目的。

Java-JWT

介绍完Cookie和Session的实现方式后,可以发现这种方式需要后端对用户状态或身份信息进行存储和管理,在集群环境下还需要集成如Redis、MySQL之类的数据库,而在使用JWT时,则不用关心这些问题。

我们知道JWT其实是一种标准,在这套标准下有多种语言的实现,如.net、Java、Python、JavaScript、Ruby等都有多个基于JWT的开源框架。下面介绍的框架是Java版本中比较常用的com.auth0:javajwt,GitHub地址是。

在build.gradle中引入java-jwt依赖包,代码如下。

implementation 'com.authO:java-jwt:3.8.0'

JWT的代码逻辑主要分为两部分:一是创建JWT,二是校验JWT。

首先,创建JWT的部分,在用户登录成功后,服务端会创建一个JWT返回客户端,客户端会存储这个JWT用于以后的请求认证,创建JWT的代码如下。

在上述代码中,直接使用JWT.create方法就可以创建一个JWT标准的Token,其中withHeader可以设置JWT的头部,withClaim可以设置JWT的JSON消息体,可以设置JWT的签发日期和过期日期,这里使用HS256的加密算法,密码是123456。当然,通常我们可以使用配置文件来 定 义 , 具 体 的 加 密 算 法 对 应 的 代 码 可 以 在 源 码com.auth0.jwt.algorithms.Algorithm 中 查 看 , 如 HS256 使 用 的 是HmacSHA256算法。

如果不使用Spring Security,只是单纯在API网关中使用JWT完成身份认证,那么我们可以直接自定义一个Servlet的Filter来进行JWT的校验工作,代码如下。

在上述代码中,从请求的自定义头部X-auth-jwt中获取客户端的JWT,然后使用JWT.require方法来获取校验对象JWTVerifier,其中JWT.require的参数会指定对应的加密算法和密钥,需要与创建JWT时保持一致,然后调用jwtVerifier.verify(token)完成验证工作,如果校验失败就会抛出JWTVerificationException异常。

如果我们想与Spring Security集成,可以利用Basic的认证方式来实现JWT的认证,以非阻塞式的WebFlux为例,首先修改createJwt的方法,代码如下。

创建JWT的代码和之前一样,重要的是最后两行代码,创建一个Basic认证的头部信息。不过这里只用加冒号“:”,默认的冒号后面的内容表示credentials,在JWT的认证中并不会用到,所以我们直接将jwt+":" 作为Authorization的头部信息,然后将信息进行base64的编码返回即可。

接下来编写登录方法,LoginController的配置和之前一样,具体的login方法代码如下。

Authenticate是登录请求时校验用户名密码的认证方法,和之前一样,这里不多解释。在校验成功后,我们不再关心Session或Cookie,只需调用之前定义好的createJwtBasicToken方法来创建一个包含JWT的Token,然后在接收到Token值后会存储在客户端,后端不再维护用户状态信息,客户端只需在每次请求头中加入该Token,依然是Basic认证的规则,消息头的key为Authorization,值为“Basic”+空格+Token,例如:

接下来只需定义一个ReactiveAuthenticationManager来完成请求的认证工作,创建一个 JwtAuthenticationManager , 实 现ReactiveAuthenticationManager接口,代码如下。

在上述代码中,当调用jwtVerifier.verify()验证成功之后,会得到DecodedJWT类型的对象,通过decodedJWT.getClaim()可以获取JWT消息体的用户信息,然后创建AuthenticationToken对象并返回。

这样我们就完成了JWT和Spring Security的集成,关于API网关的内容就介绍到这里,第6章将介绍API网关的另一种架构模式:BFF。

本文给大家讲解的内容是Spring Security下篇文章给大家讲解的是BFF用于前端的后端觉得文章不错的朋友可以转发此文关注小编;感谢大家的支持!

标签: #netsession类