oauth2 Oauth2是一种用于授权的开放标准。
比如可以让第三方客户端获取用户信息用于登录(微信、QQ)。
也可以用于自己APP的认证授权服务。
https://tools.ietf.org/html/rfc6749
https://datatracker.ietf.org/doc/html/rfc7636
http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
https://oauth.net/2/
客户端的授权模式 客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
授权码模式(authorization code) 授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动。
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
简化模式(implicit)
(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的”重定向URI”,并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
密码模式(resource owner password credentials)
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
客户端模式(client credentials)
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
授权码扩展流程PKCE(Proof Key for Code Exchange) 使用授权码授予的OAuth 2.0公共客户端是易受授权码拦截攻击。该流程可以减轻攻击,通过使用代码交换证明密钥来抵御威胁。客户端生成code_verifier和code_challenge跟认证服务器进行交互,以生成的随机认证码进行身份认证。
设备授权码模式(Device Authorization Grant) 设备授权码模式(Device Authorization Grant)主要会出现在凭证式授权类型中,为设备代码,设备流中无浏览器或输入受限的设备提供的一种认证方式,设备会让用户在另一台设备上的浏览器中访问一个网页,以进行登录。 用户登录后,设备可以获取所需的访问令牌和刷新令牌;流程如下
模式选择
如果是机器选择客户端模式
如果是第三方WEB服务器端应用,选择授权模式
如果是第三方原生APP,选择授权模式
如果是第三方单页应用SPA, 选择简化模式
如果是第一方原生APP,选择密码模式
如果是第一方单页应用SPA, 选择密码模式
OpenID Connect OpenID Connect(OIDC)是建立在OAuth 2.0授权框架之上的身份验证协议。它提供了一种标准化且可互操作的方式,使客户端(应用程序或网站)能够进行用户身份验证并从身份提供者那里获取用户信息。OpenID Connect的设计简单易实现,提供了在Web和移动应用程序中进行用户身份验证的安全而灵活的机制。
OpenID Connect被广泛采用,为应用程序提供了一种标准的方式,使其能够进行用户身份验证,而无需直接处理敏感凭据。它通常与OAuth 2.0一起使用,用于保护API并访问受保护的资源。
(Identity, Authentication) + OAuth 2.0 = OpenID Connect
实践参考 https://spring.io/projects/spring-authorization-server
https://docs.spring.io/spring-authorization-server/reference/getting-started.html
https://gitee.com/vains-Sofia/authorization-example
https://juejin.cn/post/7239953874950815804
授权码模式的认证服务器 创建项目名为:spring-boot-oath2-test
pom.xml
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 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 3.2.0</version > <relativePath /> </parent > <groupId > com.example</groupId > <artifactId > spring-boot-oath2-test</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > spring-boot-oath2-test</name > <description > Demo project for Spring Boot</description > <properties > <java.version > 17</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-oauth2-authorization-server</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
1 2 3 4 5 6 7 8 @SpringBootApplication public class SpringBootOath2TestApplication { public static void main (String[] args) { SpringApplication.run(SpringBootOath2TestApplication.class, args); } }
1 2 3 4 5 6 server: port: 9000 logging: level: org.springframework.security: trace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 package com.example.springbootoath2test;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.util.UUID;import com.nimbusds.jose.jwk.JWKSet;import com.nimbusds.jose.jwk.RSAKey;import com.nimbusds.jose.jwk.source.ImmutableJWKSet;import com.nimbusds.jose.jwk.source.JWKSource;import com.nimbusds.jose.proc.SecurityContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.annotation.Order;import org.springframework.http.MediaType;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;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.oauth2.core.AuthorizationGrantType;import org.springframework.security.oauth2.core.ClientAuthenticationMethod;import org.springframework.security.oauth2.core.oidc.OidcScopes;import org.springframework.security.oauth2.jwt.JwtDecoder;import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;import org.springframework.security.provisioning.InMemoryUserDetailsManager;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;@Configuration @EnableWebSecurity public class SecurityConfig { @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain (HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); http .exceptionHandling((exceptions) -> exceptions .defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint ("/login" ), new MediaTypeRequestMatcher (MediaType.TEXT_HTML) ) ) .oauth2ResourceServer((resourceServer) -> resourceServer .jwt(Customizer.withDefaults())); return http.build(); } @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain (HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } @Bean public UserDetailsService userDetailsService () { UserDetails userDetails = User.withDefaultPasswordEncoder() .username("admin" ) .password("123456" ) .roles("USER" ) .build(); return new InMemoryUserDetailsManager (userDetails); } @Bean public RegisteredClientRepository registeredClientRepository () { RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("clientapp" ) .clientSecret("123456" ) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("https://www.baidu.com" ) .postLogoutRedirectUri("http://127.0.0.1:9000/" ) .scope("admin" ) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true ).build()) .build(); return new InMemoryRegisteredClientRepository (oidcClient); } @Bean public JWKSource<SecurityContext> jwkSource () { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey .Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet (rsaKey); return new ImmutableJWKSet <>(jwkSet); } private static KeyPair generateRsaKey () { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA" ); keyPairGenerator.initialize(2048 ); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException (ex); } return keyPair; } @Bean public JwtDecoder jwtDecoder (JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public AuthorizationServerSettings authorizationServerSettings () { return AuthorizationServerSettings.builder().build(); } }
测试 1.访问授权码接口:
1 http:// 127.0 .0.1 :9000 /oauth2/ authorize?client_id=clientapp&response_type=code&scope=admin&redirect_uri=https:// www.baidu.com
参数解释:
1 2 3 4 client_id:客户端的id response_type:授权码模式固定为code scope:请求授权的范围 redirect_uri:回调地址
2.认证服务检测到未登录,重定向至登录页面:
输入用户名密码,点击登录,这里相当于授权码模式的步骤A
3.认证服务重定向至授权确认页面:
相当于授权码模式的步骤B
选择对应的scope并提交确认权限。
4.认证服务重定向redirectUri地址:
发现地址后带着code参数。相当于授权码模式的步骤C
5.访问获取token接口:相当于授权码模式的步骤D
/oauth2/token
因为接口是post接口,所以需要postman工具。
设置认证服务时ClientAuthenticationMethod.CLIENT_SECRET_BASIC。表示可以使用http的基本登录方式登录。
在Postman的权限标签选择basic auth;
输入接口的请求头:
content-type : application/x-www-form-urlencoded
输入接口的请求参数:
code: 上面获取的
grant_type: authorization_code
redirect_uri: https://www.baidu.com
发送请求,认证服务器返回access_token。