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

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

1.OAuth2 概念

  • OAuth2 實際上是一個關於受權的網絡標準,它制定了設計思路和運行流程,利用這個標準咱們實際上是能夠本身實現 OAuth2 的認證過程的。html

    oauth2.png

OAuth 2 有四種受權模式:前端

  • 受權碼模式(authorization code)java

  • 簡化模式(implicit)mysql

  • 密碼模式(resource owner password credentials)git

  • 客戶端模式(client credentials)github

    具體 OAuth2 是什麼,能夠參考這篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)web

2.什麼狀況下須要用 OAuth2

例子:redis

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

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

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

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對象用以檢查(認證)
     * 一、ClientDetailsService的實現類 JdbcClientDetailsService (檢查 ClientDetails 對象)
     * 二、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
     * 一、oauth_client_details表中clientSecret字段加密【ClientDetails屬性secret】
     * 二、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"
}

 

 

關注公衆號,有更多好玩的等着你!!!

img

相關文章
相關標籤/搜索