定制开发前后端分离Oauth2.0 - springsecurity + spring-authorization-server —授权码模式

序言

  1. 定制开发对于目前有很多的公司在对旧的web定制开发工程做重构,拆分服务,使用前端vue,后端springboot微服务,定制开发重构的要点之一是认证定制开发授权框架的选型。
  2. 定制开发对于原有的 spring-security-oauth 定制开发已经宣布不在进行维护,其已经被 + 所提供的oauth2.1定制开发支持所取代。
  3. 定制开发文章将介绍 spring-authorization-server 支持的 oauth2.1 ,springboot 整合 springsecurity + spring-authorization-server,对 oauth2.1 定制开发的授权码模式做实践案例,定制开发展示实践结果,定制开发过程中遇到的问题。
  4. 定制开发注意文章不在介绍 springboot + springsecurity 的整合,定制开发整合文章在csdn定制开发中有很多实践案例,定制开发可以网上搜索。

思路:

实践流程

  1. 访问 https://www.authorization.life , 将跳转到 https://www.authorization.life/login 登录页面。
  2. 定制开发输入用户名密码,用户名:qjyn1314@163.com 密码:admin
  3. 定制开发在登录接口请求成功,状态是200,定制开发在前端直接请求 /oauth2/authorize 接口,请求路径:https://www.authorization.life/auth-life/oauth2/authorize?response_type=code&client_id=passport&scope=TENANT&state=authorization-life&redirect_uri=https://www.authorization.life/login/home
  4. client定制开发信息验证通过之后,定制开发将跳转到配置的client定制开发信息中的重定向地址(redirectUri)中: https://www.authorization.life/login/home?code=gyLKC_d06yIPo-69hbKuVOFfFjps3F-EPRbAwilmQZPYO0TBkY2GORjhyZ1CXxeUeeC8d5rHY8g8j3Wykhiv_T17P-QYsbFDWvBzJcvfKk0oF8Z8Nj_CgLhSLFiIskL4&state=authorization-life
  5. login-front定制开发前端工程中的 home 定制开发页面中做一些操作,通过 网址中的 code 请求 /oauth2/token 接口 ,获取自定义的 jwt形式的 accessToken,然后将其保存到cookie中,为下一次请求接口使用。

nginx配置

#user  nobody;worker_processes  auto;events {    worker_connections  1024;}http {    include       mime.types;    default_type  application/octet-stream;		# nginx: [emerg] could not build server_names_hash, you should increase server_names_hash_bucket_size: 32   解决此错误需要增加下两行配置		server_names_hash_max_size 			2048;# 【值为域名长度总和】;		server_names_hash_bucket_size 	2048;# 【上升值】		# /sockjs-node 访问异常->		# 参考:https://blog.csdn.net/qq27229639/article/details/103069055		#       https://www.ancii.com/anbgjpemb		map $http_upgrade $connection_upgrade {    		default upgrade;    		''      close;    }    sendfile        on;    keepalive_timeout  65;        server {        listen       80;        server_name  www.authorization.life;        rewrite ^(.*)$ https://$server_name$1 permanent;    }        # HTTPS server 参考配置: https://nenufm.com/archives/g    server {        listen       443 ssl;        server_name  www.authorization.life;        ssl_certificate      D://authorization_life_aliyun//authorization_life_chain.pem;        ssl_certificate_key  D://authorization_life_aliyun//authorization_life_key.key;		    # ssl验证相关配置		    ssl_protocols 			TLSv1.3 SSLv3; #安全链接可选的加密协议        	ssl_ciphers         EECDH+AESGCM:EDH+AESGCM;       	 	ssl_ecdh_curve 			secp384r1; #为ECDHE密码指定 SEPO384Q1		    ssl_session_timeout  10m;    #缓存有效期        	ssl_session_cache   shared:SSL:10m;			ssl_prefer_server_ciphers   on;     		ssl_session_tickets         off; # Requires nginx >= 1.5.9    		ssl_stapling                on; # Requires nginx >= 1.3.7    		ssl_stapling_verify         on; # Requires nginx => 1.3.7		#后端服务gateway        location / {		        proxy_pass http://127.0.0.1:9000;        }        # 前端登录工程        location /login {		        proxy_pass http://127.0.0.1:8080;		        proxy_http_version 1.1;    				proxy_set_header Upgrade $http_upgrade;    				proxy_set_header Connection $connection_upgrade;        }    }}
  • 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

数据库

官方提供的三张表

  • oauth2_registered_client(oauth2.0中的客户端信息) ————使用自定义的 lifetime_oauth_client 表。
  • oauth2_authorization_consent (oauth2.0中的授权同意信息)————使用redis进行存储数据信息。
  • oauth2_authorization (oauth2.0中的当前登录用户信息)————使用redis进行存储数据信息。
  • lifetime_user 是系统的用户表。
  • lifetime_user_group 是用户组,用户组可以是平台系统中延伸的子系统,或者授权信息,授权域。

框架依赖版本

        <!--        由于 Spring Authorization Server 需要 Java 8 或更高版本的运行时环境,此次将直接使用 jdk17             查看版本依赖关系:            spring-cloud 官网:  https://spring.io/projects/spring-cloud            spring-cloud-alibaba 官网:  https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E            springboot 官网: https://spring.io/projects/spring-boot            2022年9月4日            得出结果:            boot:2.7.3            cloud:2021.0.3            cloud-alibaba:2.2.8.RELEASE            spring-security:5.7.3            authorization-server:0.3.1        -->        <!--基本依赖-->        <spring-boot.version>2.7.3</spring-boot.version>        <spring-cloud.version>2021.0.3</spring-cloud.version>        <spring-cloud-alibaba.version>2.2.8.RELEASE</spring-cloud-alibaba.version>        <jackson-bom.version>2.13.3</jackson-bom.version>        <!--权限认证依赖-->        <spring-security.version>5.7.3</spring-security.version>        <nimbus-jose-jwt.version>9.23</nimbus-jose-jwt.version>        <oauth2-oidc-sdk.version>9.27</oauth2-oidc-sdk.version>        <oauth2-authorization-server.version>0.3.1</oauth2-authorization-server.version>-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --             <dependency>                <groupId>com.nimbusds</groupId>                <artifactId>oauth2-oidc-sdk</artifactId>                <version>${oauth2-oidc-sdk.version}</version>            </dependency>            <dependency>                <groupId>com.nimbusds</groupId>                <artifactId>nimbus-jose-jwt</artifactId>                <version>${nimbus-jose-jwt.version}</version>            </dependency>            <dependency>                <groupId>org.springframework.security</groupId>                <artifactId>spring-security-oauth2-authorization-server</artifactId>                <version>${oauth2-authorization-server.version}</version>            </dependency>            <!--以上为security+oauth2-->            <dependency>                <groupId>com.fasterxml.jackson</groupId>                <artifactId>jackson-bom</artifactId>                <version>${jackson-bom.version}</version>                <type>pom</type>            </dependency>            <dependency>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-dependencies</artifactId>                <version>${spring-boot.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <dependency>                <groupId>org.springframework.cloud</groupId>                <artifactId>spring-cloud-dependencies</artifactId>                <version>${spring-cloud.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <dependency>                <groupId>com.alibaba.cloud</groupId>                <artifactId>spring-cloud-alibaba-dependencies</artifactId>                <version>${spring-cloud-alibaba.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <dependency>                <groupId>org.springframework.security</groupId>                <artifactId>spring-security-bom</artifactId>                <version>${spring-security.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <!--以上为基础的导入依赖-->
  • 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

集成

配置 OAuth2AuthorizationServerConfigurer

目的是为了将 oauth2.0的配置托管给 SpringSecurity,为了让 SpringSecurity对特定的路径在filter中进行拦截,convert转换参数并进行provider的校验。
    @Bean    @Order(Ordered.HIGHEST_PRECEDENCE)    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,                                                                      OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer) throws Exception {        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();        // 配置请求拦截        http.requestMatcher(endpointsMatcher)                .authorizeRequests(authorizeRequests -> authorizeRequests//                        // 无需认证即可访问                        .antMatchers(SecurityConstant.IGNORE_PERM_URLS).permitAll()                        //除以上的请求之外,都需要token                        .anyRequest().authenticated())                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))                //配置formLogin                .formLogin(Customizer.withDefaults())                //将oauth2.0的配置托管给 SpringSecurity                .apply(authorizationServerConfigurer);        // 设置accesstoken为jwt形式        http.setSharedObject(OAuth2TokenCustomizer.class, oAuth2TokenCustomizer);        // 配置 异常处理        http                .exceptionHandling()                //当未登录的情况下 该如何跳转。                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint());        return http.build();    }
  • 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

配置 RegisteredClientService

此处将使用自定义的 lifetime_oauth_client 中的信息进行封装并返回,主要重写了 findById 、findByClientId方法,将通过前端传参中的clientid作为查询条件从数据库中进行查询数据。
    /**     * 根据数据库中的client信息转换     *     * @param clientId    clientId     * @param oauthClient 数据库client     * @return RegisteredClient     */    private RegisteredClient getRegisteredClient(String clientId, OauthClient oauthClient) {        RegisteredClient.Builder builder = RegisteredClient.withId(clientId)                .clientId(oauthClient.getClientId())                .clientSecret(oauthClient.getClientSecret())                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)                .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)                .redirectUri(oauthClient.getRedirectUri())                // JWT的配置项 包括TTL  是否复用refreshToken等等                .clientSettings(ClientSettings.builder()                        //是否需要用户确认一下客户端需要获取用户的哪些权限                        .requireAuthorizationConsent(false)                        .build())                .tokenSettings(TokenSettings.builder()                        //配置使用自定义的jwtToken格式化,配置此处才会使用到 CustomizerOAuth2Token , 或者不配置此格式化的配置,将默认生成jwt的形式                        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)                        //是否可重用刷新令牌                        .reuseRefreshTokens(true)                        //accessToken 的有效期  单位:秒                        .accessTokenTimeToLive(Duration.of(oauthClient.getAccessTokenTimeout(), ChronoUnit.SECONDS))                        //refreshToken 的有效期   单位:秒                        .refreshTokenTimeToLive(Duration.of(oauthClient.getRefreshTokenTimeout(), ChronoUnit.SECONDS))                        .build());        //批量设置当前的授权类型        Arrays.stream(oauthClient.getGrantTypes().split(StrPool.COMMA))                .map(grantType -> {                    if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.AUTHORIZATION_CODE.getValue())) {                        return AuthorizationGrantType.AUTHORIZATION_CODE;                    } else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.REFRESH_TOKEN.getValue())) {                        return AuthorizationGrantType.REFRESH_TOKEN;                    } else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())) {                        return AuthorizationGrantType.CLIENT_CREDENTIALS;                    } else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.PASSWORD.getValue())) {                        return AuthorizationGrantType.PASSWORD;                    } else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.JWT_BEARER.getValue())) {                        return AuthorizationGrantType.JWT_BEARER;                    } else {                        throw new RegClientException("不支持的授权模式, [" + grantType + "]");                    }                }).forEach(builder::authorizationGrantType);        Arrays.stream(oauthClient.getScopes().split(StrPool.COMMA))                .forEach(builder::scope);        return builder.build();    }    /**     * 注册client     *     * @param clientService 自定义的client端信息     * @return RegisteredClientRepository     */    @Bean    public RegisteredClientRepository registeredClientRepository(OauthClientService clientService) {        return new RegisteredClientService(clientService);    }
  • 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

配置 OAuth2AuthorizationService 、RedisOAuth2AuthorizationConsentService

保存授权信息,由这个服务来保存,存储到redis中。从save到获取其中都是参考了官网中的jdbc的示例,OAuth2AuthorizationService的另一实现,针对不同的token类型做存储。

在此处将不再赘述。可以在文章结尾查看源码。

配置自定义access_token的实现

  • 继承 OAuth2TokenCustomizer 重写 customize(JwtEncodingContext context) 方法,可以将accessToken的信息存储到redis中,为之后的验证和获取做准备。
  • OAuth2Authorization authorization = context.getAuthorization(); 中的信息是当前登录用户的信息。
  • 工程中实现的是,重新自定义了token,把一些用户的主要信息存放到redis中,并把自定义的token信息放到了当前登录用户中,方便在退出登录时删除redis中的信息。

重点之一

对于access_token,需要哪些信息,将掌控到什么程度,在这里具体的体现,由开发人员指定,为系统中更敏捷的开发做准备。

    /**     * JWT的加密算法,说明:https://www.rfc-editor.org/rfc/rfc7515     *     * @return JWKSource     */    @Bean    public JWKSource<SecurityContext> jwkSource() {        RSAKey rsaKey = Jwks.generateRsa();        JWKSet jwkSet = new JWKSet(rsaKey);        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);    }    /**     * 将重写jwtToken的中的信息,并将其存储到redis中。     *     * @param context JwtEncodingContext     */    @Override    public void customize(JwtEncodingContext context) {        //此处的token字符串是前端拿到的jwtToken信息中解密后的字符串,在这里将自定义jwtToken的实现,将定制jwt的 header 和 claims,将此token存放到 claim 中        String token = UUID.randomUUID().toString(true);        Authentication principal = context.getPrincipal();        Authentication authorizationGrant = context.getAuthorizationGrant();        OAuth2Authorization authorization = context.getAuthorization();        Set<String> authorizedScopes = context.getAuthorizedScopes();        ProviderContext providerContext = context.getProviderContext();        RegisteredClient registeredClient = context.getRegisteredClient();        log.info("principal-{}", JSONUtil.toJsonStr(principal));        log.info("authorization-{}", JSONUtil.toJsonStr(authorization));        log.info("authorizedScopes-{}", JSONUtil.toJsonStr(authorizedScopes));        log.info("authorizationGrant-{}", JSONUtil.toJsonStr(authorizationGrant));        log.info("providerContext-{}", JSONUtil.toJsonStr(providerContext));        log.info("registeredClient-{}", JSONUtil.toJsonStr(registeredClient));        UserDetail userDetail = null;        // 目的是为了定制jwt 的header 和 claims        if (principal instanceof OAuth2ClientAuthenticationToken) {            //如果当前登录的是client,则进行封装client//            userDetail = securityAuthUserService.createUserDetailByClientId(registeredClient.getClientId());        }//        else if (principal.getPrincipal() instanceof UserDetail) {//            //如果当前登录的是系统用户,则进行封装userDetail//            userDetail = securityAuthUserService.createUserDetailByUser((UserDetails) principal.getPrincipal());//        }        else if (principal.getPrincipal() instanceof User) {            //如果当前登录的是系统用户,则进行封装userDetail            userDetail = securityAuthUserService.createUserDetailByUser((User) principal.getPrincipal());        }        //如果解析失败,则抛出异常信息。        if (Objects.isNull(userDetail)) {            log.error("在自定义token实现中, 用户信息解析异常。");            userDetail = new UserDetail();        }        //也需要将此token存放到当前登录用户中,为了在退出登录时进行获取redis中的信息并将其删除        userDetail.setToken(token);        //将用户信息放置到redis中,并设置其过期时间为 client中的过期时间        strRedisHelper.strSet(LifeSecurityConstants.getUserTokenKey(token), userDetail,                registeredClient.getTokenSettings().getAccessTokenTimeToLive().getSeconds(), TimeUnit.SECONDS);        log.info("生成的用户-token是-{},此token作为key,用户信息作为value存储到redis中", token);        //也可以在此处将当前登录用户的信息存放到jwt中,但是这样就不再安全。        context.getClaims().claim(LifeSecurityConstants.TOKEN, token).build();    }
  • 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

配置退出登录处理器 SsoLogoutHandle

重写 logout 方法,对 当前登录用户中的token进行删除,并返回 退出登录成功提示。

    @Override    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {        log.info("进入退出登录处理器。");        response.setStatus(HttpStatus.OK.value());        response.setContentType(MediaType.APPLICATION_JSON_VALUE);        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());        try {            UserDetail userDetail = UserHelper.getUserDetail();            log.debug("当前登录用户-UserDetail-是:" + userDetail);            if (Objects.nonNull(userDetail)) {                String userToken = userDetail.getToken();                log.debug("当前登录用户的token-是:" + userToken);                String cacheUserToken = KvpFormat.of(SecurityConstant.USER_DETAIL).add("token", userToken).format();                redisHelper.delete(cacheUserToken);                redisHelper.delete(KvpFormat.of(SecurityConstant.TOKEN_STORE).add("userId", userDetail.getUserId().toString()).format());            }            SecurityContextHolder.clearContext();            String token = request.getHeader(HttpHeaders.AUTHORIZATION);            log.debug("请求头-Authorization-是:" + token);            if (StrUtil.isBlank(token)) {                PrintWriter out = response.getWriter();                out.write(JSONUtil.toJsonStr(new Res<>(Res.ERROR, "未找到token,请确认已登录。", null)));                out.flush();                out.close();            }            token = token.split(" ")[1];            OAuth2Authorization auth2Authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);            log.debug("查询出来-OAuth2Authorization-是:" + JSONUtil.toJsonStr(auth2Authorization));            if (Objects.nonNull(auth2Authorization)) {                oAuth2AuthorizationService.remove(auth2Authorization);            }            PrintWriter out = response.getWriter();            out.write(JSONUtil.toJsonStr(new Res<>(Res.SUCCESS, Res.SUCCESS_DESC, null)));            out.flush();            out.close();        } catch (IOException e) {            log.error("退出登录处理器处理失败,", e);        }    }
  • 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

配置RouterFunction

此处的配置将指定了 springsecurity中的自定义登录页面,对于 RouterFunction 可自行网上查询。

重点之二

对于中如何指定登录页面,如何跳转到登录页面,这个有很长一段时间由于知识的不足而不知如何处理,此配置将解决此问题,nginx中配置 / 代理 gwteway网关工程, 当访问根目录时将重定向到 /login 路径上,nginx中对 /login 路径做代理到 前端工程中, 其 /login 所指向的前端工程就是 自定义的登录页面。

@Slf4j@Configurationpublic class RouterFunctionConfig {    /**     * 访问根目录时将重定向到登录模块     *     * @return 登录模块     */    @Bean    public RouterFunction<ServerResponse> loginRouterFunction() {        return RouterFunctions.route(                RequestPredicates.GET("/"),                request -> ServerResponse.temporaryRedirect(URI.create(request.uri() + "login")).build());    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

配置 JwtTokenGatewayFilterFactory

将解析 /oauth/token 接口中返回的accessToken信息,获取其中的 token字段对应的redis存储的用户信息,并验证是否能解析,在转换为用户信息的之后,将对请求路径做校验,判断是否拥有此权限。

重点之三

此处是gateway中做的accessToken解析,但是路由后的服务可能是不知道的,这个时候,我们需要对此用户信息再次做一层转换,转换为Jwt形式的token,在公共的core模块中对此token进行解析,并放置到 SecurityContextHolder.getContext()中。

// JwtTokenGatewayFilterFactory     @Override    public GatewayFilter apply(Object config) {        return (exchange, chain) -> {            ServerHttpRequest request = exchange.getRequest();            String token = getToken(request);            // 获取当前用户的信息            String userDetailStr = StrUtil.isBlank(token) ? null : redisHelper.strGet(LifeSecurityConstants.getUserTokenKey(token));            // 若jwt不存在,则封入一个空字符串,到权限拦截器处理。因为有些api是不需要登录的,故在此不处理。            UserDetail userDetail = StrUtil.isNotBlank(userDetailStr) ? JsonHelper.readValue(userDetailStr, UserDetail.class) : null;            userDetailStr = Optional.ofNullable(userDetailStr).orElse(StrUtil.EMPTY);            // 创建JWS对象            JWSObject jwsObject = new JWSObject(jwsHeader, new Payload(userDetailStr));            // 签名并序列化转换为真正存储用户信息的jwtToken            String jwtToken = Jwts.signAndSerialize(jwsObject, signer);            ServerWebExchange jwtExchange = exchange.mutate()                    .request(request.mutate()                            .header(Jwts.HEADER_JWT, jwtToken).build())                    .build();            return chain.filter(jwtExchange)                    .contextWrite(ctx -> ctx.put(RequestContext.CTX_KEY,                            ctx.<RequestContext>getOrEmpty(RequestContext.CTX_KEY)                                    .orElse(new RequestContext())                                    .setUserDetail(userDetail)));        };    }// JwtAuthenticationFilter    @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {        SecurityContext context = SecurityContextHolder.getContext();        Authentication authentication = context.getAuthentication();        log.info("authentication-{}",JSONUtil.toJsonStr(authentication));        log.info("请求路径是-{}", JSONUtil.toJsonStr(request.getRequestURI()));        String jwt = request.getHeader(Jwts.HEADER_JWT);        log.info("进入到-JwtAuthenticationFilter-过滤器-jwtToken-{}", jwt);        if (StrUtil.isBlank(jwt)) {            chain.doFilter(request, response);            return;        }        JWSObject jwsObject = Jwts.parse(jwt);        if (!Jwts.verify(jwsObject, verifier)) {            log.error("Jwt verify failed! JWT: [{}]", jwt);            chain.doFilter(request, response);            return;        }        UserDetail userDetail = jwsObject.getPayload().toType(payload -> StrUtil.isBlank(payload.toString()) ?                UserDetail.anonymous() : JsonHelper.readValue(payload.toString(), UserDetail.class));        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetail, null, null);        SecurityContextHolder.getContext().setAuthentication(authenticationToken);        chain.doFilter(request, response);    }
  • 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

到此处 springboot + springsecurity + spring-authorization-server 整合完毕.

遇到的问题

前端的 /sockjs-node 访问异常

解决参考:
https://blog.csdn.net/qq27229639/article/details/103069055
https://www.ancii.com/anbgjpemb

配置RouterFunction

需要放置到一个 单独的 @Configuration 配置类中,不然会不生效的。

实践结果:

Github :

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