spring-cloud-oauth2 認證授權


什么是OAuth2?

  OAuth2是一個關於授權的開放標准,核心思路是通過各類認證手段(具體什么手段OAuth2不關心)認證用戶身份,並頒發token(令牌),使得第三方應用可以使用該令牌在限定時間、限定范圍訪問指定資源。主要涉及的RFC規范有RFC6749(整體授權框架),RFC6750(令牌使用),RFC6819(威脅模型)這幾個,一般我們需要了解的就是RFC6749。獲取令牌的方式主要有四種,分別是授權碼模式簡單模式密碼模式客戶端模式。這里要先明確幾個OAuth2中的幾個重要概念:

  • resource_owner : 擁有被訪問資源的用戶
  • user-agent: 一般來說就是瀏覽器
  • client : 第三方應用
  • Authorization server : 認證服務器,用來進行用戶認證並頒發token
  • Resource server: 資源服務器,擁有被訪問資源的服務器,需要通過token來確定是否有權限訪問

  我們在瀏覽器端或者APP端做登錄的時候時常會遇到 QQ登錄、微信登陸、微博登錄 等等。這一類稱之為第三方登錄。在APP端 往往會采用OAuth2。以QQ登錄為准,通常是點擊了QQ登錄,首先跳轉到QQ登錄授權頁面進行掃碼授權。然后跳回原來網頁設定好的一個回調地址。這其實就完成了OAuth的整個授權流程。OAuth在"客戶端"與"服務提供商"之間,設置了一個授權層(authorization layer)。"客戶端"不能直接登錄"服務提供商",只能登錄授權層,以此將用戶與客戶端區分開來。"客戶端"登錄授權層所用的令牌(token),與用戶的密碼不同。用戶可以在登錄的時候,指定授權層令牌的權限范圍和有效期。"客戶端"登錄授權層以后,"服務提供商"根據令牌的權限范圍和有效期,向"客戶端"開放用戶儲存的資料。

OAuth2 運行流程:

  OAuth 2.0的運行流程如下圖,摘自RFC6749。

  1. 用戶打開客戶端以后,客戶端要求用戶給予授權。( QQ登錄跳轉到授權頁面)
  2. 用戶同意給予客戶端授權。  (用戶掃碼確定授權)
  3. 客戶端使用上一步獲得的授權,向認證服務器申請令牌。(跳轉到回調地址,且攜帶一個 code )
  4. 認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。  (通過上一步得到的code 進行授權碼認證)
  5. 客戶端使用令牌,向資源服務器申請獲取資源。 (用換取到的 access_token 進行訪問資源)
  6. 資源服務器確認令牌無誤,同意向客戶端開放資源。 (token 認證通過 返回數據)。

授權方式:

  客戶端必須得到用戶的授權(authorization grant),才能獲得令牌(access token)。OAuth 2.0定義了四種授權方式。

  • 授權碼模式(authorization code)
  • 簡化模式(implicit)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(client credentials)

  本文主要介紹 授權碼模式 跟 密碼模式。

授權認證服務實現:

  搭建認證服務 Authorization server:

1.導入依賴(包括后續要用到的一些依賴),這里 springboot 2.0.1 、springCloud 版本為 Finchley.SR3:

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-security -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
    </dependencies>

2. 認證服務器配置,要實現認證服務器其實很簡單,只要打上 @EnableAuthorizationServer 注解,然后繼承 AuthorizationServerConfigurerAdapter 進行一些簡單的配置即可。

@Configuration @EnableAuthorizationServer public class WuzzAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { //http://localhost:8766/oauth/authorize?client_id=wuzzClientId&response_type=code&redirect_uri=http://www.baidu.com&scope=all
 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("wuzzClientId")//客戶端得ID,比如我們在QQ互聯中心申請得。可以寫多個。配置 循環
                .secret(passwordEncoder().encode("wuzzSecret")) // 客戶端密鑰,需要進行加密
                .accessTokenValiditySeconds(7200)// token 有效時常 0 永久有效
                .authorizedGrantTypes("password", "implicit", "refresh_token", "authorization_code")// 支持得授權類型
                .redirectUris("http://www.baidu.com")//回調地址
                .scopes("all", "read", "write");//擁有的 scope 可選
 } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.userDetailsService(userDetailsService()) // 用戶信息得服務,一版是都數據庫
                .authenticationManager(authenticationManager())// 認證管理器。
 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients()//允許表單登錄
                .checkTokenAccess("permitAll()"); //開啟/oauth/check_token驗證端口認證權限訪問
 } @Bean // 注入認證管理器
    public AuthenticationManager authenticationManager() { AuthenticationManager authenticationManager = new AuthenticationManager() { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { return daoAuthenticationProvider().authenticate(authentication); } }; return authenticationManager; } @Bean//注入認證器
    public AuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService()); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); return daoAuthenticationProvider; } @Bean//注入 用戶信息服務
    public UserDetailsService userDetailsService() { return new MyUserDetailService(); } @Bean//注入密碼加密
    public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

3.由於 OAuth2 依賴於 Security 得配置,所以我們這里還需要配置一下  Security :

@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); } }

4.自定義的用戶信息服務類,由於Oauth 的用戶需要有個  ROLE_USER 角色 才可以訪問,所以這里寫死。

public class MyUserDetailService implements UserDetailsService { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("表單登錄用戶名:" + username); // 根據用戶名查找用戶信息 //根據查找到的用戶信息判斷用戶是否被凍結
        String password = passwordEncoder.encode("123456"); logger.info("數據庫密碼是:" + password); return new User(username, password, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_USER")); } }

5.啟動主類即可進行訪問。

  授權碼模式:

  授權碼需要訪問接口 : http://localhost:8766/oauth/authorize?client_id=wuzzClientId&response_type=code&redirect_uri=http://www.baidu.com&scope=all

  其中 client_id 為認證服務器為每個對接的第三方提供的唯一ID。response_type 返回類型,寫死為 code 。redirect_uri 回調地址。

  訪問該地址,如果用戶當前未登錄將會跳轉到用戶登錄頁面進行登錄。然后將會跳轉到下面這個頁面。詢問用戶是否為 wuzzClientId這個應用授權。

   點擊授權,將會跳轉到回調地址頁,由於沒有備案域名,這里直接跳到百度:

   可以看到這里后面攜帶了 一個 code 參數,這個參數就是認證服務器為第三方提供的授權碼。然后再用這個授權碼去換取 access_token。我這里就用 postman 進行測試:

  換取 access_token得地址為 /oauth/token,首先需要填入認證服務器頒發的 clientId、client-secret

  然后填寫參數 ,發送請求。注意這里前三個參數是必填的。

   可以看到這樣就可以成功的獲取到 access_token 了。然后第三方用戶就可以通過這個 token 去資源服務器上獲取授權的用戶信息了。后續會提到這個token 怎么用。

  密碼模式:

  相比授權碼授權方式來說,密碼模式相對簡單,我們只需要修改授權類型,增加 用戶名、密碼 字段:

 

  細心的小伙伴可能會發現,我這里用的是同一個用戶  admin 去獲取token,獲取到的 access_token、refresh_token 都是一樣的 ,唯獨 expires_in(過期時間)逐漸減少。這是Oauth 提供的機制。在這個  expires_in 時間內 access_token都是有效的。當然,refresh_token  用於刷新 access_token,避免了用戶的頻繁認證,刷新token請求如下:

資源服務器 Resource server:

1.配置資源服務器就更簡單了,新建一個 Springboot 標准工程,導入與認證服務器一樣的依賴,然后定義一個類,打上 @EnableResourceServer 注解,實現 ResourceServerConfigurerAdapter 進行簡單配置:

@Configuration @EnableResourceServer public class WuzzResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { //配置受保護的資源
        http.authorizeRequests().antMatchers("/api/order/**").authenticated(); } }

2.配置文件:

server.port = 8765 #check_token url security.oauth2.resource.token-info-uri= http://localhost:8766/oauth/check_token
security.oauth2.resource.prefer-token-info= true # authorize url security.oauth2.client.access-token-uri=http://localhost:8766/oauth/authorize
#用戶認證地址 check_token security.oauth2.client.user-authorization-uri=http://localhost:8766/oauth/check_token
security.oauth2.client.client-id=wuzzClientId security.oauth2.client.client-secret=wuzzSecret

3.提供一個測試接口

@RestController @RequestMapping("/api/order") public class OrderController {
@RequestMapping(
"addOrder") public String addOrder(){ return "addOrder"; } }

4.啟動服務,當然,你想直接訪問這個接口顯然是不行的

 

   這個時候我們帶上之前獲取到的  token ,過期的話重新獲取一個:

   這樣就實現了資源服務器與認證服務器的打通。

Token 存儲:

  OAuth2存儲token值的方式由多種,所有的實現方式都是實現了TokenStore接口

  1. InMemoryTokenStore:token存儲在本機的內存之中
  2. JdbcTokenStore:token存儲在數據庫之中
  3. JwtTokenStore:token不會存儲到任何介質中
  4. RedisTokenStore:token存儲在Redis數據庫之中

   這里使用 Redis 進行存儲演示:

1.配置 redis :

# Redis服務地址 spring.redis.host=192.168.1.101 # Redis服務端口 spring.redis.port=6379 # Redis 連接密碼 spring.redis.password=wuzhenzhao

2.新增Redis連接工廠:

@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore() { return new RedisTokenStore(redisConnectionFactory); }
}

3.配置,再 WuzzAuthorizationServerConfig 中新增如下配置。

// 自定義token存儲類型
@Autowired private TokenStore tokenStore;  @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {   endpoints.userDetailsService(userDetailsService()) // 用戶信息得服務,一版是都數據庫
    .authenticationManager(authenticationManager())// 認證管理器。
    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)     .tokenStore(tokenStore); }

4.啟動服務並且通過密碼授權獲取 access_token.然后查看Redis 上的數據變化:

   可以發現 token 已經被存儲到了 redis上面,然后我們把認證服務器重啟,然后拿着哲哥 access_token 去訪問資源服務器,發現依舊可以訪問得到。Redis token 配置成功。

JWT 整合:

  JSON Web Token(JWT)是一個開放的行業標准(RFC 7519),它定義了一種自包含、可拓展、密簽協議格式,用於在通信雙方傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT可以使用HMAC算法或使用RSA的公鑰/私鑰對來簽名,防止被篡改。

  JWT 的幾個特點

  • JWT 默認是不加密,但也是可以加密的。生成原始 Token 以后,可以用密鑰再加密一次。
  • JWT 不加密的情況下,不能將秘密數據寫入 JWT。
  • JWT 不僅可以用於認證,也可以用於交換信息。有效使用 JWT,可以降低服務器查詢數據庫的次數。
  • JWT 的最大缺點是,由於服務器不保存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非服務器部署額外的邏輯。
  • JWT 本身包含了認證信息,一旦泄露,任何人都可以獲得該令牌的所有權限。為了減少盜用,JWT 的有效期應該設置得比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證。
  • 為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。

  它是一個很長的字符串,中間用點(.)分隔成三個部分。注意,JWT 內部是沒有換行的,這里只是為了便於展示,將它寫成了幾行。JWT 的三個部分依次如下。

  • Header(頭部)
  • Payload(負載)
  • Signature(簽名)

   如下就是一個 JWT  :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJjb21wYW55IjoiYWxpYmFiYSIsImV4cCI6MTU5NDEwMTA2OSwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iLCJST0xFX1VTRVIiXSwianRpIjoiMzQ4MmM4YmEtYjdmYy00NDIxLWIwZmItYzVhYjhlOGUzYzY2IiwiY2xpZW50X2lkIjoid3V6ekNsaWVudElkIn0.
-DxGM5URWqHOZE5mmH4CgJI_bX-e9THA9WeQeT7Z5qU

  像這個 token 我們可以借助第三方進行解碼 : https://www.jsonwebtoken.io/ .通過該網址就可以看到包含的所有信息。

1.注入 Jwt 相關類:

@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @ConditionalOnProperty(prefix = "wuzz", name = "storeType", havingValue = "redis") public TokenStore redisTokenStore() { return new RedisTokenStore(redisConnectionFactory); } @Configuration @ConditionalOnProperty(prefix = "wuzz", name = "storeType", havingValue = "jwt", matchIfMissing = true) public static class JwtTokenConfig { //自包含、可拓展、密簽 //https://www.jsonwebtoken.io/ 解碼 //{ // "exp": 1593785308, // "user_name": "admin", // "authorities": [ // "admin", // "ROLE_USER" // ], // "jti": "e2e5e811-b235-49b8-8678-5bf22e265415", // "client_id": "wuzzClientId", // "scope": [ // "all" // ] //}
        @Bean// 注入 jwt 存儲 token
        public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean// 注入轉換器
        public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("wuzz");//
            return accessTokenConverter; } @Bean//添加 token 包含信息
        @ConditionalOnMissingBean(name = "jwtTokenEnhancer") public TokenEnhancer jwtTokenEnhancer() { return new WuzzJwtTokenEnhancer(); } } }

2.配置文件新增:

wuzz.storeType=jwt

3.在 WuzzAuthorizationServerConfig 中配置:

// 自定義token存儲類型
 @Autowired private TokenStore tokenStore; // jwt token
    @Autowired(required = false) private JwtAccessTokenConverter jwtAccessTokenConverter; //jwt token 附加信息
    @Autowired(required = false) private TokenEnhancer jwtTokenEnhancer; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.userDetailsService(userDetailsService()) // 用戶信息得服務,一版是都數據庫
                .authenticationManager(authenticationManager())// 認證管理器。
 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) .tokenStore(tokenStore); if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancers = new ArrayList<>(); enhancers.add(jwtTokenEnhancer); enhancers.add(jwtAccessTokenConverter); tokenEnhancerChain.setTokenEnhancers(enhancers); endpoints.tokenEnhancer(tokenEnhancerChain) .accessTokenConverter(jwtAccessTokenConverter); } }

4. 自定義 token 附加信息實現:

public class WuzzJwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { Map<String, Object> info = new HashMap<String, Object>(); info.put("company", "alibaba"); ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info); return oAuth2AccessToken; } }

5.啟動認證服務器用密碼認證方式獲取一下 access_token ,發現token已經發生了變化,而且我們在token里增加的屬性也顯示出來了:

  我們可以通過在資源服務器中寫一個解析這個 token的方法:

@RequestMapping(value = "/me", method = {RequestMethod.GET}) public Object me(Authentication user, HttpServletRequest request) throws UnsupportedEncodingException { String header = request.getHeader("Authorization"); String token = StringUtils.substringAfter(header, "Bearer "); Claims claims = Jwts.parser().setSigningKey("wuzz".getBytes("UTF-8")).parseClaimsJws(token).getBody(); String company = (String) claims.get("company"); System.out.println(company); return user; }

   然后請求該接口可以獲取到相關的信息。

 

整合 JdbcClientDetailsService :

  在上文中我們講 client的信息都是寫死在配置里面,顯然在生產環境下是不合理的,OAuth2 提供了相應的配置。

1.導入依賴:

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

2.修改配置:

@Autowired private DataSource dataSource; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // clients.inMemory().withClient("wuzzClientId")//客戶端得ID,比如我們在QQ互聯中心申請得。可以寫多個。配置 循環 // .secret(passwordEncoder().encode("wuzzSecret")) // 客戶端密鑰,需要進行加密 // .accessTokenValiditySeconds(7200)// token 有效時常 0 永久有效 // .authorizedGrantTypes("password", "implicit", "refresh_token", "authorization_code")// 支持得授權類型 // .redirectUris("http://www.baidu.com")//回調地址 // .scopes("all", "read", "write");//擁有的 scope 可選
  clients.withClientDetails(new JdbcClientDetailsService(dataSource)); }

3.新增數據庫配置:

#解決springboot2.0 后內存數據庫H2與actuator不能同時使用報datasource循環依賴 spring.cloud.refresh.refreshable=none spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://192.168.1.101:3306/study?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root spring.datasource.password=123456

4.數據庫新增對應表,並添加一條數據:

-- ---------------------------- -- Table structure for oauth_client_details -- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(48) 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; -- ---------------------------- -- Records of oauth_client_details -- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('wuzzClientId', NULL, '$2a$10$L2juyPBc606/9xkmFWu5S.5PBjfz6IXxtUnl8Bk9B2s9Bbn1TPO.2', 'all', 'password', 'http://www.baidu.com', NULL, NULL, NULL, NULL, NULL);

 5.重啟服務,按照原來的方式通過用戶名密碼進行授權,也是可以實現的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM