SpringCould整合spring-security+oauth2(親測)


SpringCould整合spring-security+oauth2(親測)

1.OAuth2 概念

  • OAuth2 其實是一個關於授權的網絡標准,它制定了設計思路和運行流程,利用這個標准我們其實是可以自己實現 OAuth2 的認證過程的。

    oauth2.png

OAuth 2 有四種授權模式:

  • 授權碼模式(authorization code)

  • 簡化模式(implicit)

  • 密碼模式(resource owner password credentials)

  • 客戶端模式(client credentials)

    具體 OAuth2 是什么,可以參考這篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)

2.什么情況下需要用 OAuth2

例子:

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

以上是使用了 OAuth2 的授權碼模式,利用第三方的權威平台實現用戶身份的認證。當然了,如果你的公司內部有很多個服務,可以專門提取出一個認證中心,這個認證中心就充當上面所說的權威認證平台的角色,所有的服務都要到這個認證中心做認證

這樣一說,發現沒,這其實就是個單點登錄的功能。這就是另外一種使用場景,對於多服務的平台,可以使用 OAuth2 實現服務的單點登錄,只做一次登錄,就可以在多個服務中自由穿行,當然僅限於授權范圍內的服務和接口。

3.具體使用

OAuth2 其實是一個關於授權的網絡標准,它制定了設計思路和運行流程,利用這個標准我們其實是可以自己實現 OAuth2 的認證過程的。今天要介紹的 spring-cloud-starter-oauth2 ,其實是 Spring Cloud 按照 OAuth2 的標准並結合 spring-security 封裝好的一個具體實現。

3.1 系統架構說明

OAuth2架構時序圖.png

  • 認證服務:OAuth2 主要實現端,Token 的生成、刷新、驗證都在認證中心完成。

  • 后台服務: 接收到請求后會到認證中心驗證

  • 前端:認證服務、后台服務之間的聯調

上圖描述了使用了 前端與OAuth2 認證服務、微服務間的請求過程。大致的過程就是前端用用戶名和密碼到后台服務登錄,成功后后台服務到認證服務端換取 token,返回給前端,前端拿着 token 去各個微服務請求數據接口,一般這個 token 是放到 header 中的。當微服務接到請求后,先要拿着 token 去認證服務端檢查 token 的合法性,如果合法,再根據用戶所屬的角色及具有的權限動態的返回數據

 

3.2 創建並配置認證服務端

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

3.2.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>

之所以引入 redis 包,是因為下面會介紹一種用 redis 存儲 token 的方式。

3.2.2 配置好 application.yml

spring:
application:
  name: auth-server
redis:
  database: 2
  host: localhost
  port: 6379

server:
port: 6001

management:
endpoint:
  health:
    enabled: true

3.2.3 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。

3.2.4 實現 UserDetailsService

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

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

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


   @Autowired
   private UserRepository userRepository;

   /**
    * Security的登錄,User賦予權限
    *
    * @param username
    * @return
    * @throws UsernameNotFoundException
    */
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       if (StringUtils.isBlank(username)) {
           throw new UsernameNotFoundException("the username is not null");
      }
       
       //校驗用戶是否存在
       User user = userRepository.getById(username);
       if (null == user){
           throw new UsernameNotFoundException("the user is not exist");
      }

       //給用戶添加角色權限
       String role = user.getRole();
       List<SimpleGrantedAuthority> authorities = new ArrayList<>();
       authorities.add(new SimpleGrantedAuthority(role));

       //返回用戶token
       return new org.springframework.security.core.userdetails.User(username, user.getOauthpassword(), authorities);
  }

3.2.5 OAuth2 配置文件

創建一個配置文件繼承自 AuthorizationServerConfigurerAdapter

@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

   /**
    * 指定密碼的加密方式
    */
   @Autowired
   public PasswordEncoder passwordEncoder;

   /**
    * 該對象為刷新token提供支持
    */
   @Autowired
   public UserDetailsService kiteUserDetailsService;

   /**
    * 該對象用來支持password模式
    */
   @Autowired
   private AuthenticationManager authenticationManager;

   /**
    * 該對象用來講令牌信息存儲到內存中
    */
   @Autowired
   private TokenStore redisTokenStore;

   /**
    * 密碼模式下配置認證管理器 AuthenticationManager,並且設置 AccessToken的存儲介質tokenStore,如       果不設置,則會默認使用內存當做存儲介質。
    * 而該AuthenticationManager將會注入 2個Bean對象用以檢查(認證)
    * 1、ClientDetailsService的實現類 JdbcClientDetailsService (檢查 ClientDetails 對象)
    * 2、UserDetailsService的實現類 KiteUserDetailsService (檢查 UserDetails 對象)
    */
   @Override
   public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
       /** redis token 方式*/
       endpoints.authenticationManager(authenticationManager)
              .userDetailsService(kiteUserDetailsService)
              .tokenStore(redisTokenStore);

  }

   /**
    * 配置 oauth_client_details【client_id和client_secret等】信息的認證【檢查ClientDetails的合       法性】服務
    * 設置 認證信息的來源:數據庫 (可選項:數據庫和內存,使用內存一般用來作測試)
    * 自動注入:ClientDetailsService的實現類 JdbcClientDetailsService (檢查 ClientDetails 對       象)
    * 1.inMemory 方式存儲的,將配置保存到內存中,相當於硬編碼了。正式環境下的做法是持久化到數據庫中,比如       mysql 中。
    * 2. secret加密是client_id:secret 然后通過base64編碼后的字符串
    */
   @Override
   public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
       //添加客戶端信息
       //使用內存存儲OAuth客服端信息
       clients.inMemory()
               // client_id 客戶單ID
              .withClient("order-client")
               // client_secret 客戶單秘鑰
              .secret(passwordEncoder.encode("order-secret-8888"))
               // 該客戶端允許的授權類型,不同的類型,則獲取token的方式不一樣
              .authorizedGrantTypes("refresh_token", "authorization_code", "password")
               // token 有效期
              .accessTokenValiditySeconds(3600)
               // 允許的授權范圍
              .scopes("all")
              .and()
              .withClient("user-client")
              .secret(passwordEncoder.encode("user-secret-8888"))
              .authorizedGrantTypes("refresh_token", "authorization_code", "password")
              .accessTokenValiditySeconds(3600)
              .scopes("all");
  }

   /**
    * 配置:安全檢查流程
    * 默認過濾器:BasicAuthenticationFilter
    * 1、oauth_client_details表中clientSecret字段加密【ClientDetails屬性secret】
    * 2、CheckEndpoint類的接口 oauth/check_token 無需經過過濾器過濾,默認值:denyAll()
    */
   @Override
   public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
       ///允許客戶表單認證
       security.allowFormAuthenticationForClients();
       //對於CheckEndpoint控制器[框架自帶的校驗]的/oauth/check端點允許所有客戶端發送器請求而不會被 Spring-security攔截
       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);
  }
}

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 中。(優化認證服務有實例)

3.3.6 創建數據庫SpringCloud、user表、實體User、UserRepository

實體bean

@Entity
@Table(
       name = "user"
)
@Setter
@Getter
public class User implements Serializable {
   @Id
   @GeneratedValue(generator = "uuidGenerator")
   @GenericGenerator(name = "uuidGenerator", strategy = "uuid")
   @Column(name = "id", nullable = false)
   private String id;
   @Column(name = "username")
   private String username;
   @Column(name = "oauth_password")
   private String oauthpassword;
   @Column(name = "role")
   private String role;
}

jpa接口

public interface UserRepository extends JpaRepository<User, String> {

   @Query("select r from User r where r.id = ?1 ")
   User getById(String username);

}

user表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`oauth_password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '$2a$10$D3PEtxvJ.N9Ko6osFaO4SO/jYcC8v7RHP34gZNk5THMvX7H5g8/NS', 'ROLE_ADMIN');
INSERT INTO `user` VALUES ('2', 'Custon', '$2a$10$D3PEtxvJ.N9Ko6osFaO4SO/jYcC8v7RHP34gZNk5THMvX7H5g8/NS', 'ROLE_ADMIN');

SET FOREIGN_KEY_CHECKS = 1;

3.2.6 啟動認證服務

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

copy.png

主要有如下幾個:

POST /oauth/authorize  授權碼模式認證授權接口
GET/POST /oauth/token 獲取 token 的接口
POST /oauth/check_token 檢查 token 合法性接口

3.3 創建用戶客戶端項目

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

3.3.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>

3.3.2 application.yml 配置文件

spring:
 application:
   name: client-user
 redis:
   database: 2
   host: localhost
   port: 6379

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 驗證,就是請求的這個接口.

3.3.3 資源配置文件

在 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 的驗證。

3.3.4 最后,添加一個 RESTful 接口

@Slf4j
@RestController
public class UserController {

  @GetMapping(value = "get")
  @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
  public Object get(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
       
      return authentication.getName();
  }
}

一個 RESTful 方法,只有當訪問用戶具有 ROLE_ADMIN 權限時才能訪問,否則返回 401 未授權。

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

3.4 測試

3.4.1 獲取token

http://localhost:6001/oauth/token?username=2&password=123456&grant_type=password&scope=all&client_id=user-client&client_secret=user-secret-8888

copy.png

 

3.4.3 校驗token

checktoken.png

接口地址 http://localhost:6001/oauth/check_token?token=5f861834-9c6f-4424-af1d-df35fefddee3

正常返回結果:

{
  "active": true,
  "exp": 1597915851,
  "user_name": "2",
  "authorities": [
      "ROLE_ADMIN"
  ],
  "client_id": "user-client",
  "scope": [
      "all"
  ]
}

校驗失敗結果:

{
  "error": "invalid_token",
  "error_description": "Token was not recognised"
}

3.4.3 獲取refresh_token

copy.png

訪問地址: http://localhost:6001/oauth/token?username=2&password=123456&grant_type=refresh_token&scope=all&client_id=user-client&client_secret=user-secret-8888&refresh_token=323a3662-c997-4af0-b5d9-ea1a7f76fc84

grant_type: refresh_token

refresh_token: 從獲取token里面取出

3.4.2 客戶端攜帶token訪問接口

test.png

http://localhost:6101/client-user/get

返回結果: “2” (登錄username)

token到了過期時間,再次訪問,返回結果

{
  "error": "invalid_token",
  "error_description": "f7520be0-fb2c-4386-9ffc-e64977314b2f"
}

 

3.5 優化方案

3.5.1 認證服務OAuth2Config的configure(ClientDetailsServiceConfigurer clients) 換成數據庫存儲

 @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      //添加客戶端信息
      //使用內存存儲OAuth客服端信息
      clients.inMemory()
              // client_id 客戶單ID
              .withClient("order-client")
              // client_secret 客戶單秘鑰
              .secret(passwordEncoder.encode("order-secret-8888"))
              // 該客戶端允許的授權類型,不同的類型,則獲取token的方式不一樣
              .authorizedGrantTypes("refresh_token", "authorization_code", "password")
              // token 有效期
              .accessTokenValiditySeconds(3600)
              // 允許的授權范圍
              .scopes("all")
              .and()
              .withClient("user-client")
              .secret(passwordEncoder.encode("user-secret-8888"))
              .authorizedGrantTypes("refresh_token", "authorization_code", "password")
              .accessTokenValiditySeconds(3600)
              .scopes("all");
  }

把OAuth2Config.java文件的configure(ClientDetailsServiceConfigurer clients)替換成下面的


  @Autowired
  private DataSource dataSource;


  /**
    * jdbc配置
    *
    * @param clients
    * @throws Exception
    */
  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
      jcsb.passwordEncoder(passwordEncoder);
  }

在application.yml添加數據庫連接

    #數據庫連接
datasource:
  url: jdbc:mysql://localhost:3306/springcloud?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
  username: root
  password: 123456
  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()之后的值。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SecurityServerSystemApplication.class)
public class OAuth2PasswordTest {
   @Autowired
   public PasswordEncoder passwordEncoder;

   @Test
   public  void passwordEncode() {
       //secret
       System.out.println(passwordEncoder.encode("user-secret-8888"));
  }
}

3.6 JWT替換 redisToke

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

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

3.6.1 認證服務端改造

3.6.1.1 添加 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 ,可以搜索一下先了解一下。

3.6.1.2 更改 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 存儲模式。

3.6.2 改造用戶客戶端

3.6.2.1 修改 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 ,導致驗證不通過。

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

3.6.3 測試

跟上面一樣(這里就不重復了)

3.6.4 增強 JWT

如果我想在 JWT 中加入額外的字段(比方說用戶的其他信息)怎么辦呢,當然可以。spring security oauth2 提供了 TokenEnhancer 增強器。其實不光 JWT ,RedisToken 的方式同樣可以。

3.6.4.1 OAuthConfig 配置類修改

聲明一個增強器

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 擴展信息加入其中。

在JwtTokenConfig.java 注入增強器 TokenEnhancer

@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;
  }

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

}

OAuthConfig.java 修改 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 );
}
3.6.4.2 測試

再次請求 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"
}

3.6.4 用戶客戶端解析 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 請求上面的接口

返回內容如下:

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


免責聲明!

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



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