OAuth2.0系列之單點(diǎn)登錄SSO實(shí)現(xiàn)

@TOC

實(shí)現(xiàn)方案:OAuth2.0+JWT+Spring Security

一、 SSO簡(jiǎn)介

1.1 單點(diǎn)登錄定義

單點(diǎn)登錄(Single sign on),英文名稱(chēng)縮寫(xiě)SSO,SSO的意思就是在多系統(tǒng)的環(huán)境中,登錄單方系統(tǒng),就可以在不用再次登錄的情況下訪問(wèn)相關(guān)受信任的系統(tǒng)。也就是說(shuō)只要登錄一次單體系統(tǒng)就可以。

1.2 單點(diǎn)登錄角色

單點(diǎn)登錄一般包括下面三種角色:

①用戶(多個(gè));

②認(rèn)證中心(一個(gè));

③Web應(yīng)用(多個(gè))。

PS:這里所說(shuō)的web應(yīng)用可以理解為SSO Client,認(rèn)證中心可以說(shuō)是SSO Server。

1.3 單點(diǎn)登錄分類(lèi)

因?yàn)閔ttp協(xié)議是無(wú)狀態(tài)的協(xié)議,所以要保持登錄狀態(tài),必須要存儲(chǔ)登錄信息,按照存儲(chǔ)方式,單點(diǎn)登錄實(shí)現(xiàn)方式主要可以分為兩種。

  • 一種是基于Cookie的,這種比較常見(jiàn),比如下文介紹的CAS也是基于Cookie的;
  • 另外一種是基于Session的,其實(shí)理解起來(lái)就是會(huì)話共享,只有實(shí)現(xiàn)不同子系統(tǒng)之間的會(huì)話共享就能實(shí)現(xiàn)單點(diǎn)登錄,詳情可以參考我之前的博客,就是實(shí)現(xiàn)會(huì)話共享實(shí)現(xiàn)單點(diǎn)登錄的,https://blog.csdn.net/u014427391/article/details/78653482

二、OAuth2.0

2.1 OAuth2.0簡(jiǎn)介

OAuth是一種開(kāi)放協(xié)議, 允許用戶讓第三方應(yīng)用以安全且標(biāo)準(zhǔn)的方式獲取該用戶在某一網(wǎng)站,移動(dòng)或者桌面應(yīng)用上存儲(chǔ)的秘密的資源(如用戶個(gè)人信息,照片,視頻,聯(lián)系人列表),而無(wú)需將用戶名和密碼提供給第三方應(yīng)用。

官網(wǎng):https://oauth.net/2/ ,官網(wǎng)只有英文版文檔,您也可以參考翻譯過(guò)來(lái)的文檔,鏈接:OAuth2 RFC6749中文翻譯

OAuth 1.0協(xié)議(RFC5849)作為一個(gè)指導(dǎo)性文檔發(fā)布,是一個(gè)小社區(qū)的工作成果。
本標(biāo)準(zhǔn)化規(guī)范在OAuth 1.0的部署經(jīng)驗(yàn)之上構(gòu)建,也包括其他使用案例以及從更廣泛的IETF社區(qū)收集到的可擴(kuò)展性需求。
OAuth 2.0協(xié)議不向后兼容OAuth 1.0。這兩個(gè)版本可以在網(wǎng)絡(luò)上共存,實(shí)現(xiàn)者可以選擇同時(shí)支持他們。

OAuth2.0在安全性方面做了比較大的提高,簡(jiǎn)單來(lái)說(shuō)OAuth2.0就是一種授權(quán)協(xié)議,可以用來(lái)授權(quán),順便點(diǎn)個(gè)網(wǎng)站,如圖這種微信、支付寶登錄就是OAuth2.0的應(yīng)用


在這里插入圖片描述

2.2 OAuth2.0角色

OAuth2.0定義如下角色:

  • 資源所有者(Resource Owner): 能夠許可受保護(hù)資源訪問(wèn)權(quán)限的實(shí)體。當(dāng)資源所有者是個(gè)人時(shí),它作為最終用戶被提及。
  • 用戶代理(User Agent): 指的的資源擁有者授權(quán)的一些渠道。一般指的是瀏覽器、APP
  • 客戶端(Client) 使用資源所有者的授權(quán)代表資源所有者發(fā)起對(duì)受保護(hù)資源的請(qǐng)求的應(yīng)用程序。術(shù)語(yǔ)“客戶端”并非特指任何特定的的實(shí)現(xiàn)特點(diǎn)(例如:應(yīng)用程序是否在服務(wù)器、臺(tái)式機(jī)或其他設(shè)備上執(zhí)行)。
  • 授權(quán)服務(wù)器(Authorization Server): 在成功驗(yàn)證資源所有者且獲得授權(quán)后頒發(fā)訪問(wèn)令牌給客戶端的服務(wù)器。
    授權(quán)服務(wù)器和資源服務(wù)器之間的交互超出了本規(guī)范的范圍。授權(quán)服務(wù)器可以和資源服務(wù)器是同一臺(tái)服務(wù)器,也可以是分離的個(gè)體。一個(gè)授權(quán)服務(wù)器可以頒發(fā)被多個(gè)資源服務(wù)器接受的訪問(wèn)令牌。
  • 資源服務(wù)器(Resource Server): 托管受保護(hù)資源的服務(wù)器,能夠接收和響應(yīng)使用訪問(wèn)令牌對(duì)受保護(hù)資源的請(qǐng)求。

2.3 協(xié)議流程

在這里插入圖片描述

引用OAuth2 RFC6749中文翻譯
(A)客戶端向從資源所有者請(qǐng)求授權(quán)。授權(quán)請(qǐng)求可以直接向資源所有者發(fā)起(如圖所示),或者更可取的是通過(guò)作為中介的授權(quán)服務(wù)器間接發(fā)起。
(B)客戶端收到授權(quán)許可,這是一個(gè)代表資源所有者的授權(quán)的憑據(jù),使用本規(guī)范中定義的四種許可類(lèi)型之一或 者使用擴(kuò)展許可類(lèi)型表示。授權(quán)許可類(lèi)型取決于客戶端請(qǐng)求授權(quán)所使用的方式以及授權(quán)服務(wù)器支持的類(lèi)型。
(C)客戶端與授權(quán)服務(wù)器進(jìn)行身份認(rèn)證并出示授權(quán)許可請(qǐng)求訪問(wèn)令牌。
(D)授權(quán)服務(wù)器驗(yàn)證客戶端身份并驗(yàn)證授權(quán)許可,若有效則頒發(fā)訪問(wèn)令牌。
(E)客戶端從資源服務(wù)器請(qǐng)求受保護(hù)資源并出示訪問(wèn)令牌進(jìn)行身份驗(yàn)證。
(F)資源服務(wù)器驗(yàn)證訪問(wèn)令牌,若有效則滿足該請(qǐng)求。

2.4 授權(quán)模式

OAuth2.0有4種授權(quán)模式:

  • 授權(quán)碼
  • 隱式授權(quán)
  • 資源所有者密碼憑據(jù)
  • 客戶端憑據(jù)

其中最常用的是授權(quán)碼模式,授權(quán)模式的詳細(xì)介紹可以參考阮一峰老師的:OAuth 2.0 的四種方式

官方的授權(quán)碼模式流程圖:


在這里插入圖片描述

三、單點(diǎn)登錄實(shí)現(xiàn)

3.1 環(huán)境準(zhǔn)備

  • IntelliJ IDEA
  • Maven3.+版本
    新建SpringBoot Initializer項(xiàng)目


    在這里插入圖片描述
在這里插入圖片描述

主要是想引入:

 <!-- Spring Cloud Oauth2-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <!-- Spring Cloud Security-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

schema腳本:官方提供的所有腳本:鏈接,官方腳本是HSQL數(shù)據(jù)庫(kù),所以我們改成mysql版,如參考代碼:


-- used in tests that use MYSQL
CREATE TABLE oauth_client_details (
  client_id VARCHAR(128) PRIMARY KEY,
  resource_ids VARCHAR(128),
  client_secret VARCHAR(128),
  scope VARCHAR(128),
  authorized_grant_types VARCHAR(128),
  web_server_redirect_uri VARCHAR(128),
  authorities VARCHAR(128),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  ENABLE TINYINT(1) DEFAULT '1',
  autoapprove VARCHAR(128)
);

CREATE TABLE oauth_client_token (
  token_id VARCHAR(128),
  token BLOB,
  authentication_id VARCHAR(128) PRIMARY KEY,
  user_name VARCHAR(128),
  client_id VARCHAR(128)
);

CREATE TABLE oauth_access_token (
  token_id VARCHAR(128),
  token BLOB,
  authentication_id VARCHAR(128) PRIMARY KEY,
  user_name VARCHAR(128),
  client_id VARCHAR(128),
  authentication BLOB,
  refresh_token VARCHAR(128)
);


CREATE TABLE oauth_refresh_token (
  token_id VARCHAR(128),
  token BLOB,
  authentication BLOB
);


CREATE TABLE oauth_code (
  CODE VARCHAR(128), authentication BLOB
);

CREATE TABLE oauth_approvals (
    userId VARCHAR(128),
    clientId VARCHAR(128),
    scope VARCHAR(128),
    STATUS VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP  
);

-- customized oauth_client_details table
CREATE TABLE ClientDetails (
  appId VARCHAR(128) PRIMARY KEY,
  resourceIds VARCHAR(128),
  appSecret VARCHAR(128),
  scope VARCHAR(128),
  grantTypes VARCHAR(128),
  redirectUrl VARCHAR(128),
  authorities VARCHAR(128),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(128)
);

本博客只用到oauth_client_details 這張表

3.2 OAuth2.0授權(quán)服務(wù)器

3.2.1 新建SpringBoot項(xiàng)目

新建SpringBoot Initializer項(xiàng)目,可以命名為Oauth2-server


在這里插入圖片描述
在這里插入圖片描述

3.2.2 @EnableResourceServer注解

啟動(dòng)類(lèi)加上@EnableResourceServer注解:


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@SpringBootApplication
@EnableResourceServer
public class JeeplatformSsoOauth2Application {

    public static void main(String[] args) {
        SpringApplication.run(JeeplatformSsoOauth2Application.class, args);
    }

}

3.2.3 OAuth2.0配置類(lèi)

環(huán)境準(zhǔn)備,要寫(xiě)點(diǎn)配置數(shù)據(jù),如果要用內(nèi)存方式,就在代碼里實(shí)現(xiàn)

insert into `oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) values('cms',NULL,'{noop}secret','all','authorization_code','http://localhost:8084/cms/login',NULL,'60','60',NULL,'true');
insert into `oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) values('oa',NULL,'{noop}secret','all','authorization_code','http://localhost:8082/oa/login',NULL,'60','60',NULL,'true');

OAuth2.0配置類(lèi):

package org.muses.jeeplatform.oauth.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * <pre>
 *  OAuth2.0配置
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2020/04/29 15:06  修改內(nèi)容:
 * </pre>
 */
@Configuration
//開(kāi)啟授權(quán)服務(wù)
@EnableAuthorizationServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;

    private static final String CLIENT_ID = "cms";
    private static final String SECRET_CHAR_SEQUENCE = "{noop}secret";
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final String USER ="user";
    private static final String ALL = "all";
    private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 2*60;
    private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 2*60;
    // 密碼模式授權(quán)模式
    private static final String GRANT_TYPE_PASSWORD = "password";
    //授權(quán)碼模式
    private static final String AUTHORIZATION_CODE = "authorization_code";
    //refresh token模式
    private static final String REFRESH_TOKEN = "refresh_token";
    //簡(jiǎn)化授權(quán)模式
    private static final String IMPLICIT = "implicit";
    //指定哪些資源是需要授權(quán)驗(yàn)證的
    private static final String RESOURCE_ID = "resource_id";

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /*clients
                // 使用內(nèi)存存儲(chǔ)
                .inMemory()
                //標(biāo)記客戶端id
                .withClient(CLIENT_ID)
                //客戶端安全碼
                .secret(SECRET_CHAR_SEQUENCE)
                //為true 直接自動(dòng)授權(quán)成功返回code
                .autoApprove(true)
                .redirectUris("http://127.0.0.1:8084/cms/login") //重定向uri
                //允許授權(quán)范圍
                .scopes(ALL)
                //token 時(shí)間秒
                .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
                //刷新token 時(shí)間 秒
                .refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS)
                //允許授權(quán)類(lèi)型
                .authorizedGrantTypes(GRANT_TYPE_PASSWORD , AUTHORIZATION_CODE , REFRESH_TOKEN , IMPLICIT);*/
        // 數(shù)據(jù)庫(kù)保存配置信息到oauth_client_details表,schema參考sql/oauth_client_details
        clients.jdbc(dataSource);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(jwtTokenStore()).authenticationManager(authenticationManager)
                .accessTokenConverter(accessTokenConverter())
                //必須注入userDetailsService否則根據(jù)refresh_token無(wú)法加載用戶信息
                .userDetailsService(userDetailsService)
                //支持獲取token方式
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST,HttpMethod.PUT,HttpMethod.DELETE,HttpMethod.OPTIONS)
                //刷新token
                .reuseRefreshTokens(true);
                //endpoints.tokenServices(createDefaultTokenServices());
        // 使用內(nèi)存保存生成的token
        //endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
    }

    /**
     * 認(rèn)證服務(wù)器的安全配置
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                //.realm(RESOURCE_ID)
                // 開(kāi)啟/oauth/token_key驗(yàn)證端口認(rèn)證權(quán)限訪問(wèn)
                .tokenKeyAccess("isAuthenticated()")
                //  開(kāi)啟/oauth/check_token驗(yàn)證端口認(rèn)證權(quán)限訪問(wèn)
                .checkTokenAccess("isAuthenticated()")
                //允許表單認(rèn)證 在授權(quán)碼模式下會(huì)導(dǎo)致無(wú)法根據(jù)code獲取token 
                .allowFormAuthenticationForClients();
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter(){
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                String grantType = authentication.getOAuth2Request().getGrantType();
                //授權(quán)碼和密碼模式才自定義token信息
                if(AUTHORIZATION_CODE.equals(grantType) || GRANT_TYPE_PASSWORD.equals(grantType)) {
                    String userName = authentication.getUserAuthentication().getName();
                    // 自定義一些token 信息
                    Map<String, Object> additionalInformation = new HashMap<String, Object>(16);
                    additionalInformation.put("user_name", userName);
                    additionalInformation = Collections.unmodifiableMap(additionalInformation);
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                }
                OAuth2AccessToken token = super.enhance(accessToken, authentication);
                return token;
            }
        };
        // 設(shè)置簽署key
        converter.setSigningKey("bcrypt");
        return converter;
    }

    @Bean
    public TokenStore jwtTokenStore() {
        //基于jwt實(shí)現(xiàn)令牌(Access Token)保存
        return new JwtTokenStore(accessTokenConverter());
    }

//    @Bean
//    public TokenStore memoryTokenStore() {
//        // 最基本的InMemoryTokenStore生成token
//        return new InMemoryTokenStore();
//    }


}

3.2.4 SpringSecurity配置

package org.muses.jeeplatform.oauth.configuration;


import org.muses.jeeplatform.oauth.component.CustomPasswordEncoder;
import org.muses.jeeplatform.oauth.filter.SimpleCORSFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;


import javax.annotation.Resource;

/**
 * <pre>
 *
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2020/04/30 15:58  修改內(nèi)容:
 * </pre>
 */
@Configuration
@EnableWebSecurity
@Order(1)
//@EnableGlobalMethodSecurity(prePostEnabled = true)
//@EnableAutoConfiguration(exclude = {
//        org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class })
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private SimpleCORSFilter simpleCORSFilter;

    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

    private static final String SECRET_CHAR_SEQUENCE = "secret";

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


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {    //auth.inMemoryAuthentication()
//        auth.inMemoryAuthentication()
//                .withUser("nicky")
//                .password("{noop}123")
//                .roles("admin");
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(new CustomPasswordEncoder());
        auth.parentAuthenticationManager(authenticationManagerBean());

    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解決靜態(tài)資源被攔截的問(wèn)題
        web.ignoring().antMatchers("/asserts/**");
        //web.ignoring().antMatchers("/favicon.ico");

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http   // 配置登錄頁(yè)并允許訪問(wèn)
                .formLogin().loginPage("/login").permitAll()
                // 配置Basic登錄
                //.and().httpBasic()
                // 配置登出頁(yè)面
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                .and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**").permitAll()
                // 其余所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
                .anyRequest().authenticated()
                // 關(guān)閉跨域保護(hù);
                .and().csrf().disable();
        //http.addFilterBefore(simpleCORSFilter, SecurityContextPersistenceFilter.class);
    }

}

UserDetailsServiceImpl類(lèi):

package org.muses.jeeplatform.oauth.service;

import lombok.extern.slf4j.Slf4j;
import org.muses.jeeplatform.oauth.entity.User;
import org.muses.jeeplatform.oauth.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

/**
 * <pre>
 *
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2020/04/30 15:15  修改內(nèi)容:
 * </pre>
 */
@Slf4j
@Service("userService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserMapper userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if(user == null){
            log.info("登錄用戶[{}]沒(méi)注冊(cè)!",username);
            throw new UsernameNotFoundException("登錄用戶["+username + "]沒(méi)注冊(cè)!");
        }
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority());
    }

    private List getAuthority() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
//        return Arrays.asList(Collections.emptyList());
    }
}

自定義加密,這里可以用MD5或者加鹽方式,我寫(xiě)了個(gè)自定義加密類(lèi),為了方便測(cè)試,不做加密:

package org.muses.jeeplatform.oauth.component;

import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * <pre>
 *   自定義PasswordEncoder
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2020/04/24 17:02  修改內(nèi)容:
 * </pre>
 */
public class CustomPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        String encodeStr = charSequence.toString() + "";
        if (encodeStr.equals(s)) {
            return true;
        }
        return false;
    }
}

3.2.5 自定義登錄頁(yè)面

注意一定要定位GET的接口,因?yàn)镺Auth2.0的認(rèn)證接口也是/login,不過(guò)為POST方式

 @GetMapping(value = {"/login"})
    public ModelAndView toLogin(){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("login");
        return modelAndView;
    }

登錄頁(yè)面,使用Themeleaf模板引擎,關(guān)鍵點(diǎn),th:action="@{/login}" method="post",注意要用POST方式

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        <meta name="description" content="" />
        <meta name="author" content="" />
        <title>Signin Template for Bootstrap</title>
        <!-- Bootstrap core CSS -->
        <link href="../static/asserts/css/bootstrap.min.css" th:href="@{asserts/css/bootstrap.min.css}" rel="stylesheet" />
        <!-- Custom styles for this template -->
        <link href="../static/asserts/css/signin.css" th:href="@{asserts/css/signin.css}" rel="stylesheet"/>
    </head>

    <body class="text-center">
        <form class="form-signin" th:action="@{/login}" method="post">
            <img class="mb-4" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72" />
            <h1 class="h3 mb-3 font-weight-normal" th:text="#{messages.tip}">Oauth2.0 Login</h1>
            <label class="sr-only" th:text="#{messages.username}">Username</label>
            <input type="text" class="form-control" name="username" th:placeholder="#{messages.username}" required="" autofocus="" value="nicky" />
            <label class="sr-only" th:text="#{messages.password} ">Password</label>
            <input type="password" class="form-control" name="password" th:placeholder="#{messages.password}" required="" value="123" />
            <div class="checkbox mb-3">
                <label>
          <input type="checkbox" value="remember-me"  /> remember me
        </label>
            </div>
            <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{messages.loginBtnName}">Sign in</button>
            <p class="mt-5 mb-3 text-muted">? 2019</p>
            <a class="btn btn-sm" th:href="@{/login(lang='zh_CN')} ">中文</a>
            <a class="btn btn-sm" th:href="@{/login(lang='en_US')} ">English</a>
        </form>

    </body>

</html>

3.2.6 測(cè)試服務(wù)端

測(cè)試:授權(quán)接口

http://localhost:8888/oauth/authorize?client_id=cms&client_secret=secret&response_type=code

在這里插入圖片描述

賬號(hào)密碼正確的,會(huì)返回重定向uri并帶上授權(quán)碼:
http://localhost:8084/cms/login?code=UyRvO9

獲取token接口:


在這里插入圖片描述

拿授權(quán)碼去調(diào)token接口,獲取token成功


在這里插入圖片描述

使用JWT方式,token格式如圖:
JWT:
{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1MzQ5NzMsInVzZXJfbmFtZSI6Im5pY2t5IiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJmMjM0M2Q0NC1hODViLTQyOGYtOWE1ZS1iNTE4NTAwNTM5ODgiLCJjbGllbnRfaWQiOiJvYSIsInNjb3BlIjpbImFsbCJdfQ.LWkN2gC2dBrGTn5uSPzfdW6yRj7jhlX87EE8scY02hI",
    "token_type": "bearer",
    "expires_in": 59,
    "scope": "all",
    "user_name": "nicky",
    "jti": "f2343d44-a85b-428f-9a5e-b51850053988"
}

3.3 OAuth2.0客戶端對(duì)接

3.3.1 新建SpringBoot項(xiàng)目

新建SpringBoot Initializer項(xiàng)目,可以命名為Oauth2-Client1


在這里插入圖片描述
在這里插入圖片描述

3.3.2 application配置

security.oauth2.client.client-secret=secret
security.oauth2.client.client-id=cms
security.oauth2.client.scope=all
security.oauth2.client.user-authorization-uri=http://localhost:8888/oauth/authorize
security.oauth2.client.access-token-uri=http://localhost:8888/oauth/token
security.oauth2.resource.user-info-uri=http://localhost:8888/user
security.oauth2.client.use-current-uri=false
security.oauth2.client.pre-established-redirect-uri=http://localhost:8084/cms/login
security.oauth2.resource.token-info-uri= http://localhost:8888/oauth/check_token
security.oauth2.resource.jwt.key-uri=http://localhost:8888/oauth/token_key
security.oauth2.authorization.check-token-access= http://localhost:8888/oauth/check_token

server.port=8084
server.servlet.context-path=/cms

# 預(yù)防cookie沖突,設(shè)置cookie name
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID

3.3.3 配置類(lèi)實(shí)現(xiàn)

關(guān)鍵注解是@EnableOAuth2Sso,單點(diǎn)登錄必須要加上

package org.muses.jeeplatform.cms.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

/**
 * <pre>
 *
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2020/05/07 16:08  修改內(nèi)容:
 * </pre>
 */
@Configuration
@EnableOAuth2Sso
//@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http.logout().logoutSuccessUrl("http://localhost:8888/logout")
                    .and()
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .csrf().disable();
       
    }
}

3.3.4 單點(diǎn)登錄測(cè)試

寫(xiě)個(gè)測(cè)試接口:

  @GetMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
        return authentication;
    }

http://localhost:8084/cms/getCurrentUser

正常情況會(huì)跳轉(zhuǎn)到登錄頁(yè)面,校驗(yàn)之后,會(huì)返回,要驗(yàn)證單點(diǎn),要新建一個(gè)Client,然后寫(xiě)入配置數(shù)據(jù):

insert into `oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) values('oa',NULL,'{noop}secret','all','authorization_code','http://localhost:8082/oa/login',NULL,'60','60',NULL,'true');

同理進(jìn)行測(cè)試,cms客戶端已經(jīng)登錄的情況,OA客戶端是不需要再登錄的

四、參考博客資料

本博客代碼例子下載:code download

http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
https://wiki.open.qq.com/wiki/website/OAuth2.0%E7%AE%80%E4%BB%8B
https://www.cnblogs.com/sky-chen/category/1418449.html
https://projects.spring.io/spring-security-oauth/docs/oauth2.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容