經過 JWT
配合 Spring Security OAuth2
使用的方式,能夠避免 每次請求 都 遠程調度 認證受權服務。資源服務器 只須要從 受權服務器 驗證一次,返回 JWT
。返回的 JWT
包含了 用戶 的全部信息,包括 權限信息。
JSON Web Token
(JWT
)是一種開放的標準(RFC 7519
),JWT
定義了一種 緊湊 且 自包含 的標準,旨在將各個主體的信息包裝爲 JSON
對象。主體信息 是經過 數字簽名 進行 加密 和 驗證 的。常用 HMAC
算法或 RSA
(公鑰/私鑰 的 非對稱性加密)算法對 JWT
進行簽名,安全性很高。
緊湊型:數據體積小,可經過 POST
請求參數 或 HTTP
請求頭 發送。
自包含:JWT
包含了主體的全部信息,避免了 每一個請求 都須要向 Uaa
服務驗證身份,下降了 服務器的負載。
JWT
的結構由三部分組成:Header
(頭)、Payload
(有效負荷)和 Signature
(簽名)。所以 JWT
一般的格式是 xxxxx.yyyyy.zzzzz
。
Header
一般是由 兩部分 組成:令牌的 類型(即 JWT
)和使用的 算法類型,如 HMAC
、SHA256
和 RSA
。例如:
{
"typ": "JWT",
"alg": "HS256"
}
複製代碼
將 Header
用 Base64
編碼做爲 JWT
的 第一部分,不建議在 JWT
的 Header
中放置 敏感信息。
第二部分 Payload
是 JWT
的 主體內容部分,它包含 聲明 信息。聲明是關於 用戶 和 其餘數據 的聲明。
聲明有三種類型: registered
、public
和 private
。
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
中放置 敏感信息。
要建立簽名部分,須要利用 祕鑰 對 Base64
編碼後的 Header
和 Payload
進行 加密,加密算法的公式以下:
HMACSHA256(
base64UrlEncode(header) + '.' +
base64UrlEncode(payload),
secret
)
複製代碼
簽名 能夠用於驗證 消息 在 傳遞過程 中有沒有被更改。對於使用 私鑰簽名 的 token
,它還能夠驗證 JWT
的 發送方 是否爲它所稱的 發送方。
客戶端 獲取 JWT
後,對於之後的 每次請求,都不須要再經過 受權服務 來判斷該請求的 用戶 以及該 用戶的權限。在微服務系統中,能夠利用 JWT
實現 單點登陸。認證流程圖以下:
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
判斷該用戶是否有 權限 訪問該資源。
工程原理示意圖以下:
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);
}
}
複製代碼
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
三個服務。
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
,則提示用戶 從新登陸。
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。