Spring Cloud OAuth2 實現用戶認證及單點登陸

文章較長,建議推薦,建議轉發,建議收藏,建議關注公衆號哈。html

OAuth 2 有四種受權模式,分別是受權碼模式(authorization code)、簡化模式(implicit)、密碼模式(resource owner password credentials)、客戶端模式(client credentials),具體 OAuth2 是什麼,能夠參考這篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)java

本文咱們將使用受權碼模式和密碼模式兩種方式來實現用戶認證和受權管理。mysql

OAuth2 實際上是一個關於受權的網絡標準,它制定了設計思路和運行流程,利用這個標準咱們實際上是能夠本身實現 OAuth2 的認證過程的。今天要介紹的 spring-cloud-starter-oauth2 ,實際上是 Spring Cloud 按照 OAuth2 的標準並結合 spring-security 封裝好的一個具體實現。git

什麼狀況下須要用 OAuth2

首先你們最熟悉的就是幾乎每一個人都用過的,好比用微信登陸、用 QQ 登陸、用微博登陸、用 Google 帳號登陸、用 github 受權登陸等等,這些都是典型的 OAuth2 使用場景。假設咱們作了一個本身的服務平臺,若是不使用 OAuth2 登陸方式,那麼咱們須要用戶先完成註冊,而後用註冊號的帳號密碼或者用手機驗證碼登陸。而使用了 OAuth2 以後,相信不少人使用過、甚至開發過公衆號網頁服務、小程序,當咱們進入網頁、小程序界面,第一次使用就無需註冊,直接使用微信受權登陸便可,大大提升了使用效率。由於每一個人都有微信號,有了微信就能夠立刻使用第三方服務,這體驗不要太好了。而對於咱們的服務來講,咱們也不須要存儲用戶的密碼,只要存儲認證平臺返回的惟一ID 和用戶信息便可。github

以上是使用了 OAuth2 的受權碼模式,利用第三方的權威平臺實現用戶身份的認證。固然了,若是你的公司內部有不少個服務,能夠專門提取出一個認證中心,這個認證中心就充當上面所說的權威認證平臺的角色,全部的服務都要到這個認證中心作認證。web

這樣一說,發現沒,這其實就是個單點登陸的功能。這就是另一種使用場景,對於多服務的平臺,可使用 OAuth2 實現服務的單點登陸,只作一次登陸,就能夠在多個服務中自由穿行,固然僅限於受權範圍內的服務和接口。redis

實現統一認證功能

本篇先介紹密碼模式實現的單點登陸,下一篇再繼續說受權碼模式。spring

在微服務橫行的今天,誰敢說本身手上沒幾個微服務。微服務減小了服務間的耦合,同時也在某些方面增長了系統的複雜度,好比說用戶認證。假設咱們這裏實現了一個電商平臺,用戶看到的就是一個 APP 或者一個 web 站點,實際上背後是由多個獨立的服務構成的,好比用戶服務、訂單服務、產品服務等。用戶只要第一次輸入用戶名、密碼完成登陸後,一段時間內,均可以任意訪問各個頁面,好比產品列表頁面、個人訂單頁面、個人關注等頁面。sql

咱們能夠想象一下,天然可以想到,在請求各個服務、各個接口的時候,必定攜帶着什麼憑證,而後各個服務才知道請求接口的用戶是哪一個,否則確定有問題,那其實這裏面的憑證簡單來講就是一個 Token,標識用戶身份的 Token。數據庫

系統架構說明

認證中心:oauth2-auth-server,OAuth2 主要實現端,Token 的生成、刷新、驗證都在認證中心完成。

訂單服務:oauth2-client-order-server,微服務之一,接收到請求後會到認證中心驗證。

用戶服務:oauth2-client-user-server,微服務之二,接收到請求後會到認證中心驗證。

客戶端:例如 APP 端、web 端 等終端

上圖描述了使用了 OAuth2 的客戶端與微服務間的請求過程。大體的過程就是客戶端用用戶名和密碼到認證服務端換取 token,返回給客戶端,客戶端拿着 token 去各個微服務請求數據接口,通常這個 token 是放到 header 中的。當微服務接到請求後,先要拿着 token 去認證服務端檢查 token 的合法性,若是合法,再根據用戶所屬的角色及具備的權限動態的返回數據。

建立並配置認證服務端

配置最多的就是認證服務端,驗證帳號、密碼,存儲 token,檢查 token ,刷新 token 等都是認證服務端的工做。

一、引入須要的 maven 包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

spring-cloud-starter-oauth2包含了 spring-cloud-starter-security,因此不用再單獨引入了。之因此引入 redis 包,是由於下面會介紹一種用 redis 存儲 token 的方式。

二、配置好 application.yml

將項目基本配置設置好,並加入有關 redis 的配置,稍後會用到。

spring:
  application:
    name: auth-server
  redis:
    database: 2
    host: localhost
    port: 32768
    password: 1qaz@WSX
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 100ms

server:
  port: 6001

management:
  endpoint:
    health:
      enabled: true

三、spring security 基礎配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 容許匿名訪問全部接口 主要是 oauth 接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**").permitAll();
    }
}

使用@EnableWebSecurity註解修飾,並繼承自WebSecurityConfigurerAdapter類。

這個類的重點就是聲明 PasswordEncoderAuthenticationManager兩個 Bean。稍後會用到。其中 BCryptPasswordEncoder是一個密碼加密工具類,它能夠實現不可逆的加密,AuthenticationManager是爲了實現 OAuth2 的 password 模式必需要指定的受權管理 Bean。

四、實現 UserDetailsService

若是你以前用過 Security 的話,那確定對這個類很熟悉,它是實現用戶身份驗證的一種方式,也是最簡單方便的一種。另外還有結合 AuthenticationProvider的方式,有機會講 Security 的時候再展開來說吧。

UserDetailsService的核心就是 loadUserByUsername方法,它要接收一個字符串參數,也就是傳過來的用戶名,返回一個 UserDetails對象。

@Slf4j
@Component(value = "kiteUserDetailsService")
public class KiteUserDetailsService implements UserDetailsService {


    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("usernameis:" + username);
        // 查詢數據庫操做
        if(!username.equals("admin")){
            throw new UsernameNotFoundException("the user is not found");
        }else{
            // 用戶角色也應在數據庫中獲取
            String role = "ROLE_ADMIN";
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(role));
            // 線上環境應該經過用戶名查詢數據庫獲取加密後的密碼
            String password = passwordEncoder.encode("123456");
            return new org.springframework.security.core.userdetails.User(username,password, authorities);
        }
    }
}

這裏爲了作演示,把用戶名、密碼和所屬角色都寫在代碼裏了,正式環境中,這裏應該是從數據庫或者其餘地方根據用戶名將加密後的密碼及所屬角色查出來的。帳號 admin ,密碼 123456,稍後在換取 token 的時候會用到。而且給這個用戶設置 "ROLE_ADMIN" 角色。

五、OAuth2 配置文件

建立一個配置文件繼承自 AuthorizationServerConfigurerAdapter.

@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    public PasswordEncoder passwordEncoder;

    @Autowired
    public UserDetailsService kiteUserDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore redisTokenStore;

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        /**
         * redis token 方式
         */
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(kiteUserDetailsService)
                .tokenStore(redisTokenStore);

    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("order-client")
                .secret(passwordEncoder.encode("order-secret-8888"))
                .authorizedGrantTypes("refresh_token", "authorization_code", "password")
                .accessTokenValiditySeconds(3600)
                .scopes("all")
                .and()
                .withClient("user-client")
                .secret(passwordEncoder.encode("user-secret-8888"))
                .authorizedGrantTypes("refresh_token", "authorization_code", "password")
                .accessTokenValiditySeconds(3600)
                .scopes("all");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
        security.checkTokenAccess("isAuthenticated()");
        security.tokenKeyAccess("isAuthenticated()");
    }
}

有三個 configure 方法的重寫。

AuthorizationServerEndpointsConfigurer參數的重寫

endpoints.authenticationManager(authenticationManager)
                .userDetailsService(kiteUserDetailsService)
                .tokenStore(redisTokenStore);

authenticationManage() 調用此方法才能支持 password 模式。

userDetailsService() 設置用戶驗證服務。

tokenStore() 指定 token 的存儲方式。

redisTokenStore Bean 的定義以下:

@Configuration
public class RedisTokenStoreConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore (){
        return new RedisTokenStore(redisConnectionFactory);
    }
}

ClientDetailsServiceConfigurer參數的重寫,在這裏定義各個端的約束條件。包括

ClientId、Client-Secret:這兩個參數對應請求端定義的 cleint-id 和 client-secret

authorizedGrantTypes 能夠包括以下幾種設置中的一種或多種:

  • authorization_code:受權碼類型。
  • implicit:隱式受權類型。
  • password:資源全部者(即用戶)密碼類型。
  • client_credentials:客戶端憑據(客戶端ID以及Key)類型。
  • refresh_token:經過以上受權得到的刷新令牌來獲取新的令牌。

accessTokenValiditySeconds:token 的有效期

scopes:用來限制客戶端訪問的權限,在換取的 token 的時候會帶上 scope 參數,只有在 scopes 定義內的,才能夠正常換取 token。

上面代碼中是使用 inMemory 方式存儲的,將配置保存到內存中,至關於硬編碼了。正式環境下的作法是持久化到數據庫中,好比 mysql 中。

具體的作法以下:

  1. 在數據庫中增長表,並插入數據
create table oauth_client_details (
    client_id VARCHAR(256) PRIMARY KEY,
    resource_ids VARCHAR(256),
    client_secret VARCHAR(256),
    scope VARCHAR(256),
    authorized_grant_types VARCHAR(256),
    web_server_redirect_uri VARCHAR(256),
    authorities VARCHAR(256),
    access_token_validity INTEGER,
    refresh_token_validity INTEGER,
    additional_information VARCHAR(4096),
    autoapprove VARCHAR(256)
);
INSERT INTO oauth_client_details
    (client_id, client_secret, scope, authorized_grant_types,
    web_server_redirect_uri, authorities, access_token_validity,
    refresh_token_validity, additional_information, autoapprove)
VALUES
    ('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all',
    'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);

INSERT INTO oauth_client_details
    (client_id, client_secret, scope, authorized_grant_types,
    web_server_redirect_uri, authorities, access_token_validity,
    refresh_token_validity, additional_information, autoapprove)
VALUES
    ('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all',
    'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);

注意: client_secret 字段不能直接是 secret 的原始值,須要通過加密。由於是用的 BCryptPasswordEncoder,因此最終插入的值應該是通過 BCryptPasswordEncoder.encode()以後的值。

  1. 而後在配置文件 application.yml 中添加關於數據庫的配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_cloud?characterEncoding=UTF-8&useSSL=false
    username: root
    password: password
    hikari:
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      maximum-pool-size: 9

Spring Boot 2.0 以後默認使用 hikari 做爲數據庫鏈接池。若是使用其餘鏈接池須要引入相關包,而後對應的增長配置。

  1. 在 OAuth2 配置類(OAuth2Config)中增長 DataSource 的注入
@Autowired
private DataSource dataSource;
  1. public void configure(ClientDetailsServiceConfigurer clients)重寫方法修改成以下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
    jcsb.passwordEncoder(passwordEncoder);
}

還有一個重寫的方法 public void configure(AuthorizationServerSecurityConfigurer security),這個方法限制客戶端訪問認證接口的權限。

security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
security.tokenKeyAccess("isAuthenticated()");

第一行代碼是容許客戶端訪問 OAuth2 受權接口,不然請求 token 會返回 401。

第二行和第三行分別是容許已受權用戶訪問 checkToken 接口和獲取 token 接口。

完成以後,啓動項目,若是你用的是 IDEA 會在下方的 Mapping 窗口中看到 oauth2 相關的 RESTful 接口。

主要有以下幾個:

POST /oauth/authorize  受權碼模式認證受權接口
GET/POST /oauth/token  獲取 token 的接口
POST  /oauth/check_token  檢查 token 合法性接口
建立用戶客戶端項目

上面建立完成了認證服務端,下面開始建立一個客戶端,對應到咱們系統中的業務相關的微服務。咱們假設這個微服務項目是管理用戶相關數據的,因此叫作用戶客戶端。

一、引用相關的 maven 包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

二、application.yml 配置文件

spring:
  application:
    name: client-user
  redis:
    database: 2
    host: localhost
    port: 32768
    password: 1qaz@WSX
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 100ms
server:
  port: 6101
  servlet:
    context-path: /client-user

security:
  oauth2:
    client:
      client-id: user-client
      client-secret: user-secret-8888
      user-authorization-uri: http://localhost:6001/oauth/authorize
      access-token-uri: http://localhost:6001/oauth/token
    resource:
      id: user-client
      user-info-uri: user-info
    authorization:
      check-token-access: http://localhost:6001/oauth/check_token

上面是常規配置信息以及 redis 配置,重點是下面的 security 的配置,這裏的配置稍有不注意就會出現 401 或者其餘問題。

client-id、client-secret 要和認證服務中的配置一致,若是是使用 inMemory 仍是 jdbc 方式。

user-authorization-uri 是受權碼認證方式須要的,下一篇文章再說。

access-token-uri 是密碼模式須要用到的獲取 token 的接口。

authorization.check-token-access 也是關鍵信息,當此服務端接收到來自客戶端端的請求後,須要拿着請求中的 token 到認證服務端作 token 驗證,就是請求的這個接口

三、資源配置文件

在 OAuth2 的概念裏,全部的接口都被稱爲資源,接口的權限也就是資源的權限,因此 Spring Security OAuth2 中提供了關於資源的註解 @EnableResourceServer,和 @EnableWebSecurity的做用相似。

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Value("${security.oauth2.client.client-secret}")
    private String secret;

    @Value("${security.oauth2.authorization.check-token-access}")
    private String checkTokenEndpointUrl;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore (){
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Bean
    public RemoteTokenServices tokenService() {
        RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setClientId(clientId);
        tokenService.setClientSecret(secret);
        tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
        return tokenService;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenServices(tokenService());
    }
}

由於使用的是 redis 做爲 token 的存儲,因此須要特殊配置一下叫作 tokenService 的 Bean,經過這個 Bean 才能實現 token 的驗證。

四、最後,添加一個 RESTful 接口

@Slf4j
@RestController
public class UserController {

    @GetMapping(value = "get")
    //@PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public Object get(Authentication authentication){
        //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        authentication.getCredentials();
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
        String token = details.getTokenValue();
        return token;
    }
}

一個 RESTful 方法,只有當訪問用戶具備 ROLE_ADMIN 權限時才能訪問,不然返回 401 未受權。

經過 Authentication 參數或者 SecurityContextHolder.getContext().getAuthentication() 能夠拿到受權信息進行查看。

測試認證功能

一、啓動認證服務端,啓動端口爲 6001

二、啓動用戶服務客戶端,啓動端口爲6101

三、請求認證服務端獲取 token

我是用 REST Client 來作訪問請求的,請求格式以下:

POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

假設我們在一個 web 端使用,grant_type 是 password,代表這是使用 OAuth2 的密碼模式。

username=admin 和 password=123456 就至關於在 web 端登陸界面輸入的用戶名和密碼,咱們在認證服務端配置中固定了用戶名是 admin 、密碼是 123456,而線上環境中則應該經過查詢數據庫獲取。

scope=all 是權限有關的,在認證服務的 OAuthConfig 中指定了 scope 爲 all 。

Authorization 要加在請求頭中,格式爲 Basic 空格 base64(clientId:clientSecret),這個微服務客戶端的 client-id 是 user-client,client-secret 是 user-secret-8888,將這兩個值經過冒號鏈接,並使用 base64 編碼(user-client:user-secret-8888)以後的值爲 dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==,能夠經過 https://www.sojson.com/base64.html 在線編碼獲取。

運行請求後,若是參數都正確的話,獲取到的返回內容以下,是一段 json 格式

{
  "access_token": "9f958300-5005-46ea-9061-323c9e6c7a4d",
  "token_type": "bearer",
  "refresh_token": "0f5871f5-98f1-405e-848e-80f641bab72e",
  "expires_in": 3599,
  "scope": "all"
}

access_token :  就是以後請求須要帶上的 token,也是本次請求的主要目的
token_type:爲 bearer,這是 access token 最經常使用的一種形式
refresh_token:以後能夠用這個值來換取新的 token,而不用輸入帳號密碼
expires_in:token 的過時時間(秒)

四、用獲取到的 token 請求資源接口

咱們在用戶客戶端中定義了一個接口 http://localhost:6101/client-user/get,如今就拿着上一步獲取的 token 來請求這個接口。

GET http://localhost:6101/client-user/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer ce334918-e666-455a-8ecd-8bd680415d84

一樣須要請求頭 Authorization,格式爲 bearer + 空格 + token,正常狀況下根據接口的邏輯,會把 token 原樣返回。

五、token 過時後,用 refresh_token 換取 access_token

通常都會設置 access_token 的過時時間小於 refresh_token 的過時時間,以便在 access_token 過時後,不用用戶再次登陸的狀況下,獲取新的 access_token。

### 換取 access_token
POST http://localhost:6001/oauth/token?grant_type=refresh_token&refresh_token=706dac10-d48e-4795-8379-efe8307a2282
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

grant_type 設置爲 refresh_token。

refresh_token 設置爲請求 token 時返回的 refresh_token 的值。

請求頭加入 Authorization,格式依然是 Basic + 空格 + base64(client-id:client-secret)

請求成功後會返回和請求 token 一樣的數據格式。

用 JWT 替換 redisToken

上面 token 的存儲用的是 redis 的方案,Spring Security OAuth2 還提供了 jdbc 和 jwt 的支持,jdbc 的暫不考慮,如今來介紹用 JWT 的方式來實現 token 的存儲。

用 JWT 的方式就不用把 token 再存儲到服務端了,JWT 有本身特殊的加密方式,能夠有效的防止數據被篡改,只要不把用戶密碼等關鍵信息放到 JWT 裏就能夠保證安全性。

認證服務端改造

先把有關 redis 的配置去掉。

添加 JwtConfig 配置類
@Configuration
public class JwtTokenConfig {

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("dev");
        return accessTokenConverter;
    }
}

JwtAccessTokenConverter是爲了作 JWT 數據轉換,這樣作是由於 JWT 有自身獨特的數據格式。若是沒有了解過 JWT ,能夠搜索一下先了解一下。

更改 OAuthConfig 配置類
@Autowired
private TokenStore jwtTokenStore;

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        /**
         * 普通 jwt 模式
         */
         endpoints.tokenStore(jwtTokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .userDetailsService(kiteUserDetailsService)
                /**
                 * 支持 password 模式
                 */
                .authenticationManager(authenticationManager);
}

注入 JWT 相關的 Bean,而後修改 configure(final AuthorizationServerEndpointsConfigurer endpoints) 方法爲 JWT 存儲模式。

改造用戶客戶端

修改 application.yml 配置文件
security:
  oauth2:
    client:
      client-id: user-client
      client-secret: user-secret-8888
      user-authorization-uri: http://localhost:6001/oauth/authorize
      access-token-uri: http://localhost:6001/oauth/token
    resource:
      jwt:
        key-uri: http://localhost:6001/oauth/token_key
        key-value: dev

注意認證服務端 JwtAccessTokenConverter設置的 SigningKey 要和配置文件中的 key-value 相同,否則會致使沒法正常解碼 JWT ,致使驗證不經過。

ResourceServerConfig 類的配置
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();

        accessTokenConverter.setSigningKey("dev");
        accessTokenConverter.setVerifierKey("dev");
        return accessTokenConverter;
    }

    @Autowired
    private TokenStore jwtTokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(jwtTokenStore);
    }
}
運行請求 token 接口的請求
POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

返回結果以下:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJleHAiOjE1NzE3NzU4OTQsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiZjdkMjg4NDUtMmU2ZC00ZmRjLTg1OGYtMWNiY2RlNzI1ZmMyIiwiY2xpZW50X2lkIjoidXNlci1jbGllbnQifQ.vk_msYtbrAr93h5sK4wy6EC2_wRD_cD_UBS8O6eRziw",
  "expires_in": 3599,
  "scope": "all",
  "jti": "8cca29af-ea77-4fe6-9fe1-327415dcd21d"
}

咱們已經看到返回的 token 是 JWT 格式了,到 JWT 在線解碼網站 https://jwt.io/ 或者 http://jwt.calebb.net/將 token 解碼看一下

看到了沒,user_name、client_id 等信息都在其中。

拿着返回的 token 請求用戶客戶端接口
GET http://localhost:6101/client-user/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM

加強 JWT

若是我想在 JWT 中加入額外的字段(比方說用戶的其餘信息)怎麼辦呢,固然能夠。spring security oauth2 提供了 TokenEnhancer 加強器。其實不光 JWT ,RedisToken 的方式一樣能夠。

聲明一個加強器
public class JWTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("jwt-ext", "JWT 擴展信息");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

經過 oAuth2Authentication 能夠拿到用戶名等信息,經過這些咱們能夠在這裏查詢數據庫或者緩存獲取更多的信息,而這些信息均可以做爲 JWT 擴展信息加入其中。

OAuthConfig 配置類修改

注入加強器

@Autowired
private TokenEnhancer jwtTokenEnhancer;

@Bean
public TokenEnhancer jwtTokenEnhancer(){
    return new JWTokenEnhancer();
}

修改 configure(final AuthorizationServerEndpointsConfigurer endpoints)方法

@Override
public void configure( final AuthorizationServerEndpointsConfigurer endpoints ) throws Exception{
    /**
     * jwt 加強模式
     */
    TokenEnhancerChain  enhancerChain   = new TokenEnhancerChain();
    List<TokenEnhancer> enhancerList    = new ArrayList<>();
    enhancerList.add( jwtTokenEnhancer );
    enhancerList.add( jwtAccessTokenConverter );
    enhancerChain.setTokenEnhancers( enhancerList );
    endpoints.tokenStore( jwtTokenStore )
    .userDetailsService( kiteUserDetailsService )
    /**
     * 支持 password 模式
     */
    .authenticationManager( authenticationManager )
    .tokenEnhancer( enhancerChain )
    .accessTokenConverter( jwtAccessTokenConverter );
}
再次請求 token ,返回內容中多了個剛剛加入的 jwt-ext 字段
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImE0NTUxZDllLWI3ZWQtNDc1OS1iMmYxLWYwYjliMjFjYTQyYyIsImV4cCI6MTU3MTc3NzU3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJmNTI3ODJlOS0wOGRjLTQ2NGUtYmJhYy03OTMwNzYwYmZiZjciLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.UQMf140CG8U0eWh08nGlctpIye9iJ7p2i6NYHkGAwhY",
  "expires_in": 3599,
  "scope": "all",
  "jwt-ext": "JWT 擴展信息",
  "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c"
}

用戶客戶端解析 JWT 數據

咱們若是在 JWT 中加入了額外信息,這些信息咱們可能會用到,而在接收到 JWT 格式的 token 以後,用戶客戶端要把 JWT 解析出來。

引入 JWT 包
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
加一個 RESTful 接口,在其中解析 JWT
@GetMapping(value = "jwt")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object jwtParser(Authentication authentication){
    authentication.getCredentials();
    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
    String jwtToken = details.getTokenValue();
    Claims claims = Jwts.parser()
                .setSigningKey("dev".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(jwtToken)
                .getBody();
    return claims;
}

一樣注意其中籤名的設置要與認證服務端相同。

用上一步的 token 請求上面的接口
### 解析 jwt
GET http://localhost:6101/client-user/jwt
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ

返回內容以下:

{
  "user_name": "admin",
  "jwt-ext": "JWT 擴展信息",
  "scope": [
    "all"
  ],
  "exp": 1571745178,
  "authorities": [
    "ROLE_ADMIN"
  ],
  "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c",
  "client_id": "user-client"
}

以上就是 password 模式的完整過程,源碼放到了 github 上,有須要的能夠去看一下。

源碼地址

不要吝惜你的「推薦」呦

歡迎關注,不按期更新本系列和其餘文章
古時的風箏 ,進入公衆號能夠加入交流羣

相關文章
相關標籤/搜索