相關
前言
通過 JWT
配合 Spring Security OAuth2
使用的方式,可以避免 每次請求 都 遠端排程 認證授權服務。資源伺服器 只需要從 授權伺服器 驗證一次,返回 JWT
。返回的 JWT
包含了 使用者 的所有資訊,包括 許可權資訊。
正文
1. 什麼是JWT
JSON Web Token
(JWT
)是一種開放的標準(RFC 7519
),JWT
定義了一種 緊湊 且 自包含 的標準,旨在將各個主體的資訊包裝為 JSON
物件。主體資訊 是通過 數字簽名 進行 加密 和 驗證 的。經常使用 HMAC
演算法或 RSA
(公鑰/私鑰 的 非對稱性加密)演算法對 JWT
進行簽名,安全性很高。
-
緊湊型:資料體積小,可通過
POST
請求引數 或HTTP
請求頭 傳送。 -
自包含:
JWT
包含了主體的所有資訊,避免了 每個請求 都需要向Uaa
服務驗證身份,降低了 伺服器的負載。
2. JWT的結構
JWT
的結構由三部分組成:Header
(頭)、Payload
(有效負荷)和 Signature
(簽名)。因此 JWT
通常的格式是 xxxxx.yyyyy.zzzzz
。
2.1. Header
Header
通常是由 兩部分 組成:令牌的 型別(即 JWT
)和使用的 演算法型別,如 HMAC
、SHA256
和 RSA
。例如:
{
"typ": "JWT",
"alg": "HS256"
}
複製程式碼
將 Header
用 Base64
編碼作為 JWT
的 第一部分,不建議在 JWT
的 Header
中放置 敏感資訊。
2.2. Payload
第二部分 Payload
是 JWT
的 主體內容部分,它包含 宣告 資訊。宣告是關於 使用者 和 其他資料 的宣告。
宣告有三種型別: registered
、public
和 private
。
- Registered claims
JWT
提供了一組 預定義 的宣告,它們不是 強制的,但是推薦使用。JWT
指定 七個預設 欄位供選擇:
註冊宣告 | 欄位含義 |
---|---|
iss | 發行人 |
exp | 到期時間 |
sub | 主題 |
aud | 使用者 |
nbf | 在此之前不可用 |
iat | 釋出時間 |
jti | 用於標識JWT的ID |
-
Public claims:可以隨意定義。
-
Private claims:用於在 同意使用 它們的各方之間 共享資訊,並且不是 註冊的 或 公開的 宣告。
下面是 Payload
部分的一個示例:
{
"sub": "123456789",
"name": "John Doe",
"admin": true
}
複製程式碼
將 Payload
用 Base64
編碼作為 JWT
的 第二部分,不建議在 JWT
的 Payload
中放置 敏感資訊。
2.3. Signature
要建立簽名部分,需要利用 祕鑰 對 Base64
編碼後的 Header
和 Payload
進行 加密,加密演算法的公式如下:
HMACSHA256(
base64UrlEncode(header) + '.' +
base64UrlEncode(payload),
secret
)
複製程式碼
簽名 可以用於驗證 訊息 在 傳遞過程 中有沒有被更改。對於使用 私鑰簽名 的 token
,它還可以驗證 JWT
的 傳送方 是否為它所稱的 傳送方。
3. JWT的工作方式
客戶端 獲取 JWT
後,對於以後的 每次請求,都不需要再通過 授權服務 來判斷該請求的 使用者 以及該 使用者的許可權。在微服務系統中,可以利用 JWT
實現 單點登入。認證流程圖如下:
4. 案例工程結構
-
eureka-server:作為 註冊服務中心,埠號為
8761
。這裡不再演示搭建。 -
auth-service:作為 授權服務,授權 需要使用者提供 客戶端 的
client Id
和Client Secret
,以及 授權使用者 的username
和password
。這些資訊 準備無誤 之後,auth-service
會返回JWT
,該JWT
包含了使用者的 基本資訊 和 許可權點資訊,並通過RSA
私鑰 進行加密。 -
user-service:作為 資源服務,它的 資源 被保護起來,需要相應的 許可權 才能訪問。
user-service
服務得到 使用者請求 的JWT
後,先通過 公鑰 解密JWT
,得到JWT
對應的 使用者資訊 和 使用者許可權資訊,再通過Spring Security
判斷該使用者是否有 許可權 訪問該資源。
工程原理示意圖如下:
5. 構建auth-service授權服務
- 新建一個
auth-service
專案模組,完整的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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.github.ostenant.springcloud</groupId>
<artifactId>auth-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--防止jks檔案被mavne編譯導致不可用-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>
複製程式碼
- 修改
auth-service
的配置檔案application.yml
檔案如下:
spring:
application:
name: auth-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
server:
port: 9999
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
複製程式碼
- 為
auth-service
配置Spring Security
安全登入管理,用於保護token
發放 和 驗證 的資源介面。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Override
public @Bean AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //關閉CSRF
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.authorizeRequests()
.antMatchers("/**").authenticated()
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder());
}
}
複製程式碼
UserServiceDetail.java
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserDao userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
複製程式碼
UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
複製程式碼
實體類 User
和上一篇文章的內容一樣,需要實現 UserDetails
介面,實體類 Role
需要實現 GrantedAuthority
介面。
User.java
@Entity
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column
private String password;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> authorities;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
複製程式碼
Role.java
@Entity
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getAuthority() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
複製程式碼
- 新建一個配置類
OAuth2Config
,為auth-service
配置 認證服務,程式碼如下:
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 將客戶端的資訊儲存在記憶體中
clients.inMemory()
// 配置一個客戶端
.withClient("user-service")
.secret("123456")
// 配置客戶端的域
.scopes("service")
// 配置驗證型別為refresh_token和password
.authorizedGrantTypes("refresh_token", "password")
// 配置token的過期時間為1h
.accessTokenValiditySeconds(3600 * 1000);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置token的儲存方式為JwtTokenStore
endpoints.tokenStore(tokenStore())
// 配置用於JWT私鑰加密的增強器
.tokenEnhancer(jwtTokenEnhancer())
// 配置安全認證管理
.authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
// 配置jks檔案
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("fzp-jwt.jks"), "fzp123".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("fzp-jwt"));
return converter;
}
}
複製程式碼
- 生成用於
Token
加密的 私鑰檔案fzp-jwt.jks
jks
檔案的生成需要使用 Java keytool
工具,保證 Java
環境變數沒問題,輸入命令如下:
$ keytool -genkeypair -alias fzp-jwt
-validity 3650
-keyalg RSA
-dname "CN=jwt,OU=jtw,O=jwt,L=zurich,S=zurich, C=CH"
-keypass fzp123
-keystore fzp-jwt.jks
-storepass fzp123
複製程式碼
其中,-alias
選項為 別名,-keyalg
為 加密演算法,-keypass
和 -storepass
為 密碼選項,-keystore
為 jks
的 檔名稱,-validity
為配置 jks
檔案 過期時間(單位:天)。
生成的 jks
檔案作為 私鑰,只允許 授權服務 所持有,用作 加密生成 JWT
。把生成的 jks
檔案放到 auth-service
模組的 src/main/resource
目錄下即可。
- 生成用於
JWT
解密的 公鑰
對於 user-service
這樣的 資源服務,需要使用 jks
的 公鑰 對 JWT
進行 解密。獲取 jks
檔案的 公鑰 的命令如下:
$ keytool -list -rfc
--keystore fzp-jwt.jks | openssl x509
-inform pem
-pubkey
複製程式碼
這個命令要求安裝 openSSL
下載地址,然後手動把安裝的 openssl.exe
所在目錄配置到 環境變數。
輸入密碼 fzp123
後,顯示的資訊很多,只需要提取 PUBLIC KEY
,即如下所示:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlCFiWbZXIb5kwEaHjW+/ 7J4b+KzXZffRl5RJ9rAMgfRXHqGG8RM2Dlf95JwTXzerY6igUq7FVgFjnPbexVt3 vKKyjdy2gBuOaXqaYJEZSfuKCNN/WbOF8e7ny4fLMFilbhpzoqkSHiR+nAHLkYct OnOKMPK1SwmvkNMn3aTEJHhxGh1RlWbMAAQ+QLI2D7zCzQ7Uh3F+Kw0pd2gBYd8W +DKTn1Tprugdykirr6u0p66yK5f1T9O+LEaJa8FjtLF66siBdGRaNYMExNi21lJk i5dD3ViVBIVKi9ZaTsK9Sxa3dOX1aE5Zd5A9cPsBIZ12spYgemfj6DjOw6lk7jkG 9QIDAQAB -----END PUBLIC KEY-----
新建一個 public.cert
檔案,將上面的 公鑰資訊 複製到 public.cert
檔案中並儲存。並將檔案放到 user-service
等 資源服務 的 src/main/resources
目錄下。至此 auth-service
搭建完畢。
- 在
pom.xml
中配置jks
檔案字尾過濾器
maven
在專案編譯時,可能會將 jks
檔案 編譯,導致 jks
檔案 亂碼,最後不可用。需要在 pom.xml
檔案中新增以下內容:
<!-- 防止jks檔案被maven編譯導致不可用 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
複製程式碼
- 最後在啟動類上配置
@EnableEurekaClient
註解開啟服務註冊功能。
@EnableEurekaClient
@SpringBootApplication
public class AuthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args);
}
}
複製程式碼
6. 構建user-service資源服務
- 新建一個
user-service
專案模組,完整的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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.github.ostenant.springcloud</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
複製程式碼
- 修改
user-service
的配置檔案application.yml
,配置 應用名稱 為user-service
,埠號 為9090
。另外,需要配置feign.hystrix.enable
為true
,即開啟Feign
的Hystrix
功能。完整的配置程式碼如下:
server:
port: 9090
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
feign:
hystrix:
enabled: true
複製程式碼
- 配置 資源服務
注入 JwtTokenStore
型別的 Bean
,同時初始化 JWT
轉換器 JwtAccessTokenConverter
,設定用於解密 JWT
的 公鑰。
@Configuration
public class JwtConfig {
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
// 用作JWT轉換器
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
//設定公鑰
converter.setVerifierKey(publicKey);
return converter;
}
}
複製程式碼
配置 資源服務 的認證管理,除了 註冊 和 登入 的介面之外,其他的介面都需要 認證。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
@Autowired
private TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/login","/user/register").permitAll()
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
}
複製程式碼
新建一個配置類 GlobalMethodSecurityConfig
,通過 @EnableGlobalMethodSecurity
註解開啟 方法級別 的 安全驗證。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalMethodSecurityConfig {
}
複製程式碼
- 實現使用者註冊介面
拷貝 auth-service
模組的 User
、Role
和 UserRepository
三個類到本模組。在 Service
層的 UserService
編寫一個 插入使用者 的方法,程式碼如下:
@Service
public class UserServiceDetail {
@Autowired
private UserRepository userRepository;
public User insertUser(String username,String password){
User user=new User();
user.setUsername(username);
user.setPassword(BPwdEncoderUtil.BCryptPassword(password));
return userRepository.save(user);
}
}
複製程式碼
配置用於使用者密碼 加密 的工具類 BPwdEncoderUtil
:
public class BPwdEncoderUtil {
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
public static String BCryptPassword(String password){
return encoder.encode(password);
}
public static boolean matches(CharSequence rawPassword, String encodedPassword){
return encoder.matches(rawPassword,encodedPassword);
}
}
複製程式碼
實現一個 使用者註冊 的 API
介面 /user/register
,程式碼如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserServiceDetail userServiceDetail;
@PostMapping("/register")
public User postUser(@RequestParam("username") String username,
@RequestParam("password") String password){
return userServiceDetail.insertUser(username, password);
}
}
複製程式碼
- 實現使用者登入介面
在 Service
層的 UserServiceDetail
中新增一個 login()
方法,程式碼如下:
@Service
public class UserServiceDetail {
@Autowired
private AuthServiceClient client;
public UserLoginDTO login(String username, String password) {
// 查詢資料庫
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UserLoginException("error username");
}
if(!BPwdEncoderUtil.matches(password,user.getPassword())){
throw new UserLoginException("error password");
}
// 從auth-service獲取JWT
JWT jwt = client.getToken("Basic dXNlci1zZXJ2aWNlOjEyMzQ1Ng==", "password", username, password);
if(jwt == null){
throw new UserLoginException("error internal");
}
UserLoginDTO userLoginDTO=new UserLoginDTO();
userLoginDTO.setJwt(jwt);
userLoginDTO.setUser(user);
return userLoginDTO;
}
}
複製程式碼
AuthServiceClient
作為 Feign Client
,通過向 auth-service
服務介面 /oauth/token
遠端呼叫獲取 JWT
。在請求 /oauth/token
的 API
介面中,需要在 請求頭 傳入 Authorization
資訊,認證型別 ( grant_type
)、使用者名稱 ( username
) 和 密碼 ( password
),程式碼如下:
@FeignClient(value = "auth-service", fallback = AuthServiceHystrix.class)
public interface AuthServiceClient {
@PostMapping("/oauth/token")
JWT getToken(@RequestHeader("Authorization") String authorization,
@RequestParam("grant_type") String type,
@RequestParam("username") String username,
@RequestParam("password") String password);
}
複製程式碼
其中,AuthServiceHystrix
為 AuthServiceClient
的 熔斷器,程式碼如下:
@Component
public class AuthServiceHystrix implements AuthServiceClient {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthServiceHystrix.class);
@Override
public JWT getToken(String authorization, String type, String username, String password) {
LOGGER.warn("Fallback of getToken is executed")
return null;
}
}
複製程式碼
JWT
包含了 access_token
、token_type
和 refresh_token
等資訊,程式碼如下:
public class JWT {
private String access_token;
private String token_type;
private String refresh_token;
private int expires_in;
private String scope;
private String jti;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public String getToken_type() {
return token_type;
}
public void setToken_type(String token_type) {
this.token_type = token_type;
}
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getJti() {
return jti;
}
public void setJti(String jti) {
this.jti = jti;
}
}
複製程式碼
UserLoginDTO
包含了一個 User
和一個 JWT
成員屬性,用於返回資料的實體:
public class UserLoginDTO {
private JWT jwt;
private User user;
public JWT getJwt() {
return jwt;
}
public void setJwt(JWT jwt) {
this.jwt = jwt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
複製程式碼
登入異常類 UserLoginException
public class UserLoginException extends RuntimeException {
public UserLoginException(String message) {
super(message);
}
}
複製程式碼
全域性異常處理 切面類 ExceptionHandle
@ControllerAdvice
@ResponseBody
public class ExceptionHandler {
@ExceptionHandler(UserLoginException.class)
public ResponseEntity<String> handleException(Exception e) {
return new ResponseEntity(e.getMessage(), HttpStatus.OK);
}
}
複製程式碼
在 Web
層的 UserController
類中新增一個登入的 API
介面 /user/login
如下:
@PostMapping("/login")
public UserLoginDTO login(@RequestParam("username") String username,
@RequestParam("password") String password) {
return userServiceDetail.login(username,password);
}
複製程式碼
- 為了測試 使用者許可權,再新增一個
/foo
介面,該介面需要ROLE_ADMIN
許可權才能正常訪問。
@RequestMapping(value = "/foo", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String getFoo() {
return "i'm foo, " + UUID.randomUUID().toString();
}
複製程式碼
- 最後在應用的啟動類上使用註解
@EnableFeignClients
開啟Feign
的功能即可。
@SpringBootApplication
@EnableFeignClients
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
複製程式碼
依次啟動 eureka-service
,auth-service
和 user-service
三個服務。
7. 使用Postman測試
- 註冊一個使用者,返回註冊成功資訊
- 使用使用者名稱密碼登入獲取
JWT
- 複製上面的
access_token
到header
頭部,請求需要 使用者許可權 的/user/foo
介面
"Authorization": "Bearer {access_token}"
複製程式碼
因為沒有許可權,訪問被拒絕。在資料庫手動新增 ROLE_ADMIN
許可權,並與該使用者關聯。重新登入並獲取 JWT
,再次請求 /user/foo
介面。
總結
在本案例中,使用者通過 登入介面 來獲取 授權服務 加密後的 JWT
。使用者成功獲取 JWT
後,在以後每次訪問 資源服務 的請求中,都需要攜帶上 JWT
。資源服務 通過 公鑰解密 JWT
,解密成功 後可以獲取 使用者資訊 和 許可權資訊,從而判斷該 JWT
所對應的 使用者 是誰,具有什麼 許可權。
- 優點:
獲取一次 Token
,多次使用,資源服務 不再每次訪問 授權服務 該 Token
所對應的 使用者資訊 和使用者的 許可權資訊。
- 缺點:
一旦 使用者資訊 或者 許可權資訊 發生了改變,Token
中儲存的相關資訊並 沒有改變,需要 重新登入 獲取新的 Token
。就算重新獲取了 Token
,如果原來的 Token
沒有過期,仍然是可以使用的。一種改進方式是在登入成功後,將獲取的 Token
快取 在 閘道器上。如果使用者的 許可權更改,將 閘道器 上快取的 Token
刪除。當請求經過 閘道器,判斷請求的 Token
在 快取 中是否存在,如果快取中不存在該 Token
,則提示使用者 重新登入。
參考
- 方誌朋《深入理解Spring Cloud與微服務構建》
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。