擁抱 Spring 全新 OAuth 解決方案:spring-authorization-server 該怎么玩?
前言
為什么使用spring-authorization-server?
真實原因:原先是因為個人原因,需要研究新版鑒權服務,看到了spring-authorization-server,使用過程中,想著能不能整合新版本cloud,因此此處先以springboot搭建spring-authorization-server,后續再替換為springcloud2021。
官方原因:原先使用Spring Security OAuth,而該項目已經逐漸被淘汰,雖然網上還是有不少該方案,但秉著技術要隨時代更新,從而使用spring-authorization-server
Spring 團隊正式宣布 Spring Security OAuth 停止維護,該項目將不會再進行任何的迭代

項目構建
以springboot搭建spring-authorization-server(即認證與資源服務器)
數據庫相關表結構構建
需要創建3張表,sql分別如下
CREATE TABLE `oauth2_authorization` ( `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `attributes` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `authorization_code_value` blob NULL, `authorization_code_issued_at` timestamp(0) NULL DEFAULT NULL, `authorization_code_expires_at` timestamp(0) NULL DEFAULT NULL, `authorization_code_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `access_token_value` blob NULL, `access_token_issued_at` timestamp(0) NULL DEFAULT NULL, `access_token_expires_at` timestamp(0) NULL DEFAULT NULL, `access_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `oidc_id_token_value` blob NULL, `oidc_id_token_issued_at` timestamp(0) NULL DEFAULT NULL, `oidc_id_token_expires_at` timestamp(0) NULL DEFAULT NULL, `oidc_id_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `refresh_token_value` blob NULL, `refresh_token_issued_at` timestamp(0) NULL DEFAULT NULL, `refresh_token_expires_at` timestamp(0) NULL DEFAULT NULL, `refresh_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; CREATE TABLE `oauth2_authorization_consent` ( `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; CREATE TABLE `oauth2_registered_client` ( `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_id_issued_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `client_secret_expires_at` timestamp(0) NULL DEFAULT NULL, `client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
先進行認證服務器相關配置
pom.xml引入依賴
注意!!!spring boot版本需2.6.x以上,是為后面升級成cloud做準備
<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>
創建自定義登錄頁面 login.html (可不要,使用自帶的登錄界面)
html>
<html lang="en"
xmlns:th="https://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="utf-8">
<meta name="author" content="test">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="This is a login page template based on Bootstrap 5">
<title>Login Pagetitle>
<style>
.is-invalid {
color: red;
}
.invalid-feedback {
color: red;
}
.mb-3 {
margin-bottom: 3px;
}
style>
<script th:inline="javascript">
/* // const baseURL = /*[[@{/}]]*/ ''; /*]]>*/
if (window !== top) {
top.location.href = location.href;
}
script>
head>
<body class="hold-transition login-page">
<div class="login-box">
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg">Sign in to start your sessionp>
<div th:if="${param.error}" class="alert alert-error">
Invalid username and password.
div>
<div th:if="${param.logout}" class="alert alert-success">
You have been logged out.
div>
<form th:action="@{/login}" method="post" id="loginForm">
<div class="input-group mb-3">
<input type="text" class="form-control" value="zxg" name="username" placeholder="Email"
autocomplete="off">
div>
<div class="input-group mb-3">
<input type="password" id="password" name="password" value="123" class="form-control"
maxlength="25" placeholder="Password"
autocomplete="off">
div>
<div class="row">
<div class="col-4">
<button type="submit" id="submitBtn">Sign Inbutton>
div>
div>
form>
<p class="mb-1">
<a href="javascript:void(0)">I forgot my passworda>
p>
<p class="mb-0">
<a href="javascript:void(0)" class="text-center">Register a new membershipa>
p>
div>
div>
div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js">script>
<script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.1.0/jsencrypt.min.js">script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/jquery.validate.min.js">script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/additional-methods.min.js">script>
<script th:inline="javascript">
$(function () {
var encrypt = new JSEncrypt();
$.validator.setDefaults({
submitHandler: function (form) {
console.log("Form successful submitted!");
form.submit();
}
});
});
script>
body>
html>
創建自定義授權頁面 consent.html(可不要,可使用自帶的授權頁面)
html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>授權頁面title>
<style>
body {
background-color: aliceblue;
}
style>
<script>
function cancelConsent() {
document.consent_form.reset();
document.consent_form.submit();
}
script>
head>
<body>
<div class="container">
<div class="py-5">
<h1 class="text-center text-primary">用戶授權確認h1>
div>
<div class="row">
<div class="col text-center">
<p>
應用
<a >span>a>
想要訪問您的賬號
<span class="font-weight-bold" th:text="${principalName}">span>
p>
div>
div>
<div class="row pb-3">
<div class="col text-center"><p>上述應用程序請求以下權限<br/>請審閱以下選項并勾選您同意的權限p>div>
div>
<div class="row">
<div class="col text-center">
<form name="consent_form" method="post" action="/oauth2/authorize">
<input type="hidden" name="client_id" th:value="${clientId}">
<input type="hidden" name="state" th:value="${state}">
<div th:each="scope: ${scopes}" class="form-group form-check py-1">
<input class="form-check-input"
type="checkbox"
name="scope"
th:value="${scope.scope}"
th:id="${scope.scope}">
<label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}">label>
<p class="text-primary" th:text="${scope.description}">p>
div>
<p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">您已對上述應用授予以下權限:p>
<div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1">
<input class="form-check-input"
type="checkbox"
th:id="${scope.scope}"
disabled
checked>
<label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}">label>
<p class="text-primary" th:text="${scope.description}">p>
div>
<div class="form-group pt-3">
<button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
同意授權
button>
div>
<div class="form-group">
<button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
取消授權
button>
div>
form>
div>
div>
<div class="row pt-4">
<div class="col text-center">
<p>
<small>
需要您同意并提供訪問權限。
<br/>如果您不同意,請單擊<span class="font-weight-bold text-primary">取消授權span>,將不會為上述應用程序提供任何您的信息。
small>
p>
div>
div>
div>
body>
html>
修改配置文件 application.yml(配置內容可自行簡略)
server: port: 9000 spring: application: name: authorization-server thymeleaf: cache: false datasource: url: jdbc:mysql://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 #認證中心端點,作為資源端的配置 application: security: excludeUrls: #excludeUrls中存放白名單地址 - "/favicon.ico" # mybatis plus配置 mybatis-plus: mapper-locations: classpath:/mapper/*Mapper.xml global-config: # 關閉MP3.0自帶的banner banner: false db-config: #主鍵類型 0:"數據庫ID自增", 1:"不操作", 2:"用戶輸入ID",3:"數字型snowflake", 4:"全局唯一ID UUID", 5:"字符串型snowflake"; id-type: AUTO #字段策略 insert-strategy: not_null update-strategy: not_null select-strategy: not_null #駝峰下劃線w轉換 table-underline: true # 邏輯刪除配置 # 邏輯刪除全局值(1表示已刪除,這也是Mybatis Plus的默認配置) logic-delete-value: 1 # 邏輯未刪除全局值(0表示未刪除,這也是Mybatis Plus的默認配置) logic-not-delete-value: 0 configuration: #駝峰 map-underscore-to-camel-case: true #打開二級緩存 cache-enabled: true # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #開啟sql日志
新增認證服務器配置文件 AuthorizationServerConfig
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
/**
* 自定義授權頁面
* 使用系統自帶的即不用
*/
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
/**
* 自定義UserDetailsService
*/
@Autowired
private UserService userService;
/**
*
* 使用默認配置進行form表單登錄
* OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.userDetailsService(userService)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}
/**
* 注冊客戶端應用
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// Save registered client in db as if in-jdbc
RegisteredClient registeredClient = 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)
// 回調地址
.redirectUri("http://www.baidu.com")
// scope自定義的客戶端范圍
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")
// client請求訪問時需要授權同意
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// token配置項信息
.tokenSettings(TokenSettings.builder()
// token有效期100分鐘
.accessTokenTimeToLive(Duration.ofMinutes(100L))
// 使用默認JWT相關格式
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
// 開啟刷新token
.reuseRefreshTokens(true)
// refreshToken有效期120分鐘
.refreshTokenTimeToLive(Duration.ofMinutes(120L))
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build()
)
.build();
// Save registered client in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
/**
* 授權服務:管理OAuth2授權信息服務
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 授權確認信息處理服務
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 加載JWK資源
* JWT:指的是 JSON Web Token,不存在簽名的JWT是不安全的,存在簽名的JWT是不可竄改的
* JWS:指的是簽過名的JWT,即擁有簽名的JWT
* JWK:既然涉及到簽名,就涉及到簽名算法,對稱加密還是非對稱加密,那么就需要加密的 密鑰或者公私鑰對。此處我們將 JWT的密鑰或者公私鑰對統一稱為 JSON WEB KEY,即 JWK。
*/
@Bean
public JWKSource jwkSource() {
RSAKey rsaKey = JwksUtils.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/**
* 配置 OAuth2.0 提供者元信息
*/
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://127.0.0.1:9000").build();
}
}
新增Security的配置文件WebSecurityConfig
@Configuration
@EnableWebSecurity(debug = true) //開啟Security
public class WebSecurityConfig {
@Autowired
private ApplicationProperties properties;
/**
* 設置加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
// // 將密碼加密方式采用委托方式,默認以BCryptPasswordEncoder方式進行加密,兼容ldap,MD4,MD5等方式
// return PasswordEncoderFactories.createDelegatingPasswordEncoder();
// 此處我們使用明文方式 不建議這樣
return NoOpPasswordEncoder.getInstance();
}
/**
* 使用WebSecurity.ignoring()忽略某些URL請求,這些請求將被Spring Security忽略
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
// 讀取配置文件application.security.excludeUrls下的鏈接進行忽略
web.ignoring().antMatchers(properties.getSecurity().getExcludeUrls().toArray(new String[]{}));
}
};
}
/**
* 針對http請求,進行攔截過濾
*
* CookieCsrfTokenRepository進行CSRF保護的工作方式:
* 1.客戶端向服務器發出GET請求,例如請求主頁
* 2.Spring發送 GET 請求的響應以及 Set-cookie 標頭,其中包含安全生成的XSRF令牌
*/
@Bean
public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests(authorizeRequests ->
authorizeRequests.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
//使用默認登錄頁面
//.formLogin(withDefaults())
//設置form登錄,設置且放開登錄頁login
.formLogin(fromlogin -> fromlogin.loginPage("/login").permitAll())
// Spring Security CSRF保護
.csrf(csrfToken -> csrfToken.csrfTokenRepository(new CookieCsrfTokenRepository()))
// //開啟認證服務器的資源服務器相關功能,即需校驗token
// .oauth2ResourceServer()
// .accessDeniedHandler(new SimpleAccessDeniedHandler())
// .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
// .jwt()
;
return httpSecurity.build();
}
}
新增讀取application配置的類 ApplicationProperties
/**
* 此步主要是獲取配置文件中配置的白名單,可自行舍去或自定義實現其他方式
**/
@Data
@Component
@ConfigurationProperties("application")
public class ApplicationProperties {
private final Security security = new Security();
@Data
public static class Security {
private Oauth2 oauth2;
private List excludeUrls = new ArrayList<>();
@Data
public static class Oauth2 {
private String issuerUrl;
}
}
}
新增 JwksUtils 類和 KeyGeneratorUtils,這兩個類作為JWT對稱加密
public final class JwksUtils {
private JwksUtils() {
}
/**
* 生成RSA加密key (即JWK)
*/
public static RSAKey generateRsa() {
// 生成RSA加密的key
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
// 公鑰
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 私鑰
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 構建RSA加密key
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
/**
* 生成EC加密key (即JWK)
*/
public static ECKey generateEc() {
// 生成EC加密的key
KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
// 公鑰
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
// 私鑰
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
// 根據公鑰參數生成曲線
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
// 構建EC加密key
return new ECKey.Builder(curve, publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
/**
* 生成HmacSha256密鑰
*/
public static OctetSequenceKey generateSecret() {
SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
return new OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
}
}
class KeyGeneratorUtils {
private KeyGeneratorUtils() {
}
/**
* 生成RSA密鑰
*/
static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
/**
* 生成EC密鑰
*/
static KeyPair generateEcKey() {
EllipticCurve ellipticCurve = new EllipticCurve(
new ECFieldFp(
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
ECPoint ecPoint = new ECPoint(
new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
ECParameterSpec ecParameterSpec = new ECParameterSpec(
ellipticCurve,
ecPoint,
new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
1);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(ecParameterSpec);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
/**
* 生成HmacSha256密鑰
*/
static SecretKey generateSecretKey() {
SecretKey hmacKey;
try {
hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return hmacKey;
}
}
新建 ConsentController,編寫登錄和認證頁面的跳轉
如果在上面沒有使用自定義的登錄和授權頁面,下面的跳轉方法按需舍去
@Slf4j
@Controller
public class ConsentController {
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationConsentService authorizationConsentService;
public ConsentController(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationConsentService authorizationConsentService) {
this.registeredClientRepository = registeredClientRepository;
this.authorizationConsentService = authorizationConsentService;
}
@ResponseBody
@GetMapping("/favicon.ico")
public String faviconico(){
return "favicon.ico";
}
@GetMapping("/login")
public String loginPage(){
return "login";
}
@GetMapping(value = "/oauth2/consent")
public String consent(Principal principal, Model model,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.STATE) String state) {
// Remove scopes that were already approved
Set scopesToApprove = new HashSet<>();
Set previouslyApprovedScopes = new HashSet<>();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
OAuth2AuthorizationConsent currentAuthorizationConsent =
this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
Set authorizedScopes;
if (currentAuthorizationConsent != null) {
authorizedScopes = currentAuthorizationConsent.getScopes();
} else {
authorizedScopes = Collections.emptySet();
}
for (String requestedScope : 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";
}
private static Set withDescription(Set scopes) {
Set scopeWithDescriptions = new HashSet<>();
for (String scope : scopes) {
scopeWithDescriptions.add(new ScopeWithDescription(scope));
}
return scopeWithDescriptions;
}
public static class ScopeWithDescription {
private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
private static final Map scopeDescriptions = new HashMap<>();
static {
scopeDescriptions.put(
"message.read",
"This application will be able to read your message."
);
scopeDescriptions.put(
"message.write",
"This application will be able to add new messages. It will also be able to edit and delete existing messages."
);
scopeDescriptions.put(
"other.scope",
"This is another scope example of a scope description."
);
}
public final String scope;
public final String description;
ScopeWithDescription(String scope) {
this.scope = scope;
this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
}
}
}
新建 UserController,User,UserService等標準的自定義用戶業務,此處僅放出UserServiceImpl
@RequiredArgsConstructor
@Slf4j
@Component
class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new LambdaQueryWrapper().eq(User::getUsername,username));
return new org.springframework.security.core.userdetails.User(username, user.getPassword(), new ArrayList<>());
}
}
啟動項目,如下圖

認證服務器整體結構圖

資源服務器相關配置
pom.xml引入資源服務器相關依賴
<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: #配置超時時間 connect-timeout: 10000 read-timeout: 10000
新增資源服務器配置文件 ResourceServerConfiguration
@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟鑒權服務
public class ResourceServerConfiguration {
@Bean
public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
// 所有請求都進行攔截
httpSecurity.authorizeRequests().anyRequest().authenticated();
// 關閉session
httpSecurity.sessionManagement().disable();
// 配置資源服務器的無權限,無認證攔截器等 以及JWT驗證
httpSecurity.oauth2ResourceServer()
.accessDeniedHandler(new SimpleAccessDeniedHandler())
.authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
.jwt();
return httpSecurity.build();
}
}
新增相關無認證無權限統一攔截回復 SimpleAccessDeniedHandler 和 SimpleAuthenticationEntryPoint
/**
* 攜帶了token 而且token合法 但是權限不足以訪問其請求的資源 403
* @author zxg
*/
public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ObjectMapper objectMapper = new ObjectMapper();
String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed("無權訪問"));
PrintWriter printWriter = response.getWriter();
printWriter.print(resBody);
printWriter.flush();
printWriter.close();
}
}
/**
* 在資源服務器中 不攜帶token 或者token無效 401
* @author zxg
*/
@Slf4j
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if (response.isCommitted()){
return;
}
Throwable throwable = authException.fillInStackTrace();
String errorMessage = "認證失敗";
if (throwable instanceof BadCredentialsException){
errorMessage = "錯誤的客戶端信息";
}else {
Throwable cause = authException.getCause();
if (cause instanceof JwtValidationException) {
log.warn("JWT Token 過期,具體內容:" + cause.getMessage());
errorMessage = "無效的token信息";
} else if (cause instanceof BadJwtException){
log.warn("JWT 簽名異常,具體內容:" + cause.getMessage());
errorMessage = "無效的token信息";
} else if (cause instanceof AccountExpiredException){
errorMessage = "賬戶已過期";
} else if (cause instanceof LockedException){
errorMessage = "賬戶已被鎖定";
// } else if (cause instanceof InvalidClientException || cause instanceof BadClientCredentialsException){
// response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"無效的客戶端")));
// } else if (cause instanceof InvalidGrantException || cause instanceof RedirectMismatchException){
// response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("無效的類型")));
// } else if (cause instanceof UnauthorizedClientException) {
// response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未經授權的客戶端")));
} else if (throwable instanceof InsufficientAuthenticationException) {
String message = throwable.getMessage();
if (message.contains("Invalid token does not contain resource id")){
errorMessage = "未經授權的資源服務器";
}else if (message.contains("Full authentication is required to access this resource")){
errorMessage = "缺少驗證信息";
}
}else {
errorMessage = "驗證異常";
}
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ObjectMapper objectMapper = new ObjectMapper();
String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed(errorMessage));
PrintWriter printWriter = response.getWriter();
printWriter.print(resBody);
printWriter.flush();
printWriter.close();
}
}
新增 ResourceController 進行接口測試
@Slf4j
@RestController
public class ResourceController {
/**
* 測試Spring Authorization Server,測試權限
*/
@PreAuthorize("hasAuthority('SCOPE_message.read')")
@GetMapping("/getTest")
public String getTest(){
return "getTest";
}
/**
* 默認登錄成功跳轉頁為 / 防止404狀態
*
* @return the map
*/
@GetMapping("/")
public Map index() {
return Collections.singletonMap("msg", "login success!");
}
@GetMapping("/getResourceTest")
public SingleResultBundle getResourceTest(){
return SingleResultBundle.success("這是resource的測試方法 getResourceTest()");
}
}
啟動項目,效果如下

項目總體結構如下

測試認證鑒權
#調用 /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 #會判斷是否登錄,若沒有,則跳轉到登錄頁面,如下圖1 #登錄完成后,會提示是否授權,若沒有,則跳轉到授權界面,如下圖2 #授權成功后,跳轉到回調地址,并帶上code,如圖3


打開postman,進行獲取access_token
#訪問 /oauth2/token 地址 #在Authorization中選擇Basic Auth模式,填入對應客戶端,其會在header中生成Authorization,如下圖右側

返回結果如下

調用ResourceController中的接口,測試token是否生效

源碼下載地址
- https://gitee.com/rjj521/authorization-server-learn
總結
至此,spring-authorization-server的基礎使用已完成,總體上和原Spring Security OAuth大差不差,個別配置項不同。期間在網上搜尋了很多資料,然后進行整合,因此文中存在與其他網上教程相同代碼,如有爭議,請聯系我刪除改正,謝謝。
來源:blog.csdn.net/qq_37182370/article/ details/124822587