1、概述
上一節中介紹了項目的搭建,並實現了授權碼模式的訪問。在上一節的基礎上,再來實現客戶端模式。【圖文詳解】搭建 Spring Authorization Server + Resource + Client 完整Demo
- 用戶通過客戶端訪問資源是 授權碼模式
- 微服務(資源)間的訪問是 客戶端模式;客戶端模式下,只需要提供註冊客戶端的ID和密鑰,就可以向授權服務器申請令牌,授權服務器覈實ID和密鑰後,會直接發放令牌,無須再認證/授權,特別適合項目內部模塊間的調用。
2、授權服務器中註冊新客戶端
爲了讓請求資源的主體更加清晰,再註冊一個客戶端
micro_service
,專門供資源服務器之間的相互調用。也可以用原來客戶端my_client
,不過要在授權模式GrantType中添加CLIENT_CREDENTIALS
- 客戶端模式直接返回token;不需要回調地址
- 在授權服務器的授權服務配置類
AuthorizationServerConfiguration.java
中添加
/**
* 定義客戶端(令牌申請方式:客戶端模式)
*
* @param clientId 客戶端ID
* @return
*/
private RegisteredClient createRegisteredClient(final String clientId) {
// JWT(Json Web Token)的配置項:TTL、是否複用refrechToken等等
TokenSettings tokenSettings = TokenSettings.builder()
// 令牌存活時間:1年
.accessTokenTimeToLive(Duration.ofDays(365))
// 令牌不可以刷新
//.reuseRefreshTokens(false)
.build();
// 客戶端相關配置
ClientSettings clientSettings = ClientSettings.builder()
// 是否需要用戶授權確認
.requireAuthorizationConsent(false)
.build();
return RegisteredClient
// 客戶端ID和密碼
.withId(UUID.randomUUID().toString())
//.withId(id)
.clientId(clientId)
//.clientSecret("{noop}123456")
.clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
// 客戶端名稱:可省略
.clientName("micro_service")
// 授權方法
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 授權模式
// ---- 【客戶端模式】
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 客戶端模式直接返回token;不需要回調地址
//.redirectUri("...")
// 授權範圍(當前客戶端的角色)
.scope("all")
// JWT(Json Web Token)配置項
.tokenSettings(tokenSettings)
// 客戶端配置項
.clientSettings(clientSettings)
.build();
}
- 修改註冊方法:該方法僅注重功能,結構不夠優雅,可以自行修改
/**
* 註冊客戶端
*
* @param jdbcTemplate 操作數據庫
* @return 客戶端倉庫
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// ---------- 1、檢查當前客戶端是否已註冊
// 操作數據庫對象
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
/*
客戶端在數據庫中記錄的區別
------------------------------------------
id:僅表示客戶端在數據庫中的這個記錄
client_id:唯一標示客戶端;請求token時,以此作爲客戶端的賬號
client_name:客戶端的名稱,可以省略
client_secret:密碼
*/
String clientId_1 = "my_client";
String clientId_2 = "micro_service";
// 查詢客戶端是否存在
RegisteredClient registeredClient_1 = registeredClientRepository.findByClientId(clientId_1);
RegisteredClient registeredClient_2 = registeredClientRepository.findByClientId(clientId_2);
// ---------- 2、添加客戶端
// 數據庫中沒有
if (registeredClient_1 == null) {
registeredClient_1 = this.createRegisteredClientAuthorizationCode(clientId_1);
registeredClientRepository.save(registeredClient_1);
}
// 數據庫中沒有
if (registeredClient_2 == null) {
registeredClient_2 = this.createRegisteredClient(clientId_2);
registeredClientRepository.save(registeredClient_2);
}
// ---------- 3、返回客戶端倉庫
return registeredClientRepository;
}
3、資源服務器之間訪問
3.1、案例說明
用 資源服務器B
調用 資源服務器A
中的資源;
具體:服務B/res1
--> 服務A/res2
;
服務A/res2
接口在前面用 my_client
是無法訪問的;
當前 資源服務器B
無安全策略,可以直接訪問
3.2、令牌申請與使用 處理邏輯
3.3、改造資源服務器B
- 配置RestTemplat
@Configuration(proxyBeanMethods = false)
public class RestTemplateConfiguration {
@Bean
public RestTemplate oauth2ClientRestTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
}
- 修改API接口類
@RestController
public class ResourceController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/res1")
public String getRes1(HttpServletRequest request) {
// 調用資源服務器A中的資源res2
return getServer("http://127.0.0.1:8001/res2", request);
//return JSON.toJSONString(new Result(200, "服務B -> 資源1"));
}
@GetMapping("/res2")
public String getRes2() {
return JSON.toJSONString(new Result(200, "服務B -> 資源2"));
}
/**
* 請求資源
*
* @param url
* @param request
* @return
*/
private String getServer(String url,
HttpServletRequest request) {
// ======== 1、從session中取token ========
HttpSession session = request.getSession();
String token = (String) session.getAttribute("micro-token");
// ======== 2、請求token ========
// 先查session中是否有token;session中沒有
if (StringUtils.isEmpty(token)) {
// ===== 去認證中心申請 =====
// 對id及密鑰加密
byte[] userpass = Base64.encodeBase64(("micro_service:123456").getBytes());
String str = "";
try {
str = new String(userpass, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 請求頭
HttpHeaders headers1 = new HttpHeaders();
// 組裝請求頭
headers1.add("Authorization", "Basic " + str);
// 請求體
HttpEntity<Object> httpEntity1 = new HttpEntity<>(headers1);
// 響應體
ResponseEntity<String> responseEntity1 = null;
try {
// 發起申請令牌請求
responseEntity1 = restTemplate.exchange("http://os.com:9000/oauth2/token?grant_type=client_credentials", HttpMethod.POST, httpEntity1, String.class);
} catch (RestClientException e) {
//
System.out.println("令牌申請失敗");
}
// 令牌申請成功
if (responseEntity1 != null) {
// 解析令牌
// String t = JSON.parseObject(responseEntity1.getBody(), MyAuth.class).getAccess_token();
Map<String, String> resMap = JSON.parseObject(responseEntity1.getBody(), HashMap.class);
String t = resMap.get("access_token");
// 存入session
session.setAttribute("micro-token", t);
// 賦於token變量
token = t;
}
}
// ======== 3、請求資源 ========
// 請求頭
HttpHeaders headers2 = new HttpHeaders();
// 組裝請求頭
headers2.add("Authorization", "Bearer " + token);
// 請求體
HttpEntity<Object> httpEntity2 = new HttpEntity<>(headers2);
// 響應體
ResponseEntity<String> responseEntity2;
try {
// 發起訪問資源請求
responseEntity2 = restTemplate.exchange(url, HttpMethod.GET, httpEntity2, String.class);
} catch (RestClientException e) {
// 令牌失效(認證失效401) --> 清除session
// e.getMessage() 信息格式:
// 401 : "{"msg":"認證失敗","uri":"/res2"}"
String str = e.getMessage();
// 判斷是否含有 401
if(StringUtils.contains(str, "401")){
// 如果有401,把session中 micro-token 的值設爲空
session.setAttribute("micro-token","");
}
// 取兩個括號中間的部分(包含兩個括號)
return str.substring(str.indexOf("{"), str.indexOf("}") + 1);
}
// 返回
return responseEntity2.getBody();
}
}
// 用於解析申請到的令牌數據
/*@Data
class MyAuth {
private String access_token;
private String scope;
private String token_type;
private long expires_in;
}*/
3.4、測試
- 啓動server、resource,無須啓動client
- 直接訪問resource_b
3.5、資源服務器B添加安全策略
- 添加依賴
<!-- 資源服務器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
- 再次啓動測試,已經無法直接訪問;需要通過客戶端去訪問
3.6、繼續改造資源服務器B
複製 資源服務器A 中配置策略到 資源服務器B 中來
- 複製cer公鑰文件
- appliction.yml中添加jtw配置
# 自定義 jwt 配置(校驗jwt)
jwt:
cert-info:
# 公鑰證書存放位置
public-key-location: myjks.cer
claims:
# 令牌的鑑發方:即授權服務器的地址
issuer: http://os.com:9000
- 複製 oauth2 配置包;如下圖
3.7、客戶端client訪問測試
- 用 maven 的
clean
清理項目 - 啓動 server、resource (a和b)、client
- 登錄客戶端
- 訪問資源服務A
- 訪問資源服務B
如果需要 資源服務器A 調用 B 中資源;可以把 B 中的實現邏輯複製過去就行。
後期會把資源服務器中的公共部分抽離出來,製成starter…
2023-4-16:用starter實現Oauth2中資源服務的統一配置
一、前言
Oauth2中的資源服務Resource需要驗證令牌,就要配置令牌的解碼器JwtDecoder,認證服務器的公鑰等等。如果有多個資源服務Resource,就要重複配置,比較繁鎖。把公共的配置信息抽取出來,製成starter,可以極大地簡化操作。
- 未使用starter的原來配置
二、製作starter
詳細步驟參考:自定義啓動器 Starter【保姆級教程】
1、完整結構圖
2、外部引用模塊
名稱:
tuwer-oauth2-config-spring-boot-starter
普通的 maven 項目
資源服務中引入該模塊的依賴即可
模塊中只有一個pom.xml文件,其餘的都可刪除
- pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.tuwer</groupId> <artifactId>tuwer-oauth2-config-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> <description>oauth2-config啓動器</description> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <!-- 編譯編碼 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- 自動配置模塊 --> <dependency> <groupId>com.tuwer</groupId> <artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigure</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
3、自動配置模塊
核心模塊
名稱:
tuwer-oauth2-config-spring-boot-starter-autoconfigure
spring boot 項目
- pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.7</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.tuwer</groupId> <artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigure</artifactId> <version>1.0-SNAPSHOT</version> <description>oauth2-config啓動器自動配置模塊</description> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <!-- 編譯編碼 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- 基礎啓動器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- 資源服務器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> </dependencies> </project>
- handler(拒絕訪問、認證失敗)處理類
package com.tuwer.config.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.http.MediaType; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; /** * <p>拒絕訪問處理器</p> * * @author 土味兒 * Date 2022/5/11 * @version 1.0 */ public class SimpleAccessDeniedHandler implements AccessDeniedHandler { @SneakyThrows @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException ) throws IOException, ServletException { //todo your business HashMap<String, String> map = new HashMap<>(2); map.put("uri", request.getRequestURI()); map.put("msg", "拒絕訪問"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(map); PrintWriter printWriter = response.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } }
package com.tuwer.config.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; /** * <p>認證失敗處理器</p> * * @author 土味兒 * Date 2022/5/11 * @version 1.0 */ public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint { @SneakyThrows @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException, ServletException { HashMap<String, String> map = new HashMap<>(2); if (authException instanceof InvalidBearerTokenException) { // 令牌失效 System.out.println("token失效"); //todo token處理邏輯 } map.put("uri", request.getRequestURI()); map.put("msg", "認證失敗"); if (response.isCommitted()) { return; } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_ACCEPTED); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(map); PrintWriter printWriter = response.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } }
property(權限、令牌)屬性類
權限屬性類:
AuthProperty
,通過application.yml來配置權限,避免在自動配置類中以硬編碼的形式寫入權限package com.tuwer.config.property; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.CollectionUtils; import java.util.Set; /** * <p>權限屬性類</p> * * @author 土味兒 * Date 2022/9/2 * @version 1.0 */ @Data @ConfigurationProperties(prefix = "resource-auth") public final class AuthProperty { private Authority authority; /** * 權限 */ @Data public static class Authority { private Set<String> roles; private Set<String> scopes; private Set<String> auths; } /** * 組裝權限字符串 * 目的:給 hasAnyAuthority() 方法生成參數 * @return */ public String getAllAuth() { StringBuilder res = new StringBuilder(); // 角色 Set<String> roles = this.authority.roles; // 角色非空時 if (!CollectionUtils.isEmpty(roles)) { for (String role : roles) { res.append(role).append("','"); } // 循環結果後,生成類似:x ',' y ',' z ',' } // 範圍 Set<String> scopes = this.authority.scopes; // 非空時 if (!CollectionUtils.isEmpty(scopes)) { for (String scope : scopes) { res.append("SCOPE_" + scope).append("','"); } // 循環結果後,生成類似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ',' } // 細粒度權限 Set<String> auths = this.authority.auths; // 非空時 if (!CollectionUtils.isEmpty(auths)) { for (String auth : auths) { res.append(auth).append("','"); } // 循環結果後,生成類似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ',' l ',' m ',' n ',' } // 如果res不爲空,去掉最後多出的三個字符 ',' int len = res.length(); if (len > 3) { res.delete(len - 3, len); } return res.toString(); } }
package com.tuwer.config.property; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** * <p>屬性配置類</p> * * @author 土味兒 * Date 2022/5/11 * @version 1.0 */ @Data @ConfigurationProperties(prefix = "jwt") public class JwtProperty { /* ======= 配置示例 ====== # 自定義 jwt 配置 jwt: cert-info: # 證書存放位置 public-key-location: myKey.cer claims: # 令牌的鑑發方:即授權服務器的地址 issuer: http://os:9000 */ /** * 證書信息(內部靜態類) * 證書存放位置... */ private CertInfo certInfo; /** * 證書聲明(內部靜態類) * 發證方... */ private Claims claims; @Data public static class Claims { /** * 發證方 */ private String issuer; /** * 有效期 */ //private Integer expiresAt; } @Data public static class CertInfo { /** * 證書存放位置 */ private String publicKeyLocation; } }
- 解碼器自動配置類
package com.tuwer.config; import com.nimbusds.jose.jwk.RSAKey; import com.tuwer.config.property.JwtProperty; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.ClassPathResource; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import javax.annotation.Resource; import java.io.InputStream; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPublicKey; import java.util.Collection; /** * <p>自定義jwt解碼器</p> * * @author 土味兒 * Date 2022/5/11 * @version 1.0 */ @EnableConfigurationProperties(JwtProperty.class) @Configuration public class JwtDecoderConfiguration { /** * 注入 JwtProperties 屬性配置類 */ @Resource private JwtProperty jwtProperty; /** * 校驗jwt發行者 issuer 是否合法 * * @return the jwt issuer validator */ @Bean JwtIssuerValidator jwtIssuerValidator() { return new JwtIssuerValidator(this.jwtProperty.getClaims().getIssuer()); } /* * * 校驗jwt是否過期 * * @return the jwt timestamp validator */ /* @Bean JwtTimestampValidator jwtTimestampValidator() { System.out.println("檢測令牌是否過期!"+ LocalDateTime.now()); return new JwtTimestampValidator(Duration.ofSeconds((long) this.jwtProperties.getClaims().getExpiresAt())); }*/ /** * jwt token 委託校驗器,集中校驗的策略{@link OAuth2TokenValidator} * * // @Primary:自動裝配時當出現多個Bean候選者時,被註解爲@Primary的Bean將作爲首選者,否則將拋出異常 * @param tokenValidators the token validators * @return the delegating o auth 2 token validator */ @Primary @Bean({"delegatingTokenValidator"}) public DelegatingOAuth2TokenValidator<Jwt> delegatingTokenValidator(Collection<OAuth2TokenValidator<Jwt>> tokenValidators) { return new DelegatingOAuth2TokenValidator<>(tokenValidators); } /** * 基於Nimbus的jwt解碼器,並增加了一些自定義校驗策略 * * // @Qualifier 當有多個相同類型的bean存在時,指定注入 * @param validator DelegatingOAuth2TokenValidator<Jwt> 委託token校驗器 * @return the jwt decoder */ @SneakyThrows @Bean public JwtDecoder jwtDecoder(@Qualifier("delegatingTokenValidator") DelegatingOAuth2TokenValidator<Jwt> validator) { // 指定 X.509 類型的證書工廠 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); // 讀取cer公鑰證書來配置解碼器 String publicKeyLocation = this.jwtProperty.getCertInfo().getPublicKeyLocation(); // 獲取證書文件輸入流 ClassPathResource resource = new ClassPathResource(publicKeyLocation); InputStream inputStream = resource.getInputStream(); // 得到證書 X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream); // 解析 RSAKey rsaKey = RSAKey.parse(certificate); // 得到公鑰 RSAPublicKey key = rsaKey.toRSAPublicKey(); // 構造解碼器 NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withPublicKey(key).build(); // 注入自定義JWT校驗邏輯 nimbusJwtDecoder.setJwtValidator(validator); return nimbusJwtDecoder; } }
- 主自動配置類:安全權限等
package com.tuwer.config; import com.tuwer.config.handler.SimpleAccessDeniedHandler; import com.tuwer.config.handler.SimpleAuthenticationEntryPoint; import com.tuwer.config.property.AuthProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; import javax.annotation.Resource; /** * <p>資源服務器配置</p> * 當解碼器JwtDecoder存在時生效 * * @author 土味兒 * Date 2022/5/11 * @version 1.0 */ @ConditionalOnBean(JwtDecoder.class) @EnableConfigurationProperties(AuthProperty.class) @Configuration public class AutoConfiguration { @Resource private AuthProperty authProperty; /** * 資源管理器配置 * * @param http the http * @return the security filter chain * @throws Exception the exception */ @Bean SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception { // 拒絕訪問處理器 401 SimpleAccessDeniedHandler accessDeniedHandler = new SimpleAccessDeniedHandler(); // 認證失敗處理器 403 SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint(); return http // security的session生成策略改爲security不主動創建session即STALELESS .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 允許【pc客戶端】或【其它微服務】訪問 .authorizeRequests() //.antMatchers("/**").hasAnyAuthority("SCOPE_client_pc","SCOPE_micro_service") // 從配置文件中讀取權限信息 .antMatchers("/**").hasAnyAuthority(authProperty.getAllAuth()) // 其餘請求都需要認證 .anyRequest().authenticated() .and() // 異常處理 .exceptionHandling(exceptionConfigurer -> exceptionConfigurer // 拒絕訪問 .accessDeniedHandler(accessDeniedHandler) // 認證失敗 .authenticationEntryPoint(authenticationEntryPoint) ) // 資源服務 .oauth2ResourceServer(resourceServer -> resourceServer .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint) .jwt() ) .build(); } /** * JWT個性化解析 * * @return */ @Bean JwtAuthenticationConverter jwtAuthenticationConverter() { JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 如果不按照規範 解析權限集合Authorities 就需要自定義key // jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes"); // OAuth2 默認前綴是 SCOPE_ Spring Security 是 ROLE_ // jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); // 用戶名 可以放sub jwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB); return jwtAuthenticationConverter; } /** * 開放一些端點的訪問控制 * 不需要認證就可以訪問的端口 * @return */ @Bean WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().antMatchers( "/actuator/**" ); } }
- spring.factories
指明自動配置類的地址,在
resources
目錄下編寫一個自己的META-INF\spring.factories
;有兩個自動配置類,中間用逗號分開注意點:
如果同一個組中有多個starter,自動配置類名稱不要相同;如果相同,將只有一個配置類生效,其餘的將失效。
如:
starterA中:com.tuwer.config.AutoConfiguration
starterB中:就不要再用 com.tuwer.config.AutoConfiguration 名稱,可以改爲 com.tuwer.config.AutoConfigurationB
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.tuwer.config.JwtDecoderConfiguration,\ com.tuwer.config.AutoConfiguration
- application.yml
令牌、權限的配置可以放在引用starter的資源服務中;如果每個資源服務的配置都一樣,可以放在starter中
# 自定義 jwt 配置(校驗jwt) jwt: cert-info: # 公鑰證書存放位置 public-key-location: myjks.cer claims: # 令牌的鑑發方:即授權服務器的地址 issuer: http://os.com:9000 # 自定義權限配置 resource-auth: # 權限 authority: # 角色名稱;不用加ROlE_,提取用戶角色權限時,自動加 roles: # 授權範圍;不用加SCOPE_,保持與認證中心中定義的一致即可; # 後臺自動加 SCOPE_ scopes: - client_pc - micro_service # 細粒度權限 auths:
- 公鑰
把認證中心的公鑰文件
myjks.cer
放到resources目錄下4、install
把starter安裝install到本地maven倉庫中
三、使用starter
1、引入starter依賴
在資源服務中引入
tuwer-oauth2-config-spring-boot-starter
2、application.yml
3、刪除資源服務中原文件