Spring Cloud實戰系列(十) - 單點登陸JWT與Spring Security OAuth

相關

  1. Spring Cloud實戰系列(一) - 服務註冊與發現Eureka php

  2. Spring Cloud實戰系列(二) - 客戶端調用Rest + Ribbon html

  3. Spring Cloud實戰系列(三) - 聲明式客戶端Feign java

  4. Spring Cloud實戰系列(四) - 熔斷器Hystrix mysql

  5. Spring Cloud實戰系列(五) - 服務網關Zuul git

  6. Spring Cloud實戰系列(六) - 分佈式配置中心Spring Cloud Configgithub

  7. Spring Cloud實戰系列(七) - 服務鏈路追蹤Spring Cloud Sleuthweb

  8. Spring Cloud實戰系列(八) - 微服務監控Spring Boot Admin算法

  9. Spring Cloud實戰系列(九) - 服務認證受權Spring Cloud OAuth 2.0 spring

  10. Spring Cloud實戰系列(十) - 單點登陸JWT與Spring Security OAuth sql

前言

經過 JWT 配合 Spring Security OAuth2 使用的方式,能夠避免 每次請求遠程調度 認證受權服務。資源服務器 只須要從 受權服務器 驗證一次,返回 JWT。返回的 JWT 包含了 用戶 的全部信息,包括 權限信息

正文

1. 什麼是JWT

JSON Web TokenJWT)是一種開放的標準(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)和使用的 算法類型,如 HMACSHA256RSA。例如:

{
    "typ": "JWT",
    "alg": "HS256"
}
複製代碼

HeaderBase64 編碼做爲 JWT第一部分,不建議在 JWTHeader 中放置 敏感信息

2.2. Payload

第二部分 PayloadJWT主體內容部分,它包含 聲明 信息。聲明是關於 用戶其餘數據 的聲明。

聲明有三種類型: registeredpublicprivate

  • Registered claimsJWT 提供了一組 預約義 的聲明,它們不是 強制的,可是推薦使用。JWT 指定 七個默認 字段供選擇:
註冊聲明 字段含義
iss 發行人
exp 到期時間
sub 主題
aud 用戶
nbf 在此以前不可用
iat 發佈時間
jti 用於標識JWT的ID
  • Public claims:能夠隨意定義。

  • Private claims:用於在 贊成使用 它們的各方之間 共享信息,而且不是 註冊的公開的 聲明。

下面是 Payload 部分的一個示例:

{
    "sub": "123456789",
    "name": "John Doe",
    "admin": true
}
複製代碼

PayloadBase64 編碼做爲 JWT第二部分,不建議在 JWTPayload 中放置 敏感信息

2.3. Signature

要建立簽名部分,須要利用 祕鑰Base64 編碼後的 HeaderPayload 進行 加密,加密算法的公式以下:

HMACSHA256(
    base64UrlEncode(header) + '.' +
    base64UrlEncode(payload),
    secret
)
複製代碼

簽名 能夠用於驗證 消息傳遞過程 中有沒有被更改。對於使用 私鑰簽名token,它還能夠驗證 JWT發送方 是否爲它所稱的 發送方

3. JWT的工做方式

客戶端 獲取 JWT 後,對於之後的 每次請求,都不須要再經過 受權服務 來判斷該請求的 用戶 以及該 用戶的權限。在微服務系統中,能夠利用 JWT 實現 單點登陸。認證流程圖以下:

4. 案例工程結構

  • eureka-server:做爲 註冊服務中心,端口號爲 8761。這裏再也不演示搭建。

  • auth-service:做爲 受權服務受權 須要用戶提供 客戶端client IdClient Secret,以及 受權用戶usernamepassword。這些信息 準備無誤 以後,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密碼選項-keystorejks文件名稱-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.enabletrue,即開啓 FeignHystrix 功能。完整的配置代碼以下:
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 模塊的 UserRoleUserRepository 三個類到本模塊。在 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/tokenAPI 接口中,須要在 請求頭 傳入 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);
}
複製代碼

其中,AuthServiceHystrixAuthServiceClient熔斷器,代碼以下:

@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_tokentoken_typerefresh_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-serviceauth-serviceuser-service 三個服務。

7. 使用Postman測試

  • 註冊一個用戶,返回註冊成功信息

  • 使用用戶名密碼登陸獲取 JWT

  • 複製上面的 access_tokenheader 頭部,請求須要 用戶權限/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與微服務構建》

歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索