|
| 1 | += spring-boot-demo-oauth-authorization-server |
| 2 | +Doc Writer <lzy@echocow.cn> |
| 3 | +v1.0, 2019-01-07 |
| 4 | +:toc: |
| 5 | + |
| 6 | +spring boot oauth2 授权服务器, |
| 7 | + |
| 8 | +- 授权码模式、密码模式、刷新令牌 |
| 9 | +- 自定义 UserDetailService |
| 10 | +- 自定义 ClientDetailService |
| 11 | +- jwt 非对称加密 |
| 12 | +- 自定义登录授权页面 |
| 13 | +
|
| 14 | +> SQL 语句 |
| 15 | +> |
| 16 | +> - DDL: `src/test/resources/schema.sql` |
| 17 | +> - DML: `src/test/resources/import.sql` |
| 18 | + |
| 19 | +测试用例使用 h2 数据库,测试数据如下: |
| 20 | + |
| 21 | +.测试客户端 |
| 22 | +|=== |
| 23 | +|客户端 id |客户端密钥 |资源服务器名称 |授权类型 | scopes| 回调地址 |
| 24 | + |
| 25 | +|oauth2 |
| 26 | +|oauth2 |
| 27 | +|oauth2 |
| 28 | +|authorization_code,password,refresh_token |
| 29 | +|READ,WRITE |
| 30 | +|http://example.com |
| 31 | + |
| 32 | +|test |
| 33 | +|oauth2 |
| 34 | +|oauth2 |
| 35 | +|authorization_code,password,refresh_token |
| 36 | +|READ |
| 37 | +|http://example.com |
| 38 | + |
| 39 | + |
| 40 | +|error |
| 41 | +|oauth2 |
| 42 | +|test |
| 43 | +|authorization_code,password,refresh_token |
| 44 | +|READ |
| 45 | +|http://example.com |
| 46 | +|=== |
| 47 | + |
| 48 | +.测试用户 |
| 49 | +|=== |
| 50 | +|用户名 |密码 |角色 |
| 51 | + |
| 52 | +|admin |
| 53 | +|123456 |
| 54 | +|ROLE_ADMIN |
| 55 | + |
| 56 | +|test |
| 57 | +|123456 |
| 58 | +|ROLE_TEST |
| 59 | + |
| 60 | +|=== |
| 61 | + |
| 62 | +== 授权码模式 |
| 63 | + |
| 64 | +> 测试用例:`com.xkcoding.oauth.oauth.AuthorizationCodeGrantTests` |
| 65 | + |
| 66 | +=== 获取授权码 |
| 67 | + |
| 68 | +- 请求地址: http://localhost:8080/oauth/authorize?response_type=code&client_id=oauth2&redirect_uri=http://example.com&scope=READ |
| 69 | +- 用户名:admin |
| 70 | +- 密码:123456 |
| 71 | + |
| 72 | +image::image/Login.png[login] |
| 73 | + |
| 74 | +=== 确认授权 |
| 75 | + |
| 76 | +登录成功以后,进入确认授权页面。已经确认过的用户,不会再次要求确认。 |
| 77 | + |
| 78 | +image::image/Confirm.png[confirm] |
| 79 | + |
| 80 | +确认授权后,获取授权码 |
| 81 | + |
| 82 | +image::image/Code.png[code] |
| 83 | + |
| 84 | +=== 请求 token |
| 85 | + |
| 86 | +使用以下代码可以直接请求 token |
| 87 | + |
| 88 | +[shell] |
| 89 | +---- |
| 90 | +curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \ |
| 91 | +--header 'Content-Type: application/x-www-form-urlencoded' \ |
| 92 | +--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \ |
| 93 | +--data-urlencode 'grant_type=authorization_code' \ |
| 94 | +--data-urlencode 'code=GgX6QD' \ |
| 95 | +--data-urlencode 'redirect_uri=http://example.com' \ |
| 96 | +--data-urlencode 'client_id=oauth2' \ |
| 97 | +--data-urlencode 'scope=READ WRITE' |
| 98 | +---- |
| 99 | + |
| 100 | +得到 token |
| 101 | + |
| 102 | +[token] |
| 103 | +---- |
| 104 | +{ |
| 105 | + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiZjAyMDhiNTUtYTJjYS00NjI4LTg5YjEtNzI5MzY4MzAxOWNhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.RqJpsin6bMnwI57cGpODTplLeW_gtNWHo_l4SimyRLsnxpCWm5oY1EOb4qVHpXvCbhNsUj69D462P7le13OOmexysZIQhaoGZ_CbIlEp63XsCnr5nSKeX3dgQlyTUDjOUL0WUtY2lKqLCGMeX_rpVhfmSh3b7MC0Ntxq5ao-943QMXGRIeRvJgSkvfY2HBN6-zx1H6rE0wxnUfBC1M08kUkFYlSmsFchiz-E_oTzJvE2D8lA9g-eEFU6cZ_els4Q77Vvc_O6SXUZ7o65vFyLyUjLvh9QF1825SGIUUdXTUYSZjnSAXChhRIAT5pLRHK-gthIzpOaWrgj6ebUoG02Eg", |
| 106 | + "token_type": "bearer", |
| 107 | + "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJhdGkiOiJmMDIwOGI1NS1hMmNhLTQ2MjgtODliMS03MjkzNjgzMDE5Y2EiLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiMGViNTU2MTQtYjgxYS00MTFmLTg1MTAtZThkMjZmODJmMjJhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.CBGcjirkf-3187SgbZr0ikauiCS8U9YLaoR4sNlRQjd-gaIeF5PChnIs_yAmG_VpqPFlPRdSl8DA05S2QnFpT3TkRjyP-LPDZgsVAPfczMAdVywU1zOKYZeq-gM6p9bmGEabbZoBlIxOImsjeyFSCui6UtRTZjNlj3AhGIzvs52T8bDqC796iHPDZvJ97MMgsEiRyu-mxDm1o1LMuBX9RHCx9rAkBVf52q36bqWMcYAlDOu1wYjpmhalSLZyWcmraQvClEitXGJI4eTFapTnuXQuWFIL-973V_5Shw98-bk65zZQOEheazHrUf-n4h-sYT4akehnYSVxX2UIg9XsCw", |
| 108 | + "expires_in": 5999, |
| 109 | + "scope": "READ", |
| 110 | + "jti": "f0208b55-a2ca-4628-89b1-7293683019ca" |
| 111 | +} |
| 112 | +---- |
| 113 | + |
| 114 | +== 密码模式 |
| 115 | + |
| 116 | +> 测试用例:`com.xkcoding.oauth.oauth.ResourceOwnerPasswordGrantTests` |
| 117 | + |
| 118 | +`test` 用户进行授权 |
| 119 | + |
| 120 | +[source] |
| 121 | +---- |
| 122 | +curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \ |
| 123 | +--header 'Content-Type: application/x-www-form-urlencoded' \ |
| 124 | +--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \ |
| 125 | +--data-urlencode 'password=123456' \ |
| 126 | +--data-urlencode 'username=test' \ |
| 127 | +--data-urlencode 'grant_type=password' \ |
| 128 | +--data-urlencode 'scope=READ WRITE' |
| 129 | +---- |
| 130 | + |
| 131 | +== 刷新令牌 |
| 132 | + |
| 133 | +携带 `refresh_token` 去请求 |
| 134 | + |
| 135 | +[source] |
| 136 | +---- |
| 137 | +curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \ |
| 138 | +--header 'Content-Type: application/x-www-form-urlencoded' \ |
| 139 | +--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \ |
| 140 | +--data-urlencode 'grant_type=refresh_token' \ |
| 141 | +--data-urlencode 'refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJhdGkiOiJmMDIwOGI1NS1hMmNhLTQ2MjgtODliMS03MjkzNjgzMDE5Y2EiLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiMGViNTU2MTQtYjgxYS00MTFmLTg1MTAtZThkMjZmODJmMjJhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.CBGcjirkf-3187SgbZr0ikauiCS8U9YLaoR4sNlRQjd-gaIeF5PChnIs_yAmG_VpqPFlPRdSl8DA05S2QnFpT3TkRjyP-LPDZgsVAPfczMAdVywU1zOKYZeq-gM6p9bmGEabbZoBlIxOImsjeyFSCui6UtRTZjNlj3AhGIzvs52T8bDqC796iHPDZvJ97MMgsEiRyu-mxDm1o1LMuBX9RHCx9rAkBVf52q36bqWMcYAlDOu1wYjpmhalSLZyWcmraQvClEitXGJI4eTFapTnuXQuWFIL-973V_5Shw98-bk65zZQOEheazHrUf-n4h-sYT4akehnYSVxX2UIg9XsCw' |
| 142 | +---- |
| 143 | + |
| 144 | +== 解析令牌 |
| 145 | + |
| 146 | +携带令牌解析 |
| 147 | + |
| 148 | +[source] |
| 149 | +---- |
| 150 | +curl --location --request POST 'http://127.0.0.1:8080/oauth/check_token' \ |
| 151 | +--header 'Content-Type: application/x-www-form-urlencoded' \ |
| 152 | +--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \ |
| 153 | +--data-urlencode 'token=' |
| 154 | +---- |
| 155 | + |
| 156 | +解析结果 |
| 157 | + |
| 158 | +[source] |
| 159 | +---- |
| 160 | +{ |
| 161 | + "aud": [ |
| 162 | + "oauth2" |
| 163 | + ], |
| 164 | + "user_name": "admin", |
| 165 | + "scope": [ |
| 166 | + "READ", |
| 167 | + "WRITE" |
| 168 | + ], |
| 169 | + "active": true, |
| 170 | + "exp": 1578389936, |
| 171 | + "authorities": [ |
| 172 | + "ROLE_ADMIN" |
| 173 | + ], |
| 174 | + "jti": "fe59fce9-6764-435e-8fa7-7320e11af811", |
| 175 | + "client_id": "oauth2" |
| 176 | +} |
| 177 | +---- |
| 178 | + |
| 179 | +== 退出登录 |
| 180 | + |
| 181 | +授权码模式登陆是在授权服务器上登录的,所以退出也要在授权服务器上退出。 |
| 182 | + |
| 183 | +携带回调地址进行退出,退出完成后跳转到回调地址: |
| 184 | + |
| 185 | +image::image/Logout.png[logout] |
| 186 | + |
| 187 | +退出以后自动跳转到回调地址(要加 `http` 或 `https`) |
| 188 | + |
| 189 | +== 获取公钥 |
| 190 | + |
| 191 | +通过访问 '/oauth/token_key' 获取 JWT 公钥 |
| 192 | + |
| 193 | +[source] |
| 194 | +---- |
| 195 | +curl --location --request GET 'http://127.0.0.1:8080/oauth/token_key' \ |
| 196 | +--header 'Content-Type: application/x-www-form-urlencoded' \ |
| 197 | +--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' |
| 198 | +---- |
| 199 | + |
| 200 | +获取后 |
| 201 | + |
| 202 | +[source] |
| 203 | +---- |
| 204 | +{ |
| 205 | + "alg": "SHA256withRSA", |
| 206 | + "value": "-----BEGIN PUBLIC KEY-----\n......\n-----END PUBLIC KEY-----" |
| 207 | +} |
| 208 | +---- |
| 209 | + |
| 210 | +== 核心配置 |
| 211 | + |
| 212 | +=== 授权服务器配置 |
| 213 | + |
| 214 | +[Oauth2AuthorizationServerConfig] |
| 215 | +---- |
| 216 | +@Override |
| 217 | +public void configure(AuthorizationServerEndpointsConfigurer endpoints) { |
| 218 | + endpoints.authenticationManager(authenticationManager) |
| 219 | + // 自定义用户 |
| 220 | + .userDetailsService(sysUserService) |
| 221 | + // 内存存储 |
| 222 | + .tokenStore(tokenStore) |
| 223 | + // jwt 令牌转换 |
| 224 | + .accessTokenConverter(jwtAccessTokenConverter); |
| 225 | +} |
| 226 | +
|
| 227 | +@Override |
| 228 | +public void configure(ClientDetailsServiceConfigurer clients) throws Exception { |
| 229 | + // 从数据库读取我们自定义的客户端信息 |
| 230 | + clients.withClientDetails(sysClientDetailsService); |
| 231 | +} |
| 232 | +
|
| 233 | +@Override |
| 234 | +public void configure(AuthorizationServerSecurityConfigurer security) { |
| 235 | + security |
| 236 | + // 获取 token key 需要进行 basic 认证客户端信息 |
| 237 | + .tokenKeyAccess("isAuthenticated()") |
| 238 | + // 获取 token 信息同样需要 basic 认证客户端信息 |
| 239 | + .checkTokenAccess("isAuthenticated()"); |
| 240 | +} |
| 241 | +---- |
| 242 | + |
| 243 | +=== 安全配置 |
| 244 | + |
| 245 | +[WebSecurityConfig] |
| 246 | +---- |
| 247 | +@Override |
| 248 | +protected void configure(HttpSecurity http) throws Exception { |
| 249 | + http |
| 250 | + // 开启表单登录,授权码模式的时候进行登录 |
| 251 | + .formLogin() |
| 252 | + // 路径等 |
| 253 | + .loginPage("/oauth/login") |
| 254 | + .loginProcessingUrl("/authorization/form") |
| 255 | + // 失败以后携带错误信息进行再次跳转登录页面 |
| 256 | + .failureHandler(clientLoginFailureHandler) |
| 257 | + .and() |
| 258 | + // 退出登录相关 |
| 259 | + .logout() |
| 260 | + .logoutUrl("/oauth/logout") |
| 261 | + .logoutSuccessHandler(clientLogoutSuccessHandler) |
| 262 | + .and() |
| 263 | + // 授权服务器安全配置 |
| 264 | + .authorizeRequests() |
| 265 | + .antMatchers("/oauth/**").permitAll() |
| 266 | + .anyRequest() |
| 267 | + .authenticated(); |
| 268 | +} |
| 269 | +---- |
| 270 | + |
| 271 | +== 参考 |
| 272 | + |
| 273 | +- https://echocow.cn/articles/2019/07/14/1563096109754.html[Spring Security Oauth2 从零到一完整实践(三)授权服务器 ] |
0 commit comments