学习自深入理解微服务
采用Spring Security OAuth2 和 JWT的方式,Uaa服务只需要验证一次,返回JWT。返回的JWT包含了用户的所有信息,包括权限信息
从三个方面讲解:
JWT详解Spring Security Oauth2和JWT保护微服务案例详解总结
JWT简介
什么是JWT
JSON Web TOken(JWT)是一种开放的标准(RFC7517),JWT定义了一种紧凑且自包含的标准,该标准旨在将各个主体的信息包装成JSON对象。主体信息通过数字签名进行加密和验证的。常使用HMAC算法或RSA算法(公钥/私钥非对称性加密)算法对JWT进行签名,安全性很高,下面解释JWT的特点:
紧凑型(compact):由于是加密后的字符串,JWT数据体积非常小,可以通过POST请求或者Http请求头发送,另外,数据体积小意味着传输速度很快自包含(self-contained):JWT包含了主体的所有信息,所以避免每个请求都需要向Uaa服务验证身份,降低了服务器的负载
JWT的结构
JWT由三个部分组成,分别以’.'分隔,组成部分如下。
header(头)Payload(有效载荷)Signature(签名)
因此JWT的通常格式如下
xxxxxxxxxxx.yyyyyyy.zzzzzz
下面来讲解这三个组成部分
Header
header通常有两部分组成,令牌的类型(即JWT)和使用的算法类型,如HMAC,SHA256和RSA。例如:
{"alg":"HS256""typ":"JWT"}
将Header用Base64编码作为JWT的第一部分
2. Payload
这是JWT的第二部分,包含了用户的一些信息和Claim(声明,权利)。有三种类型的claim:保留,私人和公开。一个典型的Payload如下:
{"sub":"1234567890""name":"John Doe""admin":true}
将Payload用Base64编码作为JWT的第二部分
3. Signature
要创建签名部分,需要将base64编码后的Header,PayLoad和密钥进行签名,一个典型的格式如下:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
JWT的应用场景
什么时候使用JWT呢?JWT的使用场景如下:
认证:这是使用JWT的最常见的场景。一旦用户登录成功获取JWT之后,后续每个请求将携带该JWT。该JWT包含了用户信息,权限点等信息,根据JWT包含的信息,资源服务可以控制该JWT可以访问的资源范围,因为JWT的开销很小,并且能够在不同的域中使用,因此单点登录是一个广泛使用JWT的场景信息交换:JWT是在各方之间安全传输信息的一种方式,JWT使用签名加密,安全性很高。另外,当使用Header和PayLoad计算签名时,还可以验证内容是否被篡改
如何使用JWT
下面来看最常见的应用场景,即认证,客户端通过提供用户名,密码向服务器请求获取JWT,服务器判断用户和密码无误后,将用户信息和权限点经过加密以后以JWT的形式返回客户端。在以后的每次请求中否不需要通过Uaa服务来判断该请求的用户一级用户的权限。在微服务系统中,可以利用JWT实现单点登录。
案例分析
案例架构设计
在本案例中有两个工程,分别为auth-service-1,hcnet-website.两个工程向Consul注册服务。auth-service负责授权,授权需要用户提供用户客户端的clientId和password,以及授权用户的username和password。这些信息准备无误后,auth-service返回JWT,该JWT包含了用户的基本信息和权限点信息,并通过RSA加密。hcnet-website作为资源服务,他的资源已经被保护起来了,需要相应的权限才能访问。hcnet-website服务得到用户请求的JWT之后,先通过公钥解密JWT,得到JWT对应用户的信息和用户的权限信息,在判断该用户是否有权限访问该资源。
在hcnet-website服务的登录API接口(登录API接口不受保护)中,当用户名和密码验证正确之后,以后的每次请求都需要在请求头中传递该JWT,从而资源服务能够根据JWT来进行授权验证。(我用的Consul)
###编写主maven程序
使用IDEA创建一个MAven工程作为主Maven工程,Spring Cloud版本为Hoxton.SR1 ,Spring Boot 的版本为2.2.4
JDK版本1.8,工程POM文件如下
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0" xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><!--指定Spring Boot版本--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><!--主Maven工程--><groupId>cn.hcnet.blog</groupId><artifactId>blog-base</artifactId><version>0.0.1-SNAPSHOT</version><!--父组件打包方式--><packaging>pom</packaging><name>blog-base</name><description>Demo project for Spring Boot</description><!--公共属性设置--><properties><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR1</spring-cloud.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding></properties><!--子Model工程--><modules><module>config-server</module><module>user-service</module><module>monitoring-server</module><module>uaa-server</module><module>gateway-server</module><module>admin-server</module><module>log-server</module><module>upload-apk</module><module>micro-consumer</module><module>hcnet-website</module><module>hcnet-website-1</module></modules><!--公共依赖--><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--测试属性--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><!--JMX远程管理组件--><dependency><groupId>org.jolokia</groupId><artifactId>jolokia-core</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></dependencies></dependencyManagement><!-- <build>--><!-- <plugins>--><!-- <plugin>--><!--<groupId>org.springframework.boot</groupId>--><!--<artifactId>spring-boot-maven-plugin</artifactId>--><!-- </plugin>--><!-- </plugins>--><!-- </build>--></project>
编写Uaa授权服务
引入依赖在主maven工程下新建一个Module工程,取名为uaa-service-1.工程的POM文件集成了主MANVEN工程的POM文件,并引入工程起步依赖,包括连接数据库驱动依赖mysql-connector-java。Mybatis依赖,MyBatis代码生成器依赖,Druid数据源依赖,Consul服务发现依赖,一级Spring Cloud Oauth2的起步依赖 spring -cloud-strter-oauth2
其中SpringCloudOauth2的起步依赖包含了Spring Security Oauth2和Spring Security JWT 的依赖等,代码如下
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0" xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>cn.hcnet.blog</groupId><artifactId>blog-base</artifactId><version>0.0.1-SNAPSHOT</version></parent><groupId>cn.hcnet.blog</groupId><artifactId>uaa-server-1</artifactId><version>0.0.1-SNAPSHOT</version><name>uaa-server-1</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><!--Spring Cloud Oauth2依赖--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><!--数据库驱动依赖--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--MyBatis依赖--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot</artifactId><version>2.1.1</version></dependency><!--MyBatis代码生成器依赖--><dependency><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-core</artifactId><version>1.4.0</version></dependency><!--集成Druid数据源--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.21</version></dependency><!--集成Consul服务注册与发现--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-consul-discovery</artifactId></dependency><!--集成健康检查--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
2.配置文件
在程序的配置文件application.yml中配置程序的名陈为uaa-server,端口号为8212,以及连接数据源配置,MyBatis映射配置,服务注册配置,代码如下
server:port: 8212 #端口spring:application:name: uaa-server #服务名cloud:consul: #服务发现host: localhostport: 8500discovery:service-name: ${spring.application.name}health-check-url: http://localhost:8212/actuator/healthhostname: localhostdatasource: #数据源type: com.alibaba.druid.pool.DruidDataSourcedruid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql:///hc_official_website_1?useUnicode=true&characterEncoding=UTF8&serverTime=Asia/Shanghaifilters: stat, wall, configinitial-size: 1max-active: 100max-wait: 60000min-idle: 1time-between-eviction-runs-millis: 60000min-evictable-idle-time-millis: 300000validation-query: select 'x'test-while-idle: truetest-on-borrow: falsetest-on-return: falsepool-prepared-statements: truemax-open-prepared-statements: 50max-pool-prepared-statement-per-connection-size: 20username: rootpassword: 123456name: druidDataSourcemybatis: #MyBatis持久层框架mapper-locations: classpath:/mappers/*Mapper.xmlmanagement: #健康监控暴露endpoint:health:show-details: alwaysendpoints:web:exposure:include: '*'
3.配置Spring Security
uaa-server服务对外提供获取JWT的API接口,uaa-server服务是一个授权服务器,同时也是一个资源服务器,需要配置该服务的Spring Security,配置代码如下
@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//设置获取基本用户信息和用户权限的信息方法类userDetailService//设置密码对比策略,采用BCrypt加密算法auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());}//登录认证过程时委托给AuthenticationManager完成的,Authentication提供了//一个默认的实现ProviderManager,而ProviderManager又将验证委托给了AuthenticationProvider//DaoAuthenticationProvider继承了AuthenticationProvider的抽象实现,AbstractUserdetailsAuthenticationProvider//完成了从DAO方式获取验证需要的用户信息,//对于我们一般用的DaoAuthenticationProvider是由UserDetailsService专门获取验证信息的//userDetailsSerrvice接口只有一个方法,loadUserByUsername(String username),一般需要我们实现此接口方法//根据用户名加载登录认证和访问授权所需要的信息,并返回一个UserDetils的实现类,//后面登录认证和访问授权搜需要用到此种的信息@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//登录认证,使用内置登录验证过滤器@Overrideprotected void configure(HttpSecurity http) throws Exception {//使用JWT默认关闭跨域攻击防御http.csrf().disable()//登录异常处理.exceptionHandling()//认证失败异常处理,返回401未授权界面.authenticationEntryPoint(((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))).and().authorizeRequests()//路径访问原则,已经认证.antMatchers("/**").authenticated().and().httpBasic();}}
在上面的配置类中,通过@EnableWebSecurity注解开启Web资源的保护功能。在configure(HttpSecurity http) 方法中配置所有请求都需要验证,如果请求验证不通过,则定位到401界面。在configure(AuthenticationManagerBuilder auth)方法中配置验证的用户信息源,密码加密策略。向IOC容器中注入AuthenticationManager对象的Bean ,该Bean 在Oauth2的配置中使用,因为只有在OAuth2中配置了AuthencationManager,密码类型的验证才会开启。在本案例中,采用的时密码类型的验证。
采用BcryptPasswordEncoder对密码进行加密,在创建用户的同时,密码加密也必须使用这个类
使用了UserDetailService这个类,需要对其进行实现
实现类代码如下
public class JwtUserDetailServiceImpl implements UserDetailsService {@Autowiredprivate SysUserService sysUserService;@Autowiredprivate SysMenuService sysMenuService;//唯一函数根据用户名过去用户信息源以及用户权限信息@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser = sysUserService.findUserByName(username);if (sysUser == null){throw new UsernameNotFoundException("用户没有找到");}Set<String> perms = sysUserService.findPermissions(username);//权限封装List<GrantedAuthority> authorities =perms.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());return new JwtUserDetails(sysUser.getName(), sysUser.getPassword(), authorities);}}
当然。我们还需要实现user类
实现代码如下
public class JwtUserDetails extends User {public JwtUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {super(username, password, true, true, true, true, authorities);}public JwtUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);}}
4.配置Authorization Server
在Oauth2Config这个类中配置AuthorizationServer,其代码如下
@Configuration//开启授权服务器注解@EnableAuthorizationServerpublic class OAuth2Config extends AuthorizationServerConfigurerAdapter {@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {super.configure(security);}/*** 配置客户端详情信息(Client Details)* 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService)* 这里写在内存中写死* @param clients* @throws Exception*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory()//客户端名.withClient("user-service")//客户端密码.secret("123456")//作用域.scopes("service")//使用令牌刷新模式和密码模式(另外两种隐形以及授权码模式)//令牌失效后可以刷新.authorizedGrantTypes("refresh_token","password")//设置过期时间.accessTokenValiditySeconds(3600);}/*** 用来配置令牌端点(Token EndPoints)一级令牌(Token)的访问端点和令牌服务* 主要是token store类型以及tokenStore签名类型* @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {/*** tokenStore设置tokenStore类型,* tokenEnhancer(token增强):设置非对称算法进行增强*/endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenEnhancer()).authenticationManager(authenticationManager);}//获取AuthenticationManager进行密码的验证@Autowired@Qualifier("authenticationManagerBean")private AuthenticationManager authenticationManager;/*** token存储方式,这里使用JwtTokenStore方式存储* JwtTokenStore需要使用JwtAccessTokenConvert进行token类型的转换* 这里是用RSA(公钥/私钥非对称加密方式),进行token类型转换* @return*/@Beanpublic TokenStore tokenStore(){//令牌存储格式以及加密转换方式return new JwtTokenStore(jwtAccessTokenEnhancer());}/*** JwtAccessToken是用来生成token的转换器的,token令牌默认是有签名的* ,资源服务器需要验证这个签名。此处加密以及验证签名有两种:* 对称加密以及非对称加密* 对称加密需要资源服务器和授权服务器存储同一个key值* 非对称加密可以使用密钥进行简爱密,暴露公钥给资源服务器验证签名* 引用:/fp2952/p/8973613.html*//*** JWTs授权令牌类型转换* 使用加密算法进行类型转换,即加密* @return*/protected JwtAccessTokenConverter jwtAccessTokenEnhancer(){//使用非对称加密RSA对JWT进行加密//导入证书KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(//获取上下文私钥,设置令牌转化类型的加密算法//或者说根据TOKEN的组成,头部,有效负载,签名//签名是对头部,有效负载以及密钥进行加密保护//即,头部,有效负载,签名保护new ClassPathResource("fzp-jwt.jks"), "fzp123".toCharArray());JwtAccessTokenConverter converter = new JwtAccessTokenConverter();//设置密钥对converter.setKeyPair(keyStoreKeyFactory.getKeyPair("fzp-jwt"));return converter;}}
5.生成jks文件(密钥)
在AuthorizationServerEndpointsConfigger的配置中。配置JwtTokenStore时需要使用jks文件爱呢作为TOken加密的密钥
生成方式截图:
获取的jks文件作为私钥只允许uaa服务持有,并用做加密JWT,那么hcnet-website这样的资源服务,如何解密JWT呢?这就需要使用jks文件的公钥。获取jks公钥的命令如下:
新建一个public.cert文件,将上面的公钥信息复制到public.cert文件中。然后将public.cert文件放到资源服务工程的Resource目录下。到目前位置Uaa授权服务已经搭建完成
需要注意,maven在项目编译的时候,可能将JKS文件编译,导致JKS文件乱码,最后不可用。
需要在POM文件中添加一下内容