《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统

文章目录

  • 《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统
  • 一、JWT 简介
    • 1.什么是 JWT
    • 2.JWT 的应用场景
    • 3.如何使用 JWT
  • 二、案例分析
    • 1.案例架构设计
    • 2.编写主 Maven 工程
    • 3.编写 Eureka Server
    • 4.编写 Uaa 授权服务
    • 5.编写 user-service 资源服务
  • 三、总结

一、JWT 简介

上一章讲述了如何通过 Spring Security OAuth2 来保护 Spring Cloud 架构的微服务系统。上一章的系统有一个缺陷,即每次请求都需要经过 Uaa 服务去验证当前 Token 的合法性,并且需要查询该 Token 对应的用户的权限。在高并发场景下,会存在性能瓶颈,改善的方法是将 Uaa 服务集群部署并加上缓存。本章针对上一章的系统缺陷,采用 Spring Security OAuth2 和 JWT 的方式,避免每次请求都需要远程调度 Uaa 服务。采用 Spring Security OAuth2 和 JWT 的方式,Uaa 服务只验证一次,返回 JWT。返回的 JWT 包含了用户的所有信息,包括权限信息

1.什么是 JWT

JSON Web Token(JWT)是一种开放的标准(RFC 7519),JWT 定义了一种紧凑且自包含的标准,该标准旨在将各个主体的信息包装为 JSON 对象。主体信息是通过数字签名进行加密和验证的。常使用 HMAC 算法或 RSA(公钥/私钥的非对称性加密)算法对 JWT 进行签名,安全性很高。下面进一步解释它的特点:

  • 紧凑性(compact):由于是加密后的字符串,JWT 数据体积非常小,可通过 POST 请求参数或 HTTP 请求头发送。另外,数据体积小意味着传输速度很快
  • 自包含(self-contained):JWT 包含了主体的所有信息,所以避免了每个请求都需要向 Uaa 服务验证身份,降低了服务器的负载

2.JWT 的应用场景

  • 认证:这时使用 JWT 最常见的场景。一旦用户登录成功获取 JWT 后,后续的每个请求将携带该 JWT。该 JWT 包含了用户信息、权限点等信息,根据该 JWT 包含的信息,资源服务可以控制该 JWT 可以访问的资源范围。因为 JWT 的开销很小,并且能够在不同的域中使用,单点登录是一个广泛使用 JWT 的场景
  • 信息交换:JWT 是在各方之间安全传输信息的一种方式,JWT 使用签名加密,安全性很高。另外,当使用 Header 和 Payload 计算签名时,还可以验证内容是否被篡改

3.如何使用 JWT

客户端通过提供用户名、密码向服务器请求获取 JWT,服务器判断用户名和密码正确无误之后,将用户信息和权限点经过加密以 JWT 的形式返回给客户端。在以后的每次请求中,获取到该 JWT 的客户端都需要携带该 JWT,这样做的好处就是以后的请求都不需要通过 Uaa 服务来判断该请求的用户以及该用户的权限。在微服务系统中,可以利用 JWT 实现单点登录

二、案例分析

1.案例架构设计

在本案例中有 3 个工程,分别为 eureka-server、auth-service 和 user-service。其中 auth-service 和 user-service 向 eureka-server 注册服务。auth-service 负责授权,授权需要用户提供客户端的 clientId 和 password,以及授权用户的 username 和 password。这些信息准备无误之后,auth-service 返回 JWT,该 JWT 包含了用户的基本信息和权限点信息,并通过 RSA 加密。user-service 作为资源服务,它的资源已经被保护起来了,需要相应的权限才能访问。user-service 服务得到用户请求的 JWT 后,先通过公钥解密 JWT,得到该 JWT 对应的用户的信息和用户的权限信息,再判断该用户是否有权限访问该资源

其中,在 user-service 服务的登录 API 接口(登录 API 接口不受保护)中,当用户名和密码验证正确之后,通过远程调用向 auth-service 获取 JWT,并返回 JWT 给用户。用户获取到 JWT 之后,以后的每次请求都需要在请求头中传递该 JWT,从而资源服务能够根据 JWT 来进行权限验证

2.编写主 Maven 工程

使用 IDEA 创建一个 Maven 工程作为主 Maven 工程,采用的 Spring Boot 版本为 2.1.0,Spring Cloud 版本为 Grenwich,JDK 版本为 1.8,在依赖管理引入的 spring-security-jwt 的版本为 1.0。9,spring-security-oauth2 的版本为 2.3.4,工程的 pom 文件的代码如下:

<?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>org.example</groupId><artifactId>JWT</artifactId><packaging>pom</packaging><version>1.0-SNAPSHOT</version><modules><module>eureka-server</module><module>uaa-service</module></modules><parent><artifactId>spring-boot-starter-parent</artifactId><groupId>org.springframework.boot</groupId><version>2.1.0.RELEASE</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><spring-cloud.version>Greenwich.RELEASE</spring-cloud.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></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><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-jwt</artifactId><version>1.0.9.RELEASE</version></dependency><dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.3.4.RELEASE</version></dependency></dependencies></dependencyManagement></project>

3.编写 Eureka Server

在主 Maven 工程下,创建一个 eureka-server 的 Moudle 工程,作为服务注册中心的工程。在工程的 pom 文件引入相应的依赖,包括继承了主 Maven 工程的 pom 文件,并引入 Eureka Server 的起步依赖,代码如下:

<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>
</dependencies>

在工程的配置文件 application.yml 中,配置程序的端口号为 8761,并配置不自注册,配置代码如下:

server:port: 8761eureka:client:register-with-eureka: falsefetch-registry: falseservice-url: defaultZone: http://localhost:${server.port}/eureka/

在程序的启动类 EurekaServerApplication 上加 @SpringBootApplication 注解,开启 Eureka Server 功能,代码如下:

package com.sisyphus;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {public static void main(String[] args) {SpringApplication.run(EurekaServerApplication.class, args);}
}

4.编写 Uaa 授权服务

引入依赖

在主 Maven 工程下新建一个 Moudle 工程,取名为 uaa-service。工程的 pom 文件继承了主 Maven 工程的 pom 文件,并引入工程所需的依赖,包括连接数据的依赖 mysql-connector-java 和 JPA 的起步依赖 spring-boot-starter-data-jpa、WEB 的起步依赖 spring-boot-starter-web、Eureka 客户端的起步依赖 spring-cloud-starter-eureka,以及 Spring Cloud OAuth2 相关依赖,包含了 Spring Security OAuth2 和 Spring Security JWT 依赖,代码如下:

<dependencies><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.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</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>
</dependencies>

配置文件

在程序的配置文件 application.yml 中配置程序的名称为 uaa-service,端口号为 9999,以及连接数据库驱动、JPA 的配置和服务的注册地址,代码如下;

spring:application:name: uaa-servicedatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8username: rootpassword: 123456jpa:hibernate:ddl-auto: updateshow-sql: trueserver:port: 9999eureka:client:service-url: defaultZone: http://localhost:8761/eureka/

配置 Spring Security

uaa-service 服务对外提供获取 JWT 的 API 接口,uaa-service 服务是一个授权服务器,同时也是资源服务器,需要配置该服务的 SPring Security,配置代码如下:

package com.sisyphus.config;import com.sisyphus.service.UserServiceDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import javax.servlet.http.HttpServletResponse;@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredUserServiceDetail userServiceDetail;@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().exceptionHandling().authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)).and().authorizeRequests().antMatchers("/**").authenticated().and().httpBasic();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder());}
}

在上面的配置类中,通过 @EnableWebSecurity 注解开启 WEB 资源的保护功能。在 configure(HttpSecurity http)方法中配置所有的请求都需要验证,如果请求验证不通过,则重定位到 401 的界面。在 configure(AuthenticationManagerBuilder auth) 方法中配置验证的用户信息源、密码加密的策略。向 IoC 容器注入 AuthenticationManager 对象的 Bean,该 Bean 在 OAuth2 的配置中使用,因为只有在 OAuth2 中配置了 AuthenticationManager,密码类型的验证才会开启。在本案例中,采用的是密码类型的验证

采用 BCryptPasswordEncoder 对密码进行加密,在创建用户时,密码加密也必须使用这个类

使用了 UserServiceDetail 这个类,这个类实现了 UserDetailsService 接口,代码如下:

package com.sisyphus.service;import com.sisyphus.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;@Service
public class UserServiceDetail implements UserDetailsService {@Autowiredprivate UserDao userRespository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return userRespository.findByUsername(username);}
}

UserDao 继承 JpaRepository,有一个根据用户名获取用户的方法,代码如下:

package com.sisyphus.service;import com.sisyphus.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;@Service
public class UserServiceDetail implements UserDetailsService {@Autowiredprivate UserDao userRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return userRepository.findByUsername(username);}
}

与之前的章节一样,User 对象需要实现 UserDetails 接口,Role 对象需要实现 GrantedAuthority 接口:

package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;@Entity
public class User implements UserDetails, Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false, unique = true)private String username;@Columnprivate 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 User() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}public void setAuthorities(List<Role> authorities) {this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;@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;}public void setName(String name) {this.name = name;}@Overridepublic String getAuthority() {return name;}@Overridepublic String toString() {return name;}
}

配置 Authorization Server

在 OAuth2Config 这个类中配置 AuthorizationServer,代码如下:

package com.sisyphus.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;import com.sisyphus.config.WebSecurityConfig;@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {@Autowired@Qualifier("authenticationManagerBean")private AuthenticationManager authenticationManager;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("user-service").secret("123456").scopes("service").authorizedGrantTypes("refresh_token", "password").accessTokenValiditySeconds(3600);}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer()).authenticationManager(authenticationManager);}@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()").allowFormAuthenticationForClients().passwordEncoder(NoOpPasswordEncoder.getInstance());}@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(jwtTokenEnhancer());}@Beanprotected JwtAccessTokenConverter jwtTokenEnhancer(){KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("sisyphus-jwt.jks"), "sisyphus".toCharArray());JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setKeyPair(keyStoreKeyFactory.getKeyPair("sisyphus-jwt"));return converter;}
}

在上面的配置代码中,OAuth2Config 类继承了 AuthorizationServerConfigureAdapter 类,并在 OAuth2Config 类加上 @EnableAuthorizationServer 注解,开启 Authorization Server 的功能。作为 Authorization Server 需要配置两个选项,即 ClientDetailsServiceConfigurer 和 AuthorizationServerEndpointsConfigurer

其中,ClientDetailsServiceConfigurer 配置了客户端的一些基本信息,clients.inMemory() 方法是将客户端的信息存储在内存中,.withClient(“user-service”) 方法创建了一个 ClientId 为 “user-service” 的客户端,.authorizedGrantTypes(“refresh_token”, “password”) 方法配置验证类型为 refresh_token 和 password,.scopes(“service”) 方法配置了客户端域为 “service”,.accessTokenValiditySeconds(3600) 方法配置了 Token 的过期时间为 3600 秒

AuthorizationServerEndpointsConfigurer 配置了 tokenStore 和 authenticationManager。其中 tokenStore 使用 JwtTokenStore,JwtTokenStore 并没有做任何存储,tokenStore 需要一个 JwtAccessTokenConverter 对象,该对象用于 Token 转换。本案例中使用了非对称性加密 RSA 对 JWT 进行加密

authenticationManger 需要配置 AuthenticationManager 这个 Bean,这个 Bean 来源于 WebSecurityConfigurerAdapter 的配置,只有配置了这个 Bean 才会开启密码类型的验证

AuthorizationServerSecurityConfigurer 配置了 Token 获取安全策略,获取 Token 的接口对外暴露,不验证安全,其他接口需要验证安全。另外,只有配置了 allowFormAuthenticationForClients,客户端才能请求获取 Token 接口

生成 jks 文件

在 AuthorizationServerEndpointsConfigurer 的配置中,配置 JwTokenStore 时需要使用 jks 文件作为 Token 加密的密钥。那么 jks 文件是怎样生成的呢?在本案例中,jks 文件是使用 Java keytool 生成的,在生成 jks 文件之前需要保证 Jdk 已经安装。打开计算机终端,输入以下命令

keytool -genkeypair -alias sisyphus-jwt -validity 3600 -keyalg RSA -dname "CN=jwt,OU=jtw,O=jtw,L=zurich,S=zurich,c=CH" -keypass sisyphus -keystore sisyphus-jwt.jks -storepass sisyphus

在上面的命令中,-alias 选项为别名,-keypass 和 -storepass 为密码选项,-validity 为配置 jks 文件的过期时间(单位:天)

命令执行成功后将 sisyphus-jwt.jks 复制到 Resources 目录下

获取的 jks 文件作为私钥,只允许 Uaa 服务持有,并用作加密 JWT。那么 user-service 这样的资源服务,是如何解密 JWT 的呢?这时就需要使用 jks 文件的公钥。获取 jks 文件的公钥命令如下:

keytool -list -rfc --keystore sisyphus-jwt.jks | openssl x509 -inform pem -pubkey

需要安装 openssl,在计算机终端输入上面的命令,提示需要密码,本例的密码为 “sisyphus”,输入即可,显示的公钥信息如下:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqn9PfxgRiMp5ZSL8UudJ
2CSwnmBi/xVfyIeZjs9KxUqSv6oG6esj6mLmeOTNSsatpMUmYZ5+qATZ55trWW5J
SfVI3joW34HfTnvCSkYDuGMTIBlorO4Q8Mv76FTEvUWNPRoWyG4TjrMDAwJvl/MH
1qMJoQDcY2H02LX+BERj51bzoDErsYVEDi0ENbalfF9gQQMXv33v9zA4gj6N1K1s
GZVZg1B5H5Hyi2Xad3q3YxhRHPINUH+OPp9xvPLxb1r/GlwVpJgwrWp1H6Ticgdi
iPNtRxww1hdms+ZB5yySVDDJr34yvt3w9WhVktAxPi+ssjjjZMFs8J07rZIZltc6
kwIDAQAB
-----END PUBLIC KEY-----

新建一个 public.cert 文件,将上面的公钥信息 PUBLIC KEY 复制到 public.cert 文件中并保存。以后将 public.cert 文件放在资源服务的工程的 Resource 目录下即可。到目前为止,Uaa 授权服务已经搭建完毕

需要注意的是,Maven 在项目编译时,可能会将 jks 文件编译,导致 jks 文件乱码,最后不可用。需要在工程的 pom 文件中添加以下内容:

<build><plugins><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>

5.编写 user-service 资源服务

依赖管理 pom 文件

user-service 工程的 pom 文件继承了主 Maven 工程的 pom 文件。在 user-service 工程的 pom 文件中引入 WEB 功能的起步依赖 spring-boot-starter-web、JWT 的依赖 spring-security-jwt,OAthu2 的依赖 spring-security-pauth2、数据库连接依赖 mysql-connector-java、JPA 的起步依赖 spring-boot-starter-data-jpa、Eureka 的起步依赖 spring-cloud-starter-eureka 和声明式调用 Feign 和 Hystrix 的起步依赖。user-service 工程的 pom 文件代码如下:

<dependencies><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.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
</dependencies>

配置文件 application.yml

在工程的配置文件 application.yml 中,配置程序名为 user-service,端口号为 9090.服务的注册地址为 http://localhost:8761/eureka/,以及连接的数据库的地址、用户名、密码和 JPA 的相关配置。另外,需要配置 feign.hystrix.enable 为 true,即开启 Feign 的Hystrix 功能。完整的配置代码如下:

server:port: 9090eureka:client:service-url: defaultZone: http://localhost:8761/eureka/spring:application:name: user-servicedatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8username: rootpassword: 123456jpa:hibernate:ddl-auto: updateshow-sql: truefeign:hystrix:enabled: true

配置 Resource Server

在配置 Resource Server 之前,需要注入 JwtTokenStore 类型的 Bean。建一个 JwtConfig 类,加上 @Configuration 注解,开启配置文件的功能。JwtTokenStore 类型的 Bean,该 Bean 用作 JWT 转换器。JwtAccessTokenConverter 需要设置 VerifierKey,VerifierKey 为公钥,存放在 Resource 目录下的 public.cert 文件中。JwtConfig 类的代码如下:

package com.sisyphus.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.util.FileCopyUtils;import java.io.IOException;@Configuration
public class JwtConfig {@AutowiredJwtAccessTokenConverter jwtAccessTokenConverter;@Bean@Qualifier("tokenStore")public TokenStore tokenStore(){return new JwtTokenStore(jwtAccessTokenConverter);}@Beanprotected JwtAccessTokenConverter jwtTokenEnhancer(){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;}
}

然后配置 Resource Server,新建一个 ResourceServerConfig 的类,该类继承了 ResourceServerConfigurerAdapter 类,在 ResourcesServerConfig 类上加 @EnableResourcesServer 注解,开启 Resource Server 功能。作为 Resource Server,需要配置 HttpSecurity 和 ResourceServerSecurityConfigurer 这两个选项。HttpSecurity 配置了哪些请求需要验证,哪些请求不需要验证。在本案例中,“/user/login”(登录)和 “/user/register”(注册)两个 API 接口不需要验证,其他请求都需要验证。ResourceServerSecurityConfigurer 需要配置 tokenStore,tokenStore 为之前注入 IoC 容器中的 tokenStore。代码如下:

package com.sisyphus.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {@AutowiredTokenStore tokenStore;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.tokenStore(tokenStore);}@Overridepublic void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/user/login", "/user/register").permitAll().antMatchers("/**").authenticated();}
}

配置 Spring Security

新建一个配置类 GlobalMethodSecurityConfig,在此类中通过 @EnableGlobalMehodSecurity(prePostEnabled = true) 注解开启方法级别的安全验证,代码如下:

package com.sisyphus.config;import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalMethodSecurityConfig {}

编写用户注册接口

这里用到了 User 和 Role 两个实体类,与之前一样,这里不再赘述:

package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;@Entity
public class User implements UserDetails, Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false, unique = true)private String username;@Columnprivate 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 User() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}public void setAuthorities(List<Role> authorities) {this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;@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;}public void setName(String name) {this.name = name;}@Overridepublic String getAuthority() {return name;}@Overridepublic String toString() {return name;}
}

DAO 层的 UserDao 继承了 JpaRepository 类,并有一个根据用户名获取用户的方法,代码如下:

package com.sisyphus.dao;import com.sisyphus.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;public interface UserDao extends JpaRepository<User, Long> {User findByUsername(String username);
}

Service 层的 UserService 写一个插入用户的方法,代码如下:

package com.sisyphus.service;import com.sisyphus.dao.UserDao;
import com.sisyphus.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserServiceDetail {@Autowiredprivate UserDao userRepository;public User insertUser(String username,String  password){User user=new User();user.setUsername(username);user.setPassword(BPwdEncoderUtil.BCryptPassword(password));return userRepository.save(user);}
}

在 UserServiceDetail 类中使用到了工具类 BPwdEncoderUtil,其中 BCryptPasswordEncoder 是 Spring Security 的加密类,BPwdEncoderUtil 类的代码如下:

package com.sisyphus.utils;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;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 encodePassword){return encoder.matches(rawPassword, encodePassword);}
}

在 WEB 层,在 UserController 中写一个注册的 API 接口 “/user/register”,代码如下:

package com.sisyphus.controller;import com.sisyphus.entity.User;
import com.sisyphus.service.UserServiceDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class UserController {@AutowiredUserServiceDetail userServiceDetail;@PostMapping("/register")public User postUser(@RequestParam("username") String username, @RequestParam("password") String password){return userServiceDetail.insertUser(username, password);}
}

启动所有工程,使用 Curl 注册一个账号,命令如下:

curl -X POST -d "username=sisyphus&password=123456" "localhost:9090/user/register"

返回结果为:

编写用户登录接口

在 Service 层中,在 UserServiceDetail 中添加一个 login(登录)方法,代码如下:

package com.sisyphus.service;import com.sisyphus.dao.UserDao;
import com.sisyphus.dto.UserLoginDTO;
import com.sisyphus.entity.JWT;
import com.sisyphus.entity.User;
import com.sisyphus.exception.UserLoginException;
import com.sisyphus.feign.AuthServiceClient;
import com.sisyphus.utils.BPwdEncoderUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserServiceDetail {@Autowiredprivate UserDao userRepository;public User insertUser(String username, String password){User user = new User();user.setUsername(username);user.setPassword(BPwdEncoderUtil.BCryptPassword(password));return userRepository.save(user);}@AutowiredAuthServiceClient client;public UserLoginDTO login(String username,String password){User user=userRepository.findByUsername(username);if (null == user) {throw new UserLoginException("error username");}if(!BPwdEncoderUtil.matches(password,user.getPassword())){throw new UserLoginException("error password");}// 获取tokenJWT 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 的客户端,所以需要在启动类上添加 @EnableFeignClients 注解

AuthServiceClient 通过向 uaa-service 服务远程调用 “/oauth/token” API 接口,获取 JWT。在 “/oauth/token” API 接口中需要在请求头传入 Authorization 信息,并需要传请求参数认证类型 grant_type、用户名 username 和密码 password,代码如下:

package com.sisyphus.feign;import com.sisyphus.entity.JWT;
import com.sisyphus.hystrix.AuthServiceHystrix;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(value = "uaa-service",fallback =AuthServiceHystrix.class )
public interface AuthServiceClient {@PostMapping(value = "/oauth/token")JWT getToken(@RequestHeader(value = "Authorization") String authorization, @RequestParam("grant_type") String type,@RequestParam("username") String username, @RequestParam("password") String password);}

其中,AuthServiceHystrix 为 AuthServiceClient 的熔断器,代码如下:

package com.sisyphus.hystrix;import com.sisyphus.entity.JWT;
import com.sisyphus.feign.AuthServiceClient;
import org.springframework.stereotype.Component;@Component
public class AuthServiceHystrix implements AuthServiceClient {@Overridepublic JWT getToken(String authorization, String type, String username, String password) {return null;}
}

JWT 为一个 JavaBean,它包含了 access_token、token_type 和 refresh_token 等信息,代码如下:

package com.sisyphus.entity;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;}@Overridepublic String toString() {return "JWT{" +"access_token='" + access_token + '\'' +", token_type='" + token_type + '\'' +", refresh_token='" + refresh_token + '\'' +", expires_in=" + expires_in +", scope='" + scope + '\'' +", jti='" + jti + '\'' +'}';}
}

UserLoginDTO 包含了一个 User 和一个 JWT 对象,用于返回数据的实体:

package com.sisyphus.dto;import com.sisyphus.entity.JWT;
import com.sisyphus.entity.User;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,继承自 RuntimeException,定义了一个构造方法,代码如下:

package com.sisyphus.exception;public class UserLoginException extends RuntimeException{public UserLoginException(String message){super(message);}
}

异常统一处理类为 ExceptionHandle 类,在该类中加上 @ControllerAdvice 注解表明该类是一个异常统一处理类。通过 @ExceptionHandler 注解配置了统一处理 UserLoginException 类的异常方法,统一返回了异常的 message 信息,代码如下:

package com.sisyphus.exception;import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvice
@ResponseBody
public class ExceptionHandle {@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);
}

在 “/user/login” API 接口中,需要的请求参数为用户名和密码。首先会根据用户名查询数据库,获取用户,如果用户存在,判断密码是否正确。如果密码正确,通过 Feign 客户端远程调用 uaa-service,获取 JWT,获取成功,将用户和 JWT 封装成 UserLoginDTO 对象返回。现在使用 Curl 调用登录 API 接口,执行命令如下:

curl user-service:123456@localhost:9999/oauth/token -d grant_type=password -d username=sisyphus -d password=123456

命令执行成功后,返回了 User 信息和 JWT 的信息:

测试

编写一个 “/foo” 的 API 接口,该 API 接口需要 “ROLE_ADMIN” 权限才能访问,代码如下:

package com.sisyphus.controller;import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import java.util.UUID;@RestController
@RequestMapping("/foo")
public class WebController {@RequestMapping(method = RequestMethod.GET)@PreAuthorize("hasAuthority('ROLE_ADMIN')")public String getFoo(){return "i am foo, " + UUID.randomUUID().toString();}
}

以 username 为 “sisyphus”,密码为 “123456” 登录,登录成功后返回了 JWT 对象,JWT 中有一个 access_token 的字符串。将该 Token 放在请求头中进行请求,命令如下:

curl -l -H "Authorization:Bearer {access_token} " -X GET "localhost:9090/foo"

返回结果如下:

从上面的返回信息可知,该用户没有权限访问该 API 接口。这是正常的,因为新注册的 “sisyphus” 这个用户并没有 “ROLE_ADMIN” 权限。为了方便演示,现在给 “sisyphus” 这个用户赋予 “ROLE_ADMIN” 的权限,直接在数据库中执行以下 SQL :

INSERT INTO role VALUES ('1', 'ROLE_USER'),('2', 'ROLE_ADMIN');
INSERT INTO user_role VALUES('1', '2');

插入数据后,重新登录并获取 access_token,重新请求 “/foo” API 接口,返回结果如下:

可见,当给 “sisyphus” 这个用户赋予 “ROLE_ADMIN” 权限之后,该用户具有访问 “/foo” API 接口的权限

三、总结

在本案例中,用户通过登录接口来获取授权服务的 Token。用户获取 Token 成功后,在以后每次访问资源服务的请求中都需要携带该 Token。资源服务通过公钥解密 Token,揭密成功后可以获取用户信息和权限信息,从而判断该 Token 所对应的用户是谁,具有什么权限

这个架构的优点在于,一次获取 Token,多次使用,不在每次询问 Uaa 服务该 Token 所对应的用户信息和用户权限信息。这个架构也有缺点,例如一旦用户的权限发生了改变,该 Token 中存储的权限信息并没有改变,需要重新登录获取新的 Token。就算重新获取了 Token,如果原来的 Token 没有过期,仍然是可以使用的,所以需要根据具体的业务场景来设置 Token 的过期时间。一种改进方式是将登陆成功后获取的 Token 缓存在网关上,如果用户的权限更改,将网关上缓存的 Token 删除。当请求经过网关,判断请求的 Token 在缓存中是否存在,如果缓存中不存在该 Token,则提示用户重新登录

《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统相关推荐

  1. 使用Spring Security Oauth2 和 JWT保护微服务--Uaa授权服务器的编写

    学习自深入理解微服务 采用Spring Security OAuth2 和 JWT的方式,Uaa服务只需要验证一次,返回JWT.返回的JWT包含了用户的所有信息,包括权限信息 从三个方面讲解: JWT ...

  2. 使用Spring Security Oauth2 和 JWT保护微服务--资源服务器的编写

    编写hcnet-website的资源服务 依赖管理pom文件 hcnet-website工程的pom文件继承主maven的pom文件.在hcnet-website工程的pom文件中添加web功能的起步 ...

  3. 《深入理解 Spring Cloud 与微服务构建》第八章 声明式调用 Feign

    ·# <深入理解 Spring Cloud 与微服务构建>第八章 声明式调用 Feign 文章目录 一.Feign 简介 1.简介 2.工作原理 二.写一个 Feign 客户端 三.Fei ...

  4. 《深入理解 Spring Cloud 与微服务构建》第三章 Spring Cloud

    <深入理解 Spring Cloud 与微服务构建>第三章 Spring Cloud 文章目录 <深入理解 Spring Cloud 与微服务构建>第三章 Spring Clo ...

  5. winserver2016 401您无权使用所提供的凭据查看此目录或页面_不用找了,30分钟帮你搞定使用 Spring Cloud 和 Docker 轻松构建微服务架构!...

    点击上方[全栈开发者社区]→右上角[...]→[设为星标⭐] [编者的话]如何使用Spring Boot.Spring Cloud.Docker和Netflix的一些开源工具来构建一个微服务架构.本文 ...

  6. iis7 您无权使用所提供的凭据查看此目录或页面。_使用 Spring Cloud 和 Docker 轻松构建微服务架构!...

    点击蓝色"架构文摘"关注我哟 加个"星标",每天上午 09:25,干货推送! 原文:https://dzone.com/articles/microservic ...

  7. 基于 Spring Security OAuth2和 JWT 构建保护微服务系统

    我们希望自己的微服务能够在用户登录之后才可以访问,而单独给每个微服务单独做用户权限模块就显得很弱了,从复用角度来说是需要重构的,从功能角度来说,也是欠缺的.尤其是前后端完全分离之后,我们的用户信息不一 ...

  8. Spring Cloud 与 Dubbo 的完美融合之手「Spring Cloud Alibaba」

    很早以前,在刚开始搞 Spring Cloud 基础教程的时候,写过这样一篇文章:<微服务架构的基础框架选择:Spring Cloud 还是 Dubbo ?>,可能不少读者也都看过.之后也 ...

  9. JWT实战 Spring Security Oauth2整合JWT 整合SSO单点登录

    文章目录 一.JWT 1.1 什么是JWT 1.2 JWT组成 头部(header) 载荷(payload) 签名(signature) 如何应用 1.3 JJWT 快速开始 创建token toke ...

最新文章

  1. 【Qt】QImage、QPixmap、QBitmap和QPicture
  2. mysql学习资料_一不小心,我就上传了 279674 字的 MySQL 学习资料到 github 上了
  3. autocad三维汇报,bim汇报,视图汇报方法
  4. 近世代数--群--怎么判断是不是群?
  5. P1744 采购特价商品(SPFA求最短路径模板)
  6. JavaSE(二十四)——冒泡排序、选择排序、直接插入排序以及二分查找
  7. python父进程调用子进程_Python2.7下,调用subprocess启动子进程,读取子进程标准输出若干问题...
  8. 【彻底解决】django migrate (mysql.W002) 【专治强迫症】
  9. 汉字取首字母(第三节蓝桥杯决赛)
  10. springboot毕设项目基于springboot的小区旧物交易系统的设计与实现j8o94(java+VUE+Mybatis+Maven+Mysql)
  11. [网络安全自学篇] 三十五.恶意代码攻击溯源及恶意样本分析
  12. Pooling反向传播
  13. VMware如何安装windows10教程
  14. OAuth2.0的refresh token
  15. lerna 项目中集成 babel lint-staged husky eslint
  16. iphone13电话噪音大怎么办 苹果13怎么设置电话降噪
  17. 卷积神经网络学习路线(二十一) | 旷世科技 ECCV 2018 ShuffleNet V2
  18. 浏览器崩溃原因大集合
  19. 工业物联网平台具备哪些特性
  20. Linux 服务器上搭建SVN服务端

热门文章

  1. [转]spring入门(六)【springMVC中各数据源配置】
  2. (转)linux获取/查看本机出口ip
  3. git使用笔记(一)入门
  4. 查看一个进程对应的端口号
  5. 一个代表年月的类YearMonth
  6. 数据库的持续集成和版本控制[转自INFOQ]
  7. python自动填日志_Selenium3+python自动化012+日志logging基本用法、高级用法
  8. java 事务实现原理_Spring中事务用法示例及实现原理详解
  9. Spark Streaming之updateStateByKey和mapWithState比较
  10. (45)FPGA同步复位与异步复位(同步复位)