crm开发定制「springcloud 2021 系列」Spring Cloud Gateway + OAuth2 + JWT 实现统一认证与鉴权

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-authsecurity-oauth2-gatewaysecurity-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

参考

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发