SpringSecurity OAuth2單點(diǎn)登錄和登出的實(shí)現(xiàn)
Spring Security OAuth 最新官方已經(jīng)不再維護(hù),以下內(nèi)容只用于學(xué)習(xí)記錄。
GitHub:shpunishment/spring-security-oauth2-demo
1. 單點(diǎn)登錄
單點(diǎn)登錄即有多個(gè)子系統(tǒng),有一個(gè)認(rèn)證中心。當(dāng)訪問其中任意一個(gè)子系統(tǒng)時(shí),如果發(fā)現(xiàn)未登錄,就跳到認(rèn)證中心進(jìn)行登錄,登錄完成后再跳回該子系統(tǒng)。此時(shí)訪問其他子系統(tǒng)時(shí),就已經(jīng)是登錄狀態(tài)了。登出統(tǒng)一從認(rèn)證中心登出,登出后各個(gè)子系統(tǒng)就無法訪問了,需要再次登錄。
Spring Security OAuth 建立在Spring Security 之上,所以大部分配置還是在Security中,Security完成對(duì)用戶的認(rèn)證和授權(quán),OAuth完成單點(diǎn)登錄。
Spring Security OAuth 的單點(diǎn)登錄主要靠@EnableOAuth2Sso實(shí)現(xiàn),簡化了從資源服務(wù)器到認(rèn)證授權(quán)服務(wù)器的SSO流程,并使用授權(quán)碼方式獲取。
1.1 使用內(nèi)存保存客戶端和用戶信息
1.1.1 認(rèn)證中心 auth-server
添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.8.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--spring2.0集成redis所需common-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency>
application.yml
server: port: 8000 servlet: context-path: /auth-server session: cookie: name: oauth-auth-server spring: redis: # Redis默認(rèn)情況下有16個(gè)分片,這里配置具體使用的分片,默認(rèn)是0 database: 0 host: localhost port: 6379 # 連接密碼(默認(rèn)為空) password: # 連接超時(shí)時(shí)間(毫秒) timeout: 10000ms lettuce: pool: # 連接池最大連接數(shù)(使用負(fù)值表示沒有限制) 默認(rèn) 8 max-active: 8 # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制) 默認(rèn) -1 max-wait: -1 # 連接池中的最大空閑連接 默認(rèn) 8 max-idle: 8 # 連接池中的最小空閑連接 默認(rèn) 0 min-idle: 0
添加授權(quán)服務(wù)器配置,主要令牌路徑的安全性,客戶端詳情和令牌存儲(chǔ)。
這里配置了一個(gè)客戶端,支持授權(quán)碼模式和刷新Token,并且將Token存在Redis中。
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private static final String RESOURCE_ID = "resource-1"; @Autowired private PasswordEncoder passwordEncoder; @Autowired private RedisConnectionFactory redisConnectionFactory; /** * 配置授權(quán)服務(wù)器的安全性,令牌端點(diǎn)的安全約束 * * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security // 開啟 /oauth/check_token .tokenKeyAccess("permitAll()") // 開啟 /oauth/token_key .checkTokenAccess("isAuthenticated()") // 允許表單認(rèn)證 // 如果配置,且url中有client_id和client_secret的,則走 ClientCredentialsTokenEndpointFilter // 如果沒有配置,但是url中沒有client_id和client_secret的,走basic認(rèn)證保護(hù) .allowFormAuthenticationForClients(); } /** * 配置客戶端,可存在內(nèi)存和數(shù)據(jù)庫中 * * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() .withClient("client_1") .resourceIds(RESOURCE_ID) .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("read") .authorities("client") .secret(passwordEncoder.encode("123456")) // 必須添加,會(huì)和請(qǐng)求時(shí)重定向地址匹配 .redirectUris("http://localhost:8001/service1/login") // 自動(dòng)批準(zhǔn),在登錄成功后不會(huì)跳到批準(zhǔn)頁面,讓資源所有者批準(zhǔn) //.autoApprove(true); } /** * * 配置授權(quán)服務(wù)器端點(diǎn)的非安全功能,例如令牌存儲(chǔ),令牌自定義,用戶批準(zhǔn)和授予類型 * * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints // 令牌存在redis .tokenStore(tokenStore()); } /** * 配置redis,使用redis存token * @return */ @Bean public TokenStore tokenStore(){ return new RedisTokenStore(redisConnectionFactory); } }
添加資源服務(wù)器配置,主要配置資源id和需要Token驗(yàn)證的url
對(duì)于相同的url,如果二者都配置了驗(yàn)證,則優(yōu)先進(jìn)入ResourceServerConfigurerAdapter,會(huì)被 OAuth2AuthenticationProcessingFilter 處理,進(jìn)行token驗(yàn)證;而不會(huì)進(jìn)行WebSecurityConfigurerAdapter 的表單認(rèn)證等。
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "resource-1"; /** * 添加特定于資源服務(wù)器的屬性 * * @param resources */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources .resourceId(RESOURCE_ID); } /** * 使用此配置安全資源的訪問規(guī)則,配置需要token驗(yàn)證的url。 默認(rèn)情況下,所有不在"/oauth/**"中的資源都受到保護(hù)。 * * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { // 只有 /security/getUserInfo 需要token驗(yàn)證 http .requestMatchers().antMatchers("/security/getUserInfo") .and() .authorizeRequests() .anyRequest().authenticated(); } }
security配置,用戶數(shù)據(jù),自定義登錄頁,成功失敗Handler,session,配置非受保護(hù)URL等。
這里添加了兩個(gè)用戶以及登錄頁等配置。
@Configuration public class ServerWebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 認(rèn)證管理器配置,用于信息獲取來源(UserDetails)以及密碼校驗(yàn)規(guī)則(PasswordEncoder) * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth // 使用內(nèi)存認(rèn)證,在內(nèi)存中保存兩個(gè)用戶 .inMemoryAuthentication() .passwordEncoder(passwordEncoder()) // admin 擁有ADMIN和USER的權(quán)限 .withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN", "USER") .and() // user 擁有USER的權(quán)限 .withUser("user").password(passwordEncoder().encode("user")).roles("USER"); } /** * 核心過濾器配置,更多使用ignoring()用來忽略對(duì)靜態(tài)資源的控制 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web .ignoring() .antMatchers("/static/js/**"); } /** * 安全過濾器鏈配置,自定義安全訪問策略 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // /login 和 /oauth/authorize 路徑配置為不需要任何身份驗(yàn)證,其他所有路徑必須經(jīng)過驗(yàn)證 .antMatchers("/login", "/oauth/authorize").permitAll() // 其他請(qǐng)求都需要已認(rèn)證 .anyRequest().authenticated() .and() // 使用表單登錄 .formLogin() // 自定義username 和password參數(shù) .usernameParameter("login_username") .passwordParameter("login_password") // 自定義登錄頁地址 .loginPage("/loginPage") // 驗(yàn)證表單的地址,由過濾器 UsernamePasswordAuthenticationFilter 攔截處理 .loginProcessingUrl("/login") .permitAll() .and() .csrf().disable(); } @Bean public static BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
獲取當(dāng)前用戶信息,供客戶端獲取
@RestController @RequestMapping("/security") public class SecurityController { @GetMapping("/getUserInfo") @ResponseBody public Principal getUserInfo(Principal principal) { return principal; } }
1.1.2 子系統(tǒng) service-1
添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.13.RELEASE</version> </dependency>
application.yml
server: port: 8001 servlet: context-path: /service1 session: cookie: name: oauth-service-1 security: oauth2: client: clientId: client_1 clientSecret: 123456 # 獲取訪問令牌的URI accessTokenUri: http://localhost:8000/auth-server/oauth/token # 將用戶重定向到的授權(quán)URI userAuthorizationUri: http://localhost:8000/auth-server/oauth/authorize resource: # 獲取當(dāng)前用戶詳細(xì)信息 userInfoUri: http://localhost:8000/auth-server/security/getUserInfo
security配置,如果需要對(duì)service-1的url進(jìn)行控制,需要添加 WebSecurityConfigurerAdapter 配置,可配置子系統(tǒng)中哪些接口需要auth-server的認(rèn)證,配置非受保護(hù)URL等。
@Configuration // @EnableOAuth2Sso 注解 在繼承 WebSecurityConfigurerAdapter 類的上面時(shí) // 代表著在該子類配置的基礎(chǔ)上增強(qiáng) OAuth2Sso 相關(guān)配置。 @EnableOAuth2Sso @EnableGlobalMethodSecurity(prePostEnabled = true) public class ClientWebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 安全過濾器鏈配置,自定義安全訪問策略??膳渲每蛻舳瞬皇鼙Wo(hù)的資源 * * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { http .antMatcher("/**") .authorizeRequests() // 訪問 / /home 不用認(rèn)證 .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() // 權(quán)限不足跳轉(zhuǎn) /401 .exceptionHandling().accessDeniedPage("/401"); } /** * 核心過濾器配置,更多使用ignoring()用來忽略對(duì)靜態(tài)資源的控制和過濾微服務(wù)間feign的接口 * * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web .ignoring() .antMatchers("/js/**"); } }
客戶端資源服務(wù)器配置,只有 /api/* 需要token驗(yàn)證
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "resource-1"; /** * 添加特定于資源服務(wù)器的屬性 * * @param resources */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources .resourceId(RESOURCE_ID); } /** * 使用此配置安全資源的訪問規(guī)則,配置需要token驗(yàn)證的url。 默認(rèn)情況下,所有不在"/oauth/**"中的資源都受到保護(hù)。 * * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { // /api/* 都需要token驗(yàn)證,會(huì)被 OAuth2AuthenticationProcessingFilter 處理 http .requestMatchers() .antMatchers("/api/*") .and() .authorizeRequests() .anyRequest().authenticated(); } }
service1控制器
@Controllerpublic class Service1Controller {<!--{C}%3C!%2D%2D%20%2D%2D%3E--> @RequestMapping(path = {<!--{C}%3C!%2D%2D%20%2D%2D%3E-->"/", "/home"}) public ModelAndView home() {<!--{C}%3C!%2D%2D%20%2D%2D%3E--> return new ModelAndView("home"); } @PreAuthorize("hasRole('USER')") @RequestMapping("/user") public ModelAndView user() {<!--{C}%3C!%2D%2D%20%2D%2D%3E--> return new ModelAndView("user"); } @PreAuthorize("hasRole('ADMIN')") @RequestMapping("/admin") public ModelAndView admin() {<!--{C}%3C!%2D%2D%20%2D%2D%3E--> return new ModelAndView("admin"); } /** * 測(cè)試 /api/* 是否被資源服務(wù)器攔截,需要token * @return */ @GetMapping("/api/getUserInfo") @ResponseBody public Principal getUserInfo() {<!--{C}%3C!%2D%2D%20%2D%2D%3E--> return SecurityContextHolder.getContext().getAuthentication(); } @GetMapping("/api2/getUserInfo") @ResponseBody public Principal getUserInfo2() {<!--{C}%3C!%2D%2D%20%2D%2D%3E--> return SecurityContextHolder.getContext().getAuthentication(); }}@Controller public class Service1Controller { @RequestMapping(path = {"/", "/home"}) public ModelAndView home() { return new ModelAndView("home"); } @PreAuthorize("hasRole('USER')") @RequestMapping("/user") public ModelAndView user() { return new ModelAndView("user"); } @PreAuthorize("hasRole('ADMIN')") @RequestMapping("/admin") public ModelAndView admin() { return new ModelAndView("admin"); } /** * 測(cè)試 /api/* 是否被資源服務(wù)器攔截,需要token * @return */ @GetMapping("/api/getUserInfo") @ResponseBody public Principal getUserInfo() { return SecurityContextHolder.getContext().getAuthentication(); } @GetMapping("/api2/getUserInfo") @ResponseBody public Principal getUserInfo2() { return SecurityContextHolder.getContext().getAuthentication(); } }
1.1.3 測(cè)試
service-2根據(jù)service-1復(fù)制一遍。
service-1和service-2不用登錄即可訪問 / /home
訪問 /user 需要認(rèn)證的資源,會(huì)先到auth-server進(jìn)行認(rèn)證
資源所有者批準(zhǔn)
批準(zhǔn)后才能訪問到 /user
service-2的 /user 也可訪問,即實(shí)現(xiàn)了單點(diǎn)登錄
訪問 /admin 用戶權(quán)限不足
1.2 使用數(shù)據(jù)庫保存客戶端和用戶信息
只需要修改auth-server中客戶端和用戶信息的獲取方式。
用戶信息部分,修改security配置,參考 Spring Security 使用 中的使用數(shù)據(jù)庫保存用戶信息。
由于將Token等信息存在了Redis中,所以在數(shù)據(jù)庫中只需要保存客戶端信息。修改 AuthorizationServerConfig
@Autowired private DataSource dataSource; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .withClientDetails(clientDetails(dataSource)); } /** * 獲取客戶端詳細(xì)信息服務(wù),JDBC實(shí)現(xiàn) * @return */ @Bean public ClientDetailsService clientDetails(DataSource dataSource) { return new JdbcClientDetailsService(dataSource); }
添加表和數(shù)據(jù),密碼使用BCrypt加密,數(shù)據(jù)和使用內(nèi)存時(shí)一致。
CREATE TABLE `oauth_client_details` ( `client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `access_token_validity` int(11) NULL DEFAULT NULL, `refresh_token_validity` int(11) NULL DEFAULT NULL, `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`client_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; INSERT INTO `oauth_client_details` VALUES ('client_1', 'resource-1', '$2a$10$TfM5Bisse4ewbmIDfqZcxuYl5dI39/lEzzvkzlxFELKglHQM78FIu', 'read', 'authorization_code,refresh_token', 'http://localhost:8001/service1/login,http://localhost:8002/service2/login', NULL, NULL, NULL, NULL, NULL);
效果與使用內(nèi)存時(shí)一致。
1.3 單點(diǎn)登錄流程
打開F12會(huì)看到以下重定向過程,可看到大致步驟:
- 請(qǐng)求授權(quán)碼,判斷未登錄,重定向登錄頁
- 登錄成功,重定向繼續(xù)請(qǐng)求授權(quán)碼,未被資源所有者批準(zhǔn),返回批準(zhǔn)頁面
- 資源所有者批準(zhǔn),重定向返回授權(quán)碼
- 客戶端獲取到授權(quán)碼,請(qǐng)求Token
- 獲取到Token,重定向 /user
1.2.1 請(qǐng)求授權(quán)碼,判斷未登錄,重定向登錄頁
訪問客戶端受保護(hù)資源 localhost:8001/service1/user,未登錄重定向到 localhost:8001/service1/login 進(jìn)行登錄認(rèn)證,因?yàn)榕渲昧藛吸c(diǎn)登錄@EnableOAuth2Sso,所以單點(diǎn)登錄攔截器會(huì)讀取授權(quán)服務(wù)器的配置,發(fā)起獲取授權(quán)碼請(qǐng)求
http://localhost:8000/auth-server/oauth/authorize?client_id=client_1&redirect_uri=http://localhost:8001/service1/login&response_type=code&state=eEoQJJ
被auth-server的 AuthorizationEndpoint.authorize() 處理,因?yàn)槲吹卿浾J(rèn)證,拋出異常
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) { throw new InsufficientAuthenticationException( "User must be authenticated with Spring Security before authorization can be completed."); }
異常在 ExceptionTranslationFilter.doFilter() 中處理
handleSpringSecurityException(request, response, chain, ase);
調(diào)用 LoginUrlAuthenticationEntryPoint.commence() 方法,獲取登錄頁地址,并重定向
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
1.2.2 登錄成功,重定向繼續(xù)請(qǐng)求授權(quán)碼,未被資源所有者批準(zhǔn),返回批準(zhǔn)頁面
在auth-server中用戶密碼由 AbstractAuthenticationProcessingFilter.doFilter() 處理,UsernamePasswordAuthenticationFilter 繼承自 AbstractAuthenticationProcessingFilter,在父類 doFilter() 方法中,會(huì)調(diào)用子類實(shí)現(xiàn)的 attemptAuthentication 方法,獲取認(rèn)證信息
authResult = attemptAuthentication(request, response);
在 attemptAuthentication() 方法中,將用戶名和密碼封裝成token并認(rèn)證,并添加額外信息后,進(jìn)行認(rèn)證
this.getAuthenticationManager().authenticate(authRequest);
getAuthenticationManager() 方法獲取 AuthenticationManager 的實(shí)現(xiàn)類 ProviderManager,在 authenticate() 方法中,找到合適的 AuthenticationProvider 處理認(rèn)證,這里是 DaoAuthenticationProvider,它父類 AbstractUserDetailsAuthenticationProvider 實(shí)現(xiàn)了該方法
result = provider.authenticate(authentication);
父類會(huì)調(diào)用 retrieveUser() 方法檢索用戶,實(shí)現(xiàn)在 DaoAuthenticationProvider
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
這里是從內(nèi)存或數(shù)據(jù)庫中獲取用戶,然后進(jìn)行密碼校驗(yàn),成功后,將信息保存到Authentication,并返回。調(diào)用成功Handler,記住我等等。
默認(rèn)登錄成功,會(huì)重定向之前請(qǐng)求的地址
http://localhost:8000/auth-server/oauth/authorize?client_id=client_1&redirect_uri=http://localhost:8001/service1/login&response_type=code&state=eEoQJJ
再次被auth-server的 AuthorizationEndpoint.authorize() 處理,這時(shí)有用戶認(rèn)證信息,獲取client信息,進(jìn)行檢查,檢查資源所有者是否批準(zhǔn)(客戶端可設(shè)置是否自動(dòng)批準(zhǔn))
如果未批準(zhǔn),返回批準(zhǔn)頁,請(qǐng)求轉(zhuǎn)發(fā) forward:/oauth/confirm_access
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
1.2.3 資源所有者批準(zhǔn),重定向返回授權(quán)碼
用戶批準(zhǔn)后,被 AuthorizationEndpoint.approveOrDeny() 方法處理,返回授權(quán)碼,并重定向用戶設(shè)置的地址(/login),并帶上code和state
return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
1.2.4 客戶端獲取到授權(quán)碼,請(qǐng)求Token
在客戶端 AbstractAuthenticationProcessingFilter 中處理
authResult = attemptAuthentication(request, response);
由子類 OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication() 處理,判斷token是否為空
accessToken = restTemplate.getAccessToken();
如果為空,在 AuthorizationCodeAccessTokenProvider.obtainAccessToken() 方法中,獲取返回的授權(quán)碼,向auth-server請(qǐng)求Token
return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),getHeadersForTokenRequest(request));
在auth-server中 TokenEndpoint.getAccessToken() 方法獲取token,進(jìn)行客戶端校驗(yàn)后生成token并返回
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
1.2.5 獲取到Token,重定向 /user
回到在客戶端 OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication() 中,獲取到token后,帶上token,向auth-server請(qǐng)求用戶信息。
默認(rèn)Token是使用uuid,生成用于認(rèn)證的token和刷新的Token。認(rèn)證Token默認(rèn)12小時(shí)過期,刷新的Token默認(rèn)30天過期。
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
在auth-server 被 OAuth2AuthenticationProcessingFilter 處理,從頭部獲取并驗(yàn)證token后,完成該請(qǐng)求。
客戶端獲取到用戶信息,在客戶端重新完成登錄的流程,最后在默認(rèn)的登錄成功Handler中獲取到重定向地址(即 /user),并重定向。
1.3 JWT Token
1.3.1 資源服務(wù)器未添加tokenServices
只需要修改auth-server中授權(quán)服務(wù)器。
添加依賴
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.11.RELEASE</version> </dependency>
自定義生成token攜帶的信息
@Component public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { final Map<String, Object> additionalInfo = new HashMap<>(2); UserDetails user = (UserDetails) authentication.getUserAuthentication().getPrincipal(); additionalInfo.put("userName", user.getUsername()); additionalInfo.put("authorities", user.getAuthorities()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; } }
修改 AuthorizationServerConfig
@Autowired private CustomTokenEnhancer customTokenEnhancer; @Autowired private AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // token增強(qiáng)配置 TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer, jwtAccessTokenConverter())); endpoints // 令牌存在redis .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) // 密碼授權(quán)方式時(shí)需要 .authenticationManager(authenticationManager) // /oauth/token 運(yùn)行g(shù)et和post .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); } /** * 用來生成token的轉(zhuǎn)換器 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); // 對(duì)稱加密,設(shè)置簽名,使用下面這個(gè)值作為密鑰 jwtAccessTokenConverter.setSigningKey("oauth"); return jwtAccessTokenConverter; }
添加客戶端2,支持密碼授權(quán)方式
INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('client_2', 'resource-1', '$2a$10$TfM5Bisse4ewbmIDfqZcxuYl5dI39/lEzzvkzlxFELKglHQM78FIu', 'read', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, NULL);
測(cè)試
使用密碼模式獲取token
使用token獲請(qǐng)求資源服務(wù)器保護(hù)的接口
流程
在auth-server的 TokenEndpoint 中驗(yàn)證信息并獲取token。然后帶著token請(qǐng)求,在service-1中被 OAuth2AuthenticationProcessingFilter 處理,doFilter() 方法會(huì)提取并驗(yàn)證token。
按上面的配置,并沒有在資源服務(wù)器中配置tokenServices
Authentication authResult = authenticationManager.authenticate(authentication);
所以在加載 Authentication 的時(shí)候,tokenServices 為 UserInfoTokenServices,就會(huì)調(diào)用配置的 userInfoUri 去auth-server獲取用戶信息
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
1.3.2 資源服務(wù)器添加tokenServices
auth-server
修改ResourceServerConfig
@Autowired private TokenStore tokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore); resources .resourceId(RESOURCE_ID) .tokenServices(defaultTokenServices); }
service-1
添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--spring2.0集成redis所需common-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency>
修改application.yml
spring: redis: # Redis默認(rèn)情況下有16個(gè)分片,這里配置具體使用的分片,默認(rèn)是0 database: 0 host: localhost port: 6379 # 連接密碼(默認(rèn)為空) password: # 連接超時(shí)時(shí)間(毫秒) timeout: 10000ms lettuce: pool: # 連接池最大連接數(shù)(使用負(fù)值表示沒有限制) 默認(rèn) 8 max-active: 8 # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制) 默認(rèn) -1 max-wait: -1 # 連接池中的最大空閑連接 默認(rèn) 8 max-idle: 8 # 連接池中的最小空閑連接 默認(rèn) 0 min-idle: 0
修改 ResourceServerConfig
@Autowired private RedisConnectionFactory redisConnectionFactory; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); resources .resourceId(RESOURCE_ID) .tokenServices(defaultTokenServices); } /** * 配置redis,使用redis存token * @return */ @Bean public TokenStore tokenStore(){ return new RedisTokenStore(redisConnectionFactory); }
流程
在auth-server的 TokenEndpoint 中驗(yàn)證信息并獲取token。然后帶著token請(qǐng)求,在service-1中被 OAuth2AuthenticationProcessingFilter 處理,doFilter() 方法會(huì)提取并驗(yàn)證token。
按上面的配置,并沒有在資源服務(wù)器中配置tokenServices
Authentication authResult = authenticationManager.authenticate(authentication);
所以在加載 Authentication 的時(shí)候,tokenServices 為 DefaultTokenServices,再加上有UserDetails的實(shí)現(xiàn)類,可以解析,就不用在調(diào)用auth-server
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
2. 單點(diǎn)登出
這里除了部分的資源服務(wù)器中配置的api需要token驗(yàn)證,其他還是依賴于Spring Security的認(rèn)證。而Spring Security是使用Cookie和Session的記錄用戶。所以可以將認(rèn)證中心和各個(gè)子系統(tǒng)的Cookie設(shè)置在同一路徑下,在認(rèn)證中心登出時(shí),將Cookie一并刪除,實(shí)現(xiàn)認(rèn)證中心和各個(gè)子系統(tǒng)的登出。各子系統(tǒng)需要知道認(rèn)證中心的登出地址。在這里是http://localhost:8000/auth-server/logout。
修改認(rèn)證中心和各個(gè)子系統(tǒng)的Cookie路徑,測(cè)試發(fā)現(xiàn),放在 / 下才可實(shí)現(xiàn)
server: servlet: session: cookie: path: /server: servlet: session: cookie: path: /
在auth-server添加登出成功的Handler
@Component public class CustomLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 將子系統(tǒng)的cookie刪掉 Cookie[] cookies = request.getCookies(); if(cookies != null && cookies.length>0){ for (Cookie cookie : cookies){ cookie.setMaxAge(0); cookie.setPath("/"); response.addCookie(cookie); } } super.handle(request, response, authentication); } }
修改auth-server的ServerWebSecurityConfig,添加logout配置
@Configuration public class ServerWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomLogoutSuccessHandler customLogoutSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http ... .and() // 默認(rèn)為 /logout .logout() .logoutSuccessHandler(customLogoutSuccessHandler) // 無效會(huì)話 .invalidateHttpSession(true) // 清除身份驗(yàn)證 .clearAuthentication(true) .permitAll() ...; } }
當(dāng)然,使用了OAuth發(fā)放token,應(yīng)該也需要使token失效。
@Autowired private TokenStore tokenStore; @GetMapping("/revokeToken") public void revokeToken(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (authHeader != null) { String tokenValue = authHeader.replace("Bearer", "").trim(); OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue); tokenStore.removeAccessToken(accessToken); } }
3. 總結(jié)
- AuthorizationEndpoint 處理 /oauth/authorize;TokenEndpoint 處理 /oauth/token。
- @EnableOAuth2Sso 會(huì)將資源服務(wù)器標(biāo)記為OAuth 2.0的客戶端, 它將負(fù)責(zé)將資源所有者(最終用戶)重定向到用戶必須輸入其憑據(jù)的授權(quán)服務(wù)器。完成后,用戶將被重定向回具有授權(quán)碼的客戶端。然后客戶端通過調(diào)用授權(quán)服務(wù)器獲取授權(quán)代碼并將其交換為訪問令牌。只有在此之后,客戶端才能使用訪問令牌調(diào)用資源服務(wù)器。
- @EnableResourceServer 意味著所屬的服務(wù)需要訪問令牌才能處理請(qǐng)求。在調(diào)用資源服務(wù)器之前,需要先從授權(quán)服務(wù)器獲取訪問令牌。
- 在資源服務(wù)器中配置的路徑,都會(huì)被 OAuth2AuthenticationProcessingFilter 處理,獲取token。
- 之前一直在糾結(jié),客戶端獲取到了token,為什么在訪問 /user 的請(qǐng)求頭中并沒有Authorization,亦可請(qǐng)求成功。其實(shí)都因?yàn)镾ecurity。沒有在資源服務(wù)器中配置的路徑,登錄認(rèn)證成功后并不需要攜帶token,而還是使用Security需要的Cookie和Session。
- 如果資源服務(wù)器沒有配置tokenService,就會(huì)調(diào)用配置的userInfoUri去auth-server獲取用戶信息;如果資源服務(wù)器配置了tokenService,再加上有UserDetails的實(shí)現(xiàn)類,可以解析,就不用在調(diào)用auth-server的接口。
參考:
Spring Security Oauth2和Spring Boot實(shí)現(xiàn)單點(diǎn)登錄
Spring Security Oauth2 單點(diǎn)登錄案例實(shí)現(xiàn)和執(zhí)行流程剖析
Spring Security OAuth2 入門
Spring security. How to log out user (revoke oauth2 token)
從零開始的Spring Security Oauth2(一)
從零開始的Spring Security Oauth2(二)
從零開始的Spring Security Oauth2(三)
Spring Security OAuth2 入門
Spring Security EnableOAuth2Sso注解實(shí)現(xiàn)原理
Spring Security OAuth2 使用Redis存儲(chǔ)token鍵值詳解
Spring Security OAuth2實(shí)現(xiàn)使用JWT
jwt 官網(wǎng)
jwt 解碼器
到此這篇關(guān)于SpringSecurity OAuth2單點(diǎn)登錄和登出的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity OAuth2單點(diǎn)登錄登出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
- springSecurity實(shí)現(xiàn)簡單的登錄功能
- SpringSecurity自定義登錄成功處理
- SpringSecurity自定義登錄界面
- SpringBoot如何整合Springsecurity實(shí)現(xiàn)數(shù)據(jù)庫登錄及權(quán)限控制
- 詳解SpringSecurity中的Authentication信息與登錄流程
- Springboot+SpringSecurity+JWT實(shí)現(xiàn)用戶登錄和權(quán)限認(rèn)證示例
- SpringSecurity動(dòng)態(tài)加載用戶角色權(quán)限實(shí)現(xiàn)登錄及鑒權(quán)功能
- SpringSecurity集成第三方登錄過程詳解(最新推薦)
相關(guān)文章
JavaWeb實(shí)現(xiàn)用戶登錄與注冊(cè)功能
這篇文章主要為大家詳細(xì)介紹了JavaWeb實(shí)現(xiàn)用戶登錄與注冊(cè)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08基于Java匯總Spock框架Mock靜態(tài)資源經(jīng)驗(yàn)
這篇文章主要介紹了基于Java匯總Spock框架Mock靜態(tài)資源經(jīng)驗(yàn),前面講了?Spock框架Mock對(duì)象、方法經(jīng)驗(yàn)總結(jié),今天分享一下Spock框架中Mock靜態(tài)資源的實(shí)踐經(jīng)驗(yàn)匯總。分成靜態(tài)資源和混合場(chǎng)景,需要的朋友可以參考一下2022-02-02Spring?Boot整合郵箱發(fā)送郵件實(shí)例
大家好,本篇文章主要講的是Spring?Boot整合郵箱發(fā)送郵件實(shí)例,感興趣的同學(xué)趕快來看一看吧,對(duì)你有幫助的話記得收藏一下2022-02-02Spring Bean初始化及銷毀多種實(shí)現(xiàn)方式
這篇文章主要介紹了Spring Bean初始化及銷毀多種實(shí)現(xiàn)方式,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11SpringBoot定時(shí)任務(wù)動(dòng)態(tài)擴(kuò)展ScheduledTaskRegistrar詳解
這篇文章主要為大家介紹了SpringBoot定時(shí)任務(wù)動(dòng)態(tài)擴(kuò)展ScheduledTaskRegistrar類示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Java數(shù)據(jù)結(jié)構(gòu)之散列表詳解
散列表(Hash table,也叫哈希表),是根據(jù)關(guān)鍵碼值(Key value)而直接進(jìn)行訪問的數(shù)據(jù)結(jié)構(gòu)。本文將為大家具體介紹一下散列表的原理及其代碼實(shí)現(xiàn)2022-01-01Retrofit+RxJava實(shí)現(xiàn)帶進(jìn)度條的文件下載
這篇文章主要為大家詳細(xì)介紹了Retrofit+RxJava實(shí)現(xiàn)帶進(jìn)度條的文件下載,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06使用SpringBoot-JPA進(jìn)行自定義保存及批量保存功能
這篇文章主要介紹了使用SpringBoot-JPA進(jìn)行自定義的保存及批量保存功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06