亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Spring Cloud OAuth2 實現(xiàn)用戶認(rèn)證及單點登錄的示例代碼

 更新時間:2019年10月23日 10:41:05   作者:風(fēng)的姿態(tài)  
這篇文章主要介紹了Spring Cloud OAuth2 實現(xiàn)用戶認(rèn)證及單點登錄的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

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

本文我們將使用授權(quán)碼模式和密碼模式兩種方式來實現(xiàn)用戶認(rèn)證和授權(quán)管理。

OAuth2 其實是一個關(guān)于授權(quán)的網(wǎng)絡(luò)標(biāo)準(zhǔn),它制定了設(shè)計思路和運行流程,利用這個標(biāo)準(zhǔn)我們其實是可以自己實現(xiàn) OAuth2 的認(rèn)證過程的。今天要介紹的 spring-cloud-starter-oauth2 ,其實是 Spring Cloud 按照 OAuth2 的標(biāo)準(zhǔn)并結(jié)合 spring-security 封裝好的一個具體實現(xiàn)。

什么情況下需要用 OAuth2

首先大家最熟悉的就是幾乎每個人都用過的,比如用微信登錄、用 QQ 登錄、用微博登錄、用 Google 賬號登錄、用 github 授權(quán)登錄等等,這些都是典型的 OAuth2 使用場景。假設(shè)我們做了一個自己的服務(wù)平臺,如果不使用 OAuth2 登錄方式,那么我們需要用戶先完成注冊,然后用注冊號的賬號密碼或者用手機驗證碼登錄。而使用了 OAuth2 之后,相信很多人使用過、甚至開發(fā)過公眾號網(wǎng)頁服務(wù)、小程序,當(dāng)我們進入網(wǎng)頁、小程序界面,第一次使用就無需注冊,直接使用微信授權(quán)登錄即可,大大提高了使用效率。因為每個人都有微信號,有了微信就可以馬上使用第三方服務(wù),這體驗不要太好了。而對于我們的服務(wù)來說,我們也不需要存儲用戶的密碼,只要存儲認(rèn)證平臺返回的唯一ID 和用戶信息即可。

以上是使用了 OAuth2 的授權(quán)碼模式,利用第三方的權(quán)威平臺實現(xiàn)用戶身份的認(rèn)證。當(dāng)然了,如果你的公司內(nèi)部有很多個服務(wù),可以專門提取出一個認(rèn)證中心,這個認(rèn)證中心就充當(dāng)上面所說的權(quán)威認(rèn)證平臺的角色,所有的服務(wù)都要到這個認(rèn)證中心做認(rèn)證。

這樣一說,發(fā)現(xiàn)沒,這其實就是個單點登錄的功能。這就是另外一種使用場景,對于多服務(wù)的平臺,可以使用 OAuth2 實現(xiàn)服務(wù)的單點登錄,只做一次登錄,就可以在多個服務(wù)中自由穿行,當(dāng)然僅限于授權(quán)范圍內(nèi)的服務(wù)和接口。

實現(xiàn)統(tǒng)一認(rèn)證功能

本篇先介紹密碼模式實現(xiàn)的單點登錄,下一篇再繼續(xù)說授權(quán)碼模式。

在微服務(wù)橫行的今天,誰敢說自己手上沒幾個微服務(wù)。微服務(wù)減少了服務(wù)間的耦合,同時也在某些方面增加了系統(tǒng)的復(fù)雜度,比如說用戶認(rèn)證。假設(shè)我們這里實現(xiàn)了一個電商平臺,用戶看到的就是一個 APP 或者一個 web 站點,實際上背后是由多個獨立的服務(wù)構(gòu)成的,比如用戶服務(wù)、訂單服務(wù)、產(chǎn)品服務(wù)等。用戶只要第一次輸入用戶名、密碼完成登錄后,一段時間內(nèi),都可以任意訪問各個頁面,比如產(chǎn)品列表頁面、我的訂單頁面、我的關(guān)注等頁面。

我們可以想象一下,自然能夠想到,在請求各個服務(wù)、各個接口的時候,一定攜帶著什么憑證,然后各個服務(wù)才知道請求接口的用戶是哪個,不然肯定有問題,那其實這里面的憑證簡單來說就是一個 Token,標(biāo)識用戶身份的 Token。

系統(tǒng)架構(gòu)說明

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

訂單服務(wù):oauth2-client-order-server,微服務(wù)之一,接收到請求后會到認(rèn)證中心驗證。

用戶服務(wù):oauth2-client-user-server,微服務(wù)之二,接收到請求后會到認(rèn)證中心驗證。

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

上圖描述了使用了 OAuth2 的客戶端與微服務(wù)間的請求過程。大致的過程就是客戶端用用戶名和密碼到認(rèn)證服務(wù)端換取 token,返回給客戶端,客戶端拿著 token 去各個微服務(wù)請求數(shù)據(jù)接口,一般這個 token 是放到 header 中的。當(dāng)微服務(wù)接到請求后,先要拿著 token 去認(rèn)證服務(wù)端檢查 token 的合法性,如果合法,再根據(jù)用戶所屬的角色及具有的權(quán)限動態(tài)的返回數(shù)據(jù)。

創(chuàng)建并配置認(rèn)證服務(wù)端

配置最多的就是認(rèn)證服務(wù)端,驗證賬號、密碼,存儲 token,檢查 token ,刷新 token 等都是認(rèn)證服務(wù)端的工作。

1、引入需要的 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 的方式。

2、配置好 application.yml

將項目基本配置設(shè)置好,并加入有關(guān) 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

3、spring security 基礎(chǔ)配置

@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是一個密碼加密工具類,它可以實現(xiàn)不可逆的加密,AuthenticationManager是為了實現(xiàn) OAuth2 的 password 模式必須要指定的授權(quán)管理 Bean。

4、實現(xiàn) UserDetailsService

如果你之前用過 Security 的話,那肯定對這個類很熟悉,它是實現(xiàn)用戶身份驗證的一種方式,也是最簡單方便的一種。另外還有結(jié)合 AuthenticationProvider的方式,有機會講 Security 的時候再展開來講吧。

UserDetailsService的核心就是 loadUserByUsername方法,它要接收一個字符串參數(shù),也就是傳過來的用戶名,返回一個 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);
    // 查詢數(shù)據(jù)庫操作
    if(!username.equals("admin")){
      throw new UsernameNotFoundException("the user is not found");
    }else{
      // 用戶角色也應(yīng)在數(shù)據(jù)庫中獲取
      String role = "ROLE_ADMIN";
      List<SimpleGrantedAuthority> authorities = new ArrayList<>();
      authorities.add(new SimpleGrantedAuthority(role));
      // 線上環(huán)境應(yīng)該通過用戶名查詢數(shù)據(jù)庫獲取加密后的密碼
      String password = passwordEncoder.encode("123456");
      return new org.springframework.security.core.userdetails.User(username,password, authorities);
    }
  }
}

這里為了做演示,把用戶名、密碼和所屬角色都寫在代碼里了,正式環(huán)境中,這里應(yīng)該是從數(shù)據(jù)庫或者其他地方根據(jù)用戶名將加密后的密碼及所屬角色查出來的。賬號 admin ,密碼 123456,稍后在換取 token 的時候會用到。并且給這個用戶設(shè)置 "ROLE_ADMIN" 角色。

5、OAuth2 配置文件

創(chuàng)建一個配置文件繼承自 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參數(shù)的重寫

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

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

userDetailsService() 設(shè)置用戶驗證服務(wù)。

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

redisTokenStore Bean 的定義如下:

@Configuration
public class RedisTokenStoreConfig {

  @Autowired
  private RedisConnectionFactory redisConnectionFactory;

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

ClientDetailsServiceConfigurer參數(shù)的重寫,在這里定義各個端的約束條件。包括

ClientId、Client-Secret:這兩個參數(shù)對應(yīng)請求端定義的 cleint-id 和 client-secret

authorizedGrantTypes 可以包括如下幾種設(shè)置中的一種或多種:

  • authorization_code:授權(quán)碼類型。
  • implicit:隱式授權(quán)類型。
  • password:資源所有者(即用戶)密碼類型。
  • client_credentials:客戶端憑據(jù)(客戶端ID以及Key)類型。
  • refresh_token:通過以上授權(quán)獲得的刷新令牌來獲取新的令牌。

accessTokenValiditySeconds:token 的有效期

scopes:用來限制客戶端訪問的權(quán)限,在換取的 token 的時候會帶上 scope 參數(shù),只有在 scopes 定義內(nèi)的,才可以正常換取 token。

上面代碼中是使用 inMemory 方式存儲的,將配置保存到內(nèi)存中,相當(dāng)于硬編碼了。正式環(huán)境下的做法是持久化到數(shù)據(jù)庫中,比如 mysql 中。

具體的做法如下:

在數(shù)據(jù)庫中增加表,并插入數(shù)據(jù)

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 的原始值,需要經(jīng)過加密。因為是用的 BCryptPasswordEncoder,所以最終插入的值應(yīng)該是經(jīng)過 BCryptPasswordEncoder.encode()之后的值。

然后在配置文件 application.yml 中添加關(guān)于數(shù)據(jù)庫的配置

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 之后默認(rèn)使用 hikari 作為數(shù)據(jù)庫連接池。如果使用其他連接池需要引入相關(guān)包,然后對應(yīng)的增加配置。

在 OAuth2 配置類(OAuth2Config)中增加 DataSource 的注入

@Autowired
private DataSource dataSource;

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),這個方法限制客戶端訪問認(rèn)證接口的權(quán)限。

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

第一行代碼是允許客戶端訪問 OAuth2 授權(quán)接口,否則請求 token 會返回 401。

第二行和第三行分別是允許已授權(quán)用戶訪問 checkToken 接口和獲取 token 接口。

完成之后,啟動項目,如果你用的是 IDEA 會在下方的 Mapping 窗口中看到 oauth2 相關(guān)的 RESTful 接口。

主要有如下幾個:

  • POST /oauth/authorize 授權(quán)碼模式認(rèn)證授權(quán)接口
  • GET/POST /oauth/token 獲取 token 的接口
  • POST /oauth/check_token 檢查 token 合法性接口

上面創(chuàng)建完成了認(rèn)證服務(wù)端,下面開始創(chuàng)建一個客戶端,對應(yīng)到我們系統(tǒng)中的業(yè)務(wù)相關(guān)的微服務(wù)。我們假設(shè)這個微服務(wù)項目是管理用戶相關(guān)數(shù)據(jù)的,所以叫做用戶客戶端。

1、引用相關(guān)的 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>

2、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

上面是常規(guī)配置信息以及 redis 配置,重點是下面的 security 的配置,這里的配置稍有不注意就會出現(xiàn) 401 或者其他問題。

client-id、client-secret 要和認(rèn)證服務(wù)中的配置一致,如果是使用 inMemory 還是 jdbc 方式。

user-authorization-uri 是授權(quán)碼認(rèn)證方式需要的,下一篇文章再說。

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

authorization.check-token-access 也是關(guān)鍵信息,當(dāng)此服務(wù)端接收到來自客戶端端的請求后,需要拿著請求中的 token 到認(rèn)證服務(wù)端做 token 驗證,就是請求的這個接口

3、資源配置文件

在 OAuth2 的概念里,所有的接口都被稱為資源,接口的權(quán)限也就是資源的權(quán)限,所以 Spring Security OAuth2 中提供了關(guān)于資源的注解 @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 才能實現(xiàn) token 的驗證。

4、最后,添加一個 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 方法,只有當(dāng)訪問用戶具有 ROLE_ADMIN 權(quán)限時才能訪問,否則返回 401 未授權(quán)。

通過 Authentication 參數(shù)或者 SecurityContextHolder.getContext().getAuthentication() 可以拿到授權(quán)信息進行查看。

測試認(rèn)證功能

1、啟動認(rèn)證服務(wù)端,啟動端口為 6001

2、啟動用戶服務(wù)客戶端,啟動端口為6101

3、請求認(rèn)證服務(wù)端獲取 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==

假設(shè)咱們在一個 web 端使用,grant_type 是 password,表明這是使用 OAuth2 的密碼模式。

username=admin 和 password=123456 就相當(dāng)于在 web 端登錄界面輸入的用戶名和密碼,我們在認(rèn)證服務(wù)端配置中固定了用戶名是 admin 、密碼是 123456,而線上環(huán)境中則應(yīng)該通過查詢數(shù)據(jù)庫獲取。

scope=all 是權(quán)限有關(guān)的,在認(rèn)證服務(wù)的 OAuthConfig 中指定了 scope 為 all 。

Authorization 要加在請求頭中,格式為 Basic 空格 base64(clientId:clientSecret),這個微服務(wù)客戶端的 client-id 是 user-client,client-secret 是 user-secret-8888,將這兩個值通過冒號連接,并使用 base64 編碼(user-client:user-secret-8888)之后的值為 dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==,可以通過 https://www.sojson.com/base64.html 在線編碼獲取。

運行請求后,如果參數(shù)都正確的話,獲取到的返回內(nèi)容如下,是一段 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 的過期時間(秒)

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

我們在用戶客戶端中定義了一個接口 http://localhost:6101/client-user/get,現(xiàn)在就拿著上一步獲取的 token 來請求這個接口。

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

同樣需要請求頭 Authorization,格式為 bearer + 空格 + token,正常情況下根據(jù)接口的邏輯,會把 token 原樣返回。

5、token 過期后,用 refresh_token 換取 access_token

一般都會設(shè)置 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 設(shè)置為 refresh_token。

refresh_token 設(shè)置為請求 token 時返回的 refresh_token 的值。

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

請求成功后會返回和請求 token 同樣的數(shù)據(jù)格式。

用 JWT 替換 redisToken

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

用 JWT 的方式就不用把 token 再存儲到服務(wù)端了,JWT 有自己特殊的加密方式,可以有效的防止數(shù)據(jù)被篡改,只要不把用戶密碼等關(guān)鍵信息放到 JWT 里就可以保證安全性。

認(rèn)證服務(wù)端改造

先把有關(guān) 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 數(shù)據(jù)轉(zhuǎn)換,這樣做是因為 JWT 有自身獨特的數(shù)據(jù)格式。如果沒有了解過 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 相關(guān)的 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

注意認(rèn)證服務(wù)端 JwtAccessTokenConverter設(shè)置的 SigningKey 要和配置文件中的 key-value 相同,不然會導(dǎo)致無法正常解碼 JWT ,導(dǎo)致驗證不通過。

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==

返回結(jié)果如下:

{
 "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"
}

我們已經(jīng)看到返回的 token 是 JWT 格式了,到 JWT 在線解碼網(wǎng)站 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 中加入額外的字段(比方說用戶的其他信息)怎么辦呢,當(dāng)然可以。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 可以拿到用戶名等信息,通過這些我們可以在這里查詢數(shù)據(jù)庫或者緩存獲取更多的信息,而這些信息都可以作為 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 ,返回內(nèi)容中多了個剛剛加入的 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 數(shù)據(jù)

我們?nèi)绻?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;
}

同樣注意其中簽名的設(shè)置要與認(rèn)證服務(wù)端相同。

用上一步的 token 請求上面的接口

### 解析 jwt
GET http://localhost:6101/client-user/jwt
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ

返回內(nèi)容如下:

{
 "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 上,有需要的可以去看一下。

源碼地址

不要吝惜你的「推薦」呦

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • 利用Java簡單實現(xiàn)一個代碼行數(shù)統(tǒng)計器方法實例

    利用Java簡單實現(xiàn)一個代碼行數(shù)統(tǒng)計器方法實例

    這篇文章主要給大家介紹了關(guān)于如何利用Java簡單實現(xiàn)一個代碼行數(shù)統(tǒng)計器的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11
  • 詳解SpringCloud是如何動態(tài)更新配置的

    詳解SpringCloud是如何動態(tài)更新配置的

    spring cloud在config配置管理的基礎(chǔ)上,提供了consul config的配置管理和動態(tài)監(jiān)聽,那么這里面到底是怎樣實現(xiàn)的,本文將為你揭秘,感興趣的小伙伴可以跟著小伙伴一起來學(xué)習(xí)
    2023-06-06
  • 使用java寫的矩陣乘法實例(Strassen算法)

    使用java寫的矩陣乘法實例(Strassen算法)

    這篇文章主要給大家介紹了關(guān)于如何使用java寫的矩陣乘法(Strassen算法)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-02-02
  • java日志LoggerFactory.getLogger的用法及說明

    java日志LoggerFactory.getLogger的用法及說明

    這篇文章主要介紹了java日志LoggerFactory.getLogger的用法及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • 解決IDEA使用Spring Initializr創(chuàng)建項目時無法連接到https://start.spring.io的問題

    解決IDEA使用Spring Initializr創(chuàng)建項目時無法連接到https://start.spring.io的問

    這篇文章主要介紹了解決IDEA使用Spring Initializr創(chuàng)建項目時無法連接到https://start.spring.io的問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-04-04
  • SpringCloud OpenFeign 自定義響應(yīng)解碼器的問題記錄

    SpringCloud OpenFeign 自定義響應(yīng)解碼器的問題記錄

    我們在使用 Spring Cloud 微服務(wù)的時候,通常將返回結(jié)果使用一個JsonResult 類進行封裝,本文重點介紹SpringCloud OpenFeign 自定義響應(yīng)解碼器的問題記錄,感興趣的朋友跟隨小編一起看看吧
    2024-06-06
  • JDK14的新特性NullPointerExceptions的使用

    JDK14的新特性NullPointerExceptions的使用

    這篇文章主要介紹了JDK14的新特性NullPointerExceptions的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-04-04
  • SpringBoot接收各種各樣參數(shù)的示例詳解

    SpringBoot接收各種各樣參數(shù)的示例詳解

    參數(shù)映射準(zhǔn)確來說是springmvc來幫我們干的活,但是由于springboot太過火爆,簡化了springmvc相關(guān)配置文件,以至于很多人會誤認(rèn)為是springboot的功能,本文將給大家介紹SpringBoot接收各種各樣參數(shù),文中有詳細(xì)的代碼講解,需要的朋友可以參考下
    2024-04-04
  • 詳解Java 打印堆棧的幾種方法

    詳解Java 打印堆棧的幾種方法

    本篇文章主要介紹了Java 打印堆棧的幾種方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-06-06
  • 簡易JDBC框架實現(xiàn)過程詳解

    簡易JDBC框架實現(xiàn)過程詳解

    這篇文章主要介紹了簡易JDBC框架實現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-02-02

最新評論