首頁 > 軟體

SpringSecurity OAuth2單點登入和登出的實現

2022-02-09 13:00:37

Spring Security OAuth 最新官方已經不再維護,以下內容只用於學習記錄。

GitHub:shpunishment/spring-security-oauth2-demo

1. 單點登入

單點登入即有多個子系統,有一個認證中心。當存取其中任意一個子系統時,如果發現未登入,就跳到認證中心進行登入,登入完成後再跳回該子系統。此時存取其他子系統時,就已經是登入狀態了。登出統一從認證中心登出,登出後各個子系統就無法存取了,需要再次登入。

Spring Security OAuth 建立在Spring Security 之上,所以大部分設定還是在Security中,Security完成對使用者的認證和授權,OAuth完成單點登入。

Spring Security OAuth 的單點登入主要靠@EnableOAuth2Sso實現,簡化了從資源伺服器到認證授權伺服器的SSO流程,並使用授權碼方式獲取。

1.1 使用記憶體儲存使用者端和使用者資訊

1.1.1 認證中心 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預設情況下有16個分片,這裡設定具體使用的分片,預設是0
    database: 0
    host: localhost
    port: 6379
    # 連線密碼(預設為空)
    password:
    # 連線超時時間(毫秒)
    timeout: 10000ms
    lettuce:
      pool:
        # 連線池最大連線數(使用負值表示沒有限制) 預設 8
        max-active: 8
        # 連線池最大阻塞等待時間(使用負值表示沒有限制) 預設 -1
        max-wait: -1
        # 連線池中的最大空閒連線 預設 8
        max-idle: 8
        # 連線池中的最小空閒連線 預設 0
        min-idle: 0

新增授權伺服器設定,主要令牌路徑的安全性,使用者端詳情和令牌儲存。

這裡設定了一個使用者端,支援授權碼模式和重新整理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;

    /**
     * 設定授權伺服器的安全性,令牌端點的安全約束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 開啟 /oauth/check_token
                .tokenKeyAccess("permitAll()")
                // 開啟 /oauth/token_key
                .checkTokenAccess("isAuthenticated()")
                // 允許表單認證
                // 如果設定,且url中有client_id和client_secret的,則走 ClientCredentialsTokenEndpointFilter
                // 如果沒有設定,但是url中沒有client_id和client_secret的,走basic認證保護
                .allowFormAuthenticationForClients();
    }

    /**
     * 設定使用者端,可存在記憶體和資料庫中
     *
     * @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"))
                // 必須新增,會和請求時重定向地址匹配
                .redirectUris("http://localhost:8001/service1/login")
                // 自動批准,在登入成功後不會跳到批准頁面,讓資源所有者批准
                //.autoApprove(true);
    }

    /**
     *
     * 設定授權伺服器端點的非安全功能,例如令牌儲存,令牌自定義,使用者批准和授予型別
     *
     * @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);
    }
}

新增資源伺服器設定,主要設定資源id和需要Token驗證的url

對於相同的url,如果二者都設定了驗證,則優先進入ResourceServerConfigurerAdapter,會被 OAuth2AuthenticationProcessingFilter 處理,進行token驗證;而不會進行WebSecurityConfigurerAdapter 的表單認證等。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "resource-1";

    /**
     * 新增特定於資源伺服器的屬性
     *
     * @param resources
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(RESOURCE_ID);
    }

    /**
     * 使用此設定安全資源的存取規則,設定需要token驗證的url。 預設情況下,所有不在"/oauth/**"中的資源都受到保護。
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 只有 /security/getUserInfo 需要token驗證
        http
                .requestMatchers().antMatchers("/security/getUserInfo")
                .and()
                .authorizeRequests()
                .anyRequest().authenticated();
    }
} 

security設定,使用者資料,自定義登入頁,成功失敗Handler,session,設定非受保護URL等。

這裡新增了兩個使用者以及登入頁等設定。

@Configuration
public class ServerWebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 認證管理器設定,用於資訊獲取來源(UserDetails)以及密碼校驗規則(PasswordEncoder)
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                // 使用記憶體認證,在記憶體中儲存兩個使用者
                .inMemoryAuthentication()
                .passwordEncoder(passwordEncoder())
                // admin 擁有ADMIN和USER的許可權
                .withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN", "USER")
                .and()
                // user 擁有USER的許可權
                .withUser("user").password(passwordEncoder().encode("user")).roles("USER");
    }

    /**
     * 核心過濾器設定,更多使用ignoring()用來忽略對靜態資源的控制
     * @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 路徑設定為不需要任何身份驗證,其他所有路徑必須經過驗證
                .antMatchers("/login", "/oauth/authorize").permitAll()
                // 其他請求都需要已認證
                .anyRequest().authenticated()
                .and()
                // 使用表單登入
                .formLogin()
                // 自定義username 和password引數
                .usernameParameter("login_username")
                .passwordParameter("login_password")
                // 自定義登入頁地址
                .loginPage("/loginPage")
                // 驗證表單的地址,由過濾器 UsernamePasswordAuthenticationFilter 攔截處理
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .csrf().disable();
    }

    @Bean
    public static BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

獲取當前使用者資訊,供使用者端獲取

@RestController
@RequestMapping("/security")
public class SecurityController {
    @GetMapping("/getUserInfo")
    @ResponseBody
    public Principal getUserInfo(Principal principal) {
        return principal;
    }
}

1.1.2 子系統 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
      # 將使用者重定向到的授權URI
      userAuthorizationUri: http://localhost:8000/auth-server/oauth/authorize
    resource:
      # 獲取當前使用者詳細資訊
      userInfoUri: http://localhost:8000/auth-server/security/getUserInfo

security設定,如果需要對service-1的url進行控制,需要新增 WebSecurityConfigurerAdapter 設定,可設定子系統中哪些介面需要auth-server的認證,設定非受保護URL等。

@Configuration
// @EnableOAuth2Sso 註解 在繼承 WebSecurityConfigurerAdapter 類的上面時
// 代表著在該子類設定的基礎上增強 OAuth2Sso 相關設定。
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ClientWebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 安全過濾器鏈設定,自定義安全存取策略。可設定使用者端不受保護的資源
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/**")
                .authorizeRequests()
                // 存取 / /home 不用認證
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                // 許可權不足跳轉 /401
                .exceptionHandling().accessDeniedPage("/401");
    }

    /**
     * 核心過濾器設定,更多使用ignoring()用來忽略對靜態資源的控制和過濾微服務間feign的介面
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring()
                .antMatchers("/js/**");
    }
}

使用者端資源伺服器設定,只有 /api/* 需要token驗證

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "resource-1";

    /**
     * 新增特定於資源伺服器的屬性
     *
     * @param resources
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(RESOURCE_ID);
    }

    /**
     * 使用此設定安全資源的存取規則,設定需要token驗證的url。 預設情況下,所有不在"/oauth/**"中的資源都受到保護。
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // /api/* 都需要token驗證,會被 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"); } /** * 測試 /api/* 是否被資源伺服器攔截,需要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");
    }

    /**
     * 測試 /api/* 是否被資源伺服器攔截,需要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 測試

service-2根據service-1複製一遍。

service-1和service-2不用登入即可存取 / /home

存取 /user 需要認證的資源,會先到auth-server進行認證

資源所有者批准

批准後才能存取到 /user

service-2的 /user 也可存取,即實現了單點登入

存取 /admin 使用者許可權不足

1.2 使用資料庫儲存使用者端和使用者資訊

只需要修改auth-server中使用者端和使用者資訊的獲取方式。

使用者資訊部分,修改security設定,參考 Spring Security 使用 中的使用資料庫儲存使用者資訊。

由於將Token等資訊存在了Redis中,所以在資料庫中只需要儲存使用者端資訊。修改 AuthorizationServerConfig

@Autowired
private DataSource dataSource;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients
            .withClientDetails(clientDetails(dataSource));
}

/**
 * 獲取使用者端詳細資訊服務,JDBC實現
 * @return
 */
@Bean
public ClientDetailsService clientDetails(DataSource dataSource) {
    return new JdbcClientDetailsService(dataSource);
}

新增表和資料,密碼使用BCrypt加密,資料和使用記憶體時一致。

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);

效果與使用記憶體時一致。

1.3 單點登入流程

開啟F12會看到以下重定向過程,可看到大致步驟:

  • 請求授權碼,判斷未登入,重定向登入頁
  • 登入成功,重定向繼續請求授權碼,未被資源所有者批准,返回批准頁面
  • 資源所有者批准,重定向返回授權碼
  • 使用者端獲取到授權碼,請求Token
  • 獲取到Token,重定向 /user

1.2.1 請求授權碼,判斷未登入,重定向登入頁

存取使用者端受保護資源 localhost:8001/service1/user,未登入重定向到 localhost:8001/service1/login 進行登入認證,因為設定了單點登入@EnableOAuth2Sso,所以單點登入攔截器會讀取授權伺服器的設定,發起獲取授權碼請求
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() 處理,因為未登入認證,丟擲異常

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);

呼叫 LoginUrlAuthenticationEntryPoint.commence() 方法,獲取登入頁地址,並重定向

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

1.2.2 登入成功,重定向繼續請求授權碼,未被資源所有者批准,返回批准頁面

在auth-server中使用者密碼由 AbstractAuthenticationProcessingFilter.doFilter() 處理,UsernamePasswordAuthenticationFilter 繼承自 AbstractAuthenticationProcessingFilter,在父類別 doFilter() 方法中,會呼叫子類實現的 attemptAuthentication 方法,獲取認證資訊

authResult = attemptAuthentication(request, response);

在 attemptAuthentication() 方法中,將使用者名稱和密碼封裝成token並認證,並新增額外資訊後,進行認證

this.getAuthenticationManager().authenticate(authRequest);

getAuthenticationManager() 方法獲取 AuthenticationManager 的實現類 ProviderManager,在 authenticate() 方法中,找到合適的 AuthenticationProvider 處理認證,這裡是 DaoAuthenticationProvider,它父類別 AbstractUserDetailsAuthenticationProvider 實現了該方法

result = provider.authenticate(authentication);

父類別會呼叫 retrieveUser() 方法檢索使用者,實現在 DaoAuthenticationProvider

user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);

這裡是從記憶體或資料庫中獲取使用者,然後進行密碼校驗,成功後,將資訊儲存到Authentication,並返回。呼叫成功Handler,記住我等等。

預設登入成功,會重定向之前請求的地址
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() 處理,這時有使用者認證資訊,獲取client資訊,進行檢查,檢查資源所有者是否批准(使用者端可設定是否自動批准)

如果未批准,返回批准頁,請求轉發 forward:/oauth/confirm_access

return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

1.2.3 資源所有者批准,重定向返回授權碼

使用者批准後,被 AuthorizationEndpoint.approveOrDeny() 方法處理,返回授權碼,並重定向使用者設定的地址(/login),並帶上code和state

return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);

1.2.4 使用者端獲取到授權碼,請求Token

在使用者端 AbstractAuthenticationProcessingFilter 中處理

authResult = attemptAuthentication(request, response);

由子類 OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication() 處理,判斷token是否為空

accessToken = restTemplate.getAccessToken();

如果為空,在 AuthorizationCodeAccessTokenProvider.obtainAccessToken() 方法中,獲取返回的授權碼,向auth-server請求Token

return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),getHeadersForTokenRequest(request));

在auth-server中 TokenEndpoint.getAccessToken() 方法獲取token,進行使用者端校驗後生成token並返回

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

1.2.5 獲取到Token,重定向 /user

回到在使用者端 OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication() 中,獲取到token後,帶上token,向auth-server請求使用者資訊。
預設Token是使用uuid,生成用於認證的token和重新整理的Token。認證Token預設12小時過期,重新整理的Token預設30天過期。

OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());

在auth-server 被 OAuth2AuthenticationProcessingFilter 處理,從頭部獲取並驗證token後,完成該請求。

使用者端獲取到使用者資訊,在使用者端重新完成登入的流程,最後在預設的登入成功Handler中獲取到重定向地址(即 /user),並重定向。

1.3 JWT Token

1.3.1 資源伺服器未新增tokenServices

只需要修改auth-server中授權伺服器。

新增依賴

<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增強設定
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer, jwtAccessTokenConverter()));

    endpoints
            // 令牌存在redis
            .tokenStore(tokenStore())
            .tokenEnhancer(tokenEnhancerChain)
            // 密碼授權方式時需要
            .authenticationManager(authenticationManager)
            // /oauth/token 執行get和post
            .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}

/**
 * 用來生成token的轉換器
 * @return
 */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
   JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
   // 對稱加密,設定簽名,使用下面這個值作為金鑰
   jwtAccessTokenConverter.setSigningKey("oauth");
   return jwtAccessTokenConverter;
}

新增使用者端2,支援密碼授權方式

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);

測試
使用密碼模式獲取token

使用token獲請求資源伺服器保護的介面

流程

在auth-server的 TokenEndpoint 中驗證資訊並獲取token。然後帶著token請求,在service-1中被 OAuth2AuthenticationProcessingFilter 處理,doFilter() 方法會提取並驗證token。

按上面的設定,並沒有在資源伺服器中設定tokenServices

Authentication authResult = authenticationManager.authenticate(authentication);

所以在載入 Authentication 的時候,tokenServices 為 UserInfoTokenServices,就會呼叫設定的 userInfoUri 去auth-server獲取使用者資訊

OAuth2Authentication auth = tokenServices.loadAuthentication(token);

1.3.2 資源伺服器新增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預設情況下有16個分片,這裡設定具體使用的分片,預設是0
    database: 0
    host: localhost
    port: 6379
    # 連線密碼(預設為空)
    password:
    # 連線超時時間(毫秒)
    timeout: 10000ms
    lettuce:
      pool:
        # 連線池最大連線數(使用負值表示沒有限制) 預設 8
        max-active: 8
        # 連線池最大阻塞等待時間(使用負值表示沒有限制) 預設 -1
        max-wait: -1
        # 連線池中的最大空閒連線 預設 8
        max-idle: 8
        # 連線池中的最小空閒連線 預設 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 中驗證資訊並獲取token。然後帶著token請求,在service-1中被 OAuth2AuthenticationProcessingFilter 處理,doFilter() 方法會提取並驗證token。

按上面的設定,並沒有在資源伺服器中設定tokenServices

Authentication authResult = authenticationManager.authenticate(authentication);

所以在載入 Authentication 的時候,tokenServices 為 DefaultTokenServices,再加上有UserDetails的實現類,可以解析,就不用在呼叫auth-server

OAuth2Authentication auth = tokenServices.loadAuthentication(token);

2. 單點登出

這裡除了部分的資源伺服器中設定的api需要token驗證,其他還是依賴於Spring Security的認證。而Spring Security是使用Cookie和Session的記錄使用者。所以可以將認證中心和各個子系統的Cookie設定在同一路徑下,在認證中心登出時,將Cookie一併刪除,實現認證中心和各個子系統的登出。各子系統需要知道認證中心的登出地址。在這裡是http://localhost:8000/auth-server/logout。

修改認證中心和各個子系統的Cookie路徑,測試發現,放在 / 下才可實現

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 {
        // 將子系統的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()
                // 預設為 /logout
                .logout()
                .logoutSuccessHandler(customLogoutSuccessHandler)
                // 無效對談
                .invalidateHttpSession(true)
                // 清除身份驗證
                .clearAuthentication(true)
                .permitAll()
                ...;
    }
}

當然,使用了OAuth發放token,應該也需要使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. 總結

  • AuthorizationEndpoint 處理 /oauth/authorize;TokenEndpoint 處理 /oauth/token。
  • @EnableOAuth2Sso 會將資源伺服器標記為OAuth 2.0的使用者端, 它將負責將資源所有者(終端使用者)重定向到使用者必須輸入其憑據的授權伺服器。完成後,使用者將被重定向回具有授權碼的使用者端。然後使用者端通過呼叫授權伺服器獲取授權程式碼並將其交換為存取令牌。只有在此之後,使用者端才能使用存取令牌呼叫資源伺服器。
  • @EnableResourceServer 意味著所屬的服務需要存取令牌才能處理請求。在呼叫資源伺服器之前,需要先從授權伺服器獲取存取令牌。
  • 在資源伺服器中設定的路徑,都會被 OAuth2AuthenticationProcessingFilter 處理,獲取token。
  • 之前一直在糾結,使用者端獲取到了token,為什麼在存取 /user 的請求頭中並沒有Authorization,亦可請求成功。其實都因為Security。沒有在資源伺服器中設定的路徑,登入認證成功後並不需要攜帶token,而還是使用Security需要的Cookie和Session。
  • 如果資源伺服器沒有設定tokenService,就會呼叫設定的userInfoUri去auth-server獲取使用者資訊;如果資源伺服器設定了tokenService,再加上有UserDetails的實現類,可以解析,就不用在呼叫auth-server的介面。

參考:

Spring Security Oauth2和Spring Boot實現單點登入
Spring Security Oauth2 單點登入案例實現和執行流程剖析
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註解實現原理
Spring Security OAuth2 使用Redis儲存token鍵值詳解
Spring Security OAuth2實現使用JWT
jwt 官網
jwt 解碼器

到此這篇關於SpringSecurity OAuth2單點登入和登出的實現的文章就介紹到這了,更多相關SpringSecurity OAuth2單點登入登出內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com