Spring Cloud之OAuth2

阅读更多
备:附件中OAuth2 授权服务器实现源码及PPT
一、Authorization code grant

Spring Cloud之OAuth2_第1张图片
 
 
The flow illustrated in Figure 1 includes the following steps:
  • (A) The client (typically, a web application) initiates the flow by directing the
  • resource owner's user agent (typically, a web browser) to the authorization
  • endpoint. The client's request includes the client identifier, requested scope,
  • local state, and a redirection URI. The authorization server directs the user
  • agent (typically, a web browser) back to the redirect URI after the access is
  • granted (or denied).
  • (B) The resource owner authenticates with the authorization server through
  • the user agent and grants or denies the client's access request.
  • (C) If the resource owner grants access, the authorization server redirects the
  • user agent (typically, a web browser) back to the client using the redirection URI
  • provided earlier (in the request or during client registration). The redirection URI
  • includes an authorization code and any local state provided by the client earlier.
  • (D) The client makes an access token request from the authorization server's token
  • endpoint by including the authorization code received in the previous step. When
  • making the request, the client authenticates with the authorization server using
  • the client credentials. The client also includes the redirection URI used to obtain
  • the authorization code for verification.
  • (E) The authorization server authenticates the client. It validates the authorization
  • code and ensures that the redirection URI received matches the URI used to redirect
  • the client in step (C). If valid, the authorization server responds back with an access
  • token and, optionally, a refresh token in case an offline access was requested.
Authorization code request
The authorization code request corresponds to steps (A) and (B) as described in Figure
1. In step (A), the client makes a request to the authorization server in the 
application/x-www-form-urlencoded  format, as shown in Listing 1.
Listing 1. Example of an authorization code request
1
2
3
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
The request must contain the following parameters:
  • response_type: Required. The value must be set to code.
  • client_id: Required. The client ID.
  • redirect_uri: Required. Used for user agent redirection.
  • scope: Optional. The scope of the access request.
  • state: Optional. To maintain the state between the request and callback.
After the request is verified by the authorization server, the server sends an HTTP
redirect code 302 response back to the client. The response will also include a redirection
URI in the http  Location  header. In step (B), the client must redirect the user agent
(typically, a web browser) to this URI. This redirection URI is usually a login page
where the resource owner can sign in with their credentials and grant/revoke access to the client's request.
Authorization code response
The authorization code response is shown in step (C) of Figure 1. If the resource
owner grants the access request, the authorization server issues an authorization
code. The authorization server redirects the user agent to the redirect URI provided
as a part of the request in step (A) and includes the authorization code as a part of
the query component of the redirection URI using the  application/x-www-form-urlencoded format.
The URI parameters are as follows:
  • Code: Required. The authorization code generated by the authorization server.
  • The code is temporary and must expire shortly after it was generated. The client
  • must not use the authorization code more than once. Any further requests using
  • the same code should be revoked by the authorization server. The authorization
  • code is bound to the client identifier and the redirection URI.
  • State: Required. If the state parameter was present in the client's authorization
  • code request, this parameter must be set to the exact value received from the client.
Access token request
This corresponds to step (D) in Figure 1. The client makes a request to the token
endpoint (authorization server) using the  application/x-www-form-urlencoded  format as shown in Listing 2.
Listing 2. Example of an access token request
1
2
3
4
5
6
7
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom&client_id=c342
The access token request must have the following parameters set:
  • grant_type: Required. The value must be set to authorization_code.
  • client_id: Required. The client ID.
  • client_secret: Optional. Secret. To authenticate with authorization server.
  • code: Required. The authorization code received from the server.
  • redirect_uri: Required. Identical to that sent in step (A).
The authorization server verifies that the code and redirect URI are valid. In the case
of confidential clients, the authorization server also authenticates the client using its
client credentials passed in the body of the request or in the authorization header.
Access token response
This corresponds to step (E) in Figure 1. If the access token request is valid and is
authorized, the authorization server returns the access token in an access token
response. An example of a successful response is shown in Listing 3.
Listing 3. Example of a successful access token response
1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"Bearer",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
If the request is not valid or unauthorized, the authorization server returns an
appropriate error message with code.
Refresh access token request
This is an optional step, which is applicable if the client requested offline access
and was provided a refresh_token  as a part of the access token request. An access token
is temporary and usually expires after an hour. After the access token expires, the
client would need to repeat the authentication process and the resource owner
would need to log in and provide authorization to enable the client to make the
access token request again.
If the client needs to refresh access tokens while the resource owner is not present
at the browser to log in and authenticate, the client uses the offline access. The
client can request an offline access while making the first authorization code
request (see step (A)). Under this scheme, the authorization server returns a refresh
token in addition to the access token. The refresh token is a long-living token that
does not expire, unless it is explicitly revoked by the resource owner. Every time
the access token expires, the client can use the refresh token to regenerate an
access token without the resource owner needing to sign in and authorize the access request.
The client makes a request to the token endpoint (authorization server) using
the  application/x-www-form-urlencoded  format, as shown in Listing 4:
Listing 4. Request to the token endpoint
1
2
3
4
5
6
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
The request parameters are defined as follows:
  • grant_type: Required. The value must be set to refresh_token.
  • refresh_token: Required. This is retrieved earlier from access token request.
  • scope: Optional. The scope of the access request.
The authorization server verifies the refresh token and issues a new access token.
Refresh access token response
If the request is successful, the authorization server returns a new access token.
An example of a successful response is shown in Listing 5.
Listing 5. Refresh access token response
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"Bearer",
"expires_in":3600,
"example_parameter":"example_value"
}
If the request is not valid or unauthorized, the authorization server returns an
appropriate error message with code.
 
 
二、用户评证
 
 
 
三、SAML/JWT


JWT:
 

Spring Cloud之OAuth2_第2张图片
 

 
 
 
 
 
SAML:

Spring Cloud之OAuth2_第3张图片
 
 
SAML   2  种典型模式
在协议的角度,  SAML  原理非常类似  CAS   Kerberos   CAS  协议依赖于  CAS Server   
Kerberos 依赖于  KDC  ,而  SAML  则依赖于  Identity Provider 
根据  Service Provider(  以下简称  SP)   Identity Provider(  以下简称  IDP)  的交互方式,  
SAML  可以分为以下几种模式:一种是  SP  拉方式,一种是  IDP  推方式。
 SAML  中,最重要的环节是  SP  如何获取对  Subject  的断言,  SP  拉方式是  SP  主动到  IDP 
去了解 Subject  的身份断言,而  IDP  推方式则是  IDP  主动把  Subject  的身份断言通过某种途径告诉  SP 
2.2.1 SAML   POST/Artifact Bindings  方式(即  SP  拉方式)
该方式的主要特点是,  SP  获得客户端的凭证    IDP   Subject  的一种身份认可   之后,主动请求  
IDP  Subject  的凭证的断言。如下图所示:  Subject  是根据凭证去访问  SP  的。凭证代表了  
Subject  的身份,它类似于“来自  IDP  证明:我就是  Peter  ,法国公民”。
现在,让我们看看  SP  拉方式是如何进行的:
Subject  访问  SP  的受保护资源,  SP  发现  Subject  的请求中没有包含任何的授权信息,
于是它重定向用户访问  IDP.
 
Spring Cloud之OAuth2_第4张图片
      
 
协议执行:
1, Subject   IDP  请求凭证   方式是提交用户名  /  密码
2, IDP  通过验证  Subject  提供的信息,来确定是否提供凭证给  Subject
3,  假如  Subject  的验证信息正确,他将获取  IDP  的凭证以及将服务请求同时提交给  SP 
4, SP  接受到  Subject  的凭证,它是提供服务之前必须验证次凭证,于是,它产生了一个  
SAML  请求,要求  IDP  对凭证断言
5,  凭证是  IDP  产生的,它当然知道凭证的内容,于是它回应一个  SAML  断言给  SP
6, SP  信任  IDP   SAML  断言,它会根据断言结果确定是否为  Subject  提供服务。
4.2.1 SAML   Redirect/POST Bindings  方式    IDP  推方式
该方式的主要特点是,  IDP  交给  Subject  的不是凭证,而是断言。
过程如下图所示:
 
Spring Cloud之OAuth2_第5张图片
 
       1   Subject  访问  SP  的授权服务,  SP  重定向  Subject   IDP  获取断言。
       2   IDP  会要求  Subject  提供能够证明它自己身份的手段  (Password   X.509  证书等
       3   Subject   IDP  提供了自己的帐号密码。
       4   IDP  验证密码之后,会重订向  Subject  到原来的  SP 
       5   SP  校验  IDP  的断言   注意,  IDP  会对自己的断言签名,  SP  信任  IDP  的证书,因此,
通过校验签名,能够确信从  Subject  过来的断言确实来自  IDP  的断言  
       6  ,如果签名正确,  SP  将向  Subject  提供该服务。
 
 
四、Spring + Auth2 + Security

Spring Cloud之OAuth2_第6张图片
 
五、Authorization Server Configuration
 
http://localhost:8080/auth/oauth/authorize?response_type=code&client_id
=acme&redirect_uri=http://example.com
 
http://localhost:8080/auth/oauth/authorize?username=admin&password=123456&scope=read&grant_type
=password&response_type=code&client_id=acme&redirect_uri=http://example.com
 
http://localhost:8080/auth/oauth/token?response_type=code&client_id=acme&
redirect_uri=http://example.com&code=SMJ6j2
 
问题:
{"timestamp":1420442772928,"status":401,"error":"Unauthorized",
"message":"Full authentication is required to access this resource","path":"/resource"}
解决办法:
Authorization:username:password
用户名密码需要通过Request Header传递(key=Authorization,value=base64(username:password))
 
Application.yml:
server.contextPath: /auth
logging:
level:
org.springframework.security: DEBUG
server:
port: 8080
keystore:
password: mySecretKey
security:
user:
name:admin
password:admin
oauth2:
client:
clientId:acme
clientSecret:acmesecret
authorized-grant-types:authorization_code,refresh_token,password
scope:openid
 
 
六、TOKEN 存储方式
(一)InMemoryTokenStore
TOKEN存储方式为默认的配置方式,AuthorizationServerEndpointsConfigurer类如下
private TokenStore tokenStore() {
if (tokenStore == null) {
if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
}
else {
this.tokenStore = new InMemoryTokenStore();
}
}
return this.tokenStore;
}
 
TOKEN生成策略接口(AuthenticationKeyGenerator),从OAuth2Authentication对
象生成唯一TOKEN KEY,基于内存TOKEN,默认生成KEY实现类为:
DefaultAuthenticationKeyGenerator,MD5算法签名

public class DefaultAuthenticationKeyGenerator implements AuthenticationKeyGenerator {
private static final String CLIENT_ID = "client_id";
private static final String SCOPE = "scope";
private static final String USERNAME = "username";
public String extractKey(OAuth2Authentication authentication) {
Map values = new LinkedHashMap();
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
values.put( USERNAME, authentication.getName());
}
values.put( CLIENT_ID, authorizationRequest.getClientId());
if (authorizationRequest.getScope() != null) {
values.put( SCOPE, OAuth2Utils.formatParameterList(new TreeSet
(authorizationRequest.getScope())));
}
return generateKey(values);
}
protected String generateKey(Map values) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
return String.format("%032x", new BigInteger(1, bytes));
} catch (NoSuchAlgorithmException nsae) {
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).", nsae);
} catch (UnsupportedEncodingException uee) {
throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).", uee);
}
}
}
 
可以调用以下方法重写TOKEN生成方式,限于高级用法
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
this.authenticationKeyGenerator = authenticationKeyGenerator;
}
 

 
 
(二)JwtTokenStore
JwtTokenStore实现需要依赖AccessTokenConverter接口,AccessTokenConverter
接口实现类有两个:
1)JWT(header、content、cryto)----JwtAccessTokenConverter接口
2)普通 ------DefaultAccessTokenConverter接口(默认实现)
参考类AuthorizationServerEndpointsConfigurer:

private AccessTokenConverter accessTokenConverter() {
if (this.accessTokenConverter == null) {
accessTokenConverter = new DefaultAccessTokenConverter();
}
return this.accessTokenConverter;
}
private TokenStore tokenStore() {
if (tokenStore == null) {
if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter)
accessTokenConverter());
}
else {
this.tokenStore = new InMemoryTokenStore();
}
}
return this.tokenStore;
}

JWT转换方式:
 
1)MAC (默认)
采用对用户(字段username)或者客户端信息(oauthrocations 中的AUTHORITIES)进签名,需要依赖
DefaultAccessTokenConverter实现,DefaultAccessTokenConverter又需要
UserAuthenticationConverter接口实现(只有一个实现类DefaultUserAuthenticationConverter),
DefaultUserAuthenticationConverter实现需要依赖UserDetailsService接口获取用户信息
签名属性:

public class DefaultUserAuthenticationConverter implements UserAuthenticationConverter {
private Collection defaultAuthorities;
private UserDetailsService userDetailsService;
/**
* Optional {@link UserDetailsService} to use when extracting an {@link Authentication}
from the incoming map.
*
* @param userDetailsService the userDetailsService to set
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* Default value for authorities if an Authentication is being created and the input has
no data for authorities.
* Note that unless this property is set, the default Authentication created by
{@link #extractAuthentication(Map)}
* will be unauthenticated.
*
* @param defaultAuthorities the defaultAuthorities to set. Default null.
*/
public void setDefaultAuthorities(String[] defaultAuthorities) {
this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.arrayToCommaDelimitedString(defaultAuthorities));
}
public Map convertUserAuthentication(Authentication authentication) {
Map response = new LinkedHashMap();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
public Map convertUserAuthentication(Authentication authentication) {
Map response = new LinkedHashMap();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}

签名方法:
// Key随机生成
private String verifierKey = new RandomValueStringGenerator().generate();
private Signer signer = new MacSigner(verifierKey);
private String signingKey = verifierKey;
 

 
/**
* Get the verification key for the token signatures.
*
* @return the key used to verify tokens
*/
public Map getKey() {
Map result = new LinkedHashMap();
result.put("alg", signer.algorithm());
result.put("value", verifierKey);
return result;
}
 
public class MacSigner implements SignerVerifier {
private static final String DEFAULT_ALGORITHM = "HMACSHA256";
public byte[] sign(byte[] bytes) {
try {
Mac mac = Mac.getInstance(algorithm);
mac.init(key);
return mac.doFinal(bytes);
}
catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}

 
2)RSA
 

public void setKeyPair(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
signer = new RsaSigner((RSAPrivateKey) privateKey);
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
verifier = new RsaVerifier(publicKey);
verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))
+ "\n-----END PUBLIC KEY-----";
}

 
 
(三)JdbcTokenStore
 
 
 
(四)RedisTokenStore
 
 
AbstractEndpoint
 
1)AuthorizationEndpoint---/oauth/authorize
 
2)TokenEndpoint---/oauth/token
 
3)CheckTokenEndpoint---/oauth/check_token
 
4)WhitelabelApprovalEndpoint---/oauth/confirm_acces
 
5)TokenKeyEndpoint---/oauth/token_key
 
 
七、Spring Security
 
一、基础配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired MyUserDetailsService detailsService;
@Override protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() .and().formLogin().loginPage("/login").permitAll()
.defaultSuccessUrl("/", true) .and().logout().logoutUrl("/logout")
.and().sessionManagement().maximumSessions(1).expiredUrl("/expired")
.and() .and().exceptionHandling().accessDeniedPage("/accessDenied");
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**", "/images/**", "/**/favicon.ico");
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
1.@EnableWebSecurity: 禁用Boot的默认Security配置,配合@Configuration启用自定义配置
(需要扩展WebSecurityConfigurerAdapter)
2.@EnableGlobalMethodSecurity(prePostEnabled = true): 启用 Security注解
例如最常用的@PreAuthorize
3.configure(HttpSecurity): Request层面的配置,对应XML Configuration中的元素
4.configure(WebSecurity): Web层面的配置,一般用来配置无需安全检查的路径
5.configure(AuthenticationManagerBuilder): 身份验证配置,用于注入
自定义身份验证Bean 密码校验规则
 
二、扩展配置
 
完成基础配置之后,下一步就是实现自己的UserDetailsService和PermissionEvaluator,
分别用于自定义Principle, Authority和Permission。
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired private LoginService loginService;
@Autowired private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String username) {
if (StringUtils.isBlank(username)) {
throw new UsernameNotFoundException("用户名为空");
}
Login login = loginService.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
Set authorities = new HashSet<>();
roleService.getRoles(login.getId()).forEach(r -> authorities.add(new SimpleGrantedAuthority(r.getName())));
return new org.springframework.security.core.userdetails.User( username, login.getPassword(),
true,//是否可用 true,//是否过期 true,//证书不过期为true true,//账户未锁定为true authorities);
}
}
创建GrantedAuthority对象时,一般名称加上ROLE_前缀。
@Component
public class MyPermissionEvaluator implements PermissionEvaluator {
@Autowired private LoginService loginService;
@Autowired private RoleService roleService;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
String username = authentication.getName();
Login login = loginService.findByUsername(username).get();
return roleService.authorized(login.getId(), targetDomainObject.toString(), permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission) {
// not supported return false;
}
}
1. hasPermission(Authentication, Object, Object)和hasPermission(Authentication,
Serializable, String, Object)
两个方法分别对应Spring Security中两个同名的表达式。
 
 
八、授权页修改
 
1) 修改管理端点URL
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
MyAuthorizationCodeService authorizationCodeServices = authorizationCodeServices();
authorizationCodeServices.setClientDetailsService(endpoints.getClientDetailsService());
endpoints.tokenStore(tokenStore())
.tokenEnhancer(jwtTokenEnhancer())
.authorizationCodeServices(authorizationCodeServices)
.authenticationManager(authenticationManager)
.pathMapping("/oauth/authorize", "/oauth2/authorize")
.pathMapping("/oauth/token", "/oauth2/token")
.pathMapping("/oauth/check_token", "/oauth2/check_token")
.pathMapping("/oauth/confirm_access", "/oauth2/confirm_access")
.pathMapping("/oauth/token_key", "/oauth2/token_key")
.pathMapping("/oauth/error", "/oauth2/error");
}
 
2)但是授权确认页面表单写死Action
端点类:WhitelabelApprovalEndpoint
@FrameworkEndpoint
@SessionAttributes("authorizationRequest")
public class WhitelabelApprovalEndpoint {
 
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map model,
HttpServletRequest request) throws Exception {
String template = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SpelView(template), model);
}
 
protected String createTemplate(Map model, HttpServletRequest request) {
String template = TEMPLATE;
if (model.containsKey("scopes") || request.getAttribute("scopes") != null) {
template = template.replace("%scopes%", createScopes(model, request)).replace("%denial%", "");
}
else {
template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
}
if (model.containsKey("_csrf") || request.getAttribute("_csrf") != null) {
template = template.replace("%csrf%", CSRF);
}
else {
template = template.replace("%csrf%", "");
}
return template;
}
 
private CharSequence createScopes(Map model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder("
    ");
@SuppressWarnings("unchecked")
Map scopes = (Map) (model.containsKey("scopes") ? model.get("scopes") : request
.getAttribute("scopes"));
for (String scope : scopes.keySet()) {
String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved)
.replace("%denied%", denied);
builder.append(value);
}
builder.append("");
return builder.toString();
}
 
private static String CSRF = "";
 
private static String DENIAL = "
type='hidden'/>%csrf%";
 
private static String TEMPLATE = "

OAuth Approval

"
+ "

Do you authorize '${authorizationRequest.clientId}' to access your protected

resources?

"
+ "
method='post'>
value='true' type='hidden'/>%csrf%%scopes%
  • Spring Cloud之OAuth2_第8张图片
  • 大小: 7.7 KB
  • Spring Cloud之OAuth2_第9张图片
  • 大小: 23.6 KB
  • Spring Cloud之OAuth2_第10张图片
  • 大小: 93.7 KB
  • Spring Cloud之OAuth2_第11张图片
  • 大小: 24.3 KB
  • Spring Cloud之OAuth2_第12张图片
  • 大小: 26.5 KB
  • Spring Cloud之OAuth2_第13张图片
  • 大小: 132.4 KB
  • Spring Cloud之OAuth2_第14张图片
  • 大小: 93.3 KB
  • OAuth2.0-Authorization-Server.rar (326.2 KB)
  • 下载次数: 22
  • 单点登录-OAuth2_N2.rar (4.6 MB)
  • 下载次数: 17
  • 查看图片附件

你可能感兴趣的:(单点登录,OAuth2)