0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

為什么使用spring-authorization-server?

Android編程精選 ? 來源:CSDN ? 2023-01-09 15:27 ? 次閱讀

前言

為什么使用spring-authorization-server?

真實(shí)原因:原先是因?yàn)閭€(gè)人原因,需要研究新版鑒權(quán)服務(wù),看到了spring-authorization-server,使用過程中,想著能不能整合新版本cloud,因此此處先以springboot搭建spring-authorization-server,后續(xù)再替換為springcloud2021。

官方原因:原先使用Spring Security OAuth,而該項(xiàng)目已經(jīng)逐漸被淘汰,雖然網(wǎng)上還是有不少該方案,但秉著技術(shù)要隨時(shí)代更新,從而使用spring-authorization-server

Spring 團(tuán)隊(duì)正式宣布 Spring Security OAuth 停止維護(hù),該項(xiàng)目將不會再進(jìn)行任何的迭代

6539b948-8f31-11ed-bfe3-dac502259ad0.png

項(xiàng)目構(gòu)建

以springboot搭建spring-authorization-server(即認(rèn)證與資源服務(wù)器)

數(shù)據(jù)庫相關(guān)表結(jié)構(gòu)構(gòu)建

需要?jiǎng)?chuàng)建3張表,sql分別如下

CREATETABLE`oauth2_authorization`(
`id`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`registered_client_id`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`principal_name`varchar(200)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`authorization_grant_type`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`attributes`varchar(4000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`state`varchar(500)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`authorization_code_value`blobNULL,
`authorization_code_issued_at`timestamp(0)NULLDEFAULTNULL,
`authorization_code_expires_at`timestamp(0)NULLDEFAULTNULL,
`authorization_code_metadata`varchar(2000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`access_token_value`blobNULL,
`access_token_issued_at`timestamp(0)NULLDEFAULTNULL,
`access_token_expires_at`timestamp(0)NULLDEFAULTNULL,
`access_token_metadata`varchar(2000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`access_token_type`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`access_token_scopes`varchar(1000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`oidc_id_token_value`blobNULL,
`oidc_id_token_issued_at`timestamp(0)NULLDEFAULTNULL,
`oidc_id_token_expires_at`timestamp(0)NULLDEFAULTNULL,
`oidc_id_token_metadata`varchar(2000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`refresh_token_value`blobNULL,
`refresh_token_issued_at`timestamp(0)NULLDEFAULTNULL,
`refresh_token_expires_at`timestamp(0)NULLDEFAULTNULL,
`refresh_token_metadata`varchar(2000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
PRIMARYKEY(`id`)USINGBTREE
)ENGINE=InnoDBCHARACTERSET=utf8mb4COLLATE=utf8mb4_unicode_ciROW_FORMAT=Dynamic;


CREATETABLE`oauth2_authorization_consent`(
`registered_client_id`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`principal_name`varchar(200)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`authorities`varchar(1000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
PRIMARYKEY(`registered_client_id`,`principal_name`)USINGBTREE
)ENGINE=InnoDBCHARACTERSET=utf8mb4COLLATE=utf8mb4_unicode_ciROW_FORMAT=Dynamic;



CREATETABLE`oauth2_registered_client`(
`id`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`client_id`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`client_id_issued_at`timestamp(0)NOTNULLDEFAULTCURRENT_TIMESTAMP(0),
`client_secret`varchar(200)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`client_secret_expires_at`timestamp(0)NULLDEFAULTNULL,
`client_name`varchar(200)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`client_authentication_methods`varchar(1000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`authorization_grant_types`varchar(1000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`redirect_uris`varchar(1000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLDEFAULTNULL,
`scopes`varchar(1000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`client_settings`varchar(2000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
`token_settings`varchar(2000)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULL,
PRIMARYKEY(`id`)USINGBTREE
)ENGINE=InnoDBCHARACTERSET=utf8mb4COLLATE=utf8mb4_unicode_ciROW_FORMAT=Dynamic;
先進(jìn)行認(rèn)證服務(wù)器相關(guān)配置

pom.xml引入依賴

注意!??!spring boot版本需2.6.x以上,是為后面升級成cloud做準(zhǔn)備

<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
dependency>


<dependency>
<groupId>com.xxxx.iovgroupId>
<artifactId>iov-cloud-framework-webartifactId>
<version>2.0.0-SNAPSHOTversion>
<exclusions>

<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
exclusion>
exclusions>
dependency>

<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.6.6version>
dependency>


<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.0version>
dependency>


<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.39version>
dependency>


<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>


<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-oauth2-authorization-serverartifactId>
<version>0.2.3version>
dependency>


<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-casartifactId>
dependency>


<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>


<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.9version>
dependency>


<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.28version>
dependency>


<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>


<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>31.1-jreversion>
dependency>

創(chuàng)建自定義登錄頁面 login.html (可不要,使用自帶的登錄界面)

html>
<htmllang="en"
xmlns:th="https://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<metacharset="utf-8">
<metaname="author"content="test">
<metaname="viewport"content="width=device-width,initial-scale=1">
<metaname="description"content="ThisisaloginpagetemplatebasedonBootstrap5">
<title>LoginPagetitle>
<style>
.is-invalid{
color:red;
}

.invalid-feedback{
color:red;
}

.mb-3{
margin-bottom:3px;
}
style>
<scriptth:inline="javascript">
/**/
if(window!==top){
top.location.href=location.href;
}
script>
head>
<bodyclass="hold-transitionlogin-page">
<divclass="login-box">
<divclass="card">
<divclass="card-bodylogin-card-body">
<pclass="login-box-msg">Signintostartyoursessionp>
<divth:if="${param.error}"class="alertalert-error">
Invalidusernameandpassword.
div>
<divth:if="${param.logout}"class="alertalert-success">
Youhavebeenloggedout.
div>
<formth:action="@{/login}"method="post"id="loginForm">
<divclass="input-groupmb-3">
<inputtype="text"class="form-control"value="zxg"name="username"placeholder="Email"
autocomplete="off">
div>
<divclass="input-groupmb-3">
<inputtype="password"id="password"name="password"value="123"class="form-control"
maxlength="25"placeholder="Password"
autocomplete="off">
div>
<divclass="row">
<divclass="col-4">
<buttontype="submit"id="submitBtn">SignInbutton>
div>
div>
form>
<pclass="mb-1">
<ahref="javascript:void(0)">Iforgotmypassworda>
p>
<pclass="mb-0">
<ahref="javascript:void(0)"class="text-center">Registeranewmembershipa>
p>
div>
div>
div>

<scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js">script>
<scriptsrc="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.1.0/jsencrypt.min.js">script>
<scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/jquery.validate.min.js">script>
<scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/additional-methods.min.js">script>

<scriptth:inline="javascript">

$(function(){
varencrypt=newJSEncrypt();

$.validator.setDefaults({
submitHandler:function(form){
console.log("Formsuccessfulsubmitted!");
form.submit();
}
});

});
script>
body>
html>

創(chuàng)建自定義授權(quán)頁面 consent.html(可不要,可使用自帶的授權(quán)頁面)

html>
<htmllang="en">
<head>
<metacharset="utf-8">
<metaname="viewport"content="width=device-width,initial-scale=1,shrink-to-fit=no">
<linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z"crossorigin="anonymous">
<title>授權(quán)頁面title>
<style>
body{
background-color:aliceblue;
}
style>
<script>
functioncancelConsent(){
document.consent_form.reset();
document.consent_form.submit();
}
script>
head>
<body>
<divclass="container">
<divclass="py-5">
<h1class="text-centertext-primary">用戶授權(quán)確認(rèn)h1>
div>
<divclass="row">
<divclass="coltext-center">
<p>
應(yīng)用
<ahref="https://felord.cn"><spanclass="font-weight-boldtext-primary"th:text="${clientName}">span>a>
想要訪問您的賬號
<spanclass="font-weight-bold"th:text="${principalName}">span>
p>
div>
div>
<divclass="rowpb-3">
<divclass="coltext-center"><p>上述應(yīng)用程序請求以下權(quán)限<br/>請審閱以下選項(xiàng)并勾選您同意的權(quán)限p>div>
div>
<divclass="row">
<divclass="coltext-center">
<formname="consent_form"method="post"action="/oauth2/authorize">
<inputtype="hidden"name="client_id"th:value="${clientId}">
<inputtype="hidden"name="state"th:value="${state}">

<divth:each="scope:${scopes}"class="form-groupform-checkpy-1">
<inputclass="form-check-input"
type="checkbox"
name="scope"
th:value="${scope.scope}"
th:id="${scope.scope}">
<labelclass="form-check-labelfont-weight-bold"th:for="${scope.scope}"th:text="${scope.scope}">label>
<pclass="text-primary"th:text="${scope.description}">p>
div>

<pth:if="${not#lists.isEmpty(previouslyApprovedScopes)}">您已對上述應(yīng)用授予以下權(quán)限:p>
<divth:each="scope:${previouslyApprovedScopes}"class="form-groupform-checkpy-1">
<inputclass="form-check-input"
type="checkbox"
th:id="${scope.scope}"
disabled
checked>
<labelclass="form-check-labelfont-weight-bold"th:for="${scope.scope}"th:text="${scope.scope}">label>
<pclass="text-primary"th:text="${scope.description}">p>
div>

<divclass="form-grouppt-3">
<buttonclass="btnbtn-primarybtn-lg"type="submit"id="submit-consent">
同意授權(quán)
button>
div>
<divclass="form-group">
<buttonclass="btnbtn-linkregular"type="button"id="cancel-consent"onclick="cancelConsent();">
取消授權(quán)
button>
div>
form>
div>
div>
<divclass="rowpt-4">
<divclass="coltext-center">
<p>
<small>
需要您同意并提供訪問權(quán)限。
<br/>如果您不同意,請單擊<spanclass="font-weight-boldtext-primary">取消授權(quán)span>,將不會為上述應(yīng)用程序提供任何您的信息。
small>
p>
div>
div>
div>
body>
html>

修改配置文件 application.yml(配置內(nèi)容可自行簡略)

server:
port:9000

spring:
application:
name:authorization-server
thymeleaf:
cache:false
datasource:
url:jdbc//192.168.1.69:3306/test
username:root
password:root
driver-class-name:com.mysql.cj.jdbc.Driver
security:
oauth2:
resourceserver:
jwt:
issuer-uri:http://127.0.0.1:9000#認(rèn)證中心端點(diǎn),作為資源端的配置

application:
security:
excludeUrls:#excludeUrls中存放白名單地址
-"/favicon.ico"

#mybatisplus配置
mybatis-plus:
mapper-locations:classpath:/mapper/*Mapper.xml
global-config:
#關(guān)閉MP3.0自帶的banner
banner:false
db-config:
#主鍵類型0:"數(shù)據(jù)庫ID自增",1:"不操作",2:"用戶輸入ID",3:"數(shù)字型snowflake",4:"全局唯一IDUUID",5:"字符串型snowflake";
id-type:AUTO
#字段策略
insert-strategy:not_null
update-strategy:not_null
select-strategy:not_null
#駝峰下劃線w轉(zhuǎn)換
table-underline:true
#邏輯刪除配置
#邏輯刪除全局值(1表示已刪除,這也是MybatisPlus的默認(rèn)配置)
logic-delete-value:1
#邏輯未刪除全局值(0表示未刪除,這也是MybatisPlus的默認(rèn)配置)
logic-not-delete-value:0
configuration:
#駝峰
map-underscore-to-camel-case:true
#打開二級緩存
cache-enabled:true
#log-impl:org.apache.ibatis.logging.stdout.StdOutImpl#開啟sql日志

新增認(rèn)證服務(wù)器配置文件 AuthorizationServerConfig

@Configuration(proxyBeanMethods=false)
publicclassAuthorizationServerConfig{
/**
*自定義授權(quán)頁面
*使用系統(tǒng)自帶的即不用
*/
privatestaticfinalStringCUSTOM_CONSENT_PAGE_URI="/oauth2/consent";

/**
*自定義UserDetailsService
*/
@Autowired
privateUserServiceuserService;


/**
*
*使用默認(rèn)配置進(jìn)行form表單登錄
*OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
publicSecurityFilterChainauthorizationServerSecurityFilterChain(HttpSecurityhttp)throwsException{
OAuth2AuthorizationServerConfigurerauthorizationServerConfigurer=newOAuth2AuthorizationServerConfigurer<>();

authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint->authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));

RequestMatcherendpointsMatcher=authorizationServerConfigurer.getEndpointsMatcher();

http
.requestMatcher(endpointsMatcher)
.userDetailsService(userService)
.authorizeRequests(authorizeRequests->authorizeRequests.anyRequest().authenticated())
.csrf(csrf->csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
returnhttp.formLogin(Customizer.withDefaults()).build();
}

/**
*注冊客戶端應(yīng)用
*/
@Bean
publicRegisteredClientRepositoryregisteredClientRepository(JdbcTemplatejdbcTemplate){
//Saveregisteredclientindbasifin-jdbc
RegisteredClientregisteredClient=RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("zxg")
.clientSecret("123")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//回調(diào)地址
.redirectUri("http://www.baidu.com")
//scope自定義的客戶端范圍
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")
//client請求訪問時(shí)需要授權(quán)同意
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
//token配置項(xiàng)信息
.tokenSettings(TokenSettings.builder()
//token有效期100分鐘
.accessTokenTimeToLive(Duration.ofMinutes(100L))
//使用默認(rèn)JWT相關(guān)格式
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
//開啟刷新token
.reuseRefreshTokens(true)
//refreshToken有效期120分鐘
.refreshTokenTimeToLive(Duration.ofMinutes(120L))
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build()
)
.build();

//Saveregisteredclientindbasifin-memory
JdbcRegisteredClientRepositoryregisteredClientRepository=newJdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
returnregisteredClientRepository;
}

/**
*授權(quán)服務(wù):管理OAuth2授權(quán)信息服務(wù)
*/
@Bean
publicOAuth2AuthorizationServiceauthorizationService(JdbcTemplatejdbcTemplate,RegisteredClientRepositoryregisteredClientRepository){
returnnewJdbcOAuth2AuthorizationService(jdbcTemplate,registeredClientRepository);
}

/**
*授權(quán)確認(rèn)信息處理服務(wù)
*/
@Bean
publicOAuth2AuthorizationConsentServiceauthorizationConsentService(JdbcTemplatejdbcTemplate,RegisteredClientRepositoryregisteredClientRepository){
returnnewJdbcOAuth2AuthorizationConsentService(jdbcTemplate,registeredClientRepository);
}

/**
*加載JWK資源
*JWT:指的是JSONWebToken,不存在簽名的JWT是不安全的,存在簽名的JWT是不可竄改的
*JWS:指的是簽過名的JWT,即擁有簽名的JWT
*JWK:既然涉及到簽名,就涉及到簽名算法,對稱加密還是非對稱加密,那么就需要加密的密鑰或者公私鑰對。此處我們將JWT的密鑰或者公私鑰對統(tǒng)一稱為JSONWEBKEY,即JWK。
*/
@Bean
publicJWKSourcejwkSource(){
RSAKeyrsaKey=JwksUtils.generateRsa();
JWKSetjwkSet=newJWKSet(rsaKey);
return(jwkSelector,securityContext)->jwkSelector.select(jwkSet);
}

/**
*配置OAuth2.0提供者元信息
*/
@Bean
publicProviderSettingsproviderSettings(){
returnProviderSettings.builder().issuer("http://127.0.0.1:9000").build();
}

}

新增Security的配置文件WebSecurityConfig

@Configuration
@EnableWebSecurity(debug=true)//開啟Security
publicclassWebSecurityConfig{
@Autowired
privateApplicationPropertiesproperties;

/**
*設(shè)置加密方式
*/
@Bean
publicPasswordEncoderpasswordEncoder(){
////將密碼加密方式采用委托方式,默認(rèn)以BCryptPasswordEncoder方式進(jìn)行加密,兼容ldap,MD4,MD5等方式
//returnPasswordEncoderFactories.createDelegatingPasswordEncoder();

//此處我們使用明文方式不建議這樣
returnNoOpPasswordEncoder.getInstance();
}

/**
*使用WebSecurity.ignoring()忽略某些URL請求,這些請求將被SpringSecurity忽略
*/
@Bean
WebSecurityCustomizerwebSecurityCustomizer(){
returnnewWebSecurityCustomizer(){
@Override
publicvoidcustomize(WebSecurityweb){
//讀取配置文件application.security.excludeUrls下的鏈接進(jìn)行忽略
web.ignoring().antMatchers(properties.getSecurity().getExcludeUrls().toArray(newString[]{}));
}
};
}

/**
*針對http請求,進(jìn)行攔截過濾
*
*CookieCsrfTokenRepository進(jìn)行CSRF保護(hù)的工作方式:
*1.客戶端向服務(wù)器發(fā)出GET請求,例如請求主頁
*2.Spring發(fā)送GET請求的響應(yīng)以及Set-cookie標(biāo)頭,其中包含安全生成的XSRF令牌
*/
@Bean
publicSecurityFilterChainhttpSecurityFilterChain(HttpSecurityhttpSecurity)throwsException{
httpSecurity
.authorizeRequests(authorizeRequests->
authorizeRequests.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)

//使用默認(rèn)登錄頁面
//.formLogin(withDefaults())

//設(shè)置form登錄,設(shè)置且放開登錄頁login
.formLogin(fromlogin->fromlogin.loginPage("/login").permitAll())

//SpringSecurityCSRF保護(hù)
.csrf(csrfToken->csrfToken.csrfTokenRepository(newCookieCsrfTokenRepository()))

////開啟認(rèn)證服務(wù)器的資源服務(wù)器相關(guān)功能,即需校驗(yàn)token
//.oauth2ResourceServer()
//.accessDeniedHandler(newSimpleAccessDeniedHandler())
//.authenticationEntryPoint(newSimpleAuthenticationEntryPoint())
//.jwt()
;
returnhttpSecurity.build();
}

}

新增讀取application配置的類 ApplicationProperties

/**
*此步主要是獲取配置文件中配置的白名單,可自行舍去或自定義實(shí)現(xiàn)其他方式
**/
@Data
@Component
@ConfigurationProperties("application")
publicclassApplicationProperties{
privatefinalSecuritysecurity=newSecurity();

@Data
publicstaticclassSecurity{
privateOauth2oauth2;
privateListexcludeUrls=newArrayList<>();

@Data
publicstaticclassOauth2{
privateStringissuerUrl;

}
}
}

新增 JwksUtils 類和 KeyGeneratorUtils,這兩個(gè)類作為JWT對稱加密

publicfinalclassJwksUtils{

privateJwksUtils(){
}

/**
*生成RSA加密key(即JWK)
*/
publicstaticRSAKeygenerateRsa(){
//生成RSA加密的key
KeyPairkeyPair=KeyGeneratorUtils.generateRsaKey();
//公鑰
RSAPublicKeypublicKey=(RSAPublicKey)keyPair.getPublic();
//私鑰
RSAPrivateKeyprivateKey=(RSAPrivateKey)keyPair.getPrivate();
//構(gòu)建RSA加密key
returnnewRSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}

/**
*生成EC加密key(即JWK)
*/
publicstaticECKeygenerateEc(){
//生成EC加密的key
KeyPairkeyPair=KeyGeneratorUtils.generateEcKey();
//公鑰
ECPublicKeypublicKey=(ECPublicKey)keyPair.getPublic();
//私鑰
ECPrivateKeyprivateKey=(ECPrivateKey)keyPair.getPrivate();
//根據(jù)公鑰參數(shù)生成曲線
Curvecurve=Curve.forECParameterSpec(publicKey.getParams());
//構(gòu)建EC加密key
returnnewECKey.Builder(curve,publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}

/**
*生成HmacSha256密鑰
*/
publicstaticOctetSequenceKeygenerateSecret(){
SecretKeysecretKey=KeyGeneratorUtils.generateSecretKey();
returnnewOctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
}
}


classKeyGeneratorUtils{

privateKeyGeneratorUtils(){
}

/**
*生成RSA密鑰
*/
staticKeyPairgenerateRsaKey(){
KeyPairkeyPair;
try{
KeyPairGeneratorkeyPairGenerator=KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair=keyPairGenerator.generateKeyPair();
}catch(Exceptionex){
thrownewIllegalStateException(ex);
}
returnkeyPair;
}

/**
*生成EC密鑰
*/
staticKeyPairgenerateEcKey(){
EllipticCurveellipticCurve=newEllipticCurve(
newECFieldFp(
newBigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
newBigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
newBigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
ECPointecPoint=newECPoint(
newBigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
newBigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
ECParameterSpececParameterSpec=newECParameterSpec(
ellipticCurve,
ecPoint,
newBigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
1);

KeyPairkeyPair;
try{
KeyPairGeneratorkeyPairGenerator=KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(ecParameterSpec);
keyPair=keyPairGenerator.generateKeyPair();
}catch(Exceptionex){
thrownewIllegalStateException(ex);
}
returnkeyPair;
}

/**
*生成HmacSha256密鑰
*/
staticSecretKeygenerateSecretKey(){
SecretKeyhmacKey;
try{
hmacKey=KeyGenerator.getInstance("HmacSha256").generateKey();
}catch(Exceptionex){
thrownewIllegalStateException(ex);
}
returnhmacKey;
}
}

新建 ConsentController,編寫登錄和認(rèn)證頁面的跳轉(zhuǎn)

如果在上面沒有使用自定義的登錄和授權(quán)頁面,下面的跳轉(zhuǎn)方法按需舍去

@Slf4j
@Controller
publicclassConsentController{

privatefinalRegisteredClientRepositoryregisteredClientRepository;
privatefinalOAuth2AuthorizationConsentServiceauthorizationConsentService;

publicConsentController(RegisteredClientRepositoryregisteredClientRepository,
OAuth2AuthorizationConsentServiceauthorizationConsentService){
this.registeredClientRepository=registeredClientRepository;
this.authorizationConsentService=authorizationConsentService;
}

@ResponseBody
@GetMapping("/favicon.ico")
publicStringfaviconico(){
return"favicon.ico";
}

@GetMapping("/login")
publicStringloginPage(){
return"login";
}

@GetMapping(value="/oauth2/consent")
publicStringconsent(Principalprincipal,Modelmodel,
@RequestParam(OAuth2ParameterNames.CLIENT_ID)StringclientId,
@RequestParam(OAuth2ParameterNames.SCOPE)Stringscope,
@RequestParam(OAuth2ParameterNames.STATE)Stringstate){

//Removescopesthatwerealreadyapproved
SetscopesToApprove=newHashSet<>();
SetpreviouslyApprovedScopes=newHashSet<>();
RegisteredClientregisteredClient=this.registeredClientRepository.findByClientId(clientId);
OAuth2AuthorizationConsentcurrentAuthorizationConsent=
this.authorizationConsentService.findById(registeredClient.getId(),principal.getName());
SetauthorizedScopes;
if(currentAuthorizationConsent!=null){
authorizedScopes=currentAuthorizationConsent.getScopes();
}else{
authorizedScopes=Collections.emptySet();
}
for(StringrequestedScope:StringUtils.delimitedListToStringArray(scope,"")){
if(authorizedScopes.contains(requestedScope)){
previouslyApprovedScopes.add(requestedScope);
}else{
scopesToApprove.add(requestedScope);
}
}

model.addAttribute("clientId",clientId);
model.addAttribute("state",state);
model.addAttribute("scopes",withDescription(scopesToApprove));
model.addAttribute("previouslyApprovedScopes",withDescription(previouslyApprovedScopes));
model.addAttribute("principalName",principal.getName());

return"consent";
}

privatestaticSetwithDescription(Setscopes){
SetscopeWithDescriptions=newHashSet<>();
for(Stringscope:scopes){
scopeWithDescriptions.add(newScopeWithDescription(scope));

}
returnscopeWithDescriptions;
}

publicstaticclassScopeWithDescription{
privatestaticfinalStringDEFAULT_DESCRIPTION="UNKNOWNSCOPE-Wecannotprovideinformationaboutthispermission,usecautionwhengrantingthis.";
privatestaticfinalMapscopeDescriptions=newHashMap<>();
static{
scopeDescriptions.put(
"message.read",
"Thisapplicationwillbeabletoreadyourmessage."
);
scopeDescriptions.put(
"message.write",
"Thisapplicationwillbeabletoaddnewmessages.Itwillalsobeabletoeditanddeleteexistingmessages."
);
scopeDescriptions.put(
"other.scope",
"Thisisanotherscopeexampleofascopedescription."
);
}

publicfinalStringscope;
publicfinalStringdescription;

ScopeWithDescription(Stringscope){
this.scope=scope;
this.description=scopeDescriptions.getOrDefault(scope,DEFAULT_DESCRIPTION);
}
}

}

新建 UserController,User,UserService等標(biāo)準(zhǔn)的自定義用戶業(yè)務(wù),此處僅放出UserServiceImpl

@RequiredArgsConstructor
@Slf4j
@Component
classUserServiceImplimplementsUserService{
privatefinalUserMapperuserMapper;

@Override
publicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{
Useruser=userMapper.selectOne(newLambdaQueryWrapper().eq(User::getUsername,username));
returnneworg.springframework.security.core.userdetails.User(username,user.getPassword(),newArrayList<>());
}
}

啟動(dòng)項(xiàng)目,如下圖

65494228-8f31-11ed-bfe3-dac502259ad0.png

認(rèn)證服務(wù)器整體結(jié)構(gòu)圖

6574f22e-8f31-11ed-bfe3-dac502259ad0.png
資源服務(wù)器相關(guān)配置

pom.xml引入資源服務(wù)器相關(guān)依賴


<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-resource-serverartifactId>
dependency>


<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>

新增配置文件 application.yaml

server:
port:9003
spring:
application:
name:resource
security:
oauth2:
resourceserver:
jwt:
issuer-uri:http://127.0.0.1:9000
feign:
client:
config:
default:#配置超時(shí)時(shí)間
connect-timeout:10000
read-timeout:10000

新增資源服務(wù)器配置文件 ResourceServerConfiguration

@Configuration
@EnableWebSecurity(debug=true)
@EnableGlobalMethodSecurity(prePostEnabled=true)//開啟鑒權(quán)服務(wù)
publicclassResourceServerConfiguration{

@Bean
publicSecurityFilterChainhttpSecurityFilterChain(HttpSecurityhttpSecurity)throwsException{
//所有請求都進(jìn)行攔截
httpSecurity.authorizeRequests().anyRequest().authenticated();
//關(guān)閉session
httpSecurity.sessionManagement().disable();
//配置資源服務(wù)器的無權(quán)限,無認(rèn)證攔截器等以及JWT驗(yàn)證
httpSecurity.oauth2ResourceServer()
.accessDeniedHandler(newSimpleAccessDeniedHandler())
.authenticationEntryPoint(newSimpleAuthenticationEntryPoint())
.jwt();
returnhttpSecurity.build();
}

}

新增相關(guān)無認(rèn)證無權(quán)限統(tǒng)一攔截回復(fù) SimpleAccessDeniedHandlerSimpleAuthenticationEntryPoint

/**
*攜帶了token而且token合法但是權(quán)限不足以訪問其請求的資源403
*@authorzxg
*/
publicclassSimpleAccessDeniedHandlerimplementsAccessDeniedHandler{

@Override
publicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptionaccessDeniedException)throwsIOException,ServletException{
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ObjectMapperobjectMapper=newObjectMapper();
StringresBody=objectMapper.writeValueAsString(SingleResultBundle.failed("無權(quán)訪問"));
PrintWriterprintWriter=response.getWriter();
printWriter.print(resBody);
printWriter.flush();
printWriter.close();
}
}


/**
*在資源服務(wù)器中不攜帶token或者token無效401
*@authorzxg
*/
@Slf4j
publicclassSimpleAuthenticationEntryPointimplementsAuthenticationEntryPoint{
@Override
publicvoidcommence(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionauthException)throwsIOException,ServletException{
if(response.isCommitted()){
return;
}

Throwablethrowable=authException.fillInStackTrace();

StringerrorMessage="認(rèn)證失敗";

if(throwableinstanceofBadCredentialsException){
errorMessage="錯(cuò)誤的客戶端信息";
}else{
Throwablecause=authException.getCause();

if(causeinstanceofJwtValidationException){
log.warn("JWTToken過期,具體內(nèi)容:"+cause.getMessage());
errorMessage="無效的token信息";
}elseif(causeinstanceofBadJwtException){
log.warn("JWT簽名異常,具體內(nèi)容:"+cause.getMessage());
errorMessage="無效的token信息";
}elseif(causeinstanceofAccountExpiredException){
errorMessage="賬戶已過期";
}elseif(causeinstanceofLockedException){
errorMessage="賬戶已被鎖定";
//}elseif(causeinstanceofInvalidClientException||causeinstanceofBadClientCredentialsException){
//response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"無效的客戶端")));
//}elseif(causeinstanceofInvalidGrantException||causeinstanceofRedirectMismatchException){
//response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("無效的類型")));
//}elseif(causeinstanceofUnauthorizedClientException){
//response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未經(jīng)授權(quán)的客戶端")));
}elseif(throwableinstanceofInsufficientAuthenticationException){
Stringmessage=throwable.getMessage();
if(message.contains("Invalidtokendoesnotcontainresourceid")){
errorMessage="未經(jīng)授權(quán)的資源服務(wù)器";
}elseif(message.contains("Fullauthenticationisrequiredtoaccessthisresource")){
errorMessage="缺少驗(yàn)證信息";
}
}else{
errorMessage="驗(yàn)證異常";
}
}

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ObjectMapperobjectMapper=newObjectMapper();
StringresBody=objectMapper.writeValueAsString(SingleResultBundle.failed(errorMessage));
PrintWriterprintWriter=response.getWriter();
printWriter.print(resBody);
printWriter.flush();
printWriter.close();
}
}

新增 ResourceController 進(jìn)行接口測試

@Slf4j
@RestController
publicclassResourceController{

/**
*測試SpringAuthorizationServer,測試權(quán)限
*/
@PreAuthorize("hasAuthority('SCOPE_message.read')")
@GetMapping("/getTest")
publicStringgetTest(){
return"getTest";
}

/**
*默認(rèn)登錄成功跳轉(zhuǎn)頁為/防止404狀態(tài)
*
*@returnthemap
*/
@GetMapping("/")
publicMapindex(){
returnCollections.singletonMap("msg","loginsuccess!");
}

@GetMapping("/getResourceTest")
publicSingleResultBundlegetResourceTest(){
returnSingleResultBundle.success("這是resource的測試方法getResourceTest()");
}
}

啟動(dòng)項(xiàng)目,效果如下

658e2a32-8f31-11ed-bfe3-dac502259ad0.png

項(xiàng)目總體結(jié)構(gòu)如下

65a090fa-8f31-11ed-bfe3-dac502259ad0.png
測試認(rèn)證鑒權(quán)
#調(diào)用/oauth2/authorize,獲取code
http://127.0.0.1:9000/oauth2/authorize?client_id=zxg&response_type=code&scope=message.read&redirect_uri=http://www.baidu.com
#會判斷是否登錄,若沒有,則跳轉(zhuǎn)到登錄頁面,如下圖1
#登錄完成后,會提示是否授權(quán),若沒有,則跳轉(zhuǎn)到授權(quán)界面,如下圖2
#授權(quán)成功后,跳轉(zhuǎn)到回調(diào)地址,并帶上code,如圖3
65db3ebc-8f31-11ed-bfe3-dac502259ad0.png65f825d6-8f31-11ed-bfe3-dac502259ad0.png

打開postman,進(jìn)行獲取access_token

#訪問/oauth2/token地址
#在Authorization中選擇BasicAuth模式,填入對應(yīng)客戶端,其會在header中生成Authorization,如下圖右側(cè)
6618d678-8f31-11ed-bfe3-dac502259ad0.png

返回結(jié)果如下

6644dade-8f31-11ed-bfe3-dac502259ad0.png

調(diào)用ResourceController中的接口,測試token是否生效

667bd0ca-8f31-11ed-bfe3-dac502259ad0.png

源碼下載地址

  • https://gitee.com/rjj521/authorization-server-learn

總結(jié)

至此,spring-authorization-server的基礎(chǔ)使用已完成,總體上和原Spring Security OAuth大差不差,個(gè)別配置項(xiàng)不同。期間在網(wǎng)上搜尋了很多資料,然后進(jìn)行整合,因此文中存在與其他網(wǎng)上教程相同代碼,如有爭議,請聯(lián)系我刪除改正,謝謝。


	

審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 服務(wù)器
    +關(guān)注

    關(guān)注

    12

    文章

    8963

    瀏覽量

    85087
  • 數(shù)據(jù)庫
    +關(guān)注

    關(guān)注

    7

    文章

    3752

    瀏覽量

    64233
  • spring
    +關(guān)注

    關(guān)注

    0

    文章

    338

    瀏覽量

    14296

原文標(biāo)題:擁抱 Spring 全新 OAuth 解決方案:spring-authorization-server 該怎么玩?

文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    Spring事務(wù)實(shí)現(xiàn)原理

    作者:京東零售 范錫軍 1、引言 springspring-tx模塊提供了對事務(wù)管理支持,使用spring事務(wù)可以讓我們從復(fù)雜的事務(wù)處理中得到解脫,無需要去處理獲得連接、關(guān)閉連接、事務(wù)提交和回滾等
    的頭像 發(fā)表于 11-08 10:10 ?436次閱讀
    <b class='flag-5'>Spring</b>事務(wù)實(shí)現(xiàn)原理

    Nat server技術(shù)原理和配置過程

    Nat server:指定公有地址:端口和私有地址:端口形成一對一映射關(guān)系——映射表。這也是Nat server與其他nat的區(qū)別之一,Nat server可以指定端口進(jìn)行映射。
    的頭像 發(fā)表于 10-10 14:38 ?458次閱讀
    Nat <b class='flag-5'>server</b>技術(shù)原理和配置過程

    Spring Cloud Gateway網(wǎng)關(guān)框架

    Spring Cloud Gateway網(wǎng)關(guān)框架 本軟件微服務(wù)架構(gòu)中采用Spring Cloud Gateway網(wǎng)關(guān)控制框架,Spring Cloud Gateway是Spring C
    的頭像 發(fā)表于 08-22 09:58 ?413次閱讀
    <b class='flag-5'>Spring</b> Cloud Gateway網(wǎng)關(guān)框架

    玩轉(zhuǎn)Spring狀態(tài)機(jī)

    說起Spring狀態(tài)機(jī),大家很容易聯(lián)想到這個(gè)狀態(tài)機(jī)和設(shè)計(jì)模式中狀態(tài)模式的區(qū)別是啥呢?沒錯(cuò),Spring狀態(tài)機(jī)就是狀態(tài)模式的一種實(shí)現(xiàn),在介紹Spring狀態(tài)機(jī)之前,讓我們來看看設(shè)計(jì)模式中的狀態(tài)模式
    的頭像 發(fā)表于 06-25 14:21 ?859次閱讀
    玩轉(zhuǎn)<b class='flag-5'>Spring</b>狀態(tài)機(jī)

    Spring事務(wù)傳播性的相關(guān)知識

    本文主要介紹了Spring事務(wù)傳播性的相關(guān)知識。
    的頭像 發(fā)表于 01-10 09:29 ?388次閱讀
    <b class='flag-5'>Spring</b>事務(wù)傳播性的相關(guān)知識

    Spring狀態(tài)機(jī)的實(shí)現(xiàn)原理和使用方法

    說起 Spring 狀態(tài)機(jī),大家很容易聯(lián)想到這個(gè)狀態(tài)機(jī)和設(shè)計(jì)模式中狀態(tài)模式的區(qū)別是啥呢?沒錯(cuò),Spring 狀態(tài)機(jī)就是狀態(tài)模式的一種實(shí)現(xiàn),在介紹 Spring 狀態(tài)機(jī)之前,讓我們來看看設(shè)計(jì)模式中的狀態(tài)模式。
    的頭像 發(fā)表于 12-26 09:39 ?1834次閱讀
    <b class='flag-5'>Spring</b>狀態(tài)機(jī)的實(shí)現(xiàn)原理和使用方法

    Spring事務(wù)失效的十種常見場景

    Spring針對Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API(JPA)等事務(wù) API,實(shí)現(xiàn)了一致的編程模型,而
    的頭像 發(fā)表于 12-11 15:03 ?836次閱讀

    【飛騰派4G版免費(fèi)試用】Spring Boot和飛騰派融合構(gòu)建的農(nóng)業(yè)物聯(lián)網(wǎng)系統(tǒng)-環(huán)境搭建篇

    ntpdate-u 120.25.108.11) 7.安裝MySQL服務(wù)器(sudo apt install mysql -server) 遇到問題: 1測試中板子TF卡處發(fā)熱較為嚴(yán)重。 下期更新: Spring Boot和飛騰派融合構(gòu)建的農(nóng)業(yè)物聯(lián)網(wǎng)系統(tǒng)-實(shí)現(xiàn)篇1
    發(fā)表于 12-11 15:00

    dubbo和spring cloud區(qū)別

    Dubbo和Spring Cloud是兩個(gè)非常流行的微服務(wù)框架,各有自己的特點(diǎn)和優(yōu)勢。在本文中,我們將詳細(xì)介紹Dubbo和Spring Cloud的區(qū)別。 1.架構(gòu)設(shè)計(jì): Dubbo是阿里巴巴開源
    的頭像 發(fā)表于 12-04 14:47 ?1550次閱讀

    Spring MVC的工作原理

    Spring MVC是一種基于Java的Web應(yīng)用程序框架,它采用了Model-View-Controller(MVC)設(shè)計(jì)模式來分離應(yīng)用程序的不同方面。Spring MVC的工作原理涉及多個(gè)關(guān)鍵
    的頭像 發(fā)表于 12-03 11:49 ?726次閱讀

    Spring MVC的工作原理是怎樣的

    Spring MVC是一種基于Java的Web框架,它充分利用了Java的優(yōu)點(diǎn),如面向?qū)ο缶幊?、模塊化、可重用性和可擴(kuò)展性。Spring MVC的工作原理可以總結(jié)為以下幾個(gè)步驟:請求的發(fā)送、請求
    的頭像 發(fā)表于 11-22 16:53 ?810次閱讀

    Spring依賴注入的方式

    Spring 是一個(gè)開源的輕量級框架,可以用于構(gòu)建企業(yè)級應(yīng)用程序。其最重要的特性之一是依賴注入(Dependency Injection,DI),這是一種設(shè)計(jì)模式,它可以幫助我們解耦代碼、提高
    的頭像 發(fā)表于 11-22 15:12 ?444次閱讀

    mysql和sql server區(qū)別

    MySQL和SQL Server是兩種常見的關(guān)系型數(shù)據(jù)庫管理系統(tǒng)(RDBMS),用于存儲和管理數(shù)據(jù)庫。雖然它們都支持SQL語言,但在其他方面存在一些顯著的區(qū)別。以下是MySQL和SQL Server
    的頭像 發(fā)表于 11-21 11:07 ?1452次閱讀

    spring分布式框架有哪些

    Spring分布式框架是一套基于Spring框架的解決方案,用于構(gòu)建分布式系統(tǒng)。它提供了一系列的組件和模塊,可以幫助開發(fā)人員輕松地構(gòu)建可擴(kuò)展、高可用、高性能的分布式應(yīng)用程序。下面將詳細(xì)介紹一些常用
    的頭像 發(fā)表于 11-16 10:58 ?736次閱讀

    Spring布能用來搭建基礎(chǔ)架構(gòu)嗎

    Spring Boot 是一個(gè)用于簡化 Spring 應(yīng)用程序開發(fā)的框架,它利用 Spring 框架的強(qiáng)大功能,使得基礎(chǔ)架構(gòu)的搭建變得更加簡單、輕量級、易于維護(hù)。在本文中,我們將詳細(xì)討論
    的頭像 發(fā)表于 11-16 10:56 ?362次閱讀