文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

OAuth2 详细介绍!

2023-09-15 14:21

关注

目录

一、文章介绍

二、OAth2

2.1 简介

2.2 OAuth2  授权总体流程

2.3 四种授权模式

1.授权码模式

2.简化模式

3.密码模式

4. 客户端模式

2.4 OAuth2 标准接口

2.5 GitHub 授权登录

1.创建 OAuth应用

 2.项目开发

3.原理分析

3.原理总结

三、Spring Security OAuth2

3.1 授权、资源服务器

1.基于内存授权服务器搭建

2 基于数据库客户端和令牌存储

3.资源服务器搭建

四、JWT的应用

4.1 授权服务器颁发 JWT 令牌

4.2.使用 JWT 令牌资源服务器


一、文章介绍

如今很多互联网应用中,OAuth2 是一个非常重要的认证协议,很多场景下都会用到它,Spring Security 对 OAuth2 协议提供了相应的支持。开发者非常方便的使用 OAuth2 协议

二、OAth2

2.1 简介

        OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源 (如头像、照片、视频等),并且在这个过程中无须将用户名和密码提供给第三方应用。通过令牌 (token) 可以实现这一功能。每一个令牌授权一个特定的网站在特定的时间段内允许可访问特定的资源。OAuth 让用户可以授权第三方网站灵活访问它们存储在另外一些资源服务器上的特定信息,而非所有的内容。对于用户而言,我们在互联网应用中最常见的 OAuth 应用就是各种第三方登录,例如 QQ授权登录、微信授权登录、微博授权登录、GitHub 授权登录等。

        例如用户想登录 Ruby China ,传统方式是使用用户名和密码但是这样并不安全,因为网站会存储你的用户名密码,这样可能会导致密码泄露。这种授权方式安全隐患很大如果使用 OAuth 协议就能很好地解决这一问题。

        oAuth2协议解决了多个网站登录问题,账号密码不安全的问题,比如一些小众的网站就可以不用注册登录,使用 oAuth2,也就是通过第三方向要访问的网站发送请求获取Token,第三方网站每次请求写到Token就可以访问到内容。

472c41a595f137154dc248ff0382b383.png

注意:OAuth2 是 OAuth 协议的下一个版本,但不兼容 OAth 1.0 ,OAth2 关注客户端开发者的简易性,同时为 Web 应用、桌面应用、移动设备、 IOT 设备提供专门的认证流程。

以上大致流程,角色不完善。

        oAuth2就是对用户的信息进步的保护,如很多喜欢将所有密码设置为同样的,就可能会泄露,但是通过已经注册过的网站用户信息,来进行授权给第三方网站信息进行登录则免去了注册,oAuth主要做的就是认证保护用户隐私安全

2.2 OAuth2  授权总体流程

角色梳理 :第三方应用 <--------> 存储用户私密信息应用 ----------> 授权服务器 ------> 资源服务器

整体流程如下:(图片来自 RFC6749 文档 https://tools.ietf.org/html/rfc6749)

#                                            官网流程 
- (A) 用户打开客户端以后,客户端要求用户给予授权。
- (B) 用户同意给予客户端授权。
- (C) 客户端使用上一步获得的授权,向认证服务器申请令牌。
- (D) 认证服务器对用户端进行认证以后,确认无误,同意发放令牌。
- (E) 客户端使用令牌,向资源服务器申请资源。
- (F) 资源服务器确认令牌无误,同意向客户端开放资源。

  #                                  例子流程
  - 1.用户打开第三方网站如 (京东),用户点击了微信授权登录,此时 就会跳转到 微信的授权页面。
  - 2.用户点击授权给京东后,进行授权认证,授权成功会进行回调到京东回调页面。
  - 3.授权页面会发起请求向授权服务器索要授权令牌。
  - 4.授权服务器将授权令牌进行返回,用户此时可以在第三方网站(京东)向 微信服务器携带令牌获取部分用户信息 。
  - 5.用户此时可以在第三方网站(京东)向 微信服务器携带令牌获取部分用户信息 。
  - 6.资源服务器将资源返回给第三方网站

从上图中我们可以看出六个步骤之中,B是关键,即用户怎么才能给于客户端授权。同时会发现 OAuth2 中包含四种不同角色:

授权服务器和资源服务器可以放一起,但是在如今的互联网和分布的推动下,都是分别存储。

2.3 四种授权模式

OAuth2 协议一种支持四种不同的授权模式

  1. 授权码模式:常见的第三方平台登录功能基本都是使用这种模式。(安全性高)
  2. 简化模式:简化模式是不需要第三方服务端(客户端)参与,直接在浏览器中向授权服务器申请令牌(token),如果网站是纯静态页面,则可以采用这种方式。
  3. 密码模式:密码模式是用户把用户名/密码直接告诉客户端,客户端使用在这些信息项授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务器提供商就是同一家公司。
  4. 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向授权服务器提供申请授权。严格来说,客户端模式并不能算作 OAuth 协议解决问题的一种解决方案,但是对于开发者而言,在一些为移动端提供的授权服务器上使用这种模式还是非常方便的。

用的最多的就是 最多的就是授权码模式

无论那种授权模式,其授权流程都是相似的,只不过在个别步骤上有差异而已。如上图所示。

1.授权码模式

        授权码模式(Authorization Code ) 是功能最完整,流程最严密、最全并且使用最广泛的一种 OAuth2 授权模式。同时也是最复杂的一种授权模式,它的特点就是通过客户端的后台服务器,与服务器提供商的认证服务器进行交互。其具体授权流程如下

(图片来自 RFC6749 文档 https://tools.ietf.org/html/rfc6749)

#                                             授权流程 
- AB 两步在进行获取授权码
- 1.用户点击授权登录,通过浏览器,会先到Github授权服务器
- 2.授权服务器此时会有一张页面是否允许授权登录
- 3.允许后才是把客户端信息发送给 授权服务器.
- 4.授权服务根据客户端密钥,授权模式等进行认证,成功后会颁发一个对象数据返回,如授权码模式就会颁发一个授权码给浏览器,最终浏览器跟重定向会我们的客户端。
- 5.客户端会再一次发送请求带着授权码以及重定向的URI再次想授权服务器获取令牌
- 6.授权服务器根据授权码进行比较,如果成功将返回一个令牌。

注意:授权码是一次性的。认证服务器和授权服务器是可以放在一起

主要分为两部:1.客户端向授权服务器索要授权码,2.获取授权码后再次向授权服务器携带授权码索要令牌。

在最后,拿到令牌后,再拿着令牌向资源服务器索要资源。

具体流程如下:

核心参数:

https://wx.com/oauth/authorize?response_type=code&client_id=CLENT_Id&redirect_uri=http://www.baidu.com&scope=read

2.简化模式

简化模式(implicitgrant type ) 不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了 "授权码" 这个步骤,因此得名。所有步骤在浏览器中完成,令牌访问者是可见的,而客户端不需要认证。其具体的授权流程如图所示

(图片来自 RFC6749 文档 http://tools.ietf.org/html/rfc6749)

这种模式令牌解析比较都在浏览器不是在安全,spring sercuirty 不支持。

核心参数:

 https://wx.com/auth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=http://www.baidu.com&scope=read

3.密码模式

        密码模式(Resource Owner Password Credentials Grant) 中,用户向客户端提供自己的用户名和密码,客户端使用这些信息,向” 服务提供商“ 索要授权。这种模式中,用户必现把自己的密码给客户端,但是客户端不得存储密码,这通常在用户对客户端高度信任的清下,比如客户端是操作系统的一部分,或者由一个相同公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。其具体授权流程图所示

(图片来自 RFC6749 https://tools.ieft.org/html/rfc6749)

具体步骤如下:

核心参数:

https://wx/com/token?grant_type=password&username=Username&password=PASSWORD&client_id =CLIENT_ID

4. 客户端模式

        客户端模式(Client Credentials Grant) 指客户端以自己的名义,而不是用户的名义,向 ”服务提供商“进行认证,严格地说,客户端模式并不属于 OAuth框架索要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求 ”服务提供商“ 提供服务,其实不存在授权问题。

步骤如下:

https://wx.com/token?grant_type=client_credentials&client_idCLIENT_ID&client_secret=CLEINT_SECRET

这种方式没有授权的过程,是直接那种密钥和客户端编号向要授权服务器去要。这种适合于有凭证信息,有某种协议,直接要直接颁发令牌。

2.4 OAuth2 标准接口

以上是规范化固定接口 ,端口就是路径

2.5 GitHub 授权登录

        

该项目是实现创建一个应用,调用第三方Github方式进行认证操作 。

主要流程:

1.创建 OAuth应用

访问 github 并登录,在 Sign in to GitHub · GitHub 中找到Developer settings 选项

认证成功后会生成 Client_id 和secret 在授权登录时需要使用到

 2.项目开发

1.创建 SpringBoot 应用 ,并引入依赖

                        org.springframework.boot            spring-boot-starter-oauth2-client                                    org.springframework.boot            spring-boot-starter-security                            org.springframework.boot            spring-boot-starter-web        

2.创建测试 Controller

@RestControllerpublic class IndexController {    @GetMapping("/getOAuthInfo")    public DefaultOAuth2User getOAuthInfo(){                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();        return (DefaultOAuth2User) authentication.getPrincipal();    }}

3.配置 Security 配置类

@Configurationpublic class SecurityWebConfig extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                .anyRequest().authenticated()                .and()                .formLogin().and() // 支持 oAuth2 和其他方式同时开启认证方式                 .oauth2Login(); // 使用 oAuth2 认证 需要在配置文件中配置认证服务    }}

4.配置配置文件

# 注册客户端编号spring.security.oauth2.client.registration.github.client-id=178210b433b87f612163# 注册客户端密钥spring.security.oauth2.client.registration.github.client-secret=9cf77bbbf0b164113bc0554d5130f65adbe60b37# 授权成功的重定向密钥spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github

可以指定如下信息

5.代码测试

点击 GitHub登录后会跳转到 GitHub的授权页面 ,携带必要的参数

https://github.com/login/oauth/authorize?response_type=code&client_id=178210b433b87f612163&scope=read:user&state=pBk0kcLrcD98l0mTD-ACDYFUIOhL9iYuVp248BJqZdQ=&redirect_uri=http://localhost:8080/login/oauth2/code/github 

 

总结

        如果向让自己开发的应用使用oAuth2 进行授权,首先第一点需要在第三方应用注册 获取到clientId 和 client secret,第二点创建 SpringBoot 应用 添加 spring-security-client 依赖,客户端依赖并配置授权登录认证方式即可。

3.原理分析

        众所周知,Spring Security 的底层实现是通过大量的Filter 实现的,那么OAuth2的实现也离不开 Filter。

OAuth2LoginAuthenticationFilter

处理 OAuth2 认证的

NO

OAuth2AuthorizationCodeGrantFilter

处理OAuth2认证中授权码

NO

1.oauth2Login 源码分析

package org.springframework.security.config.annotation.web.configurers.oauth2.client;import java.util.ArrayList;import java.util.Collections;import java.util.HashMap;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import org.springframework.beans.factory.BeanFactoryUtils;import org.springframework.beans.factory.NoUniqueBeanDefinitionException;import org.springframework.context.ApplicationContext;import org.springframework.core.ResolvableType;import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.web.HttpSecurityBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;import org.springframework.security.oauth2.client.registration.ClientRegistration;import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;import org.springframework.security.oauth2.client.userinfo.CustomUserTypesOAuth2UserService;import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;import org.springframework.security.oauth2.client.userinfo.DelegatingOAuth2UserService;import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;import org.springframework.security.oauth2.core.OAuth2AuthenticationException;import org.springframework.security.oauth2.core.OAuth2Error;import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;import org.springframework.security.oauth2.core.oidc.OidcScopes;import org.springframework.security.oauth2.core.oidc.user.OidcUser;import org.springframework.security.oauth2.core.user.OAuth2User;import org.springframework.security.oauth2.jwt.JwtDecoderFactory;import org.springframework.security.web.AuthenticationEntryPoint;import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;import org.springframework.security.web.savedrequest.RequestCache;import org.springframework.security.web.util.matcher.AndRequestMatcher;import org.springframework.security.web.util.matcher.AntPathRequestMatcher;import org.springframework.security.web.util.matcher.NegatedRequestMatcher;import org.springframework.security.web.util.matcher.OrRequestMatcher;import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;import org.springframework.security.web.util.matcher.RequestMatcher;import org.springframework.util.Assert;import org.springframework.util.ClassUtils;public final class OAuth2LoginConfigurer> extendsAbstractAuthenticationFilterConfigurer, OAuth2LoginAuthenticationFilter> {private final AuthorizationEndpointConfig authorizationEndpointConfig = new AuthorizationEndpointConfig();private final TokenEndpointConfig tokenEndpointConfig = new TokenEndpointConfig();private final RedirectionEndpointConfig redirectionEndpointConfig = new RedirectionEndpointConfig();private final UserInfoEndpointConfig userInfoEndpointConfig = new UserInfoEndpointConfig();private String loginPage;private String loginProcessingUrl = OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;public OAuth2LoginConfigurer clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);return this;}public OAuth2LoginConfigurer authorizedClientRepository(OAuth2AuthorizedClientRepository authorizedClientRepository) {Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository);return this;}public OAuth2LoginConfigurer authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) {Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");this.authorizedClientRepository(new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService));return this;}@Overridepublic OAuth2LoginConfigurer loginPage(String loginPage) {Assert.hasText(loginPage, "loginPage cannot be empty");this.loginPage = loginPage;return this;}@Overridepublic OAuth2LoginConfigurer loginProcessingUrl(String loginProcessingUrl) {Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");this.loginProcessingUrl = loginProcessingUrl;return this;}public AuthorizationEndpointConfig authorizationEndpoint() {return this.authorizationEndpointConfig;}public OAuth2LoginConfigurer authorizationEndpoint(Customizer authorizationEndpointCustomizer) {authorizationEndpointCustomizer.customize(this.authorizationEndpointConfig);return this;}public class AuthorizationEndpointConfig {private String authorizationRequestBaseUri;private OAuth2AuthorizationRequestResolver authorizationRequestResolver;private AuthorizationRequestRepository authorizationRequestRepository;private AuthorizationEndpointConfig() {}public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) {Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty");this.authorizationRequestBaseUri = authorizationRequestBaseUri;return this;}public AuthorizationEndpointConfig authorizationRequestResolver(OAuth2AuthorizationRequestResolver authorizationRequestResolver) {Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null");this.authorizationRequestResolver = authorizationRequestResolver;return this;}public AuthorizationEndpointConfig authorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) {Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");this.authorizationRequestRepository = authorizationRequestRepository;return this;}public OAuth2LoginConfigurer and() {return OAuth2LoginConfigurer.this;}}public TokenEndpointConfig tokenEndpoint() {return this.tokenEndpointConfig;}public OAuth2LoginConfigurer tokenEndpoint(Customizer tokenEndpointCustomizer) {tokenEndpointCustomizer.customize(this.tokenEndpointConfig);return this;}public class TokenEndpointConfig {private OAuth2AccessTokenResponseClient accessTokenResponseClient;private TokenEndpointConfig() {}public TokenEndpointConfig accessTokenResponseClient(OAuth2AccessTokenResponseClient accessTokenResponseClient) {Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");this.accessTokenResponseClient = accessTokenResponseClient;return this;}public OAuth2LoginConfigurer and() {return OAuth2LoginConfigurer.this;}}public RedirectionEndpointConfig redirectionEndpoint() {return this.redirectionEndpointConfig;}public OAuth2LoginConfigurer redirectionEndpoint(Customizer redirectionEndpointCustomizer) {redirectionEndpointCustomizer.customize(this.redirectionEndpointConfig);return this;}public class RedirectionEndpointConfig {private String authorizationResponseBaseUri;private RedirectionEndpointConfig() {}public RedirectionEndpointConfig baseUri(String authorizationResponseBaseUri) {Assert.hasText(authorizationResponseBaseUri, "authorizationResponseBaseUri cannot be empty");this.authorizationResponseBaseUri = authorizationResponseBaseUri;return this;}public OAuth2LoginConfigurer and() {return OAuth2LoginConfigurer.this;}}public UserInfoEndpointConfig userInfoEndpoint() {return this.userInfoEndpointConfig;}public OAuth2LoginConfigurer userInfoEndpoint(Customizer userInfoEndpointCustomizer) {userInfoEndpointCustomizer.customize(this.userInfoEndpointConfig);return this;}public class UserInfoEndpointConfig {private OAuth2UserService userService;private OAuth2UserService oidcUserService;private Map> customUserTypes = new HashMap<>();private UserInfoEndpointConfig() {}public UserInfoEndpointConfig userService(OAuth2UserService userService) {Assert.notNull(userService, "userService cannot be null");this.userService = userService;return this;}public UserInfoEndpointConfig oidcUserService(OAuth2UserService oidcUserService) {Assert.notNull(oidcUserService, "oidcUserService cannot be null");this.oidcUserService = oidcUserService;return this;}public UserInfoEndpointConfig customUserType(Class customUserType, String clientRegistrationId) {Assert.notNull(customUserType, "customUserType cannot be null");Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");this.customUserTypes.put(clientRegistrationId, customUserType);return this;}public UserInfoEndpointConfig userAuthoritiesMapper(GrantedAuthoritiesMapper userAuthoritiesMapper) {Assert.notNull(userAuthoritiesMapper, "userAuthoritiesMapper cannot be null");OAuth2LoginConfigurer.this.getBuilder().setSharedObject(GrantedAuthoritiesMapper.class, userAuthoritiesMapper);return this;}public OAuth2LoginConfigurer and() {return OAuth2LoginConfigurer.this;}}@Overridepublic void init(B http) throws Exception {OAuth2LoginAuthenticationFilter authenticationFilter =new OAuth2LoginAuthenticationFilter(OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()),this.loginProcessingUrl);this.setAuthenticationFilter(authenticationFilter);super.loginProcessingUrl(this.loginProcessingUrl);if (this.loginPage != null) {// Set custom login pagesuper.loginPage(this.loginPage);super.init(http);} else {Map loginUrlToClientName = this.getLoginLinks();if (loginUrlToClientName.size() == 1) {// Setup auto-redirect to provider login page// when only 1 client is configuredthis.updateAuthenticationDefaults();this.updateAccessDefaults(http);String providerLoginPage = loginUrlToClientName.keySet().iterator().next();this.registerAuthenticationEntryPoint(http, this.getLoginEntryPoint(http, providerLoginPage));} else {super.init(http);}}OAuth2AccessTokenResponseClient accessTokenResponseClient =this.tokenEndpointConfig.accessTokenResponseClient;if (accessTokenResponseClient == null) {accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();}OAuth2UserService oauth2UserService = getOAuth2UserService();OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider =new OAuth2LoginAuthenticationProvider(accessTokenResponseClient, oauth2UserService);GrantedAuthoritiesMapper userAuthoritiesMapper = this.getGrantedAuthoritiesMapper();if (userAuthoritiesMapper != null) {oauth2LoginAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);}http.authenticationProvider(this.postProcess(oauth2LoginAuthenticationProvider));boolean oidcAuthenticationProviderEnabled = ClassUtils.isPresent("org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader());if (oidcAuthenticationProviderEnabled) {OAuth2UserService oidcUserService = getOidcUserService();OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider =new OidcAuthorizationCodeAuthenticationProvider(accessTokenResponseClient, oidcUserService);JwtDecoderFactory jwtDecoderFactory = this.getJwtDecoderFactoryBean();if (jwtDecoderFactory != null) {oidcAuthorizationCodeAuthenticationProvider.setJwtDecoderFactory(jwtDecoderFactory);}if (userAuthoritiesMapper != null) {oidcAuthorizationCodeAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);}http.authenticationProvider(this.postProcess(oidcAuthorizationCodeAuthenticationProvider));} else {http.authenticationProvider(new OidcAuthenticationRequestChecker());}this.initDefaultLoginFilter(http);}@Overridepublic void configure(B http) throws Exception {OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter;if (this.authorizationEndpointConfig.authorizationRequestResolver != null) {authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(this.authorizationEndpointConfig.authorizationRequestResolver);} else {String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri;if (authorizationRequestBaseUri == null) {authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;}authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri);}if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {authorizationRequestFilter.setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);}RequestCache requestCache = http.getSharedObject(RequestCache.class);if (requestCache != null) {authorizationRequestFilter.setRequestCache(requestCache);}http.addFilter(this.postProcess(authorizationRequestFilter));OAuth2LoginAuthenticationFilter authenticationFilter = this.getAuthenticationFilter();if (this.redirectionEndpointConfig.authorizationResponseBaseUri != null) {authenticationFilter.setFilterProcessesUrl(this.redirectionEndpointConfig.authorizationResponseBaseUri);}if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {authenticationFilter.setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);}super.configure(http);}@Overrideprotected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {return new AntPathRequestMatcher(loginProcessingUrl);}@SuppressWarnings("unchecked")private JwtDecoderFactory getJwtDecoderFactoryBean() {ResolvableType type = ResolvableType.forClassWithGenerics(JwtDecoderFactory.class, ClientRegistration.class);String[] names = this.getBuilder().getSharedObject(ApplicationContext.class).getBeanNamesForType(type);if (names.length > 1) {throw new NoUniqueBeanDefinitionException(type, names);}if (names.length == 1) {return (JwtDecoderFactory) this.getBuilder().getSharedObject(ApplicationContext.class).getBean(names[0]);}return null;}private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() {GrantedAuthoritiesMapper grantedAuthoritiesMapper =this.getBuilder().getSharedObject(GrantedAuthoritiesMapper.class);if (grantedAuthoritiesMapper == null) {grantedAuthoritiesMapper = this.getGrantedAuthoritiesMapperBean();if (grantedAuthoritiesMapper != null) {this.getBuilder().setSharedObject(GrantedAuthoritiesMapper.class, grantedAuthoritiesMapper);}}return grantedAuthoritiesMapper;}private GrantedAuthoritiesMapper getGrantedAuthoritiesMapperBean() {Map grantedAuthoritiesMapperMap =BeanFactoryUtils.beansOfTypeIncludingAncestors(this.getBuilder().getSharedObject(ApplicationContext.class),GrantedAuthoritiesMapper.class);return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null);}private OAuth2UserService getOidcUserService() {if (this.userInfoEndpointConfig.oidcUserService != null) {return this.userInfoEndpointConfig.oidcUserService;}ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2UserService.class, OidcUserRequest.class, OidcUser.class);OAuth2UserService bean = getBeanOrNull(type);if (bean == null) {return new OidcUserService();}return bean;}private OAuth2UserService getOAuth2UserService() {if (this.userInfoEndpointConfig.userService != null) {return this.userInfoEndpointConfig.userService;}ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2UserService.class, OAuth2UserRequest.class, OAuth2User.class);OAuth2UserService bean = getBeanOrNull(type);if (bean == null) {if (!this.userInfoEndpointConfig.customUserTypes.isEmpty()) {List> userServices = new ArrayList<>();userServices.add(new CustomUserTypesOAuth2UserService(this.userInfoEndpointConfig.customUserTypes));userServices.add(new DefaultOAuth2UserService());return new DelegatingOAuth2UserService<>(userServices);} else {return new DefaultOAuth2UserService();}}return bean;}private  T getBeanOrNull(ResolvableType type) {ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);if (context == null) {return null;}String[] names =  context.getBeanNamesForType(type);if (names.length == 1) {return (T) context.getBean(names[0]);}return null;}private void initDefaultLoginFilter(B http) {DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class);if (loginPageGeneratingFilter == null || this.isCustomLoginPage()) {return;}loginPageGeneratingFilter.setOauth2LoginEnabled(true);loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(this.getLoginLinks());loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage());loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl());}@SuppressWarnings("unchecked")private Map getLoginLinks() {Iterable clientRegistrations = null;ClientRegistrationRepository clientRegistrationRepository =OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder());ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {clientRegistrations = (Iterable) clientRegistrationRepository;}if (clientRegistrations == null) {return Collections.emptyMap();}String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri != null ?this.authorizationEndpointConfig.authorizationRequestBaseUri :OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;Map loginUrlToClientName = new HashMap<>();clientRegistrations.forEach(registration -> loginUrlToClientName.put(authorizationRequestBaseUri + "/" + registration.getRegistrationId(),registration.getClientName()));return loginUrlToClientName;}private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLoginPage) {RequestMatcher loginPageMatcher = new AntPathRequestMatcher(this.getLoginPage());RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico");RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(http);RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher(new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);RequestMatcher notXRequestedWith = new NegatedRequestMatcher(new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));LinkedHashMap entryPoints = new LinkedHashMap<>();entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)),new LoginUrlAuthenticationEntryPoint(providerLoginPage));DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint());return loginEntryPoint;}private static class OidcAuthenticationRequestChecker implements AuthenticationProvider {@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {OAuth2LoginAuthenticationToken authorizationCodeAuthentication =(OAuth2LoginAuthenticationToken) authentication;// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest// scope// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.if (authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)) {OAuth2Error oauth2Error = new OAuth2Error("oidc_provider_not_configured","An OpenID Connect Authentication Provider has not been configured. " +"Check to ensure you include the dependency 'spring-security-oauth2-jose'.",null);throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}return null;}@Overridepublic boolean supports(Class authentication) {return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);}}}

1.编写 oauth2Login() 方法后 ,HttpSecurity 底层 会创建 OAuth2LoginConfigurer 配置类,并且将他应用。

b560f30e1d16131d3c90eb742ae16c96.png

2. OAuth2LoginConfigurer 这个类中它的继承Filter 类型就是 OAuth2LoginAuthenticationFilter ,用于做OAuth2 认证的Filter 。

8bd40dddc746240802464583a9ead0b8.png

3.通过clientRegistrationRepository 获取到注册的第三方Provider 客户端信息, 默认是基于内存的

8b493276da6a8a13ffb48656ebee44ef.png

4.通过 authorizedClientRepository 生成授权成功之后的存储授权信息的库

5.在这里里还会进行添加一些其他所需要的参数信息,进行装配。最终认证的方法在 OAuth2LoginAuthenticationFilter类中。


2.OAuth2LoginAuthenticationFilter 源码分析

        OAuth 2.0登录AbstractAuthenticationProcessingFilter的实现。 此身份验证筛选器处理授权代码授予流的OAuth 2.0授权响应,并将OAuth2LoginAuthenticationToken委托给AuthentizationManager以登录最终用户。 OAuth 2.0授权响应的处理方式如下: 假设最终用户(资源所有者)已授予客户端访问权限,授权服务器将把代码和状态参数附加到redirect_uri(在授权请求中提供),并将最终用户的用户代理重定向回该筛选器(客户端)。 然后,此筛选器将使用收到的代码创建OAuth2LoginAuthenticationToken,并将其委托给AuthentizationManager进行身份验证。 成功身份验证后,将创建OAuth2AuthenticationToken(代表最终用户主体),并使用OAuth2AauthorizedClientRepository与授权客户端关联。

        以上是 OAuth2LoginAuthenticationFilter 的介绍 ,它是实现 OAuth2 的Filter ,他会将验证成功的结果 OAuth2LoginAuthenticationToken 交给 AuthentizationManager 进行处理 。

1.默认定义好了授权成功后回调的URI路径

2.核心认证方法 attemptAuthentication 在进行认证授权和授权码验证

@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException {MultiValueMap params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}OAuth2AuthorizationRequest authorizationRequest =this.authorizationRequestRepository.removeAuthorizationRequest(request, response);if (authorizationRequest == null) {OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);if (clientRegistration == null) {OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,"Client Registration not found with Id: " + registrationId, null);throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)).replaceQuery(null).build().toUriString();OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));authenticationRequest.setDetails(authenticationDetails);OAuth2LoginAuthenticationToken authenticationResult =(OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(authenticationResult.getPrincipal(),authenticationResult.getAuthorities(),authenticationResult.getClientRegistration().getRegistrationId());oauth2Authentication.setDetails(authenticationDetails);OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(authenticationResult.getClientRegistration(),oauth2Authentication.getName(),authenticationResult.getAccessToken(),authenticationResult.getRefreshToken());this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);return oauth2Authentication;}

3.首先获取到请求参数的 code 和state 值

4.验证请求参数是否有效 验证参数中是否 code 和state 并验证是否有值 。

1921f7ca3c5d2d3cd750e8b39bfc9f48.png

5.移除之前授权信息,并将授权信息返回

40a43203ee525d270ba43840ce747524.png

6.判断授权客户端信息是否null

f255113efaef6c7de2dc7aa9180b441a.png

7.获取当前注册的授权服务器类型

8.根据注册类型从缓存 InMemoryClientRegistrationRepository 中获取当前注册过的授权服务器信息

9.拼装重定向URI

10.将uri和参数进行转成,获取授权码信息

11.创建 OAuth2LoginAuthenticationToken

12.将封装好的 OAuth2LoginAuthenticationToken 交给 AuthenticationManager 进行认证。认证是有OAuth2LoginAuthenticationProvider 进行认证 调用 authenticate方法,在 authenticate ;里会调用 OAuth2AuthorizationCodeAuthenticationProviderauthenticate 进行验证码认证。

c9997cd5026ed65261b928bab9b5309d.png

OAuth2LoginAuthenticationProvider 的 authenticate

@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {OAuth2LoginAuthenticationToken loginAuthenticationToken =(OAuth2LoginAuthenticationToken) authentication;// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest// scope// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains("openid")) {// This is an OpenID Connect Authentication Request so return null// and let OidcAuthorizationCodeAuthenticationProvider handle it insteadreturn null;}OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;try {authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(loginAuthenticationToken.getClientRegistration(),loginAuthenticationToken.getAuthorizationExchange()));} catch (OAuth2AuthorizationException ex) {OAuth2Error oauth2Error = ex.getError();throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();Map additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));Collection mappedAuthorities =this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(loginAuthenticationToken.getClientRegistration(),loginAuthenticationToken.getAuthorizationExchange(),oauth2User,mappedAuthorities,accessToken,authorizationCodeAuthenticationToken.getRefreshToken());authenticationResult.setDetails(loginAuthenticationToken.getDetails());return authenticationResult;}

OAuth2AuthorizationCodeAuthenticationProvider 的 authenticate

@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =(OAuth2AuthorizationCodeAuthenticationToken) authentication;OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationResponse();if (authorizationResponse.statusError()) {throw new OAuth2AuthorizationException(authorizationResponse.getError());}OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest();if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);throw new OAuth2AuthorizationException(oauth2Error);}OAuth2AccessTokenResponse accessTokenResponse =this.accessTokenResponseClient.getTokenResponse(new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),authorizationCodeAuthentication.getAuthorizationExchange()));OAuth2AuthorizationCodeAuthenticationToken authenticationResult =new OAuth2AuthorizationCodeAuthenticationToken(authorizationCodeAuthentication.getClientRegistration(),authorizationCodeAuthentication.getAuthorizationExchange(),accessTokenResponse.getAccessToken(),accessTokenResponse.getRefreshToken(),accessTokenResponse.getAdditionalParameters());authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());return authenticationResult;}

13.最终他将获取的数据进行存储,并Authentication 进行封装成进行返回 ,不管什么类型授权方式最后转化为的对象都是相同的

82f21d084189af695712a641b25feef7.png

3.原理总结

        通过原理分析得知,当使用OAuth2 进行认证时,如果是授权码模式,则是 授权携带客户端信息向第三方应用发送请求跳转授权页面,如果用户允许授权,则会获取授权码,获取授权码后,跳转到重定向页面,此时浏览器会再次发送请求获取令牌。

        由此得出,自定义的第三方应用是在获取的授权码后,进行验证授权码的合法性,在向第三方服务器发送请求获取令牌的方式。

三、Spring Security OAuth2

        Spring Security 对 OAuth2 提供了很好的支持,这使得我们在 Spring Security 中使用 OAuth2 非常地方便,然后由于历史原因,Spring Security 对 OAuth2 的支持比较混乱,这里简单梳理一下。

        大概十年前,Spring 引入一个社区驱动的开源项目 Spring Security OAuth ,并将其纳入 Spring 项目组合中。到今天为止,这个项目已经发展为一个成熟的项目,可以支持大部分的OAuth 规范,包括资源服务器,客户端和授权服务器等 。

然而早期的项目存在一些问题,例如:

    • OAuth 是在早前完成的,开发者无法预料未来的变化以及这些代码到底要被怎么使用。这导致很多 Spring 项目提供了自己的 OAuth 支持,也就带来了 OAuth 支持的碎片化。
    • 最早的 OAuth 项目同时支持 OAuth1.0 和 OAuth 2.0,而现在 OAuth1.0 早已经不再使用,可以放弃了。
    • 现在我们有更多的库可以选择,可以在这些库的基础上去开发,以便更好地支持JWT 等新技术。

                基于以上这些原因,官方决定重写 Spring Security OAuth ,以便更好地协调 Spring 和 Auth ,并简化代码库,使 Spring 的 OAuth 支持更加灵活。然后,在重写的过程中,发生了不少波折。

2018年1月30日,Spring 官方发一个通知,表示要逐渐停止现有的 OAuth2支持 ,然后在 Spring Security 5 中构建下一代 OAuth2.0 支持。这么做的原因是因为当时 OAuth2 的落地方案比较混乱,在 Spring Security OAuth 、Spring Colud Security 、Spring Boot 1.5.x 以及当时最新的Spring Security 5.x 中都提供了对 OAuth2 的实现。以至于当开发者需要使用 OAuth2 时,不得不问,到底那个依赖合适呢?

        所以 Spring 官方决定有必要将 OAuth2.0 的支持统一到一个项目中,以便为用户提供明确的选择,并避免任何潜在的混乱,同时 OAuth2.0 的开发文档也要重新编写,以方便开发人员学习。所有的决定在 Spring Security 5 中开始,构建下一代 OAuth2.0的支持、从哪个时候起,Spring Security OAuth 项目就正式处于维护模式。官方将提供至少一年的错识/安全修复程序,并且会考虑添加次要功能,但不会添加主要功能。同时将 Spring Security OAuth 中的所有功能重构到 Spring Security 5.x 中 。

        到了2019年11月14日,Spring 官方又发布一个通知,这次的通知首先表示 Spring Security OAuth 在迁往 Spring Security 5.x 的过程非常顺利,大多分迁工程工作已经完成了,剩下的将在5.3 版本中完成迁移,在迁移的过程中添加许多新功能。包括对 OpenID Connection1.0 支持 。同时还宣布将不在支持授权服务器,不支持的原因有两个:

  1. 在2019年,已经有大量的商业和开源授权服务器可用
  2. 授权服务器是使用一个库来构建产品,而 Spring Security 作为框架,并不适合做这件事情

        一石激起千层浪,许多开发者表示对此难以接受。这件事在 Spring 社区引起了激烈的讨论,好在 Spring 官方愿意倾听来自社区的声音。

        到了2020年4月15日,Spring 官方宣布启动 Spring Authorization server 项目 。这是一个由 Spring Security 团队领导的社区驱动的项目,致力于向 Spring 社区提供 Authorization Server 支持,也就是说,Spring 又重新支持授权服务器了。

        2020年8月21日,Spring uthorization Server 0.0.1 正式发布!

        这就是 OAuth2 在 Spring 家族中的发展历程了。在后面的学习中,客户端和资源服务器都将采用最新的方式来构建,授权服务器依然采用旧的方式来构建。因为目前的 Spring Authentication Server 0.0.1 功能较少且 BUG 较多 。

        一般来说,当我们在项目中使用 OAuth2 时,都是开发客户端,授权服务器和资源服务器由外部提供。例如我们想在自己搭建网站上集成 GitHub 第三方登录,只需要开发自己客户端即可,认证服务器和授权服务器由 GitHub 提供的 。        

3.1 授权、资源服务器

前面的 GitHub 授权登录主要向大家展示了 OAuth2 中客户端的工作模式。对于大部分的开发者而言,日常接触到的 OAuth2 都是客户端,例如接入 QQ登录 、接入微信登录等。不过也有少量场景,可能需要开发者提供授权服务器与资源服务器,接下来我们就通过一个完整的案例演示如何搭建授权服务器与资源服务器。

搭建授权服务器,我们可以选择一些线程的开源项目,直接运行即可,例如 :

    • Keycloak :RedFat 公司提供的开源工具,提供了很多实用功能,例如单点登录、支持OpenID、可视化后台管理等。
    • Apache Oltu:Apache 上的开源项目,最近几年没怎么维护了。

接下来我们将搭建一个包含授权服务器、资源服务器以及客户端在内的 OAuth2 案例。

    • 授权服务器:采用较早的 spring-cloud-starter-oauth2 来搭建授权服务器。
    • 资源服务器:采用最新的 Spring Security 5.x 搭建资源服务器。
    • 客户端:采用最新的 Spring Security 5.x 搭建客户端。

注意:Spring Boot 的版本不能过高 ,因为 Spring-cloud-starter-oatuh2 的版本不能过高 。

1.基于内存授权服务器搭建

做授权服务器的话,

1.需要提供一个授权登录的功能

2.注册功能,其他服务器向授权服务器注册的功

1.1 基于内存客户端和令牌存储

        创建 SpringBoot 应用 ,并引入依赖

    2.2.5.RELEASE                                    org.springframework.boot            spring-boot-starter-web                                    org.springframework.cloud            spring-cloud-starter-oauth2            2.2.5.RELEASE                                    org.springframework.boot            spring-boot-starter-security        

编写配置类 ,添加 security 配置类以及 oAuth 配置类

package com.bjpowenrode.security.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {    @Bean    public PasswordEncoder passwordEncoder (){        return  new BCryptPasswordEncoder();    }        @Override    @Bean    public UserDetailsService userDetailsService (){        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();        inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder().encode("123")).roles("admin").build());        return inMemoryUserDetailsManager ;    }    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth.userDetailsService(userDetailsService());    }    @Bean    @Override // 将内容 authorizationManager 暴露    public AuthenticationManager authenticationManagerBean() throws Exception {        return super.authenticationManagerBean();    }    @Override    public void configure(WebSecurity web) throws Exception {        super.configure(web);    }        @Override    protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                .anyRequest().authenticated()                .and()                .formLogin()                .and()                .csrf().disable() ;    }}
package com.bjpowenrode.oauth.config;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;@Configuration@EnableAuthorizationServer // 启用授权服务器public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    private final PasswordEncoder passwordEncoder ;    private final UserDetailsService userDetailsService ;        private final AuthenticationManager authenticationManager ;    public AuthorizationServerConfig(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService, AuthenticationManager authenticationManager) {        this.passwordEncoder = passwordEncoder;        this.userDetailsService = userDetailsService;        this.authenticationManager = authenticationManager;    }        @Override    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {            clients                    .inMemory() // 表示基于内存                    .withClient("clientId" ) // 设置客户端编号                    .secret(passwordEncoder.encode("secret")) // 设置客户端密钥 ,这里官方规定secret必现加密 可以使用 passwordEncode                    .redirectUris("http://www.baidu.com") // 重定向URI                    // authorization_code 授权码   refresh_token 刷新令牌  implicit 简化模式   password 密码模式  client_credentials 客户端模式                    // 暂不支持简化模式, 因为需要解析一段脚本来获取令牌                    .authorizedGrantTypes("authorization_code","refresh_token","implicit","password","client_credentials") // 表示使用那种授权模式 可以设置支持多种,授权码模式                    .scopes("read:user");// 令牌允许获取资源权限    }    // 授权码这种模式:    // 1.请求用户是否授权 /oauth/authorize    // 完整路径 : http://localhost:8080/oauth/authorize?client_id=clientId&response_type=code&redirect_uri=http://www.baidu.com    // 2.授权之后根据获取的授权码获取令牌 /oauth/token 参数 : client_id secret redirect_uri code    // 完整路径 :curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=2qewq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"    // 3.支持令牌刷新 /oauth/token 参数 id secret 授权类型 :refresh_token 刷新的令牌:refresh_token    // 完整路径  :curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=refresh_token&refresh_token=2qewq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"        @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        endpoints.userDetailsService(userDetailsService); // 开启刷新令牌必现指定        endpoints.authenticationManager(authenticationManager); // 密码模式需要注入 authenticationManager    }}

启动服务,登录之后进行授权获取

1.先登录

2.访问授权路径 获取授权码

http://localhost:8080/oauth/authorize?client_id=clientId&response_type=code&redirect_uri=http://www.baidu.com

3.允许授权后会从客户端授权码,并跳转到重定向URI

4.根据授权码,申请令牌

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d ' grant_type=authorization_code&code=IWvwq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"

注意授权码是一次性的

PostMian请求 方法:

使用介绍:Spring Cloud OAuth2中访问/oauth/token报401 Unauthorized问题的解决(POSTMAN) - mangoubiubiu - 博客园

路径:http://client:secret@localhost:8080/oauth/token

参数:

  • grant_type:授权类型,填写authorization_code,表示授权码模式
  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • client_id:客户端标识
  • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
  • scope:授权范围。

还需要在Authorzation 写上你配的client_id 和 client-secret

 

5.刷新令牌

首先需要开启了刷新令牌功能

刷新令牌的路径和获取令牌的路径是一样的

支持令牌刷新 /oauth/token 参数 id secret 授权类型 :refresh_token 刷新的令牌:refresh_token

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=refresh_token&refresh_token=2qewq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"

Postmain 使用

6.简化模式

7.密码模式

8.密码模式刷新令牌

9.客户端模式获取令牌

2 基于数据库客户端和令牌存储

将客户端信息和令牌信息转为数据库存储

在上面的案例中,TokenStore 的默认实现 为 InMemoryTokenStore 即内存存储,对于 Client 信息,ClientDetailsService 接口负责从存储仓库中读取数据,在上面的案例中默认使用的也是 InMemoryClientDetailsService 实现类

如果要想使用数据库存储,只要提供这些接口的实现类即可,而框架已经为我们写好 JdbcTokenStoreJdbcDetailsService

建表:

https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

# 注意:并用BLOB 替换语句中的 LONGVARBINARY 类型-- used in tests that use HSQLcreate table oauth_client_details (  client_id VARCHAR(256) PRIMARY KEY,  resource_ids VARCHAR(256),  client_secret VARCHAR(256),  scope VARCHAR(256),  authorized_grant_types VARCHAR(256),  web_server_redirect_uri VARCHAR(256),  authorities VARCHAR(256),  access_token_validity INTEGER,  refresh_token_validity INTEGER,  additional_information VARCHAR(4096),  autoapprove VARCHAR(256));create table oauth_client_token (  token_id VARCHAR(256),  token BLOB,  authentication_id VARCHAR(256) PRIMARY KEY,  user_name VARCHAR(256),  client_id VARCHAR(256));create table oauth_access_token (  token_id VARCHAR(256),  token BLOB,  authentication_id VARCHAR(256) PRIMARY KEY,  user_name VARCHAR(256),  client_id VARCHAR(256),  authentication BLOB,  refresh_token VARCHAR(256));create table oauth_refresh_token (  token_id VARCHAR(256),  token BLOB,  authentication BLOB);create table oauth_code (  code VARCHAR(256), authentication LONGVARBINARY);create table oauth_approvals (userId VARCHAR(256),clientId VARCHAR(256),scope VARCHAR(256),status VARCHAR(10),expiresAt TIMESTAMP,lastModifiedAt TIMESTAMP);-- customized oauth_client_details tablecreate table ClientDetails (  appId VARCHAR(256) PRIMARY KEY,  resourceIds VARCHAR(256),  appSecret VARCHAR(256),  scope VARCHAR(256),  grantTypes VARCHAR(256),  redirectUrl VARCHAR(256),  authorities VARCHAR(256),  access_token_validity INTEGER,  refresh_token_validity INTEGER,  additionalInformation VARCHAR(4096),  autoApproveScopes VARCHAR(256));-- 写入客户端信息INSERT INTO `oauth_client_details` VALUES ('clientId', '', '$2a$10$rvtuV.bPeFA5zfowgr9OJuQSj07AFJfHl5Y7QND0thykzlB7S8hW6', 'read', 'authorization_code,refresh_token', 'http://www.baidu.com', NULL, NULL, NULL, NULL, NULL);

主要用到表:

oauth_client_details : 客户端信息 oauth_client_details 的 auhoapprove 是否自动授权

oauth_client_token :存放令牌信息

1.引入依赖

                        mysql            mysql-connector-java                            org.springframework.boot            spring-boot-starter-jdbc        

2.编写配置文件

# 添加数据库配置spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/oauth?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=falsespring.datasource.username=rootspring.datasource.password=admin

3.编写数据库信息实现

package com.bjpowenrode.oauth.config;import jdk.nashorn.internal.parser.Token;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth2.provider.ClientDetailsService;import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;import org.springframework.security.oauth2.provider.token.DefaultTokenServices;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;import javax.sql.DataSource;import java.util.concurrent.TimeUnit;@Configuration@EnableAuthorizationServerpublic class JdbcAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    private final AuthenticationManager authenticationManager ;    private final PasswordEncoder passwordEncoder ;    private final DataSource dataSource ;    public JdbcAuthorizationServerConfig(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, DataSource dataSource) {        this.authenticationManager = authenticationManager;        this.passwordEncoder = passwordEncoder;        this.dataSource = dataSource;    }        @Bean    public TokenStore tokenStore (){        return  new JdbcTokenStore(dataSource);    }        @Bean    public ClientDetailsService detailsService (){        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder); // 用于加密和解密  密钥        return jdbcClientDetailsService;    }        @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        endpoints.authenticationManager(authenticationManager) ; // 认证管理器        endpoints.tokenStore(tokenStore()); // 配置令牌存储为数据库存储        // 配置 TokenService 参数        DefaultTokenServices tokenServices = new DefaultTokenServices(); // 修改默认生成令牌服务        tokenServices.setTokenStore(endpoints.getTokenStore()); // 基于数据库令牌生成        tokenServices.setSupportRefreshToken(true); // 是否支持刷新令牌        tokenServices.setReuseRefreshToken(true); // 是否支持重复使用刷新令牌 (直到过期)        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());// 设置客户端信息        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); // 用来控制令牌存储增强策略        // 访问令牌的默认有效 (以秒为单位) 。过期的令牌为零或负数。        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)) ;// 30天        // 刷新令牌的有效性(以秒为单位)。如果小于或等于零,则令牌将不会过期        tokenServices.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(3));// 3天        endpoints.tokenServices(tokenServices); // 使用配置令牌服务    }    @Override    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {        clients.withClientDetails(detailsService()); // 使用JDBC存储    }}

4.启动测试,发现数据库中已经存储相关令牌。

 

5.总结

        切换方式主要是通过 TokenStore 它是一个接口有多种实现方式

3.资源服务器搭建

        资源服务器不仅需要引入资源服务器依赖还需要引入授权服务器依赖,因为资源服务器需要认证请求所携带的令牌,令牌验证可以通过 http请求或者两者基于同一个数据库查询方式,或者使用JWT的方式,两者使用相同的 secret

        资源服务器接受到请求后首先需要进行令牌校验,当客户端拿着颁发的额令牌向资源服务器索要资源时,资源服务器就需要验证令牌,验证令牌可以通过http请求向授权访问器校验,但这样不是百分百能获取的,还有一种方式是通过授权服务器和资源服务器共享同一个数据库,数据库中存放这令牌信息资源服务器直接根据数据库进行校验就可以了,可以使用redis解决分布式的方式,也就是黑板模式,引入授权服的依赖就是为了访问数据库进行令牌机制.

1.引入依赖

    4.0.0    com.bjpowernode    ch022-spring-security-oauth2-resource-server    0.0.1-SNAPSHOT    ch022-spring-security-oauth2-resource-server    ch022-spring-security-oauth2-resource-server            1.8        UTF-8        UTF-8        2.3.7.RELEASE        Hoxton.SR9                                    org.springframework.boot            spring-boot-starter-web                                    org.springframework.cloud            spring-cloud-starter-oauth2                                    org.springframework.security            spring-security-oauth2-resource-server                                    org.springframework.boot            spring-boot-starter-security                                    mysql            mysql-connector-java                                    org.springframework.boot            spring-boot-starter-jdbc                            org.springframework.boot            spring-boot-starter-test            test                                                org.junit.vintage                    junit-vintage-engine                                                                                org.springframework.cloud                spring-cloud-dependencies                ${spring-cloud.version}                pom                import                                        org.springframework.boot                spring-boot-dependencies                ${spring-boot.version}                pom                import                                                                org.apache.maven.plugins                maven-compiler-plugin                3.8.1                                    1.8                    1.8                    UTF-8                                                        org.springframework.boot                spring-boot-maven-plugin                2.3.7.RELEASE                                    com.bjpowernode.Ch022SpringSecurityOauth2ResourceServerApplication                                                                            repackage                        repackage                                                                                    

2.创建资源

package com.bjpowernode.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class IndexController {    @GetMapping("/hello")    public String hello(){        return "hello!";    }}

3.编写资源服务器配置类

package com.bjpowernode.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;import javax.sql.DataSource;@Configuration@EnableResourceServer // 启动资源服务器public class ResourceServer extends ResourceServerConfigurerAdapter {    private final DataSource dataSource;    public ResourceServer(DataSource dataSource) {        this.dataSource = dataSource;    }        @Bean    public TokenStore tokenStore (){        return new JdbcTokenStore(dataSource);    }    @Override    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {        resources.tokenStore(tokenStore());    }}

4.编写配置文件

# 应用名称spring.application.name=ch022-spring-security-oauth2-resource-server# 应用服务 WEB 访问端口server.port=8081# 添加数据库配置spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/oauth?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=falsespring.datasource.username=rootspring.datasource.password=admin

5.启动测试,生成令牌之后带有令牌访问:

curl -H "Authorization:Bearer deff--213-21312-213jk213-1231j2k" http://localhost:8081/hello

测试时,需要在 请求头中携带 Authorization:Bearer deff--213-21312-213jk213-1231j2k

四、JWT的应用

将授权服务器和资源服务器的类型令牌类型都替换为JWT的方式,这种方式效率最高。

资源服务器验证授权令牌常见的两种方式都有弊端:

1.通过http请求向授权服务器方式请求认证,可能会受到网络的问题,

2.通过将令牌存放到数据库或redis中,资源服务器和授权服务器使用同一个,这样如果分布式分库就会有问题’

3.所以最好是使用jwt ,使用jwt的好处,就是jwt有自己一套规范,授权办法的jwt令牌,资源服务器只需要使用相对的jwt规范进行验证就行

jwt生成是三部分,最核心的就是密钥,它是根据密钥进行生成和解析的,所有授权服务器和认证服务器的密钥要是相同的

4.1 授权服务器颁发 JWT 令牌

1.配置颁发 JWT 令牌

JWT header.payload.sin + 秘钥OPSTATA jwt+Resource ServerOPSATA

package com.bjpowenrode.oauth.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.jwt.Jwt;import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth2.provider.ClientDetailsService;import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;import org.springframework.security.oauth2.provider.token.DefaultTokenServices;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;import javax.sql.DataSource;import java.util.concurrent.TimeUnit;@Configuration@EnableAuthorizationServerpublic class JWTAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    private final AuthenticationManager authenticationManager ;    private final PasswordEncoder passwordEncoder ;    private final DataSource dataSource ;    public JWTAuthorizationServerConfig(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, DataSource dataSource) {        this.authenticationManager = authenticationManager;        this.passwordEncoder = passwordEncoder;        this.dataSource = dataSource;    }        @Bean    public TokenStore tokenStore (){        return  new JwtTokenStore(jwtAccessTokenConverter());    }        @Bean    public ClientDetailsService detailsService (){        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder); // 用于加密和解密  密钥        return jdbcClientDetailsService;    }        @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        endpoints.tokenStore(tokenStore())                .accessTokenConverter(jwtAccessTokenConverter())                .authenticationManager(authenticationManager);    }    // 使用同一个密钥来编码 JWT OAuth2 令牌    @Bean    public JwtAccessTokenConverter jwtAccessTokenConverter (){        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();        jwtAccessTokenConverter.setSigningKey("123"); // 可以采用属性注入方式,生产中建议加密 ,密钥的key 很重要        return jwtAccessTokenConverter;    }    @Override    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {        clients.withClientDetails(detailsService()); // 使用JDBC存储    }}

2.启动服务,根据授权码获取令牌

4.2.使用 JWT 令牌资源服务器

1.配置资源服务器解析 jwt

@Configuration@EnableResourceServer // 启动资源服务器public class JWTResourceServer extends ResourceServerConfigurerAdapter {    private final DataSource dataSource;    public JWTResourceServer(DataSource dataSource) {        this.dataSource = dataSource;    }        @Bean    public TokenStore tokenStore (){        return new JwtTokenStore(jwtAccessTokenConverter());    }    // 使用同一个密钥来编码 JWT OAuth2 令牌    @Bean    public JwtAccessTokenConverter jwtAccessTokenConverter (){        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();        jwtAccessTokenConverter.setSigningKey("123"); // 可以采用属性注入方式,生产中建议加密 ,密钥的key 很重要        return jwtAccessTokenConverter;    }    @Override    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {        resources.tokenStore(tokenStore());    }}

2.启动测试,通过jwt测试访问资源

curl -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjM4MjA5MTYsInVzZXJfbmFtZSI6InJvb3QiLCJhdXRob3JpdGllcyI6WyJST0xFX2FkbWluIl0sImp0aSI6IjQyYmNiMDIzLTJmZTAtNDk1My05Y2Q3LWExNmVlNGU1N2RkMyIsImNsaWVudF9pZCI6ImNsaWVudElkIiwic2NvcGUiOlsicmVhZCJdfQ._wMSXarEFbtEjZmXEzv2qfPni4cpiIm4krXWLy6c_-Y" http://localhost:8081/hello

来源地址:https://blog.csdn.net/weixin_52834606/article/details/126995138

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯