前言
為什么使用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)行任何的迭代
項(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)目,如下圖
認(rèn)證服務(wù)器整體結(jié)構(gòu)圖
資源服務(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ù) SimpleAccessDeniedHandler
和 SimpleAuthenticationEntryPoint
/**
*攜帶了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)目,效果如下
項(xiàng)目總體結(jié)構(gòu)如下
測試認(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
打開postman,進(jìn)行獲取access_token
#訪問/oauth2/token地址
#在Authorization中選擇BasicAuth模式,填入對應(yīng)客戶端,其會在header中生成Authorization,如下圖右側(cè)
返回結(jié)果如下
調(diào)用ResourceController
中的接口,測試token是否生效
源碼下載地址
- 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)系我刪除改正,謝謝。
審核編輯 :李倩
-
服務(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)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論