本文简单的介绍Spring Boot与OAuth2的集成、OAuth client、OAuth authorization server、OAuth resource server的示例。

👉单点登录Github(客户端)

👐创建项目

略。

示例项目地址

👏项目结构

image.png

👏添加首页

  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8"/>
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
  6. <title>Demo</title>
  7. <meta name="description" content=""/>
  8. <meta name="viewport" content="width=device-width"/>
  9. <base href="/"/>
  10. <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
  11. <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
  12. <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
  13. <script src="/webjars/js-cookie/js.cookie.js" type="text/javascript"></script>
  14. </head>
  15. <body>
  16. <h1>Demo</h1>
  17. <div class="container"></div>
  18. </body>
  19. </html>

👏POM

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <artifactId>springboot-demo</artifactId>
  7. <groupId>com.example</groupId>
  8. <version>0.0.1-SNAPSHOT</version>
  9. </parent>
  10. <artifactId>springboot-oauth-client</artifactId>
  11. <version>0.0.1-SNAPSHOT</version>
  12. <name>springboot-oauth-client</name>
  13. <description>Demo project for Spring Boot and OAuth Client</description>
  14. <properties>
  15. <java.version>1.8</java.version>
  16. </properties>
  17. <dependencies>
  18. <!--CSS\JS等资源-->
  19. <dependency>
  20. <groupId>org.webjars</groupId>
  21. <artifactId>jquery</artifactId>
  22. <version>3.4.1</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.webjars</groupId>
  26. <artifactId>bootstrap</artifactId>
  27. <version>4.3.1</version>
  28. </dependency>
  29. <dependency>
  30. <groupId>org.webjars</groupId>
  31. <artifactId>js-cookie</artifactId>
  32. <version>2.1.0</version>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.webjars</groupId>
  36. <artifactId>webjars-locator-core</artifactId>
  37. </dependency>
  38. <!--客户端-->
  39. <dependency>
  40. <groupId>org.springframework.boot</groupId>
  41. <artifactId>spring-boot-starter-oauth2-client</artifactId>
  42. </dependency>
  43. <dependency>
  44. <groupId>org.springframework.boot</groupId>
  45. <artifactId>spring-boot-starter-security</artifactId>
  46. </dependency>
  47. <dependency>
  48. <groupId>org.springframework.boot</groupId>
  49. <artifactId>spring-boot-starter-web</artifactId>
  50. </dependency>
  51. <dependency>
  52. <groupId>org.springframework.boot</groupId>
  53. <artifactId>spring-boot-starter-test</artifactId>
  54. <scope>test</scope>
  55. <exclusions>
  56. <exclusion>
  57. <groupId>org.junit.vintage</groupId>
  58. <artifactId>junit-vintage-engine</artifactId>
  59. </exclusion>
  60. </exclusions>
  61. </dependency>
  62. <dependency>
  63. <groupId>org.springframework.security</groupId>
  64. <artifactId>spring-security-test</artifactId>
  65. <scope>test</scope>
  66. </dependency>
  67. <!--支持热部署-->
  68. <dependency>
  69. <groupId>org.springframework.boot</groupId>
  70. <artifactId>spring-boot-devtools</artifactId>
  71. <optional>true</optional>
  72. <scope>true</scope>
  73. </dependency>
  74. </dependencies>
  75. <build>
  76. <plugins>
  77. <plugin>
  78. <groupId>org.springframework.boot</groupId>
  79. <artifactId>spring-boot-maven-plugin</artifactId>
  80. <configuration>
  81. <!--Flag to indicate if the run processes should be forked. Disabling forking will
  82. disable some features such as an agent, custom JVM arguments, devtools or
  83. specifying the working directory to use.-->
  84. <!--原文,意思就是不加会导致一些特性被关闭,比如devtools-->
  85. <fork>true</fork>
  86. </configuration>
  87. </plugin>
  88. </plugins>
  89. </build>
  90. </project>

👏配置

  1. server:
  2. port: 8081
  3. spring:
  4. security:
  5. oauth2:
  6. client:
  7. registration:
  8. github:
  9. #这是在oauth authorization server注册的id、secret
  10. clientId: "********"
  11. clientSecret: "**********"

👏启动类

  1. package com.example.springbootoauth;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.http.HttpMethod;
  7. import org.springframework.http.HttpStatus;
  8. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  10. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  11. import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
  12. import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
  13. import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
  14. import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
  15. import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
  16. import org.springframework.security.oauth2.core.OAuth2Error;
  17. import org.springframework.security.oauth2.core.user.OAuth2User;
  18. import org.springframework.security.web.authentication.HttpStatusEntryPoint;
  19. import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
  20. import org.springframework.web.bind.annotation.GetMapping;
  21. import org.springframework.web.bind.annotation.RestController;
  22. import javax.servlet.http.HttpServletRequest;
  23. import java.io.BufferedReader;
  24. import java.io.IOException;
  25. import java.io.InputStream;
  26. import java.io.InputStreamReader;
  27. import java.net.HttpURLConnection;
  28. import java.net.URL;
  29. import java.util.Collections;
  30. import java.util.List;
  31. import java.util.Map;
  32. @SpringBootApplication
  33. public class SpringbootOauthClientApplication extends WebSecurityConfigurerAdapter {
  34. @Override
  35. protected void configure (HttpSecurity http) throws Exception {
  36. // @formatter:off
  37. // 配置除了"/favicon.ico", "/error", "/webjars/**"这些地址不需经过认证之外其他所有都需认证
  38. // 配置所有认证失败返回401状态码(默认403状态码)
  39. // 配置OAuth2.0认证
  40. http.authorizeRequests(a ->
  41. a.antMatchers("/favicon.ico", "/error", "/webjars/**").permitAll().anyRequest().authenticated()
  42. ).exceptionHandling(e ->
  43. e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
  44. ).oauth2Login();
  45. // @formatter:on
  46. }
  47. public static void main (String[] args) {
  48. SpringApplication.run(SpringbootOauthClientApplication.class, args);
  49. }
  50. }

🤙Github OAuth app

我们必须先去Github申请一个新的oauth app用来接入Github,Add a new OAuth app

image.png
Application name 、 Application description按照个人需求填写,Homepage URL按照上面的配置的话应该是localhost:8081
Authorization callback URL按照上面的配置的话应该是localhost:8081/login/oauth2/code/github

callback URL在Spring Boot中的模板是:{baseUrl}/login/oauth2/code/{registrationId}

image.png
注册完毕之后,我们可以看到注册的app的Client ID、Client Secret,这两个要放到配置中去。

🤙启动项目

就这么简单

  1. 创建项目
  2. 加一个简单的首页
  3. POM加一点儿依赖
  4. 简单加两个配置
  5. 稍微配置下启动类

一个OAuth客户端就“能用了”

启动项目,并访问主页locahost:8081,会发现跳转到了Github认证

https://github.com/login/oauth/authorize?response_type=code&client_id=**&scope=read:user&state=pYyqimQWllgp9KbLb0-t_qOk-9ZhyftDvyRB8oDjCx8%3D&redirect_uri=http://localhost:8081/login/oauth2/code/github

github使用的认证模式是code模式,即授权码(authorization-code)模式。rfc6749

image.png
点击Authorize并认证成功之后便会跳转到你的homepage,也就是localhost:8081

只要你仍然在Github保持登录状态,本地app不需要重新认证,即使打开一个新的窗口(无痕、别的浏览器[没有登录github]除外)。

🤙请求链

从启动应用后的第一次访问开始分析

👌Ⅰ访问首页

General:

  1. Request URL: http://localhost:8081/
  2. Request Method: GET
  3. Status Code: 302
  4. Remote Address: [::1]:8081
  5. Referrer Policy: strict-origin-when-cross-origin

Request:

  1. GET / HTTP/1.1
  2. Host: localhost:8081
  3. Connection: keep-alive
  4. Cache-Control: max-age=0
  5. Upgrade-Insecure-Requests: 1
  6. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
  7. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9
  8. Sec-Fetch-Site: none
  9. Sec-Fetch-Mode: navigate
  10. Sec-Fetch-User: ?1
  11. Sec-Fetch-Dest: document
  12. Accept-Encoding: gzip, deflate, br
  13. Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6

Response:

  1. HTTP/1.1 302
  2. Set-Cookie: JSESSIONID=118A9185FDC6BA0D68C20B62583C5BBF; Path=/; HttpOnly
  3. X-Content-Type-Options: nosniff
  4. X-XSS-Protection: 1; mode=block
  5. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  6. Pragma: no-cache
  7. Expires: 0
  8. X-Frame-Options: DENY
  9. Location: http://localhost:8081/oauth2/authorization/github
  10. Content-Length: 0
  11. Date: Wed, 20 May 2020 10:00:07 GMT
  12. Keep-Alive: timeout=60
  13. Connection: keep-alive

因为没有认证信息,所以服务器返回302,重定向至Location:http://localhost:8081/oauth2/authorization/github
上面地址的模板是:{baseUrl}/oauth2/authorization/{provider}
在这个地址的处理里根据provider的信息正式向provider进行认证

👌Ⅱ重定向

被重定向至:localhost:8081/**oauth2/authorization/github**
_
General:

  1. Request URL: http://localhost:8081/oauth2/authorization/github
  2. Request Method: GET
  3. Status Code: 302
  4. Remote Address: [::1]:8081
  5. Referrer Policy: strict-origin-when-cross-origin

Request:

  1. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  2. Accept-Encoding: gzip, deflate, br
  3. Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6
  4. Cache-Control: max-age=0
  5. Connection: keep-alive
  6. Cookie: JSESSIONID=118A9185FDC6BA0D68C20B62583C5BBF
  7. Host: localhost:8081
  8. Sec-Fetch-Dest: document
  9. Sec-Fetch-Mode: navigate
  10. Sec-Fetch-Site: none
  11. Sec-Fetch-User: ?1
  12. Upgrade-Insecure-Requests: 1
  13. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36

Response:

  1. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  2. Connection: keep-alive
  3. Content-Length: 0
  4. Date: Wed, 20 May 2020 10:00:07 GMT
  5. Expires: 0
  6. Keep-Alive: timeout=60
  7. Location: https://github.com/login/oauth/authorize?response_type=code&client_id=********&scope=read:user&state=iTdcnTtCIwpC36VQoWEt_3We2E8DUNdk488QlDdRHa8%3D&redirect_uri=http://localhost:8081/login/oauth2/code/github
  8. Pragma: no-cache
  9. X-Content-Type-Options: nosniff
  10. X-Frame-Options: DENY
  11. X-XSS-Protection: 1; mode=block

Github是知名的provider,所以我们只需要提供client id,client secret即可,其他信息,比如:

等信息已经内置在框架中。 当我们搭建服务器端时,我们作为Provider也需要提供这些信息。

将response_type(授权模式)、client_id(客户端id)、重定向地址等放到地址参数中被重定向至
https://github.com/login/oauth/authorize?response_type=code&client_id=*&scope=read:user&state=iTdcnTtCIwpC36VQoWEt_3We2E8DUNdk488QlDdRHa8%3D&redirect_uri=http://localhost:8081/login/oauth2/code/github获取授权码

👌Ⅲ重定向

被重定向至:https://github.com/login/oauth/authorize?response_type=code&client_id=*&scope=read:user&state=iTdcnTtCIwpC36VQoWEt_3We2E8DUNdk488QlDdRHa8%3D&redirect_uri=http://localhost:8081/login/oauth2/code/github**

General:

  1. Request URL: https://github.com/login/oauth/authorize?response_type=code&client_id=********&scope=read:user&state=iTdcnTtCIwpC36VQoWEt_3We2E8DUNdk488QlDdRHa8%3D&redirect_uri=http://localhost:8081/login/oauth2/code/github
  2. Request Method: GET
  3. Status Code: 302 Found
  4. Remote Address: 140.82.113.3:443
  5. Referrer Policy: strict-origin-when-cross-origin

Request:

  1. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  2. Accept-Encoding: gzip, deflate, br
  3. Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6
  4. Cache-Control: max-age=0
  5. Connection: keep-alive
  6. Cookie: _octo=GH1.1.647508601.1588919991; _ga=GA1.2.1354001975.1588920084; tz=Asia%2FShanghai; _device_id=1a9cd9d4b86d40b8bb966bc1bd48a5c8; has_recent_activity=1; user_session=nohRAJiFcnuRdw2m63bkJf4SEZUHKBQekx-cqAAJ0_6o6IJ2; __Host-user_session_same_site=nohRAJiFcnuRdw2m63bkJf4SEZUHKBQekx-cqAAJ0_6o6IJ2; logged_in=yes; dotcom_user=****; _gh_sess=nxyLIvIbckSbSXcX%2BOQUq3iriiB0KgldKr6XG%2FNuX3Iv1UyTsaDDYXWshvAEobcOZt759Yxwgqi4%2BNgpU8gK8EPk51abjHjAgTCqLpd80bRxIV%2Bic8Ywik25NmiOYbFmyoLP2CC90NPNhG14yVHdHZXUAi9e2IJncrLo9pwIjvLgkB3PH0%2Bns%2Fsg6FmYiMpx7ercCkdy2L04V5ByYLSXBu4MYHXJ1b%2FGYB1b%2F4esQmSK63mHNj4v9JbLtcgcCSKbaVPUmnAdjgpiRCLQXA88b7kqmMHepXhghy8CDbUKY%2BJSXlwjHUcoSTykaXx2E0VICsOSH0qcwH7C2kUSxLvcD8rQGXffUjLiojRtsx4%2BN4HlJBXueuXQ6Rv7yOOn5nccbNF3BLP5YcoVAyu0BBMBnKqNErTsrD82SpYEDcaQ2Rbk65uSuL00BUfZH4ZetwM2wNB4YgUExkQIk5giujaPLLtS2LOSj%2Bj0UYyZj3Jx2v9%2F7U9uYGW2Le3afXncrURtxXbNlJ5WV0Bi7z9lNRcO7vYpGeGA5qzH%2BJzwxVQsqaM2CdGiIK8R8DV%2B0VrfxmtBBzmxXYlOmJJ%2BHCSBMcnMFKNbyGZ3bmHDeKtTRr1yZRphBEoN5jJO1WYvAnPvgQB6jfa5FD712TATuCrAQLtsTmgMuWa8IJX6NVgjvPhyIuJrFGz8ib6gooToJdCqfjNq7SupCIATqKbgQ1uvXhV44%2Fnh2cQF9VlhHFmOuMjTZADPTJmsZI4LYoZqiQ0qUQgVmufWUZwqkZglhguypjtyR42yu%2FFKhA2ZPc5JUqHEqNHH098J52%2BqDHdD1eQYxWvLniaTHq3oC4UjG%2Bwxaei0K86hVpByP%2FVl4z2xABTpFO5Z9MfbOjdUDwu696e4%2FmBDWfHBbzmjUsiZrZuRGc0%2FucpShxJpFzo4xJhpB4e5OhAYhQbgiFhnVkCIitZZenVMFSn5g8odutB4Oe1R30QVD%2BI5%2FHpOhYF0C2sUxlqV7DClLwIgg4WrPPN7dKIvsP%2F%2ByMBqzuO7Zom%2BgLd7Fxei2XFIJgC706jOB0%2FR%2F%2FhF30XFXeyH8WJN4pXiJMzwmx7s0%2F%2F0ljGxaVGA8AvV14vq8rChu2vFlE2IU8xu0fYk7ujSfgcKvU9TwBkh3Rh%2FDYVeDCy7CAqRDfwi9Z7VzlUmtgUa5onahRc1nmFtPTZ3oaQFLlzZOWrQn9Of%2BIHCeyE55ZIF%2FqHmn8sN1g4VvGKmYdWmj%2B7jaZIwjx8yCePz4PQatrNNf332QphRdy9mQfx9teHSuDTsZMlnNr%2FA6cFuqdAUd%2FFlSIEwmq5WRWVKUazcxlwyUrTxRT%2FyRuGnjGnw4CWKDzGd7N%2FqijkspsLLu%2BfyOKu0y1GZIwd%2FTgogWKQJSRXigLd6g1XVmefYQiHqiJKGcFxUDxw79xoSDI5mNKpnOYitHNEzmi6QibhW1weD%2FhNdgPL9ygZ%2F5F%2FWUScWv7OaPzT5xlvihMP1a2hchqot3nYIUJrKWAjO93eerzWp9A6mXnVX1ms4eae%2BixjcF598yLvXsjYqYtGvkP%2B78GemsMs%3D--pQrErwMIAeqxff%2Fa--J9T4jq%2BJRlwpgRxjFXGUKg%3D%3D
  7. Host: github.com
  8. Sec-Fetch-Dest: document
  9. Sec-Fetch-Mode: navigate
  10. Sec-Fetch-Site: none
  11. Sec-Fetch-User: ?1
  12. Upgrade-Insecure-Requests: 1
  13. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36

Response:

  1. Cache-Control: no-cache
  2. Content-Security-Policy: default-src 'none'; base-uri 'self'; block-all-mixed-content; connect-src 'self' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com cdn.optimizely.com logx.optimizely.com/v1/events wss://live.github.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com; frame-ancestors 'self'; frame-src render.githubusercontent.com; img-src 'self' data: github.githubassets.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com; manifest-src 'self'; media-src 'none'; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; worker-src github.com/socket-worker.js
  3. Content-Type: text/html; charset=utf-8
  4. Date: Wed, 20 May 2020 10:00:08 GMT
  5. Expect-CT: max-age=2592000, report-uri="https://api.github.com/_private/browser/errors"
  6. Location: http://localhost:8081/login/oauth2/code/github?code=6ab6a582334378ef9fe9&state=iTdcnTtCIwpC36VQoWEt_3We2E8DUNdk488QlDdRHa8%3D
  7. Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
  8. Server: GitHub.com
  9. Set-Cookie: user_session=nohRAJiFcnuRdw2m63bkJf4SEZUHKBQekx-cqAAJ0_6o6IJ2; path=/; expires=Wed, 03 Jun 2020 10:00:08 GMT; secure; HttpOnly; SameSite=Lax
  10. Set-Cookie: __Host-user_session_same_site=nohRAJiFcnuRdw2m63bkJf4SEZUHKBQekx-cqAAJ0_6o6IJ2; path=/; expires=Wed, 03 Jun 2020 10:00:08 GMT; secure; HttpOnly; SameSite=Strict
  11. Set-Cookie: has_recent_activity=1; path=/; expires=Wed, 20 May 2020 11:00:08 GMT; secure; HttpOnly; SameSite=Lax
  12. Set-Cookie: _gh_sess=7l6Hza0JKOdzQMjOmOsHXfFHWPslmhVQ6t7ipQ%2BUk1ldTIMrO62DFMbohKaVxqGEQbeECENJBv5LOC0h4uyIu5%2FMg4hqvSHV95kB0IGBXB%2F4lRnXBeLitB%2BURP3kcxaCmQHgd6VuKk8ieZ8Ez3FaqMTo5ChyccBmtIT1bxpj16dkO33tLVglYHkD2BvaIXRxcOfwRqeGXddvEPt7aD9x9RSgholNbsMpNsxdMP%2FIP0S%2FFR1%2Fh4KXqSVdLAXfc6dpF1j7AkP3qxMFLqqvXJpU4JbeZIkJA00PMFuuVY0Dy5wBeV7lZx9OBO7%2FKZT7qxEowRtfS%2FlsX1mQ%2FnZWGJNbWihdASEykuWfzgGRcDGrcyVuOPIDbrUw9XmZcGKWaAFd%2BwQv08f6yIAIkVKPyCqh7bwcN3gFihHxPOoEnqycBQY2%2BKxr4uiegv9FNVzzmRRLgqNf1X%2BPpcj02nWcCebvMPto%2F%2BtOewHplfs4Zn%2Bum9gufgK8U%2BXg28274Mn5%2BcatL5qY8cfOsaeGt3qMhO%2BlY1CrXWqcIagTJY%2B%2BqobhmklA8LsKaHx7EmCgGCMM95GdVidfqOfZxIH1jPTFcyHmTuyvCCQqXpzaE0o%2FtheS19JcvSfDNuoxazwkSRKHubvwU%2FaIDMDGlKCwO3euW%2FDDutUZ4Se%2FnIdm0U2y8ZMeyuWs1T%2FK%2FGOsChvn%2B%2BsdV6DF0GtFeF02hjUgXCu3tepnBZdYhP5qqVXO4Z1gqgvznFiU8SMUVaUi4kfnKqPf%2FkHHYeXSJBACn0x4%2FYehxjPeQodRx%2Fz6HnSL6gmnvZm0lZD8WbwyoAxxkIUHGW3SAF9Sg3q8Pm23wtZdv3ET0lKgB04v8CMKyxGZaFV826fLSJeru4uf3hWVy2N09g6sc8uGxn2y7gb63%2BV3pzs9CCK6uigmoq2wk86lY4%2Bvte9DGriLGGeepU0hjhh9Gwzt%2B7AG1FHne1%2F9DSyFCmCx5FDTtLELfemc9%2BLvS18lvU9nh6Ex3Ei%2FkAfH6cztYg%2BXegSrqpeXO2T1ULp3t6ftkWevzkmcpWj%2FSH5I704u3j2hXZW%2BmoV6NrKKV75aGG1hIP%2FLrdotp0BxBAtZorIh8PEkuYTKBWNy3RywLjMD%2B7%2BMcHhpExSrZhXTCZNwQo3iPgbOfb2TEPkIEbEGTMwEyiIo94GiJhIaNA13NGcvQS%2BR2st9z8rePhslv4VnYk2cvPFhQvv5MdmdgWcnf6G8UYNseAM27DxslJF22f%2ForeHmv0kNnsVLPpDmnbwaeMLGloF5eu65Xvcjjzm3zXxm7MYTAwp9QYQQCweZI8tnFkY7mlxw5lwHs99FU4PN4WX1gx76JHsSqRqNry7xc8ybW5Dl%2FjN8gfMay%2F9WFCjMBCmLGjcWWWlINvSH1ug%2FqaqSOsf2xw8t0Fy%2BHCcGBiS9lH9K7J5azdVfCc%2BZ6MQycavU8%2Fj3fDGNvNWF7XbgHI0ZSrH9LMLQyp7ILlLn0ylbflKYvhnZ93CFkMs8TU6Bc6IAY6DithW1ZQEH1qzfJ8hCsUUBpaCr%2F8OHfaUkX381AkZXQMEV1wI%3D--u3tauRwfmBSRrVZq--uHRBGGLCS0GU1K84C%2BRM0g%3D%3D; path=/; secure; HttpOnly; SameSite=Lax
  13. Status: 302 Found
  14. Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
  15. Transfer-Encoding: chunked
  16. Vary: X-PJAX
  17. Vary: Accept-Encoding, Accept, X-Requested-With
  18. X-Content-Type-Options: nosniff
  19. X-Frame-Options: sameorigin
  20. X-GitHub-Request-Id: 8C0F:221B:136D9:29086:5EC4FFA7
  21. X-XSS-Protection: 1; mode=block

从github.com认证成功获取授权码之后被重定向至客户端

👌Ⅳ重定向


被重定向至:localhost:8081/login/oauth2/code/github

General:

  1. Request URL: http://localhost:8081/login/oauth2/code/github?code=6ab6a582334378ef9fe9&state=iTdcnTtCIwpC36VQoWEt_3We2E8DUNdk488QlDdRHa8%3D
  2. Request Method: GET
  3. Status Code: 302
  4. Remote Address: [::1]:8081
  5. Referrer Policy: strict-origin-when-cross-origin

Request:

  1. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  2. Accept-Encoding: gzip, deflate, br
  3. Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6
  4. Cache-Control: max-age=0
  5. Connection: keep-alive
  6. Cookie: JSESSIONID=118A9185FDC6BA0D68C20B62583C5BBF
  7. Host: localhost:8081
  8. Sec-Fetch-Dest: document
  9. Sec-Fetch-Mode: navigate
  10. Sec-Fetch-Site: none
  11. Sec-Fetch-User: ?1
  12. Upgrade-Insecure-Requests: 1
  13. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36

Response:

  1. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  2. Connection: keep-alive
  3. Content-Length: 0
  4. Date: Wed, 20 May 2020 10:00:15 GMT
  5. Expires: 0
  6. Keep-Alive: timeout=60
  7. Location: http://localhost:8081/
  8. Pragma: no-cache
  9. Set-Cookie: JSESSIONID=074B338C2B61B85D31F71F64FC4362FB; Path=/; HttpOnly
  10. X-Content-Type-Options: nosniff
  11. X-Frame-Options: DENY
  12. X-XSS-Protection: 1; mode=block

拿到授权码之后在客户端服务器内部完成向授权服务器Token的请求,成功后发送homepage的重定向响应。

👌Ⅴ重定向至首页

General:

  1. Request URL: http://localhost:8081/
  2. Request Method: GET
  3. Status Code: 200
  4. Remote Address: [::1]:8081
  5. Referrer Policy: strict-origin-when-cross-origin

Request:

  1. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  2. Accept-Encoding: gzip, deflate, br
  3. Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6
  4. Cache-Control: max-age=0
  5. Connection: keep-alive
  6. Cookie: JSESSIONID=074B338C2B61B85D31F71F64FC4362FB
  7. Host: localhost:8081
  8. Sec-Fetch-Dest: document
  9. Sec-Fetch-Mode: navigate
  10. Sec-Fetch-Site: none
  11. Sec-Fetch-User: ?1
  12. Upgrade-Insecure-Requests: 1
  13. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36

Response:

  1. Accept-Ranges: bytes
  2. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  3. Connection: keep-alive
  4. Content-Language: en-GB
  5. Content-Length: 609
  6. Content-Type: text/html;charset=UTF-8
  7. Date: Wed, 20 May 2020 10:00:15 GMT
  8. Expires: 0
  9. Keep-Alive: timeout=60
  10. Last-Modified: Wed, 20 May 2020 08:29:31 GMT
  11. Pragma: no-cache
  12. Vary: Origin
  13. Vary: Access-Control-Request-Method
  14. Vary: Access-Control-Request-Headers
  15. X-Content-Type-Options: nosniff
  16. X-Frame-Options: DENY
  17. X-XSS-Protection: 1; mode=block

至此整个认证的流程结束,用户认证信息保存在Spring Security context

👌Ⅵ认证过程请求链

image.png

🤙增加欢迎页、登出

接下来改造应用,使用户可以选择App认证,并且可以登出。

👌主页改造

body内容更改为下面的:

  1. <div class="container unauthenticated">
  2. With GitHub: <a href="/oauth2/authorization/github">click here</a>
  3. </div>
  4. <!--添加一个google的登录选项-->
  5. <div>
  6. With Google: <a href="/oauth2/authorization/google">click here</a>
  7. </div>
  8. <div class="container authenticated" style="display:none">
  9. Logged in as: <span id="user"></span>
  10. <div>
  11. <button onClick="logout()" class="btn btn-primary">Logout</button>
  12. </div>
  13. </div>

添加脚本:

  1. <script type="text/javascript">
  2. $.ajaxSetup({
  3. beforeSend: function (xhr, settings) {
  4. if (settings.type == 'POST' || settings.type == 'PUT'
  5. || settings.type == 'DELETE') {
  6. if (!(/^http:.*/.test(settings.url) || /^https:.*/
  7. .test(settings.url))) {
  8. // Only send the token to relative URLs i.e. locally.
  9. xhr.setRequestHeader("X-XSRF-TOKEN",
  10. Cookies.get('XSRF-TOKEN'));
  11. }
  12. }
  13. }
  14. });
  15. $.get("/user", function (data) {
  16. $("#user").html(data.name);
  17. $(".unauthenticated").hide()
  18. $(".authenticated").show()
  19. });
  20. var logout = function () {
  21. $.post("/logout", function () {
  22. $("#user").html('');
  23. $(".unauthenticated").show();
  24. $(".authenticated").hide();
  25. })
  26. return true;
  27. }
  28. </script>

👌后端改造

增加 /user Endpoint、登出处理、csrf、白名单

  1. package com.example.springbootoauth;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.http.HttpStatus;
  5. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  6. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  7. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  8. import org.springframework.security.oauth2.core.user.OAuth2User;
  9. import org.springframework.security.web.authentication.HttpStatusEntryPoint;
  10. import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
  11. import org.springframework.web.bind.annotation.GetMapping;
  12. import org.springframework.web.bind.annotation.RestController;
  13. import java.util.Collections;
  14. import java.util.Map;
  15. @RestController
  16. @SpringBootApplication
  17. public class SpringbootOauthApplication extends WebSecurityConfigurerAdapter {
  18. // 用于欢迎页显示用户信息
  19. @GetMapping("/user")
  20. public Map<String, Object> user (@AuthenticationPrincipal OAuth2User principal) {
  21. return Collections.singletonMap("name", principal.getAttribute("name"));
  22. }
  23. @Override
  24. protected void configure (HttpSecurity http) throws Exception {
  25. // @formatter:off
  26. // 配置除了"/"(新增的,因为首页需要让用户选择app), "/favicon.ico", "/error", "/webjars/**"这些地址不需经过认证之外其他所有都需认证
  27. // 配置所有认证失败返回401状态码(默认403状态码)
  28. // 配置OAuth2.0认证
  29. http.authorizeRequests(a ->
  30. a.antMatchers("/", "/favicon.ico", "/error", "/webjars/**").permitAll().anyRequest().authenticated()
  31. ).exceptionHandling(e ->
  32. e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
  33. ).oauth2Login();
  34. // 新增
  35. http.logout(l
  36. -> l.logoutSuccessUrl("/").permitAll()
  37. ).csrf(c ->
  38. c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
  39. );
  40. // 结束
  41. // @formatter:on
  42. }
  43. public static void main (String[] args) {
  44. SpringApplication.run(SpringbootOauthApplication.class, args);
  45. }
  46. }

👌测试

🖥重启项目,首页成了以下模样
image.png
⌨️点击登录,变成了这样
image.png
⌨️点击登出,又变成了这样
image.png

🤙增加组织认证

Github认证成功之后,查询Github的组织信息,根据组织信息判断是否通过认证。

  1. @Bean
  2. public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
  3. DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
  4. return request -> {
  5. OAuth2User user = delegate.loadUser(request);
  6. if (!"github".equals(request.getClientRegistration().getRegistrationId())) {
  7. return user;
  8. }
  9. OAuth2AuthorizedClient client = new OAuth2AuthorizedClient
  10. (request.getClientRegistration(), user.getName(), request.getAccessToken());
  11. // 这个属性是userInfoUri接口返回的信息中有的
  12. String url = user.getAttribute("organizations_url");
  13. if (url != null) {
  14. try {
  15. // 请求并读取组织信息并判断
  16. URL urlurl = new URL(url);
  17. HttpURLConnection httpURLConnection = (HttpURLConnection) urlurl.openConnection();
  18. httpURLConnection.setRequestMethod(HttpMethod.GET.name());
  19. httpURLConnection.connect();
  20. if (httpURLConnection.getResponseCode() == 200) {
  21. try (InputStream inputStream = httpURLConnection.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
  22. String result;
  23. StringBuilder sb = new StringBuilder();
  24. while ((result = bufferedReader.readLine()) != null) {
  25. sb.append(result);
  26. }
  27. ObjectMapper objectMapper = new ObjectMapper();
  28. List r = objectMapper.readValue(sb.toString(), List.class);
  29. if (r.size() != 0) {
  30. for (Object o : r) {
  31. if (o.equals("my_team")) {
  32. return user;
  33. }
  34. }
  35. }
  36. }
  37. }
  38. } catch (IOException e) {
  39. return user;
  40. }
  41. }
  42. throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token", "Not in Team", ""));
  43. };
  44. }

👋🏻未认证用户错误页面

⛔️增加认证失败处理器

  1. http.authorizeRequests(a -> a.antMatchers("/", "/favicon.ico", "/error", "/webjars/**").permitAll().anyRequest().authenticated()).exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))).oauth2Login(o -> o.failureHandler((request, response, exception) -> {
  2. // 错误信息放session里并且返回重定向
  3. request.getSession().setAttribute("error.message", exception.getMessage());
  4. response.sendRedirect("/");
  5. }));

⛔️增加error endpoint

  1. @GetMapping("/error")
  2. public String error(HttpServletRequest request) {
  3. String message = (String) request.getSession().getAttribute("error.message");
  4. request.getSession().removeAttribute("error.message");
  5. return message;
  6. }

⛔️首页增加消息展示区域

  1. <div class="container text-danger error"></div>
  1. $.get("/error", function(data) {
  2. if (data) {
  3. $(".error").html(data);
  4. } else {
  5. $(".error").html('');
  6. }
  7. });

⛔️测试

因为前面增加了组织认证,而我的Github并没有组织,逻辑是没有组织就验证不通过,所以我们请求下认证然后验证下认证错误页面
image.png

🥰最终的启动类:

  1. package com.example.springbootoauth;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.http.HttpMethod;
  7. import org.springframework.http.HttpStatus;
  8. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  10. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  11. import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
  12. import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
  13. import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
  14. import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
  15. import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
  16. import org.springframework.security.oauth2.core.OAuth2Error;
  17. import org.springframework.security.oauth2.core.user.OAuth2User;
  18. import org.springframework.security.web.authentication.HttpStatusEntryPoint;
  19. import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
  20. import org.springframework.web.bind.annotation.GetMapping;
  21. import org.springframework.web.bind.annotation.RestController;
  22. import javax.servlet.http.HttpServletRequest;
  23. import java.io.BufferedReader;
  24. import java.io.IOException;
  25. import java.io.InputStream;
  26. import java.io.InputStreamReader;
  27. import java.net.HttpURLConnection;
  28. import java.net.URL;
  29. import java.util.Collections;
  30. import java.util.List;
  31. import java.util.Map;
  32. @RestController
  33. @SpringBootApplication
  34. public class SpringbootOauthClientApplication extends WebSecurityConfigurerAdapter {
  35. @GetMapping("/user")
  36. public Map<String, Object> user (@AuthenticationPrincipal OAuth2User principal) {
  37. return Collections.singletonMap("name", principal.getAttribute("name"));
  38. }
  39. @GetMapping("/error")
  40. public String error (HttpServletRequest request) {
  41. String message = (String) request.getSession().getAttribute("error.message");
  42. request.getSession().removeAttribute("error.message");
  43. return message;
  44. }
  45. @Override
  46. protected void configure (HttpSecurity http) throws Exception {
  47. // @formatter:off
  48. http.authorizeRequests(a -> a.antMatchers("/", "/favicon.ico", "/error", "/webjars/**").permitAll().anyRequest().authenticated()).exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))).oauth2Login(o -> o.failureHandler((request, response, exception) -> {
  49. request.getSession().setAttribute("error.message", exception.getMessage());
  50. response.sendRedirect("/");
  51. }));
  52. http.logout(l
  53. -> l.logoutSuccessUrl("/").permitAll()
  54. ).csrf(c ->
  55. c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
  56. );
  57. // @formatter:on
  58. }
  59. public static void main (String[] args) {
  60. SpringApplication.run(SpringbootOauthClientApplication.class, args);
  61. }
  62. @Bean
  63. public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService () {
  64. DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
  65. return request -> {
  66. OAuth2User user = delegate.loadUser(request);
  67. if (!"github".equals(request.getClientRegistration().getRegistrationId())) {
  68. return user;
  69. }
  70. OAuth2AuthorizedClient client = new OAuth2AuthorizedClient(request.getClientRegistration(), user.getName(), request.getAccessToken());
  71. String url = user.getAttribute("organizations_url");
  72. if (url != null) {
  73. try {
  74. URL urlurl = new URL(url);
  75. HttpURLConnection httpURLConnection = (HttpURLConnection) urlurl.openConnection();
  76. httpURLConnection.setRequestMethod(HttpMethod.GET.name());
  77. httpURLConnection.connect();
  78. if (httpURLConnection.getResponseCode() == 200) {
  79. try (InputStream inputStream = httpURLConnection.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
  80. String result;
  81. StringBuilder sb = new StringBuilder();
  82. while ((result = bufferedReader.readLine()) != null) {
  83. sb.append(result);
  84. }
  85. ObjectMapper objectMapper = new ObjectMapper();
  86. List r = objectMapper.readValue(sb.toString(), List.class);
  87. if (r.size() != 0) {
  88. for (Object o : r) {
  89. if (o.equals("my_team")) {
  90. return user;
  91. }
  92. }
  93. }
  94. }
  95. }
  96. } catch (IOException e) {
  97. return user;
  98. }
  99. }
  100. throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token", "Not in Team", ""));
  101. };
  102. }
  103. }

🥰最终的首页:

  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8"/>
  5. <meta content="IE=edge" http-equiv="X-UA-Compatible"/>
  6. <title>Demo</title>
  7. <meta content="" name="description"/>
  8. <meta content="width=device-width" name="viewport"/>
  9. <base href="/"/>
  10. <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
  11. <script src="/webjars/jquery/jquery.min.js" type="text/javascript"></script>
  12. <script src="/webjars/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
  13. <script src="/webjars/js-cookie/js.cookie.js" type="text/javascript"></script>
  14. </head>
  15. <body>
  16. <h1>Demo</h1>
  17. <div class="container unauthenticated">
  18. <div>
  19. With GitHub: <a href="/oauth2/authorization/github">click here</a>
  20. </div>
  21. <div>
  22. With Google: <a href="/oauth2/authorization/google">click here</a>
  23. </div>
  24. </div>
  25. <div class="container authenticated" style="display:none">
  26. Logged in as: <span id="user"></span>
  27. <div>
  28. <button onClick="logout()" class="btn btn-primary">Logout</button>
  29. </div>
  30. </div>
  31. <div class="container text-danger error"></div>
  32. </body>
  33. <script type="text/javascript">
  34. $.ajaxSetup({
  35. beforeSend: function (xhr, settings) {
  36. if (settings.type == 'POST' || settings.type == 'PUT'
  37. || settings.type == 'DELETE') {
  38. if (!(/^http:.*/.test(settings.url) || /^https:.*/
  39. .test(settings.url))) {
  40. // Only send the token to relative URLs i.e. locally.
  41. xhr.setRequestHeader("X-XSRF-TOKEN",
  42. Cookies.get('XSRF-TOKEN'));
  43. }
  44. }
  45. }
  46. });
  47. $.get("/user", function (data) {
  48. $("#user").html(data.name);
  49. $(".unauthenticated").hide()
  50. $(".authenticated").show()
  51. });
  52. $.get("/error", function (data) {
  53. if (data) {
  54. $(".error").html(data);
  55. } else {
  56. $(".error").html('');
  57. }
  58. });
  59. var logout = function () {
  60. $.post("/logout", function () {
  61. $("#user").html('');
  62. $(".unauthenticated").show();
  63. $(".authenticated").hide();
  64. })
  65. return true;
  66. }
  67. </script>
  68. </html>

🥰最终的配置:

  1. server:
  2. port: 8081
  3. spring:
  4. security:
  5. oauth2:
  6. client:
  7. registration:
  8. github:
  9. clientId: "github-client-id"
  10. clientSecret: "github-client-secret"
  11. google:
  12. client-id: "google-client-id"
  13. client-secret: "google-client-secret"

😭完

🤕服务端

Spring Security OAuth 2.x被官方弃用(作者认为授权服务应当是一个专业的项目,而不应该做在框架里) Spring Security 5.2.x提供OAuth2的Client、ResourceServer支持,但是不提供AuthorizationServer支持 官方提供了从Spring Security OAuth 2.x迁移至Spring Security 5.2.x的文档,链接 社区与开发人员仍在对线中…..Spring Security OAuth 会在2021年5月正式停止更新及维护 不过目前有AuthorizationServer的社区版项目,链接

上面客户端介绍中使用的是Spring Security 5.x 为了简单,服务端仍然使用Spring Security OAuth 2.x Actually,管他弃不弃用,我们仍然能很开心的使用

😆创建项目

略。

示例项目地址

🥺项目结构

image.png

📚视图

服务端有四个视图:

  • index.jsp 用于登录成功后的登出
  • login.jsp 用于登录
  • access_confirmation.jsp 用于确认授权
  • oauth_error.jsp 用于显示授权错误信息

📖POM

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>springboot-demo</artifactId>
  7. <groupId>com.example</groupId>
  8. <version>0.0.1-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>springboot-oauth-server</artifactId>
  12. <dependencies>
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-tomcat</artifactId>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.apache.tomcat.embed</groupId>
  19. <artifactId>tomcat-embed-jasper</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>javax.servlet</groupId>
  23. <artifactId>jstl</artifactId>
  24. </dependency>
  25. <dependency>
  26. <groupId>javax.servlet</groupId>
  27. <artifactId>javax.servlet-api</artifactId>
  28. </dependency>
  29. <dependency>
  30. <groupId>javax.servlet.jsp</groupId>
  31. <artifactId>javax.servlet.jsp-api</artifactId>
  32. <version>2.3.1</version>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.webjars</groupId>
  36. <artifactId>jquery</artifactId>
  37. <version>3.4.1</version>
  38. </dependency>
  39. <dependency>
  40. <groupId>org.webjars</groupId>
  41. <artifactId>bootstrap</artifactId>
  42. <version>4.3.1</version>
  43. </dependency>
  44. <dependency>
  45. <groupId>org.webjars</groupId>
  46. <artifactId>js-cookie</artifactId>
  47. <version>2.1.0</version>
  48. </dependency>
  49. <dependency>
  50. <groupId>org.webjars</groupId>
  51. <artifactId>webjars-locator-core</artifactId>
  52. </dependency>
  53. <dependency>
  54. <groupId>org.springframework.security</groupId>
  55. <artifactId>spring-security-taglibs</artifactId>
  56. </dependency>
  57. <dependency>
  58. <groupId>org.springframework.security.oauth</groupId>
  59. <artifactId>spring-security-oauth2</artifactId>
  60. <version>2.3.6.RELEASE</version>
  61. <!-- <version>[2.3.6.RELEASE,)</version>-->
  62. </dependency>
  63. <dependency>
  64. <groupId>org.springframework.boot</groupId>
  65. <artifactId>spring-boot-starter-web</artifactId>
  66. </dependency>
  67. <dependency>
  68. <groupId>org.springframework.boot</groupId>
  69. <artifactId>spring-boot-devtools</artifactId>
  70. <optional>true</optional>
  71. <scope>true</scope>
  72. </dependency>
  73. </dependencies>
  74. <build>
  75. <resources>
  76. <resource>
  77. <directory>src/main/resources</directory>
  78. <includes>
  79. <include>**/*</include>
  80. </includes>
  81. </resource>
  82. </resources>
  83. <plugins>
  84. <plugin>
  85. <groupId>org.springframework.boot</groupId>
  86. <artifactId>spring-boot-maven-plugin</artifactId>
  87. <configuration>
  88. <fork>true</fork>
  89. </configuration>
  90. </plugin>
  91. </plugins>
  92. </build>
  93. </project>

🔧配置

  1. server:
  2. port: 8082
  3. spring:
  4. mvc:
  5. view:
  6. suffix: ".jsp"

👏启动类

  1. package com.example.springbootoauth;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.http.HttpEntity;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.ResponseEntity;
  8. import org.springframework.security.authentication.AuthenticationManager;
  9. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  10. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  11. import org.springframework.security.config.annotation.web.builders.WebSecurity;
  12. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  13. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  14. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  15. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  16. import org.springframework.security.crypto.password.PasswordEncoder;
  17. import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
  18. import org.springframework.web.bind.annotation.GetMapping;
  19. import org.springframework.web.bind.annotation.RestController;
  20. import java.security.Principal;
  21. import java.util.Collections;
  22. import java.util.Map;
  23. @RestController
  24. @SpringBootApplication
  25. @EnableWebSecurity(debug = true)
  26. //@EnableWebMvc
  27. public class SpringbootOauthServerApplication extends WebSecurityConfigurerAdapter {
  28. // 这是授权服务器的userInfoUri映射,用于成功获取token之后获取用户信息
  29. // userInfoUri是Provider的一个必须提供的信息
  30. @GetMapping("/user")
  31. public HttpEntity<Map<String, String>> user (@AuthenticationPrincipal Principal principal) {
  32. return new ResponseEntity<>(Collections.singletonMap("name", principal.getName()), HttpStatus.OK);
  33. }
  34. @Override
  35. protected void configure (HttpSecurity http) throws Exception {
  36. // @formatter:off
  37. // 除了login.jsp外其他任何请求都需要有USER角色
  38. // 请求拒绝跳转页面是/login.jsp?authorization_error=true
  39. // 禁用/oauth/authorize的csrf
  40. // 登出地址为/logout
  41. // 登出成功过地址为/login.jsp
  42. // 登录处理地址是/login
  43. // 登录失败跳转地址是/login.jsp?authentication_error=true
  44. // 登录页面为/login.jsp
  45. // 关闭nosniff header
  46. http.authorizeRequests()
  47. .antMatchers("/login.jsp")
  48. .permitAll()
  49. .anyRequest()
  50. .hasRole("USER")
  51. .and()
  52. .exceptionHandling()
  53. .accessDeniedPage("/login.jsp?authorization_error=true")
  54. .and()
  55. .csrf()
  56. .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/oauth/authorize"))
  57. .disable()
  58. .logout()
  59. .logoutUrl("/logout")
  60. .logoutSuccessUrl("/login.jsp")
  61. .and()
  62. .formLogin()
  63. .loginProcessingUrl("/login")
  64. // .successForwardUrl("/index.jsp")
  65. .failureUrl("/login.jsp?authentication_error=true").loginPage("/login.jsp").and()
  66. // 关闭nosniff
  67. .headers().contentTypeOptions().disable();
  68. // @formatter:on
  69. }
  70. @Override
  71. public void configure (WebSecurity web) {
  72. // 忽略这些资源
  73. web.ignoring().antMatchers("/webjars/**", "/favicon.ico", "/images/**");
  74. }
  75. @Override
  76. protected void configure (AuthenticationManagerBuilder auth) throws Exception {
  77. // 添加一些用户测试用,到时可以直接登录
  78. auth.inMemoryAuthentication()
  79. .withUser("balala")
  80. // 密码balabala(BCrypt摘要)
  81. .password("$2a$10$QOhLotan6kumoHRp8Z2ajOZfBhKgaMvgqeaxrSXxrKN2FgR3hlJ9.")
  82. .roles("USER")
  83. .and()
  84. .withUser("moxianbao")
  85. // 密码moxianbao
  86. .password("$2a$10$RpHbnvFdFoUgpK89iVnSzugfqZAfDeKvVKp3LoVBxCj6fuwqtdBlO")
  87. .roles("USER");
  88. }
  89. public static void main (String[] args) {
  90. SpringApplication.run(SpringbootOauthServerApplication.class, args);
  91. }
  92. @Bean
  93. @Override
  94. public AuthenticationManager authenticationManagerBean () throws Exception {
  95. return super.authenticationManagerBean();
  96. }
  97. @Bean
  98. public PasswordEncoder passwordEncoder () {
  99. return new BCryptPasswordEncoder();
  100. }
  101. }

😘服务器端配置

  1. package com.example.springbootoauth;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.*;
  4. import org.springframework.security.authentication.AuthenticationManager;
  5. import org.springframework.security.crypto.password.PasswordEncoder;
  6. import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  7. import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  8. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  9. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  10. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  11. import org.springframework.security.oauth2.provider.ClientDetailsService;
  12. import org.springframework.security.oauth2.provider.approval.ApprovalStore;
  13. import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
  14. import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
  15. import org.springframework.security.oauth2.provider.token.TokenStore;
  16. import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
  17. @Configuration
  18. @EnableAuthorizationServer
  19. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  20. @Autowired
  21. private AuthenticationManager authenticationManager;
  22. @Autowired
  23. private PasswordEncoder passwordEncoder;
  24. @Autowired
  25. private UserApprovalHandler userApprovalHandler;
  26. @Autowired
  27. private TokenStore tokenStore;
  28. @Autowired
  29. private ClientDetailsService clientDetailsService;
  30. @Override
  31. public void configure (AuthorizationServerSecurityConfigurer security) throws Exception {
  32. super.configure(security);
  33. }
  34. @Override
  35. public void configure (ClientDetailsServiceConfigurer clients) throws Exception {
  36. super.configure(clients);
  37. // 这里的客户端信息相当于前面客户端在授权服务端申请的OAuth app
  38. // demo以及encode过的abc相当于 client_id和client_secret
  39. // redirectUri就是服务器端发完code之后重定向的客户端地址
  40. // authorizedGrantTypes是支持此客户端的认证类型
  41. // scopes与资源服务器配置有关,确定了你这个客户端可以访问哪些资源
  42. // resourceIds与资源服务器配置有关,必须一致
  43. clients.inMemory()
  44. .withClient("demo")
  45. .secret(passwordEncoder
  46. .encode("abc"))
  47. .authorizedGrantTypes("authorization_code", "password")
  48. .resourceIds("rg_demo")
  49. .scopes("read")
  50. .redirectUris("http://localhost:8081/login/oauth2/code/moxianbao");
  51. }
  52. @Bean
  53. TokenStore tokenStore () {
  54. return new InMemoryTokenStore();
  55. }
  56. @Override
  57. public void configure (AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  58. super.configure(endpoints);
  59. //password grants are switched on by injecting an AuthenticationManager
  60. endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore()).userApprovalHandler(userApprovalHandler);
  61. //refresh_token
  62. // .userDetailsService(userDetailsService);
  63. }
  64. @Bean
  65. public ApprovalStore approvalStore () {
  66. TokenApprovalStore store = new TokenApprovalStore();
  67. store.setTokenStore(tokenStore);
  68. return store;
  69. }
  70. // 这个bean用来记住approval、以及automatic approval,根据认证的user和client去查找保存的token,从token中查询请求的scope是否授权
  71. @Bean
  72. @Lazy
  73. @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
  74. public UserApprovalHandler userApprovalHandler () throws Exception {
  75. UserApprovalHandler handler = new UserApprovalHandler();
  76. handler.setApprovalStore(approvalStore());
  77. handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
  78. handler.setClientDetailsService(clientDetailsService);
  79. handler.setUseApprovalStore(true);
  80. return handler;
  81. }
  82. }

UserApprovalHandler

  1. package com.example.springbootoauth;
  2. import org.springframework.security.core.Authentication;
  3. import org.springframework.security.oauth2.provider.AuthorizationRequest;
  4. import org.springframework.security.oauth2.provider.ClientDetails;
  5. import org.springframework.security.oauth2.provider.ClientDetailsService;
  6. import org.springframework.security.oauth2.provider.ClientRegistrationException;
  7. import org.springframework.security.oauth2.provider.approval.ApprovalStoreUserApprovalHandler;
  8. import java.util.Collection;
  9. public class UserApprovalHandler extends ApprovalStoreUserApprovalHandler {
  10. private boolean useApprovalStore = true;
  11. private ClientDetailsService clientDetailsService;
  12. /**
  13. * Service to load client details (optional) for auto approval checks.
  14. *
  15. * @param clientDetailsService a client details service
  16. */
  17. public void setClientDetailsService (ClientDetailsService clientDetailsService) {
  18. this.clientDetailsService = clientDetailsService;
  19. super.setClientDetailsService(clientDetailsService);
  20. }
  21. /**
  22. * @param useApprovalStore the useTokenServices to set
  23. */
  24. public void setUseApprovalStore (boolean useApprovalStore) {
  25. this.useApprovalStore = useApprovalStore;
  26. }
  27. /**
  28. * Allows automatic approval for a white list of clients in the implicit grant case.
  29. *
  30. * @param authorizationRequest The authorization request.
  31. * @param userAuthentication the current user authentication
  32. * @return An updated request if it has already been approved by the current user.
  33. */
  34. @Override
  35. public AuthorizationRequest checkForPreApproval (AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
  36. boolean approved = false;
  37. // If we are allowed to check existing approvals this will short circuit the decision
  38. if (useApprovalStore) {
  39. authorizationRequest = super.checkForPreApproval(authorizationRequest, userAuthentication);
  40. approved = authorizationRequest.isApproved();
  41. } else {
  42. if (clientDetailsService != null) {
  43. Collection<String> requestedScopes = authorizationRequest.getScope();
  44. try {
  45. ClientDetails client = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
  46. for (String scope : requestedScopes) {
  47. if (client.isAutoApprove(scope)) {
  48. approved = true;
  49. break;
  50. }
  51. }
  52. } catch (ClientRegistrationException e) {
  53. System.out.println(e.getMessage());
  54. }
  55. }
  56. }
  57. authorizationRequest.setApproved(approved);
  58. return authorizationRequest;
  59. }
  60. }

AccessConfirmationController

userApprovalPage和errorPage的endpoint:

  1. package com.example.springbootoauth;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.security.oauth2.common.util.OAuth2Utils;
  4. import org.springframework.security.oauth2.provider.AuthorizationRequest;
  5. import org.springframework.security.oauth2.provider.ClientDetails;
  6. import org.springframework.security.oauth2.provider.ClientDetailsService;
  7. import org.springframework.security.oauth2.provider.approval.Approval;
  8. import org.springframework.security.oauth2.provider.approval.ApprovalStore;
  9. import org.springframework.stereotype.Controller;
  10. import org.springframework.web.bind.annotation.RequestMapping;
  11. import org.springframework.web.bind.annotation.SessionAttributes;
  12. import org.springframework.web.servlet.ModelAndView;
  13. import java.security.Principal;
  14. import java.util.LinkedHashMap;
  15. import java.util.Map;
  16. @Controller
  17. @SessionAttributes("authorizationRequest")
  18. public class AccessConfirmationController {
  19. private ClientDetailsService clientDetailsService;
  20. private ApprovalStore approvalStore;
  21. @RequestMapping("/oauth/confirm_access")
  22. public ModelAndView getAccessConfirmation (Map<String, Object> model, Principal principal) {
  23. AuthorizationRequest clientAuth = (AuthorizationRequest) model.remove("authorizationRequest");
  24. ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
  25. model.put("auth_request", clientAuth);
  26. model.put("client", client);
  27. Map<String, String> scopes = new LinkedHashMap<>();
  28. for (String scope : clientAuth.getScope()) {
  29. scopes.put(OAuth2Utils.SCOPE_PREFIX + scope, "false");
  30. }
  31. for (Approval approval : approvalStore.getApprovals(principal.getName(), client.getClientId())) {
  32. if (clientAuth.getScope().contains(approval.getScope())) {
  33. scopes.put(OAuth2Utils.SCOPE_PREFIX + approval.getScope(), approval.getStatus() == Approval.ApprovalStatus.APPROVED ? "true" : "false");
  34. }
  35. }
  36. model.put("scopes", scopes);
  37. return new ModelAndView("/access_confirmation", model);
  38. }
  39. @RequestMapping("/oauth/error")
  40. public String handleError (Map<String, Object> model) {
  41. // We can add more stuff to the model here for JSP rendering. If the client was a machine then
  42. // the JSON will already have been rendered.
  43. model.put("message", "There was a problem with the OAuth2 protocol");
  44. return "/oauth_error";
  45. }
  46. @Autowired
  47. public void setClientDetailsService (ClientDetailsService clientDetailsService) {
  48. this.clientDetailsService = clientDetailsService;
  49. }
  50. @Autowired
  51. public void setApprovalStore (ApprovalStore approvalStore) {
  52. this.approvalStore = approvalStore;
  53. }
  54. }

😙资源服务器配置

  1. package com.example.springbootoauth;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  5. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
  6. import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
  7. import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
  8. import org.springframework.security.oauth2.provider.token.TokenStore;
  9. @Configuration
  10. @EnableResourceServer
  11. public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  12. @Autowired
  13. TokenStore tokenStore;
  14. @Override
  15. public void configure (ResourceServerSecurityConfigurer resources) throws Exception {
  16. super.configure(resources);
  17. // 资源服务器的id
  18. // 拥有此resourceId的客户端授权可以访问此资源服务器的资源
  19. // stateless标识是否只有在token-based认证时才可以使用资源
  20. resources.resourceId("rg_demo").stateless(false).tokenStore(tokenStore);
  21. // .authenticationEntryPoint();
  22. }
  23. @Override
  24. public void configure (HttpSecurity http) throws Exception {
  25. // security DSL
  26. // 在启动类有一个/user的endpoint,配置此endpoint只有在has read scope或 has ROLE_USER role时才可以访问
  27. http.requestMatchers().antMatchers("/user/**").and().authorizeRequests().antMatchers("/user").access("#oauth2.hasScope('read') or #hasRole('ROLE_USER')");
  28. }
  29. }

你可以提供多个实现此接口的实例(如果实例间有相同的配置,最后一个配置生效,也就是说你可以用@Order),这样就能达到资源分组的目的,比如上面的resourceId是rg_demo,你可以再来一个叫rg_pro,resourceId是客户端必须的配置,可以有多个。

😙方法安全配置

  1. package com.example.springbootoauth;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
  4. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  5. import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
  6. import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
  7. @Configuration
  8. @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
  9. public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
  10. @Override
  11. protected MethodSecurityExpressionHandler createExpressionHandler () {
  12. return new OAuth2MethodSecurityExpressionHandler();
  13. }
  14. }

我们需要覆盖默认的方法安全表达式处理器,不然没法用oauth2.hasScope SpEl。

😙客户端配置

上面的内容我们已经完成了服务器端以及资源服务器端的简单配置,接下来我们需要更改客户端,增加我们的授权服务器信息

😙增加Provider以及Registration

  1. server:
  2. port: 8081
  3. spring:
  4. security:
  5. oauth2:
  6. client:
  7. provider:
  8. moxianbao:
  9. # 注意不要在同一host,否则session将被覆盖,无法正常认证
  10. authorizationUri: "http://10.25.92.50:8082/oauth/authorize"
  11. tokenUri: "http://10.25.92.50:8082/oauth/token"
  12. userInfoUri: "http://10.25.92.50:8082/user"
  13. clientName: "moxianbao"
  14. userNameAttribute: "name"
  15. registration:
  16. github:
  17. clientId: "github-client-id"
  18. clientSecret: "github-client-secret"
  19. google:
  20. client-id: "google-client-id"
  21. client-secret: "google-client-secret"
  22. moxianbao:
  23. clientId: "demo"
  24. clientSecret: "abc"
  25. authorizationGrantType: "authorization_code"
  26. redirectUri: "{baseUrl}/{action}/oauth2/code/{registrationId}"

一定要注意的是授权服务器与客户端绝对不能在一个host下,否则JSESSIONID会覆盖,不可能认证成功。
这里的moxianbao client provider提供了授权服务器的基本信息,包括认证地址、token地址等。
这里的moxianbao client registration要与我们在授权服务器中内置的那个client信息保持一致。

😙首页增加魔仙堡登录选项

  1. <div>
  2. With moxianbao: <a href="/oauth2/authorization/moxianbao">click here</a>
  3. </div>

🙃测试

🙃浏览器测试:

客户端首页:
image.png
点击使用moxianbao登录。

服务端登录页面:
image.png
点击Login。

服务端Approval页面:
image.png
选Approve,点Submit。

客户端首页:
image.png
返回客户端,并读取用户信息,授权成功。

🙃Postman测试

image.png

选择TYPE为OAuth2.0,点击Get New Access Token

image.png

填写信息,点击Request Token

因为我们浏览器测试时候已经Approve了,所以这里直接申请成功了(但是JSESSIONID是一样的奥:))

image.png

点击Use Token,然后我们去获取下用户信息

image.png

点击发送,可以看到用户信息的返回。

image.png

认证信息存在Header中,Authorization Bearer后面的就是Token:)

😭完

以上简单配置的基础上,再加改进,细粒度控制,持久化存储,就能成为一个”合格”的OAuth server了:)