crm开发定制通过认证服务进行统一认证,crm开发定制然后通过网关来统一校crm开发定制验认证和鉴权。
将采用 crm开发定制作为注册中心,Gateway 作为网关,使用
nimbus-jose-jwt
JWT 库操作 JWT 令牌
理论介绍
Spring Security crm开发定制是强大的且容易定制的,基于 Spring crm开发定制开发的实现crm开发定制认证登录与资源授权crm开发定制的应用安全
SpringSecurity crm开发定制的核心功能:
- Authentication:身份认证,crm开发定制用户登陆的验证(crm开发定制解决你是谁的问题)
- Authorization:访问授权,crm开发定制授权系统资源的访问权限(crm开发定制解决你能干什么的问题)
- 安全防护,crm开发定制防止跨站请求,session 攻击等
SpringSecurity 配置类
-
configure(HttpSecurity httpSecurity)
crm开发定制用于配置需要拦截的 url 路径、jwt crm开发定制过滤器及出异常后的处理器
-
configure(AuthenticationManagerBuilder auth)
用于配置 UserDetailsService 及 PasswordEncoder
-
RestfulAccessDeniedHandler
crm开发定制当用户没有时的处理器,用于返回 JSON crm开发定制格式的处理结果
-
RestAuthenticationEntryPoint
crm开发定制当未登录或 token 失效时,返回 JSON crm开发定制格式的结果
-
UserDetailsService
SpringSecurity crm开发定制定义的核心接口,用于根据用户名获取用户信息,需要自行实现
-
UserDetails
SpringSecurity 定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现
-
PasswordEncoder
SpringSecurity 定义的用于对密码进行编码及比对的接口,目前使用的是 BCryptPasswordEncoder
-
JwtAuthenticationTokenFilter
在用户名和密码校验前添加的过滤器,如果有 jwt 的 token,会自行根据 token 信息进行登录
JWT 单点登录
用户在应用 A 上登录认证,应用 A 会颁发给他一个 JWT 令牌(一个包含若干用户状态信息的字符串)。当用户访问应用 B 接口的时候,将这个字符串交给应用 B,应用 B 根据 Token 中的内容进行鉴权。不同的应用之间按照统一标准发放 JWT令牌,统一标准验证 JWT 令牌。从而你在应用 A 上获得的令牌,在应用 B 上也被认可,当然这样这些应用之间底层数据库必须是同一套用户、角色、权限数据。
- 认证 Controller 代码统一
- 鉴权 Filter 代码统一、校验规则是一样的
- 使用同一套授权数据
- 同一个用于签名和解签的 secret
JWT 存在的问题
说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:
-
续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了
-
注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便
-
密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret
-
基于第2点和第3点,一般建议不同用户取不同secret
OAuth2
以上的所有的单点集群登陆方案,都是有一个前提就是:应用A、应用B、应用1、应用2、应用3都是你们公司的,你们公司内部应用之间进行单点登陆验证。
但是大家都见过这样一个场景:登录某一个网站,然后使用的是在QQ、微信上保存的用户数据。也就是说第三方应用想使用某个权威平台的用户数据做登录认证,那么这个权威平台该如何对第三方应用提供认证服务?目前比较通用的做法就是OAuth2(现代化的社交媒体网站登录基本都使用OAuth2)
- Spring Security OAuth 项目进入维护状态,不再做新特性的开发。只做功能维护和次要特性开发
- 未来所有的基于 Spring 的 OAuth2.0 的支持都基于 Spring Security 5.2 版本开发。即:Spring Security 5.2 以后的版本是 OAuth2.0 支持库,用来替换 Spring Security OAuth
OAuth2 需求场景
在说明OAuth2需求及使用场景之前,需要先介绍一下OAuth2授权流程中的各种角色:
- 资源拥有者(User) - 指应用的用户,通常指的是系统的登录用户
- 认证服务器 (Authorization Server)- 提供登录认证接口的服务器,比如:github登录、QQ登录、微信登录等
- 资源服务器 (Resources Server) - 提供资源接口及服务的服务器,比如:用户信息接口等。通常和认证服务器是同一个应用。
- 第三方客户端(Client) - 第三方应用,希望使用资源服务器提供的资源
- 服务提供商(Provider): 认证服务和资源服务归属于一个机构,该机构就是服务提供商
OAuth2 四种授权模式
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
密码模式与授权码模式最大的区别在于:
- 授权码模式申请授权码的过程是用户直接与认证服务器进行交互,然后授权结果由认证服务器告知第三方客户端,也就是不会向第三方客户端暴露服务提供商的用户密码信息
- 密码模式,是用户将用户密码信息交给第三方客户端,然后由第三方向服务提供商进行认证和资源请求。绝大多数的服务提供商都会选择使用授权码模式,避免自己的用户密码暴漏给第三方。所以密码模式只适用于服务提供商对第三方厂商(第三方应用)高度信任的情况下才能使用,或者这个“第三方应用”实际就是服务提供商自己的应用
整合 JWT
因为 Spring Security OAuth “认证服务器”支持多种认证模式,所以不想抛弃它。但是想把最后的"资源访问令牌",由 AccessToken 换成 JWT 令牌。因为 AccessToken 不带有任何的附加信息,就是一个字符串,JWT 是可以携带附加信息的。
应用架构
理想的解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他 API 服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑
相关服务划分:
- security-oauth2-gateway:网关服务,负责请求转发和鉴权功能,整合 Spring Security+Oauth2
- security-oauth2-auth:Oauth2认证服务,负责对登录用户进行认证,整合 Spring Security+Oauth2
- security-oauth2-api:受保护的 API 服务,用户鉴权通过后可以访问该服务,不整合 Spring Security+Oauth2
方案实现
下面介绍下这套解决方案的具体实现,依次搭建认证服务、网关服务和 API 服务
security-oauth2-auth 认证
首先来搭建认证服务,它将作为 Oauth2 的认证服务使用,并且网关服务的鉴权功能也需要依赖它
pom 依赖
- 在
pom.xml
中添加相关依赖,主要是 Spring Security、Oauth2、JWT、Redis 相关依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-rsa</artifactId> </dependency> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> </dependency> </dependencies>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 在
application.yml
中添加相关配置,主要是 Nacos 和 Redis 相关配置
server: port: 9401spring: profiles: active: dev application: name: security-oauth2-auth cloud: nacos: discovery: server-addr: 192.168.123.22:8848 username: nacos password: nacos redis: port: 6379 host: localhost password: xxx management: endpoints: web: exposure: include: "*"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
认证服务配置
生成密钥库
- 使用
keytool
生成 RSA 证书jwt.jks
,复制到resource
目录下,在 JDK 的bin
目录下使用如下命令即可
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
- 1
加载用户信息
- 创建
UserServiceImpl
类实现 Spring Security 的UserDetailsService
接口,用于加载用户信息
/** * 用户管理业务类 */@Servicepublic class UserDetailsServiceImpl implements UserDetailsService{ @Autowired private UmsAdminService adminService; @Autowired private HttpServletRequest request; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String clientId = request.getParameter("client_id"); UserDto userDto = null; if (AuthConstant.ADMIN_CLIENT_ID.equals(clientId)) { userDto = adminService.loadUserByUsername(username); } if (null == userDto) { throw new UsernameNotFoundException(EC.ERROR_USER_PASSWORD_INCORRECT.getMsg()); } SecurityUserDetails securityUser = new SecurityUserDetails(userDto); if (!securityUser.isEnabled()) { throw new DisabledException(EC.ERROR_USER_ENABLED.getMsg()); } else if (!securityUser.isAccountNonLocked()) { throw new LockedException(EC.ERROR_USER_LOCKED.getMsg()); } else if (!securityUser.isAccountNonExpired()) { throw new AccountExpiredException(EC.ERROR_USER_EXPIRE.getMsg()); } else if (!securityUser.isCredentialsNonExpired()) { throw new CredentialsExpiredException(EC.ERROR_USER_UNAUTHORIZED.getMsg()); } return securityUser; }}@Componentpublic class UmsAdminService{ @Autowired private PasswordEncoder passwordEncoder; public UserDto loadUserByUsername(String username) { String password = passwordEncoder.encode("123456a"); if("admin".equals(username)) { return new UserDto("admin", password, 1, "", CollUtil.toList("ADMIN")); } else if("langya".equals(username)) { return new UserDto("langya", password, 1, "", CollUtil.toList("ADMIN", "TEST")); } return null; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
认证服务配置
- 添加认证服务相关配置
Oauth2ServerConfig
,需要配置加载用户信息的服务UserServiceImpl
及 RSA 的钥匙对KeyPair
/** * 认证服务器配置 */@AllArgsConstructor@Configuration@EnableAuthorizationServerpublic class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter{ private final PasswordEncoder passwordEncoder; private final UserDetailsServiceImpl userDetailsService; private final AuthenticationManager authenticationManager; private final JwtTokenEnhancer jwtTokenEnhancer; /** * 客户端信息配置 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(AuthConstant.ADMIN_CLIENT_ID) .secret(passwordEncoder.encode("123456")) .scopes("all") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600 * 24) .refreshTokenValiditySeconds(3600 * 24 * 7) .and() .withClient(AuthConstant.PORTAL_CLIENT_ID) .secret(passwordEncoder.encode("123456")) .scopes("all") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600 * 24) .refreshTokenValiditySeconds(3600 * 24 * 7); } /** * 配置授权(authorization)以及令牌(token) */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> delegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(accessTokenConverter()); //配置JWT的内容增强器 enhancerChain.setTokenEnhancers(delegates); endpoints.authenticationManager(authenticationManager) //配置加载用户信息的服务 .userDetailsService(userDetailsService) .accessTokenConverter(accessTokenConverter()) .tokenEnhancer(enhancerChain); } /** * 允许表单认证 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); } /** * 使用非对称加密算法对token签名 */ @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); //or 设置对称签名 //jwtAccessTokenConverter.setSigningKey("2430B31859314947BC84697E70B3D31F"); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; } /** * 从classpath下的密钥库中获取密钥对(公钥+私钥) */ @Bean public KeyPair keyPair() { //从classpath下的证书中获取秘钥对 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
jwt 增强
- 如果你想往 JWT 中添加自定义信息的话,比如说
登录用户的ID
,可以自己实现TokenEnhancer
接口
/** * JWT 内容增强器 */@Componentpublic class JwtTokenEnhancer implements TokenEnhancer{ @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUserDetails securityUser = (SecurityUserDetails) authentication.getPrincipal(); //把用户名设置到JWT中 Map<String, Object> info = new HashMap<>(); info.put("user_name", securityUser.getUsername()); info.put("client_id", securityUser.getClientId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
公钥获取接口
- 由于的网关服务需要 RSA 的公钥来验证签名是否合法,所以认证服务需要有个接口把公钥暴露出来
/** * 获取RSA公钥接口 */@RestControllerpublic class KeyPairController{ @Autowired private KeyPair keyPair; @GetMapping("/rsa/publicKey") public Map<String, Object> getKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
安全配置
- 不要忘了还需要配置 Spring Security,允许获取公钥接口的访问
/** * SpringSecurity 安全配置 */@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/rsa/publicKey").permitAll() .anyRequest().authenticated(); } /** * 如果不配置 SpringBoot 会自动配置一个 AuthenticationManager 覆盖掉内存中的用户 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
资源角色映射缓存
- 创建一个资源服务
ResourceServiceImpl
,初始化的时候把资源与角色匹配关系缓存到 Redis 中,方便网关服务进行鉴权的时候获取
/** * 资源与角色匹配关系管理业务类 * <p> * 初始化的时候把资源与角色匹配关系缓存到Redis中,方便网关服务进行鉴权的时候获取 */@Servicepublic class ResourceServiceImpl{ @Autowired private RedisTemplate<String, Object> redisTemplate; private Map<String, List<String>> resourceRolesMap; @PostConstruct public void initData() { resourceRolesMap = new TreeMap<>(); resourceRolesMap.put("/admin/hello", CollUtil.toList("ADMIN")); resourceRolesMap.put("/admin/user/currentUser", CollUtil.toList("ADMIN", "TEST")); redisTemplate.opsForHash().putAll(AuthConstant.RESOURCE_ROLES_MAP_KEY, resourceRolesMap); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
如果资源权限存储到数据库,也可以直接使用 SQL 语句形成结果集,如:
security-oauth2-gateway 鉴权
接下来就可以搭建网关服务了,它将作为 Oauth2 的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作
pom 依赖
- 在
pom.xml
中添加相关依赖,主要是 Gateway、Oauth2 和 JWT 相关依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--lb:// need--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> </dependency></dependencies>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 在
application.yml
中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置
server: port: 9201spring: main: #springcloudgateway 的内部是通过 netty+webflux 实现的 #webflux 实现和 spring-boot-starter-web 依赖冲突 web-application-type: reactive profiles: active: dev application: name: security-oauth2-gateway cloud: nacos: discovery: server-addr: localhost:8848 username: nacos password: nacos gateway: routes: #配置路由路径 - id: oauth2-api-route uri: lb://security-oauth2-api predicates: - Path=/admin/** filters: - StripPrefix=1 - id: oauth2-auth-route uri: lb://security-oauth2-auth predicates: - Path=/auth/** filters: - StripPrefix=1 discovery: locator: #开启从注册中心动态创建路由的功能 enabled: true #使用小写服务名,默认是大写 lower-case-service-id: true security: oauth2: resourceserver: jwt: #配置RSA的公钥访问地址 jwk-set-uri: 'http://localhost:9401/rsa/publicKey' redis: host: 192.168.123.22 port: 6379 password: Hacfin_Redis8 timeout: 6000mssecure: ignore: #配置白名单路径 urls: - "/actuator/**" - "/auth/oauth/token"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
资源服务器配置
- 对网关服务进行配置安全配置,由于 Gateway 使用的是
WebFlux
,所以需要使用@EnableWebFluxSecurity
注解开启
/** * 资源服务器配置 */@AllArgsConstructor@Configuration@EnableWebFluxSecuritypublic class ResourceServerConfig{ private final AuthorizationManager authorizationManager; private final IgnoreUrlsConfig ignoreUrlsConfig; private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); //自定义处理JWT请求头过期或签名错误的结果 http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint); //对白名单路径,直接移除JWT请求头 http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION); http.authorizeExchange() //白名单配置 .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll() //鉴权管理器配置 .anyExchange().access(authorizationManager) .and() .exceptionHandling() //处理未授权 .accessDeniedHandler(restfulAccessDeniedHandler) //处理未认证 .authenticationEntryPoint(restAuthenticationEntryPoint) .and() .csrf().disable(); return http.build(); } /** * @linkhttps://blog.csdn.net/qq_24230139/article/details/105091273 * ServerHttpSecurity 没有将 jwt 中 authorities 的负载部分当做 Authentication * 需要把 jwt 的 Claim 中的 authorities 加入 * 方案:重新定义 ReactiveAuthenticationManager 权限管理器,默认转换器 JwtGrantedAuthoritiesConverter */ @Bean public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
鉴权管理器
- 在
WebFluxSecurity
中自定义鉴权操作需要实现ReactiveAuthorizationManager
接口
@Componentpublic class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> { @Autowired private RedisTemplate<String,Object> redisTemplate; @Override public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) { //从Redis中获取当前路径可访问角色列表 URI uri = authorizationContext.getExchange().getRequest().getURI(); Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath()); List<String> authorities = Convert.toList(String.class,obj); authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList()); //认证通过且角色匹配的用户可访问当前路径 return mono .filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) .map(GrantedAuthority::getAuthority) .any(authorities::contains) .map(AuthorizationDecision::new) .defaultIfEmpty(new AuthorizationDecision(false)); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
过滤器
- 这里还需要实现一个全局过滤器
AuthGlobalFilter
,当鉴权通过后将 JWT 令牌中的用户信息解析出来,然后存入请求的 Header 中,这样后续服务就不需要解析 JWT 令牌了,可以直接从请求的 Header 中获取到用户信息
/** * 将登录用户的JWT转化成用户信息的全局过滤器 */@Componentpublic class AuthGlobalFilter implements GlobalFilter, Ordered{ @Autowired private RedisTemplate redisTemplate; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //认证信息从Header 或 请求参数 中获取 ServerHttpRequest serverHttpRequest = exchange.getRequest(); String token = serverHttpRequest.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER); if (Objects.isNull(token)) { token = serverHttpRequest.getQueryParams().getFirst(AuthConstant.JWT_TOKEN_HEADER); } if (StrUtil.isEmpty(token)) { return chain.filter(exchange); } try { //从token中解析用户信息并设置到Header中去 String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); // 黑名单token(登出、修改密码)校验 JSONObject jsonObject = JSONUtil.parseObj(userStr); String jti = jsonObject.getStr("jti"); Boolean isBlack = redisTemplate.hasKey(AuthConstant.TOKEN_BLACKLIST_PREFIX + jti); if (isBlack) { } // 存在token且不是黑名单,request写入JWT的载体信息 ServerHttpRequest request = serverHttpRequest.mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).build(); exchange = exchange.mutate().request(request).build(); } catch (ParseException e) { e.printStackTrace(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
security-oauth2-api
最后搭建一个API服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它
pom 依赖
- 在
pom.xml
中添加相关依赖,就添加了一个web依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>
- 1
- 2
- 3
- 4
- 5
- 6
登录信息接口
- 创建一个
LoginUserHolder
组件,用于从请求的 Header 中直接获取登录用户信息
/** * 获取登录用户信息 */@Componentpublic class LoginUserHolder{ public UserDto getCurrentUser(HttpServletRequest request) { String userStr = request.getHeader(AuthConstant.USER_TOKEN_HEADER); JSONObject userJsonObject = new JSONObject(userStr); UserDto userDTO = new UserDto(); userDTO.setUserName(userJsonObject.getStr("user_name")); userDTO.setClientId(userJsonObject.getStr("client_id")); userDTO.setRoles(Convert.toList(String.class, userJsonObject.get(AuthConstant.AUTHORITY_CLAIM_NAME))); return userDTO; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 创建一个获取当前用户信息的接口
@RestController@RequestMapping("/user")public class UserController{ @Autowired private LoginUserHolder loginUserHolder; @GetMapping("/currentUser") public UserDTO currentUser() { return loginUserHolder.getCurrentUser(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
功能演示
接下来来演示下微服务系统中的统一认证鉴权功能,所有请求均通过网关访问
-
启动 Nacos 和 Redis 服务
-
启动
security-oauth2-auth
、security-oauth2-gateway
及security-oauth2-api
服务
- 使用密码模式获取 JWT 令牌,POST 访问地址:http://localhost:9201/auth/oauth/token
- 使用获取到的JWT令牌访问获取当前登录用户信息的接口,访问地址:http://localhost:9201/admin/user/currentUser
- 当 JWT 令牌过期时,使用 refresh_token 获取新的 JWT令牌,访问地址:http://localhost:9201/auth/oauth/token