├── .gitattributes ├── maven-version-rules.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── revengemission │ │ │ └── sso │ │ │ └── oauth2 │ │ │ └── client │ │ │ ├── Oauth2ClientApplication.java │ │ │ ├── controller │ │ │ └── FrontIndexController.java │ │ │ └── config │ │ │ ├── CustomAuthenticationSuccessHandler.java │ │ │ ├── RefreshExpiredTokenFilter.java │ │ │ └── SecurityConfig.java │ └── resources │ │ ├── templates │ │ ├── index.html │ │ └── securedPage.html │ │ ├── application.properties │ │ └── static │ │ └── assets │ │ └── localforage.min.js └── test │ └── java │ └── com │ └── revengemission │ └── sso │ └── oauth2 │ └── client │ └── ApplicationTests.java ├── .editorconfig ├── LICENSE ├── README.md ├── .gitignore └── pom.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | text=lf 3 | -------------------------------------------------------------------------------- /maven-version-rules.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | .*[-_\.](alpha|Alpha|ALPHA|b|beta|Beta|BETA|rc|RC|M|EA)[-_\.]?[0-9]* 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/revengemission/sso/oauth2/client/Oauth2ClientApplication.java: -------------------------------------------------------------------------------- 1 | package com.revengemission.sso.oauth2.client; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2client 8 | */ 9 | @SpringBootApplication 10 | public class Oauth2ClientApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(Oauth2ClientApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | spring security oauth2 client 6 | 7 | 8 | 9 |
10 |
11 |

spring security oauth2 client

12 | see personal information 13 |
14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | # EditorConfig is awesome: https://EditorConfig.org 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # Unix-style newlines with a newline ending every file 9 | [*] 10 | end_of_line = lf 11 | insert_final_newline = true 12 | charset = utf-8 13 | 14 | [*.java] 15 | indent_style = space 16 | indent_size = 4 17 | 18 | # Matches multiple files with brace expansion notation 19 | # Set default charset 20 | [*.{js,py}] 21 | charset = utf-8 22 | 23 | # 4 space indentation 24 | [*.py] 25 | indent_style = space 26 | indent_size = 4 27 | 28 | # Tab indentation (no size specified) 29 | [Makefile] 30 | indent_style = tab 31 | 32 | # Indentation override for all JS under lib directory 33 | [lib/**.js] 34 | indent_style = space 35 | indent_size = 2 36 | 37 | # Matches the exact files either package.json or .travis.yml 38 | [{package.json,.travis.yml}] 39 | indent_style = space 40 | indent_size = 2 41 | 42 | -------------------------------------------------------------------------------- /src/main/resources/templates/securedPage.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Spring Security SSO 7 | 8 | 9 | 10 |
11 |
12 |
13 |

Secured Page

14 | Welcome, Name
15 | authorities, Authorities
16 |
17 |
18 | 19 |
20 | 超级用户 21 |
22 | 23 |
24 | 普通用户 25 |
26 | 27 |
28 | logout 29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 mission 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 基于spring boot2 sso Oauth2 Client JWT 2 | ## 启动前修改application.properties中的相关参数 3 | ### 当client和server在一台主机时,请用域名访问,否则cookies会相互覆盖,影响测试,以下配置仅供参考 4 | * hosts文件 5 | ````hosts 6 | 127.0.0.1 client.sso.com 7 | 127.0.0.1 server.sso.com 8 | ```` 9 | * nginx配置 10 | ````nginx 11 | server { 12 | server_name client.sso.com; 13 | listen 80; 14 | listen [::]:80; 15 | 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header REMOTE-HOST $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | 21 | 22 | index index.html; 23 | 24 | location / { 25 | proxy_pass http://localhost:10480/; 26 | } 27 | } 28 | 29 | server { 30 | server_name server.sso.com; 31 | listen 80; 32 | listen [::]:80; 33 | 34 | proxy_set_header Host $host; 35 | proxy_set_header X-Real-IP $remote_addr; 36 | proxy_set_header REMOTE-HOST $remote_addr; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | 39 | 40 | index index.html; 41 | 42 | location / { 43 | proxy_pass http://localhost:10380/; 44 | } 45 | } 46 | ```` 47 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=10480 2 | 3 | server.servlet.session.cookie.name=my_session_id 4 | 5 | spring.security.oauth2.client.registration.sso-login.provider=sso-provider 6 | spring.security.oauth2.client.registration.sso-login.client-id=SampleClientId 7 | spring.security.oauth2.client.registration.sso-login.client-name=sample client application 8 | spring.security.oauth2.client.registration.sso-login.client-secret=tgb.258 9 | spring.security.oauth2.client.registration.sso-login.client-authentication-method=client_secret_post 10 | spring.security.oauth2.client.registration.sso-login.authorization-grant-type=authorization_code 11 | spring.security.oauth2.client.registration.sso-login.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} 12 | spring.security.oauth2.client.registration.sso-login.scope=profile 13 | 14 | spring.security.oauth2.client.provider.sso-provider.authorization-uri=http://localhost:35080/oauth2/authorize 15 | spring.security.oauth2.client.provider.sso-provider.token-uri=http://localhost:35080/oauth2/token 16 | spring.security.oauth2.client.provider.sso-provider.user-info-uri=http://localhost:35080/user/me 17 | spring.security.oauth2.client.provider.sso-provider.user-name-attribute=sub 18 | 19 | spring.thymeleaf.cache=false 20 | logging.level.root=info 21 | logging.level.org.springframework.security=debug 22 | logging.file.path=/data/logs/oauth2-client 23 | logging.file.max-history=45 24 | 25 | host.api=http://api.sso.com 26 | oauth2.token.cookie.domain=sso.com 27 | -------------------------------------------------------------------------------- /src/test/java/com/revengemission/sso/oauth2/client/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.revengemission.sso.oauth2.client; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.SerializationFeature; 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 6 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; 7 | import org.junit.jupiter.api.Disabled; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 11 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 12 | import org.springframework.web.client.RestTemplate; 13 | 14 | import java.time.LocalDateTime; 15 | import java.time.format.DateTimeFormatter; 16 | 17 | @SpringBootTest 18 | public class ApplicationTests { 19 | 20 | @Disabled 21 | @Test 22 | public void contextLoads() { 23 | 24 | //用登陆后的token ,请求api资源 25 | //header格式,Authorization : Bearer xxxxx 26 | 27 | RestTemplate restTemplate = new RestTemplate(); 28 | JavaTimeModule module = new JavaTimeModule(); 29 | LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer( 30 | DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); 31 | module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer); 32 | 33 | ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(module) 34 | .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build(); 35 | 36 | MappingJackson2HttpMessageConverter jsonMessageConverter = new MappingJackson2HttpMessageConverter(); 37 | jsonMessageConverter.setObjectMapper(objectMapper); 38 | restTemplate.getMessageConverters().add(0, jsonMessageConverter); 39 | 40 | // request some api 41 | 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###################### 2 | # Project Specific 3 | ###################### 4 | /target/www/** 5 | 6 | ###################### 7 | # Node 8 | ###################### 9 | /node/** 10 | /node_tmp/** 11 | /node_modules/** 12 | 13 | ###################### 14 | # SASS 15 | ###################### 16 | .sass-cache/** 17 | 18 | ###################### 19 | # Eclipse 20 | ###################### 21 | *.pydevproject 22 | .project 23 | .metadata 24 | /bin/** 25 | /tmp/** 26 | /tmp/**/* 27 | *.tmp 28 | *.bak 29 | *.swp 30 | *~.nib 31 | local.properties 32 | .classpath 33 | .settings/** 34 | .loadpath 35 | /src/main/resources/rebel.xml 36 | 37 | # External tool builders 38 | .externalToolBuilders/** 39 | 40 | # Locally stored "Eclipse launch configurations" 41 | *.launch 42 | 43 | # CDT-specific 44 | .cproject 45 | 46 | # PDT-specific 47 | .buildpath 48 | 49 | ###################### 50 | # Intellij 51 | ###################### 52 | .idea/ 53 | *.iml 54 | *.iws 55 | *.ipr 56 | *.ids 57 | *.orig 58 | **/target/** 59 | 60 | ###################### 61 | # Maven 62 | ###################### 63 | /log/** 64 | /target/** 65 | 66 | ###################### 67 | # Gradle 68 | ###################### 69 | .gradle/** 70 | 71 | ###################### 72 | # Package Files 73 | ###################### 74 | *.jar 75 | *.war 76 | *.ear 77 | *.db 78 | 79 | ###################### 80 | # Windows 81 | ###################### 82 | # Windows image file caches 83 | Thumbs.db 84 | 85 | # Folder config file 86 | Desktop.ini 87 | 88 | ###################### 89 | # Mac OSX 90 | ###################### 91 | .DS_Store 92 | .svn 93 | 94 | # Thumbnails 95 | ._* 96 | 97 | # Files that might appear on external disk 98 | .Spotlight-V100 99 | .Trashes 100 | 101 | ###################### 102 | # Directories 103 | ###################### 104 | /build/** 105 | /bin/ 106 | /spring_loaded/** 107 | /deploy/** 108 | 109 | ###################### 110 | # Logs 111 | ###################### 112 | *.log 113 | 114 | ###################### 115 | # Others 116 | ###################### 117 | *.class 118 | *.*~ 119 | *~ 120 | .merge_file* 121 | 122 | ###################### 123 | # Gradle Wrapper 124 | ###################### 125 | !gradle/wrapper/gradle-wrapper.jar 126 | 127 | ###################### 128 | # Maven Wrapper 129 | ###################### 130 | !.mvn/wrapper/maven-wrapper.jar 131 | 132 | ###################### 133 | # ESLint 134 | ###################### 135 | .eslintcache -------------------------------------------------------------------------------- /src/main/java/com/revengemission/sso/oauth2/client/controller/FrontIndexController.java: -------------------------------------------------------------------------------- 1 | package com.revengemission.sso.oauth2.client.controller; 2 | 3 | import org.springframework.boot.web.client.RestTemplateBuilder; 4 | import org.springframework.http.HttpEntity; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpMethod; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 10 | import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; 11 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.ui.Model; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.ResponseBody; 16 | import org.springframework.web.client.RestTemplate; 17 | 18 | 19 | @Controller 20 | public class FrontIndexController { 21 | 22 | RestTemplate restTemplate; 23 | 24 | public FrontIndexController(RestTemplateBuilder restTemplateBuilder) { 25 | super(); 26 | //可以全局配置converter等 27 | this.restTemplate = restTemplateBuilder.build(); 28 | } 29 | 30 | @GetMapping(value = {"/", "/index"}) 31 | public String index(Authentication authentication, 32 | Model model) { 33 | return "index"; 34 | } 35 | 36 | @GetMapping(value = "/user") 37 | public String user(OAuth2AuthenticationToken oAuth2AuthenticationToken, 38 | Authentication authentication, 39 | @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, 40 | Model model) { 41 | model.addAttribute("authentication", authentication); 42 | return "securedPage"; 43 | } 44 | 45 | @ResponseBody 46 | @GetMapping(value = "/resource") 47 | public Object resource(OAuth2AuthenticationToken oAuth2AuthenticationToken, 48 | @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, 49 | Model model) { 50 | String url = "http://localhost:10580/coupon/list"; 51 | HttpHeaders headers = new HttpHeaders(); 52 | headers.add("Authorization", "Bearer " + authorizedClient.getAccessToken().getTokenValue()); 53 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), Object.class); 54 | return response.getBody(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/revengemission/sso/oauth2/client/config/CustomAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.revengemission.sso.oauth2.client.config; 2 | 3 | import io.micrometer.core.instrument.util.StringUtils; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.Cookie; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 12 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; 13 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 14 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 15 | import org.springframework.security.web.savedrequest.HttpSessionRequestCache; 16 | import org.springframework.security.web.savedrequest.RequestCache; 17 | import org.springframework.security.web.savedrequest.SavedRequest; 18 | import org.springframework.stereotype.Component; 19 | 20 | import java.io.IOException; 21 | 22 | @Component 23 | public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { 24 | 25 | RequestCache requestCache = new HttpSessionRequestCache(); 26 | 27 | @Value("${oauth2.token.cookie.domain}") 28 | private String cookieDomain; 29 | 30 | 31 | @Autowired 32 | OAuth2AuthorizedClientService authorizedClientService; 33 | 34 | @Override 35 | public void onAuthenticationSuccess(HttpServletRequest request, 36 | HttpServletResponse response, Authentication authentication) 37 | throws IOException, ServletException { 38 | 39 | String redirectUrl = ""; 40 | SavedRequest savedRequest = requestCache.getRequest(request, response); 41 | if (savedRequest != null && StringUtils.isNotEmpty(savedRequest.getRedirectUrl())) { 42 | redirectUrl = savedRequest.getRedirectUrl(); 43 | } 44 | 45 | 46 | // 根据需要设置 cookie,js携带token直接访问api接口等 47 | if (authentication instanceof OAuth2AuthenticationToken) { 48 | OAuth2AuthorizedClient client = authorizedClientService 49 | .loadAuthorizedClient( 50 | ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(), 51 | authentication.getName()); 52 | String token = client.getAccessToken().getTokenValue(); 53 | Cookie tokenCookie = new Cookie("access_token", token); 54 | tokenCookie.setHttpOnly(true); 55 | tokenCookie.setDomain(cookieDomain); 56 | tokenCookie.setPath("/"); 57 | response.addCookie(tokenCookie); 58 | } 59 | 60 | //设置回调成功的页面, 61 | if (StringUtils.isNotEmpty(redirectUrl)) { 62 | super.onAuthenticationSuccess(request, response, authentication); 63 | } else { 64 | response.sendRedirect("/"); 65 | } 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.revengemission.sso 7 | oauth2-client 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | oauth2-client 12 | oauth2-client 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 3.3.2 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 17 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-logging 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-thymeleaf 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-security 46 | 47 | 48 | 49 | org.springframework.security 50 | spring-security-oauth2-client 51 | 52 | 53 | 54 | org.thymeleaf.extras 55 | thymeleaf-extras-springsecurity6 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-actuator 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-devtools 66 | runtime 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-starter-test 72 | test 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-maven-plugin 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-compiler-plugin 86 | 87 | ${java.version} 88 | ${java.version} 89 | 90 | -Xlint:deprecation 91 | -Xlint:unchecked 92 | 93 | 94 | 95 | 96 | 97 | 98 | org.codehaus.mojo 99 | versions-maven-plugin 100 | 101 | file:///${project.basedir}/maven-version-rules.xml 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/com/revengemission/sso/oauth2/client/config/RefreshExpiredTokenFilter.java: -------------------------------------------------------------------------------- 1 | package com.revengemission.sso.oauth2.client.config; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.Cookie; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.core.context.SecurityContextHolder; 14 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 15 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; 16 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 17 | import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; 18 | import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; 19 | import org.springframework.security.oauth2.client.registration.ClientRegistration; 20 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 21 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; 22 | import org.springframework.security.oauth2.core.OAuth2AccessToken; 23 | import org.springframework.security.oauth2.core.OAuth2AuthorizationException; 24 | import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; 25 | import org.springframework.security.oauth2.core.user.OAuth2User; 26 | import org.springframework.stereotype.Component; 27 | import org.springframework.web.filter.OncePerRequestFilter; 28 | 29 | import java.io.IOException; 30 | import java.time.Clock; 31 | import java.time.Duration; 32 | import java.time.Instant; 33 | 34 | /** 35 | * 刷新过期access_token 36 | */ 37 | @Component 38 | public class RefreshExpiredTokenFilter extends OncePerRequestFilter { 39 | 40 | private static final Logger log = LoggerFactory.getLogger(RefreshExpiredTokenFilter.class); 41 | 42 | @Value("${oauth2.token.cookie.domain}") 43 | String cookieDomain; 44 | 45 | @Autowired 46 | OAuth2AuthorizedClientService oAuth2AuthorizedClientService; 47 | 48 | private Duration accessTokenExpiresSkew = Duration.ofMillis(10000); 49 | 50 | private Clock clock = Clock.systemUTC(); 51 | 52 | @Autowired 53 | OAuth2UserService oAuth2UserService; 54 | 55 | private DefaultRefreshTokenTokenResponseClient accessTokenResponseClient; 56 | 57 | public RefreshExpiredTokenFilter() { 58 | super(); 59 | this.accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient(); 60 | } 61 | 62 | @Override 63 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 64 | throws ServletException, IOException { 65 | log.debug("entering Refresh ExpiredToken Filter......"); 66 | /** 67 | * check if authentication is done. 68 | */ 69 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 70 | if (null != authentication && authentication instanceof OAuth2AuthenticationToken) { 71 | 72 | OAuth2AuthenticationToken oldOAuth2Token = (OAuth2AuthenticationToken) authentication; 73 | OAuth2AuthorizedClient authorizedClient = this.oAuth2AuthorizedClientService 74 | .loadAuthorizedClient(oldOAuth2Token.getAuthorizedClientRegistrationId(), oldOAuth2Token.getName()); 75 | /** 76 | * Check whether token is expired. 77 | */ 78 | if (authorizedClient != null && isExpired(authorizedClient.getAccessToken())) { 79 | 80 | try { 81 | log.info("===================== Token Expired , trying to refresh"); 82 | ClientRegistration clientRegistration = authorizedClient.getClientRegistration(); 83 | /* 84 | * Call Auth server token endpoint to refresh token. 85 | */ 86 | OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(clientRegistration, authorizedClient.getAccessToken(), authorizedClient.getRefreshToken()); 87 | OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(refreshTokenGrantRequest); 88 | 89 | OAuth2User newOAuth2User = oAuth2UserService.loadUser(new OAuth2UserRequest(clientRegistration, accessTokenResponse.getAccessToken())); 90 | 91 | /* 92 | * Create new authentication(OAuth2AuthenticationToken). 93 | */ 94 | OAuth2AuthenticationToken updatedUser = new OAuth2AuthenticationToken(newOAuth2User, newOAuth2User.getAuthorities(), oldOAuth2Token.getAuthorizedClientRegistrationId()); 95 | /* 96 | * Update access_token and refresh_token by saving new authorized client. 97 | */ 98 | OAuth2AuthorizedClient updatedAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration, 99 | oldOAuth2Token.getName(), accessTokenResponse.getAccessToken(), 100 | accessTokenResponse.getRefreshToken()); 101 | this.oAuth2AuthorizedClientService.saveAuthorizedClient(updatedAuthorizedClient, updatedUser); 102 | /* 103 | * Set new authentication in SecurityContextHolder. 104 | */ 105 | SecurityContextHolder.getContext().setAuthentication(updatedUser); 106 | 107 | Cookie tokenCookie = new Cookie("access_token", accessTokenResponse.getAccessToken().getTokenValue()); 108 | tokenCookie.setHttpOnly(true); 109 | tokenCookie.setDomain(cookieDomain); 110 | tokenCookie.setPath("/"); 111 | response.addCookie(tokenCookie); 112 | log.info("===================== Refresh Token Done !"); 113 | } catch (OAuth2AuthorizationException e) { 114 | log.info("Refresh ExpiredToken exception", e); 115 | SecurityContextHolder.getContext().setAuthentication(null); 116 | } 117 | 118 | } 119 | 120 | } 121 | log.debug("exit Refresh ExpiredToken Filter......"); 122 | filterChain.doFilter(request, response); 123 | } 124 | 125 | private Boolean isExpired(OAuth2AccessToken oAuth2AccessToken) { 126 | Instant now = this.clock.instant(); 127 | Instant expiresAt = oAuth2AccessToken.getExpiresAt(); 128 | return now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew)); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/revengemission/sso/oauth2/client/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.revengemission.sso.oauth2.client.config; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.nimbusds.jwt.SignedJWT; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 13 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 14 | import org.springframework.security.core.GrantedAuthority; 15 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 16 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 17 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; 18 | import org.springframework.security.oauth2.core.OAuth2AccessToken; 19 | import org.springframework.security.oauth2.core.user.DefaultOAuth2User; 20 | import org.springframework.security.oauth2.core.user.OAuth2User; 21 | import org.springframework.security.web.SecurityFilterChain; 22 | import org.springframework.util.StringUtils; 23 | 24 | import java.util.*; 25 | 26 | @EnableWebSecurity 27 | @Configuration 28 | public class SecurityConfig { 29 | 30 | private Logger log = LoggerFactory.getLogger(this.getClass()); 31 | 32 | @Autowired 33 | CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; 34 | 35 | @Autowired 36 | ObjectMapper objectMapper; 37 | 38 | @Bean 39 | SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 40 | 41 | http.authorizeHttpRequests(requestMatcherRegistry -> 42 | requestMatcherRegistry.requestMatchers("/", "/login/**", "/oauth2/**", "/assets/**").permitAll() 43 | .anyRequest().authenticated()) 44 | .csrf(AbstractHttpConfigurer::disable) 45 | .logout(logoutCustomizer -> 46 | logoutCustomizer 47 | .logoutUrl("/logout") 48 | .logoutSuccessUrl("/")) 49 | .oauth2Login(oauth2Login -> 50 | oauth2Login.successHandler(customAuthenticationSuccessHandler) 51 | .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(oauth2UserService()))); 52 | 53 | return http.build(); 54 | } 55 | 56 | /** 57 | * 从access_token中直接抽取角色等信息 58 | * https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2login-advanced-map-authorities-oauth2userservice 59 | * 60 | * @return 61 | */ 62 | @SuppressWarnings("unchecked") 63 | @Bean 64 | public OAuth2UserService oauth2UserService() { 65 | 66 | return (userRequest) -> { 67 | String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); 68 | if (!StringUtils.hasText(userNameAttributeName)) { 69 | userNameAttributeName = "sub"; 70 | } 71 | OAuth2AccessToken accessToken = userRequest.getAccessToken(); 72 | Collection grantedAuthorities = new ArrayList<>(); 73 | try { 74 | SignedJWT jwt = SignedJWT.parse(accessToken.getTokenValue()); 75 | String claimJsonString = jwt.getJWTClaimsSet().toString(); 76 | 77 | Collection roles = new HashSet<>(); 78 | JsonNode treeNode = objectMapper.readTree(claimJsonString); 79 | List jsonNodes = treeNode.findValues("roles"); 80 | jsonNodes.forEach(jsonNode -> { 81 | if (jsonNode.isArray()) { 82 | jsonNode.elements().forEachRemaining(e -> { 83 | roles.add(e.asText()); 84 | }); 85 | } else { 86 | roles.add(jsonNode.asText()); 87 | } 88 | }); 89 | 90 | jsonNodes = treeNode.findValues("authorities"); 91 | jsonNodes.forEach(jsonNode -> { 92 | if (jsonNode.isArray()) { 93 | jsonNode.elements().forEachRemaining(e -> { 94 | roles.add(e.asText()); 95 | }); 96 | } else { 97 | roles.add(jsonNode.asText()); 98 | } 99 | }); 100 | 101 | for (String authority : roles) { 102 | grantedAuthorities.add(new SimpleGrantedAuthority(authority)); 103 | } 104 | Map userAttributes = new HashMap<>(16); 105 | userAttributes.put(userNameAttributeName, treeNode.findValue(userNameAttributeName)); 106 | userAttributes.put("preferred_username", treeNode.findValue("preferred_username")); 107 | userAttributes.put("email", treeNode.findValue("email")); 108 | OAuth2User oAuth2User = new DefaultOAuth2User(grantedAuthorities, userAttributes, userNameAttributeName); 109 | 110 | return oAuth2User; 111 | } catch (Exception e) { 112 | log.error("oauth2UserService Exception", e); 113 | } 114 | return null; 115 | }; 116 | } 117 | 118 | /** 119 | * 自动刷新token 120 | * https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2Client-webclient-servlet 121 | * 122 | * @param clientRegistrationRepository 123 | * @param authorizedClientRepository 124 | * @return 125 | */ 126 | /* 127 | @Bean 128 | public OAuth2AuthorizedClientManager authorizedClientManager( 129 | ClientRegistrationRepository clientRegistrationRepository, 130 | OAuth2AuthorizedClientRepository authorizedClientRepository) { 131 | 132 | OAuth2AuthorizedClientProvider authorizedClientProvider = 133 | OAuth2AuthorizedClientProviderBuilder.builder() 134 | .authorizationCode() 135 | .refreshToken() 136 | .clientCredentials() 137 | .password() 138 | .build(); 139 | 140 | DefaultOAuth2AuthorizedClientManager authorizedClientManager = 141 | new DefaultOAuth2AuthorizedClientManager( 142 | clientRegistrationRepository, authorizedClientRepository); 143 | authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); 144 | 145 | return authorizedClientManager; 146 | } 147 | 148 | @Bean 149 | WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { 150 | ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = 151 | new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); 152 | 153 | return WebClient.builder() 154 | .apply(oauth2Client.oauth2Configuration()) 155 | .build(); 156 | }*/ 157 | } 158 | -------------------------------------------------------------------------------- /src/main/resources/static/assets/localforage.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | localForage -- Offline Storage, Improved 3 | Version 1.5.7 4 | https://localforage.github.io/localForage 5 | (c) 2013-2017 Mozilla, Apache License 2.0 6 | */ 7 | !function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.localforage=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c||a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g=43)}}).catch(function(){return!1})}function m(a){return"boolean"==typeof ma?oa.resolve(ma):l(a).then(function(a){return ma=a})}function n(a){var b=na[a.name],c={};c.promise=new oa(function(a){c.resolve=a}),b.deferredOperations.push(c),b.dbReady?b.dbReady=b.dbReady.then(function(){return c.promise}):b.dbReady=c.promise}function o(a){var b=na[a.name],c=b.deferredOperations.pop();c&&c.resolve()}function p(a,b){var c=na[a.name],d=c.deferredOperations.pop();d&&d.reject(b)}function q(a,b){return new oa(function(c,d){if(a.db){if(!b)return c(a.db);n(a),a.db.close()}var e=[a.name];b&&e.push(a.version);var f=la.open.apply(la,e);b&&(f.onupgradeneeded=function(b){var c=f.result;try{c.createObjectStore(a.storeName),b.oldVersion<=1&&c.createObjectStore(pa)}catch(c){if("ConstraintError"!==c.name)throw c;console.warn('The database "'+a.name+'" has been upgraded from version '+b.oldVersion+" to version "+b.newVersion+', but the storage "'+a.storeName+'" already exists.')}}),f.onerror=function(a){a.preventDefault(),d(f.error)},f.onsuccess=function(){c(f.result),o(a)}})}function r(a){return q(a,!1)}function s(a){return q(a,!0)}function t(a,b){if(!a.db)return!0;var c=!a.db.objectStoreNames.contains(a.storeName),d=a.versiona.db.version;if(d&&(a.version!==b&&console.warn('The database "'+a.name+"\" can't be downgraded from version "+a.db.version+" to version "+a.version+"."),a.version=a.db.version),e||c){if(c){var f=a.db.version+1;f>a.version&&(a.version=f)}return!0}return!1}function u(a){return new oa(function(b,c){var d=new FileReader;d.onerror=c,d.onloadend=function(c){var d=btoa(c.target.result||"");b({__local_forage_encoded_blob:!0,data:d,type:a.type})},d.readAsBinaryString(a)})}function v(a){return g([k(atob(a.data))],{type:a.type})}function w(a){return a&&a.__local_forage_encoded_blob}function x(a){var b=this,c=b._initReady().then(function(){var a=na[b._dbInfo.name];if(a&&a.dbReady)return a.dbReady});return i(c,a,a),c}function y(a){n(a);for(var b=na[a.name],c=b.forages,d=0;d>4,k[i++]=(15&d)<<4|e>>2,k[i++]=(3&e)<<6|63&f;return j}function L(a){var b,c=new Uint8Array(a),d="";for(b=0;b>2],d+=ua[(3&c[b])<<4|c[b+1]>>4],d+=ua[(15&c[b+1])<<2|c[b+2]>>6],d+=ua[63&c[b+2]];return c.length%3==2?d=d.substring(0,d.length-1)+"=":c.length%3==1&&(d=d.substring(0,d.length-2)+"=="),d}function M(a,b){var c="";if(a&&(c=La.call(a)),a&&("[object ArrayBuffer]"===c||a.buffer&&"[object ArrayBuffer]"===La.call(a.buffer))){var d,e=xa;a instanceof ArrayBuffer?(d=a,e+=za):(d=a.buffer,"[object Int8Array]"===c?e+=Ba:"[object Uint8Array]"===c?e+=Ca:"[object Uint8ClampedArray]"===c?e+=Da:"[object Int16Array]"===c?e+=Ea:"[object Uint16Array]"===c?e+=Ga:"[object Int32Array]"===c?e+=Fa:"[object Uint32Array]"===c?e+=Ha:"[object Float32Array]"===c?e+=Ia:"[object Float64Array]"===c?e+=Ja:b(new Error("Failed to get type for BinaryArray"))),b(e+L(d))}else if("[object Blob]"===c){var f=new FileReader;f.onload=function(){var c=va+a.type+"~"+L(this.result);b(xa+Aa+c)},f.readAsArrayBuffer(a)}else try{b(JSON.stringify(a))}catch(c){console.error("Couldn't convert value into a JSON string: ",a),b(null,c)}}function N(a){if(a.substring(0,ya)!==xa)return JSON.parse(a);var b,c=a.substring(Ka),d=a.substring(ya,Ka);if(d===Aa&&wa.test(c)){var e=c.match(wa);b=e[1],c=c.substring(e[0].length)}var f=K(c);switch(d){case za:return f;case Aa:return g([f],{type:b});case Ba:return new Int8Array(f);case Ca:return new Uint8Array(f);case Da:return new Uint8ClampedArray(f);case Ea:return new Int16Array(f);case Ga:return new Uint16Array(f);case Fa:return new Int32Array(f);case Ha:return new Uint32Array(f);case Ia:return new Float32Array(f);case Ja:return new Float64Array(f);default:throw new Error("Unkown type: "+d)}}function O(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"!=typeof a[d]?a[d].toString():a[d];var e=new oa(function(a,d){try{c.db=openDatabase(c.name,String(c.version),c.description,c.size)}catch(a){return d(a)}c.db.transaction(function(e){e.executeSql("CREATE TABLE IF NOT EXISTS "+c.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],function(){b._dbInfo=c,a()},function(a,b){d(b)})})});return c.serializer=Ma,e}function P(a,b){var c=this;a=j(a);var d=new oa(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName+" WHERE key = ? LIMIT 1",[a],function(a,c){var d=c.rows.length?c.rows.item(0).value:null;d&&(d=e.serializer.deserialize(d)),b(d)},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function Q(a,b){var c=this,d=new oa(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName,[],function(c,d){for(var f=d.rows,g=f.length,h=0;h0)return void f(R.apply(e,[a,h,c,d-1]));g(b)}})})}).catch(g)});return h(f,c),f}function S(a,b,c){return R.apply(this,[a,b,c,1])}function T(a,b){var c=this;a=j(a);var d=new oa(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("DELETE FROM "+e.storeName+" WHERE key = ?",[a],function(){b()},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function U(a){var b=this,c=new oa(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("DELETE FROM "+d.storeName,[],function(){a()},function(a,b){c(b)})})}).catch(c)});return h(c,a),c}function V(a){var b=this,c=new oa(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT COUNT(key) as c FROM "+d.storeName,[],function(b,c){var d=c.rows.item(0).c;a(d)},function(a,b){c(b)})})}).catch(c)});return h(c,a),c}function W(a,b){var c=this,d=new oa(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT key FROM "+e.storeName+" WHERE id = ? LIMIT 1",[a+1],function(a,c){var d=c.rows.length?c.rows.item(0).key:null;b(d)},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function X(a){var b=this,c=new oa(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT key FROM "+d.storeName,[],function(b,c){for(var d=[],e=0;e0}function _(a){var b=this,c={};if(a)for(var d in a)c[d]=a[d];return c.keyPrefix=c.name+"/",c.storeName!==b._defaultConfig.storeName&&(c.keyPrefix+=c.storeName+"/"),$()?(b._dbInfo=c,c.serializer=Ma,oa.resolve()):oa.reject()}function aa(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo.keyPrefix,c=localStorage.length-1;c>=0;c--){var d=localStorage.key(c);0===d.indexOf(a)&&localStorage.removeItem(d)}});return h(c,a),c}function ba(a,b){var c=this;a=j(a);var d=c.ready().then(function(){var b=c._dbInfo,d=localStorage.getItem(b.keyPrefix+a);return d&&(d=b.serializer.deserialize(d)),d});return h(d,b),d}function ca(a,b){var c=this,d=c.ready().then(function(){for(var b=c._dbInfo,d=b.keyPrefix,e=d.length,f=localStorage.length,g=1,h=0;h