Spring Security Oauth2

Spring Security OAuth2

一. Oauth2.0

? 是開放授權(quán)的一個(gè)標(biāo)準(zhǔn)汽抚,旨在讓用戶允許第三方應(yīng)用去訪問用戶在某服務(wù)器中的特定私有資源殊橙,而可以不提供其在某服務(wù)器的賬號(hào)密碼給到第三方應(yīng)用膨蛮。通俗的話可以這樣去理解季研,假如你們公司正在開發(fā)一個(gè) 第三方應(yīng)用XXX,該應(yīng)用會(huì)需要在微信中分享出來一個(gè)活動(dòng)頁(yè)惹谐,該活動(dòng)需要讓微信用戶去參與驼卖,你們的應(yīng)用需要收集到用戶的姓名,頭像怎囚,地域等信息桥胞,那么問題來了?你的應(yīng)用如何才能拿到所有參與活動(dòng)的微信用戶的基本信息呢?

? 根據(jù)如上的描述催烘,我們可以將OAuth2分為四個(gè)角色:

  • Resource Owner:資源所有者 即上述中的微信用戶
  • Resource Server:資源服務(wù)器 即上述中的微信服務(wù)器,提供微信用戶基本信息給到第三方應(yīng)用
  • Client:第三方應(yīng)用客戶端 即上述中你公司正在開發(fā)的第三方應(yīng)用
  • Authorication Server:授權(quán)服務(wù)器 該角色可以理解為管理其余三者關(guān)系的中間層

其具體的執(zhí)行流程如下圖所示:

[圖片上傳失敗...(image-cd3d38-1589169864698)]

1.1 OAuth2的四種授權(quán)方式

1.1.1 授權(quán)碼(authorization-code)

? 這種方式是最常用的流程伊群,安全性也最高在岂,它適用于那些有后端的 Web 應(yīng)用蛮寂。授權(quán)碼通過前端傳送,令牌則是儲(chǔ)存在后端及老,而且所有與資源服務(wù)器的通信都在后端完成范抓。這樣的前后端分離,可以避免令牌泄漏匕垫。

<font color="red">第一步</font>,A 網(wǎng)站提供一個(gè)鏈接寞秃,用戶點(diǎn)擊后就會(huì)跳轉(zhuǎn)到 B 網(wǎng)站,授權(quán)用戶數(shù)據(jù)給 A 網(wǎng)站使用朗涩。下面就是 A 網(wǎng)站跳轉(zhuǎn) B 網(wǎng)站的一個(gè)示意鏈接绑改。

https://b.com/oauth/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL

? 上面 URL 中,response_type參數(shù)表示要求返回授權(quán)碼(code)识腿,client_id參數(shù)讓 B 知道是誰(shuí)在請(qǐng)求造壮,redirect_uri參數(shù)是 B 接受或拒絕請(qǐng)求后的跳轉(zhuǎn)網(wǎng)址,scope參數(shù)表示要求的授權(quán)范圍(這里是只讀)硝全。

<font color="red">第二步</font>,用戶跳轉(zhuǎn)后伟众,B 網(wǎng)站會(huì)要求用戶登錄召廷,然后詢問是否同意給予 A 網(wǎng)站授權(quán)。用戶表示同意先紫,這時(shí) B 網(wǎng)站就會(huì)跳回redirect_uri參數(shù)指定的網(wǎng)址遮精。跳轉(zhuǎn)時(shí),會(huì)傳回一個(gè)授權(quán)碼本冲,就像下面這樣劫扒。

https://a.com/callback?code=AUTHORIZATION_CODE

<font color="red">第三步</font>,A 網(wǎng)站拿到授權(quán)碼以后添怔,就可以在后端,向 B 網(wǎng)站請(qǐng)求令牌广料。

https://b.com/oauth/token?
 client_id=CLIENT_ID&
 client_secret=CLIENT_SECRET&
 grant_type=authorization_code&
 code=AUTHORIZATION_CODE&
 redirect_uri=CALLBACK_URL

? 上面 URL 中,client_id參數(shù)和client_secret參數(shù)用來讓 B 確認(rèn) A 的身份(client_secret參數(shù)是保密的拦止,因此只能在后端發(fā)請(qǐng)求)糜颠,grant_type參數(shù)的值是AUTHORIZATION_CODE萧求,表示采用的授權(quán)方式是授權(quán)碼夸政,code參數(shù)是上一步拿到的授權(quán)碼,redirect_uri參數(shù)是令牌頒發(fā)后的回調(diào)網(wǎng)址守问。

<font color="red">第四步</font>,B 網(wǎng)站收到請(qǐng)求以后穆端,就會(huì)頒發(fā)令牌仿便。具體做法是向redirect_uri指定的網(wǎng)址,發(fā)送一段 JSON 數(shù)據(jù)荒勇。

{    
  "access_token":"ACCESS_TOKEN",
  "token_type":"bearer",
  "expires_in":2592000,
  "refresh_token":"REFRESH_TOKEN",
  "scope":"read",
  "uid":100101,
  "info":{...}
}

上面 JSON 數(shù)據(jù)中闻坚,access_token字段就是令牌,A 網(wǎng)站在后端拿到了仅偎。

1.1.2 隱藏式(implicit)

? 有些 Web 應(yīng)用是純前端應(yīng)用哨颂,沒有后端相种。這時(shí)就不能用上面的方式了品姓,必須將令牌儲(chǔ)存在前端章贞。

<font color="red">第一步</font>弛槐,A 網(wǎng)站提供一個(gè)鏈接粟按,要求用戶跳轉(zhuǎn)到 B 網(wǎng)站翔烁,授權(quán)用戶數(shù)據(jù)給 A 網(wǎng)站使用友驮。

https://b.com/oauth/authorize?
  response_type=token&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL

? 上面 URL 中驾锰,response_type參數(shù)為token,表示要求直接返回令牌耻瑟。

<font color="red">第二步</font>,用戶跳轉(zhuǎn)到 B 網(wǎng)站喳整,登錄后同意給予 A 網(wǎng)站授權(quán)裸扶。這時(shí),B 網(wǎng)站就會(huì)跳回redirect_uri參數(shù)指定的跳轉(zhuǎn)網(wǎng)址瞬项,并且把令牌作為 URL 參數(shù)何荚,傳給 A 網(wǎng)站。

https://a.com/callback?token=ACCESS_TOKEN

? 這種方式把令牌直接傳給前端妥衣,是很不安全的戒傻。因此,只能用于一些安全要求不高的場(chǎng)景芦倒,并且令牌的有效期必須非常短兵扬,通常就是會(huì)話期間(session)有效,瀏覽器關(guān)掉器钟,令牌就失效了。

1.1.3 密碼式(password)

? 如果你高度信任某個(gè)應(yīng)用傲霸,RFC 6749 也允許用戶把用戶名和密碼昙啄,直接告訴該應(yīng)用。該應(yīng)用就使用你的密碼孵睬,申請(qǐng)令牌伶跷,這種方式稱為"密碼式"(password)秘狞。

<font color="red">第一步</font>,A 網(wǎng)站要求用戶提供 B 網(wǎng)站的用戶名和密碼雇初。拿到以后减响,A 就直接向 B 請(qǐng)求令牌支示。

ttps://oauth.b.com/token?
  grant_type=password&
  username=USERNAME&
  password=PASSWORD&
  client_id=CLIENT_ID

? 上面 URL 中,grant_type參數(shù)是授權(quán)方式颂鸿,這里的password表示"密碼式",usernamepassword是 B 的用戶名和密碼败晴。

<font color="red">第二步</font>栽渴,B 網(wǎng)站驗(yàn)證身份通過后,直接給出令牌慢味。注意僚祷,這時(shí)不需要跳轉(zhuǎn)辙谜,而是把令牌放在 JSON 數(shù)據(jù)里面,作為 HTTP 回應(yīng)装哆,A 因此拿到令牌定嗓。

這種方式需要用戶給出自己的用戶名/密碼,顯然風(fēng)險(xiǎn)很大宵溅,因此只適用于其他授權(quán)方式都無法采用的情況,而且必須是用戶高度信任的應(yīng)用恃逻。

1.1.4 客戶端憑證(client credentials)

? 最后一種方式是憑證式(client credentials)雏搂,適用于沒有前端的命令行應(yīng)用,即在命令行下請(qǐng)求令牌寇损。

<font color="red">第一步</font>凸郑,A 應(yīng)用在命令行向 B 發(fā)出請(qǐng)求。

https://oauth.b.com/token?
  grant_type=client_credentials&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

? 上面 URL 中矛市,grant_type參數(shù)等于client_credentials表示采用憑證式芙沥,client_idclient_secret用來讓 B 確認(rèn) A 的身份浊吏。

<font color="red">第二步</font>,B 網(wǎng)站驗(yàn)證通過以后歌憨,直接返回令牌躺孝。

這種方式給出的令牌植袍,是針對(duì)第三方應(yīng)用的于个,而不是針對(duì)用戶的厅篓,即有可能多個(gè)用戶共享同一個(gè)令牌。

二. 授權(quán)服務(wù)器的搭建

2.1 依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.9.RELEASE</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.17</version>
</dependency>

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

2.2 配置

spring:
  datasource:
    url: jdbc:mysql://mysql:3306/oauth2?useSSL=false&serverTimezone=UTC
    username: root
    password:
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      initial-size: 20
      max-active: 50
      min-idle: 15
      validation-query: 'select 1'
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      # psCache, 緩存preparedStatement, 對(duì)支持游標(biāo)的數(shù)據(jù)庫(kù)性能有巨大的提升,oracle開啟档押,mysql建議關(guān)閉
      pool-prepared-statements: false
      # psCache開啟的時(shí)候有效
      max-open-prepared-statements: 100
      # 一個(gè)連接在被驅(qū)逐出連接池的時(shí)候令宿,在連接池中最小的空閑時(shí)間粒没,單位為毫秒
      min-evictable-idle-time-millis: 30000
      # 距離上次釋放空閑連接的時(shí)間間隔
      time-between-eviction-runs-millis: 30000

2.3 數(shù)據(jù)庫(kù)表的創(chuàng)建

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

create table oauth_client_token (
  token_id VARCHAR(256),
  token blob,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token blob,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication blob,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token blob,
  authentication blob
);

create table oauth_code (
  code VARCHAR(256), authentication blob
);

create table oauth_approvals (
    userId VARCHAR(256),
    clientId VARCHAR(256),
    scope VARCHAR(256),
    status VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

說明:數(shù)據(jù)庫(kù)表是依據(jù)spring-security的官網(wǎng)爽撒,地址為:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql匆浙,但是在創(chuàng)建的時(shí)候?qū)⑺械淖侄晤愋蚅ONGVARBINARY改為BLOB類型。

數(shù)據(jù)庫(kù)的說明參考:http://andaily.com/spring-oauth-server/db_table_description.html

2.4 用戶登錄認(rèn)證

@Component
public class UserSecurityService implements UserDetailsService {

    private static Logger logger = LoggerFactory.getLogger(UserSecurityService.class);

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("用戶名:" + username);

        return new User(username, "$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga",
                Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
    }
}

2.5 web安全配置

@Configuration
public class WebAuthorizationConfig extends WebSecurityConfigurerAdapter {

    // 密碼的加解密
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() 
                .loginPage("/login.html") 
                .loginProcessingUrl("/authentication/form")
                .and()
                .authorizeRequests() 
                .antMatchers("/login.html").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable();

    }
}

2.6 授權(quán)服務(wù)器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private LoginAuthencation loginAuthencation;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Resource
    private DataSource dataSource;

    // 根據(jù)用戶的client_id查詢用戶的授權(quán)信息
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

    //用于將token信息存放在數(shù)據(jù)庫(kù)中
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    // authentication_code放入到數(shù)據(jù)中
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 采用數(shù)據(jù)庫(kù)的方式查詢用戶的授權(quán)信息
        clients.withClientDetails(clientDetails());

        /**  供學(xué)習(xí)使用
        clients.inMemory()
                // client_id
                .withClient("client")
                // client_secret
                .secret("secret")
                // 該client允許的授權(quán)類型言秸,不同的類型举畸,則獲得token的方式不一樣抄沮。
                .authorizedGrantTypes("authorization_code")
                .scopes("all")
                //回調(diào)uri叛买,在authorization_code與implicit授權(quán)方式時(shí)率挣,用以接收服務(wù)器的返回信息
                .redirectUris("http://localhost:9090/login");
         */
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 存數(shù)據(jù)庫(kù)
        endpoints.tokenStore(tokenStore())
                .authorizationCodeServices(authorizationCodeServices())
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);

        // 配置tokenServices參數(shù)
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(false);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        // token的過期時(shí)間為1天
        tokenServices.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); 
        endpoints.tokenServices(tokenServices);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        /**
         * 作用是使用client_id和client_secret來做登錄認(rèn)證椒功,如果是在瀏覽器的情況下动漾,會(huì)讓用戶
         * 輸入用戶名和密碼
         */
        oauthServer.allowFormAuthenticationForClients();
        oauthServer.checkTokenAccess("isAuthenticated()");
        oauthServer.passwordEncoder(passwordEncoder);
    }
}

2.7 更改默認(rèn)授權(quán)頁(yè)面

@Controller
@SessionAttributes("authorizationRequest") // 必須配置
public class AuthController {

/**
 * org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint 
    默認(rèn)的授權(quán)頁(yè)面
 */
    @RequestMapping("/oauth/confirm_access")
    public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
         AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");

        System.out.println(authorizationRequest.getScope());

        return "/oauth.html";
    }
}

2.8 獲取code

? 獲取授權(quán)碼的地址:http://127.0.0.1:8080/oauth/authorize?client_id=other_client&response_type=code&redirect_uri=http://localhost:9090/login

? [圖片上傳失敗...(image-4ddc11-1589169864698)]

? 在瀏覽器地址欄的重定向地址上可以看到 code值旱眯。

2.9 獲取access_token

? 根據(jù)上一步獲取到的code值獲取acccess_token, 請(qǐng)求的地址為:

http://127.0.0.1:8080/oauth/token?client_id=other_client&client_secret=1&grant_type=authorization_code&code=tuvCj9&redirect_uri=http://localhost:9090/login

? 其中code的值為上一步請(qǐng)求獲取到的code的數(shù)據(jù)键思,返回內(nèi)容如下:

[圖片上傳失敗...(image-d0a6d4-1589169864698)]

2.10 獲取額外的信息

? Oauth2在獲取用戶額外信息的時(shí)候吼鳞,內(nèi)部實(shí)現(xiàn)上并沒有去做赔桌,所以需要我們自己去實(shí)現(xiàn),實(shí)現(xiàn)的方式就是去重寫其代碼音诫,思路是從數(shù)據(jù)庫(kù)查詢到的信息封裝到 ClientDetails中竭钝,但是內(nèi)部卻沒有開放出來香罐,所以需要去找到是在何處查詢數(shù)據(jù)庫(kù)时肿,根據(jù)源代碼的追蹤螃成,發(fā)現(xiàn)查詢數(shù)據(jù)庫(kù)的操作是在ApprovalStoreUserApprovalHandler這個(gè)類中寸宏,所以我們需要手動(dòng)的去修改其源代碼击吱,修改的內(nèi)容如下:

[圖片上傳失敗...(image-444a2f-1589169864698)]

三. 資源服務(wù)器的搭建

? 資源服務(wù)器就是用戶想要真正獲取資源的服務(wù)器,我們必須要通過2.9節(jié)中獲取到的access_token來獲取炭臭。

3.1依賴

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.9.RELEASE</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.9.RELEASE</version>
    </dependency>
</dependencies>

3.2 配置

security:
  oauth2:
    resource:
      # access_token的驗(yàn)證地址
      token-info-uri: http://localhost:8080/oauth/check_token

    client:
      client-id: resources_client
      client-secret: 1

3.3 資源服務(wù)賬號(hào)

? 我們需要在授權(quán)服務(wù)器上創(chuàng)建client_id和client_secret鞋仍,當(dāng)資源服務(wù)器拿到第三方的access_token后需要到授權(quán)服務(wù)器上驗(yàn)證access_token的來源是否合法威创。

3.4 資源服務(wù)器安全配置

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() //
                .antMatchers("/user").access("#oauth2.hasAnyScope('all','read')");
    }
}

3.5 獲取資源

通過調(diào)用如下接口去獲取資源服務(wù)器的資源:

http://localhost:8081/user?access_token=92de29ea-df7d-4d35-b585-c740322f9028

四. JWT(Json Web Token)

4.1 傳統(tǒng)跨域登錄流程

  1. 用戶向服務(wù)器發(fā)送用戶名和密碼肚豺。
  2. 驗(yàn)證服務(wù)器后吸申,相關(guān)數(shù)據(jù)(如用戶角色截碴,登錄時(shí)間等)將保存在當(dāng)前會(huì)話中日丹。
  3. 服務(wù)器向用戶返回session_id蚯嫌,session信息都會(huì)寫入到用戶的Cookie齐帚。
  4. 用戶的每個(gè)后續(xù)請(qǐng)求都將通過在Cookie中取出session_id傳給服務(wù)器对妄。
  5. 服務(wù)器收到session_id并對(duì)比之前保存的數(shù)據(jù)剪菱,確認(rèn)用戶的身份。

[圖片上傳失敗...(image-a616f0-1589169864698)]

? 這種模式最大的問題是孝常,沒有分布式架構(gòu)构灸,無法支持橫向擴(kuò)展喜颁。如果使用一個(gè)服務(wù)器,該模式完全沒有問題隔披。但是奢米,如果它是服務(wù)器群集或面向服務(wù)的跨域體系結(jié)構(gòu)的話鬓长,則需要一個(gè)統(tǒng)一的session數(shù)據(jù)庫(kù)庫(kù)來保存會(huì)話數(shù)據(jù)實(shí)現(xiàn)共享尝江,這樣負(fù)載均衡下的每個(gè)服務(wù)器才可以正確的驗(yàn)證用戶身份。

? 但是在實(shí)際中常見的單點(diǎn)登陸的需求:站點(diǎn)A和站點(diǎn)B提供統(tǒng)一公司的相關(guān)服務(wù)〉□澹現(xiàn)在要求用戶只需要登錄其中一個(gè)網(wǎng)站少态,然后它就會(huì)自動(dòng)登錄到另一個(gè)網(wǎng)站彼妻。怎么做侨歉?

? 一種解決方案是聽過持久化session數(shù)據(jù)幽邓,寫入數(shù)據(jù)庫(kù)或文件持久層等牵舵。收到請(qǐng)求后,驗(yàn)證服務(wù)從持久層請(qǐng)求數(shù)據(jù)担巩。該解決方案的優(yōu)點(diǎn)在于架構(gòu)清晰涛癌,而缺點(diǎn)是架構(gòu)修改比較費(fèi)勁先匪,整個(gè)服務(wù)的驗(yàn)證邏輯層都需要重寫,工作量相對(duì)較大假颇。而且由于依賴于持久層的數(shù)據(jù)庫(kù)或者問題系統(tǒng)笨鸡,會(huì)有單點(diǎn)風(fēng)險(xiǎn)形耗,如果持久層失敗激涤,整個(gè)認(rèn)證體系都會(huì)掛掉倦踢。

4.2 Jwt

? 針對(duì)如上的問題辱挥,另外一種解決方案就是JWT(Json Web Token)晤碘,其原則是在服務(wù)器驗(yàn)證之后园爷,將生產(chǎn)的一個(gè)Json對(duì)象返回給用戶童社,格式如下:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzEyNDE2NTksInVzZXJfbmFtZSI6ImFhIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJlZTczOTI4OC0zNDMwLTQxMjUtODBhOC1lMDU0Njg5OWQ2ODIiLCJjbGllbnRfaWQiOiJteV9jbGllbnQiLCJzY29wZSI6WyJhbGwiXX0.Ji4xYQJuJRrZeCTTMOb1e2GiOESAyiI9NzbWffKzcJ0",
    "token_type": "bearer",
    "expires_in": 43198,
    "scope": "all",
    "jti": "ee739288-3430-4125-80a8-e0546899d682"
}
4.2.1 Jwt數(shù)據(jù)結(jié)構(gòu)

? 如上代碼返回的access_token為一個(gè)字符串甘改,之間用 . 分隔為為三段,其中第一段為 header(頭)腾节,第二段為 payload (負(fù)載)案腺,第三段為signature(簽名)劈榨,如下圖所示:

[圖片上傳失敗...(image-588c21-1589169864698)]

? 我們可以在 <https://jwt.io/> 在線解析這個(gè)JWT token, 如下圖所示

[圖片上傳失敗...(image-f2bd36-1589169864698)]

header

字段名 描述
alg 算法
typ 令牌類型

payload

字段名 描述
exp 超時(shí)時(shí)間
jti JWT ID

signature

? 簽名拷姿,為了驗(yàn)證發(fā)送過來的access_token是否有效响巢,通過如下算法得到:

[圖片上傳失敗...(image-280921-1589169864698)]

4.2.2 用法與問題

? 客戶端接收服務(wù)器返回的JWT棒妨,將其存儲(chǔ)在Cookie或localStorage中券腔。此后纷纫,客戶端將在與服務(wù)器交互中都會(huì)帶JWT涛酗。如果將它存儲(chǔ)在Cookie中商叹,就可以自動(dòng)發(fā)送,因此一般是將它放入HTTP請(qǐng)求的Header Authorization字段中卵洗。當(dāng)跨域時(shí)过蹂,也可以將JWT被放置于POST請(qǐng)求的數(shù)據(jù)主體中酷勺。

? 但是JWT也面臨著諸多的問題脆诉,如下所示:

  1. JWT默認(rèn)不加密,但可以加密亏狰。生成原始令牌后,可以使用改令牌再次對(duì)其進(jìn)行加密辰斋。
  2. 當(dāng)JWT未加密方法是亡呵,一些私密數(shù)據(jù)無法通過JWT傳輸锰什。
  3. JWT不僅可用于認(rèn)證汁胆,還可用于信息交換嫩码。善用JWT有助于減少服務(wù)器請(qǐng)求數(shù)據(jù)庫(kù)的次數(shù)铸题。
  4. JWT的最大缺點(diǎn)是服務(wù)器不保存會(huì)話狀態(tài),所以在使用期間不可能取消令牌或更改令牌的權(quán)限探熔。也就是說诀艰,一旦JWT簽發(fā),在有效期內(nèi)將會(huì)一直有效绿满。
  5. JWT本身包含認(rèn)證信息棒口,因此一旦信息泄露,任何人都可以獲得令牌的所有權(quán)限厂抖。為了減少盜用七蜘,JWT的有效期不宜設(shè)置太長(zhǎng)橡卤。對(duì)于某些重要操作碧库,用戶在使用時(shí)應(yīng)該每次都進(jìn)行進(jìn)行身份驗(yàn)證嵌灰。
  6. 為了減少盜用和竊取沽瞭,JWT不建議使用HTTP協(xié)議來傳輸代碼驹溃,而是使用加密的HTTPS協(xié)議進(jìn)行傳輸。

4.3 jwt授權(quán)服務(wù)器搭建

依賴:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.9.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.6.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.9.RELEASE</version>
    </dependency>
</dependencies>

jwt授權(quán)服務(wù)器

@Configuration
@EnableAuthorizationServer
public class OauthAuthenticationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("123"); //設(shè)置簽名
        return jwtAccessTokenConverter;
    }

    // 基于內(nèi)存的授權(quán)碼
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()  //
                .withClient("my_client") //
                .secret("$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga")  //
                .scopes("all") //
                .authorizedGrantTypes("authorization_code")
                .redirectUris("http://localhost:9090/login");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter())
                 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .passwordEncoder(passwordEncoder);
    }
}

4.4 jwt資源服務(wù)器

@Configuration
@EnableResourceServer
public class ReourceServerConfig extends ResourceServerConfigurerAdapter {

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

    @Bean
    public DefaultTokenServices defaultTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(jwtTokenStore());
        return defaultTokenServices;
    }

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

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()  //
                .anyRequest() //
                .authenticated() //
                .and() //
                .csrf().disable();
    }

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

五. 單點(diǎn)登錄

? 單點(diǎn)登錄(Singal Sign On)是很多企業(yè)經(jīng)常使用的一種登錄方式拐辽,那么何為單點(diǎn)登錄呢俱诸?為了解決什么樣的問題呢睁搭?舉個(gè)例子,在淘寶公司內(nèi)部,有天貓锌唾、淘寶晌涕、阿里云重窟、聚劃算等眾多的產(chǎn)品線巡扇,這些產(chǎn)品線是不同的服務(wù)器來支撐運(yùn)行霎迫,但是作為用戶的我們卻可以使用同一套賬戶名和密碼進(jìn)行登錄他幾乎所有的產(chǎn)品,登錄服務(wù)器只有一個(gè)涩赢,根據(jù)登錄服務(wù)器返回的特定的信息筒扒,可以去訪問它所有的產(chǎn)品線花墩。著就是所謂的單點(diǎn)登錄。

? 在具體的實(shí)現(xiàn)的時(shí)候祠肥,我們使用JWT的方式來實(shí)現(xiàn)單點(diǎn)登錄仇箱。他的處理流程如下:

5.1 認(rèn)證服務(wù)器

依賴

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.9.RELEASE</version>
    </dependency>
</dependencies>

授權(quán)服務(wù)

@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationConfigServer extends AuthorizationServerConfigurerAdapter {
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("abcxyz");
        return jwtAccessTokenConverter;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()  //
                .withClient("client-a") //
                .secret("$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga")  //
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all")
                .redirectUris("http://localhost:8081/clientA/login")
                .autoApprove(true)
                .and()
                .withClient("client-b") //
                .secret("$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga")  //
                .authorizedGrantTypes("authorization_code")
                .scopes("all")
                .redirectUris("http://localhost:8082/clientB/login")
                .autoApprove(true);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints//.tokenStore(jwtTokenStore())
                 .accessTokenConverter(jwtAccessTokenConverter());
    }
}

5.2 子系統(tǒng)

依賴

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.9.RELEASE</version>
    </dependency>
</dependencies>

配置

server:
  port: 8081
  servlet:
    context-path: /clientA


security:
  oauth2:
    client:
      client-id: client-a
      client-secret: 1
      access-token-uri: http://localhost:7070/auth/oauth/token
      user-authorization-uri: http://localhost:7070/auth/oauth/authorize

    resource:
      jwt:
        key-value: abcxyz

啟動(dòng)類配置

@SpringBootApplication
@EnableOAuth2Sso
public class SsoClientApplicationA {
    public static void main( String[] args ) {
        SpringApplication.run(SsoClientApplicationA.class, args);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子奔垦,更是在濱河造成了極大的恐慌椿猎,老刑警劉巖犯眠,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異量蕊,居然都是意外死亡残炮,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抱完,“玉大人巧娱,你說我怎么就攤上這事撮胧±锨蹋” “怎么了铺峭?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)钓账。 經(jīng)常有香客問我,道長(zhǎng)啦粹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮撩荣,結(jié)果婚禮上餐曹,老公的妹妹穿的比我還像新娘。我一直安慰自己曹步,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布雏婶。 她就那樣靜靜地躺著块差,像睡著了一般状蜗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜜氨,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天郎汪,我揣著相機(jī)與錄音煞赢,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的阀趴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼据块,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼怕犁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起思杯,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤杈湾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后悍汛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡术陶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片历谍。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脱衙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤躯喇,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站弧可,受9級(jí)特大地震影響价脾,放射性物質(zhì)發(fā)生泄漏秋柄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一说榆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧稍味,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)颅崩。三九已至饱搏,卻和暖如春恨锚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背倍靡。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工猴伶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塌西。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓他挎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親捡需。 傳聞我的和親對(duì)象是個(gè)殘疾皇子办桨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355