认证:验证⽤户的合法身份,⽐如输⼊⽤户名和密码,系统会在后台验证⽤户名和密码是否合法,合法的前提下,才能够进⾏后续的操作,访问受保护的资源
9.1 微服务架构下统⼀认证场景
分布式系统的每个服务都会有认证需求,如果每个服务都实现⼀套认证逻辑会⾮常冗余,考虑分布式系统共享性的特点,需要由独⽴的认证服务处理系统认证的请求。
9.2 微服务架构下统⼀认证思路
- 基于Session的认证⽅式 在分布式的环境下,基于session的认证会出现⼀个问题,每个应⽤服务都需要在sessio中存储⽤户身份信息,通过负载均衡将本地的请求分配到另⼀个应⽤服务需要将session信息带过去,否则会重新认证。我们可以使⽤Session共享、Session黏贴等⽅案。Session⽅案也有缺点,⽐如基于cookie,移动端不能有效使⽤等
基于token的认证⽅式 基于token的认证⽅式,服务端不⽤存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地⽅,并且可以实现web和app统⼀认证机制。其缺点也很明显,token由于⾃包含信息,因此⼀般数据量较⼤,⽽且每次请求 都需要传递,因此⽐较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。
9.3 OAuth2开放授权协议/标准
9.3.1 OAuth2介绍
OAuth(开放授权)是⼀个开放协议/标准,允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容。
允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容
结合“使⽤QQ登录拉勾”这个场景拆分理解上述那句话
⽤户:我们⾃⼰
第三⽅应⽤:拉勾⽹
另外的服务提供者:QQ
OAuth2是OAuth协议的延续版本,但不向后兼容OAuth1即完全废⽌了OAuth1。9.3.2 OAuth2协议⻆⾊和流程
拉勾⽹要开发使⽤QQ登录这个功能的话,那么拉勾⽹是需要提前到QQ平台进⾏登记的(否则QQ凭什么陪着拉勾⽹玩授权登录这件事)
1)拉勾⽹——登记——>QQ平台
2)QQ 平台会颁发⼀些参数给拉勾⽹,后续上线进⾏授权登录的时候(刚才打开授权⻚⾯)需要携带这些参数
client_id :客户端id(QQ最终相当于⼀个认证授权服务器,拉勾⽹就相当于⼀个客户端了,所以会给⼀个客户端id),相当于账号
secret:相当于密码资源所有者(Resource Owner):可以理解为⽤户⾃⼰
- 客户端(Client):我们想登陆的⽹站或应⽤,⽐如拉勾⽹
- 认证服务器(Authorization Server):可以理解为微信或者QQ
资源服务器(Resource Server):可以理解为微信或者QQ
9.3.3 什么情况下需要使⽤OAuth2?
第三⽅授权登录的场景:⽐如,我们经常登录⼀些⽹站或者应⽤的时候,可以选择使⽤第三⽅授权登录
的⽅式,⽐如:微信授权登录、QQ授权登录、微博授权登录等,这是典型的 OAuth2 使⽤场景。
单点登录的场景:如果项⽬中有很多微服务或者公司内部有很多服务,可以专⻔做⼀个认证中⼼(充当认证平台⻆⾊),所有的服务都要到这个认证中⼼做认证,只做⼀次登录,就可以在多个授权范围内的服务中⾃由串⾏。9.3.4 OAuth2的颁发Token授权⽅式
1)授权码(authorization-code)
- 2)密码式(password)提供⽤户名+密码换取token令牌
- 3)隐藏式(implicit)
- 4)客户端凭证(client credentials)
授权码模式使⽤到了回调地址,是最复杂的授权⽅式,微博、微信、QQ等第三⽅登录就是这种模式。
我们重点讲解接⼝对接中常使⽤的password密码模式(提供⽤户名+密码换取token)。
9.4 Spring Cloud OAuth2 + JWT 实现
9.4.1 Spring Cloud OAuth2介绍
Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以⽤来做多个微服务的统⼀认证(验证身份合法性)授权(验证权限)。通过向OAuth2服务(统⼀认证授权服务)发送某个类型的grant_type进⾏集中认证和授权,从⽽获得access_token(访问令牌),⽽这个token是受其他微服务信任的。
注意:使⽤OAuth2解决问题的本质是,引⼊了⼀个认证授权层,认证授权层连接了资源的拥有者,在授权层⾥⾯,资源的拥有者可以给第三⽅应⽤授权去访问我们的某些受保护资源。
9.4.2 Spring Cloud OAuth2构建微服务统⼀认证服务思路
注意:在我们统⼀认证的场景中,Resource Server其实就是我们的各种受保护的微服务,微服务中的各种API访问接⼝就是资源,发起http请求的浏览器就是Client客户端(对应为第三⽅应⽤)
9.4.3 搭建认证服务器(Authorization Server)
认证服务器(Authorization Server),负责颁发token
新建项⽬lagou-cloud-oauth-server-9999
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">
<parent>
<artifactId>lagou-parent</artifactId>
<groupId>com.lagou.edu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lagou-cloud-oauth-server-9999</artifactId>
<dependencies>
<!--导入Eureka Client依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--导入spring cloud oauth2依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
<!--引入security对oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--操作数据库需要事务控制-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.lagou.edu</groupId>
<artifactId>lagou-service-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml(构建认证服务器,配置⽂件⽆特别之处)
server:
port: 9999
Spring:
application:
name: lagou-cloud-oauth-server
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: 123456
druid:
initialSize: 10
minIdle: 10
maxActive: 30
maxWait: 50000
eureka:
client:
serviceUrl: # eureka server的路径
defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
instance:
#使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
prefer-ip-address: true
#自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
⼊⼝类⽆特殊之处
认证服务器配置类
package com.lagou.edu.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.jwt.crypto.sign.Signer;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
/**
* 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private LagouAccessTokenConvertor lagouAccessTokenConvertor;
private String sign_key = "lagou123"; // jwt签名密钥
/**
* 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
* 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
// 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
security
// 允许客户端表单认证
.allowFormAuthenticationForClients()
// 开启端口/oauth/token_key的访问权限(允许)
.tokenKeyAccess("permitAll()")
// 开启端口/oauth/check_token的访问权限(允许)
.checkTokenAccess("permitAll()");
}
/**
* 客户端详情配置,
* 比如client_id,secret
* 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
* 颁发client_id等必要参数,表明客户端是谁
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 从内存中加载客户端详情
/*clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
.withClient("client_lagou") // 添加一个client配置,指定其client_id
.secret("abcxyz") // 指定客户端的密码/安全码
.resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
// 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
.authorizedGrantTypes("password","refresh_token")
// 客户端的权限范围,此处配置为all全部即可
.scopes("all");*/
// 从数据库中加载客户端详情
clients.withClientDetails(createJdbcClientDetailsService());
}
@Autowired
private DataSource dataSource;
@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
return jdbcClientDetailsService;
}
/**
* 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储,
* 那么存储在哪里呢?都是在这里配置)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints
.tokenStore(tokenStore()) // 指定token的存储方法
.tokenServices(authorizationServerTokenServices()) // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
.authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
}
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 针对jwt令牌的添加
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 设置令牌有效时间(一般设置为2个小时)
defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
}
- 关于三个configure⽅法
- configure(ClientDetailsServiceConfigurer clients)
⽤来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这⾥进⾏初始化,你
能够把客户端详情信息写死在这⾥或者是通过数据库来存储调取详情信息
- configure(AuthorizationServerEndpointsConfigurer endpoints)
⽤来配置令牌(token)的访问端点和令牌服务(token services)
- configure(AuthorizationServerSecurityConfigurer oauthServer)
⽤来配置令牌端点的安全约束.
- 关于 TokenStore
- InMemoryTokenStore
默认采⽤,它可以完美的⼯作在单服务器上(即访问并发量 压⼒不⼤的情况下,并且它在失败的时候不会进⾏备份),⼤多数的项⽬都可以使⽤这个版本的实现来进⾏ 尝试,你可以在开发的时候使⽤它来进⾏管理,因为不会被保存到磁盘中,所以更易于调试。
- JdbcTokenStore
这是⼀个基于JDBC的实现版本,令牌会被保存进关系型数据库。使⽤这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使⽤这个版本的时候请注意把”spring jdbc”这个依赖加⼊到你的 classpath当中。
- JwtTokenStore
这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进⾏编码(因此对于后端服务来说,它不需要进⾏存储,这将是⼀个重⼤优势),缺点就是这个令牌占⽤的空间会⽐较⼤,如果你加⼊了⽐较多⽤户凭证信息,JwtTokenStore 不会保存任何数据
认证服务器安全配置类
package com.lagou.edu.config;
import com.lagou.edu.service.JdbcUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.cglib.proxy.NoOp;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
/**
* 该配置类,主要处理用户名和密码的校验等事宜
*/
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
/**
* 注册一个认证管理器对象到容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编码对象(密码不进行加密处理)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 处理用户名和密码验证事宜
* 1)客户端传递username和password参数到认证服务器
* 2)一般来说,username和password会存储在数据库中的用户表中
* 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
// 实例化一个用户对象(相当于数据表中的一条用户记录)
/*UserDetails user = new User("admin","123456",new ArrayList<>());
auth.inMemoryAuthentication()
.withUser(user).passwordEncoder(passwordEncoder);*/
auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
}
}
- endpoint:/oauth/token
- 获取token携带的参数
- client_id:客户端id
- client_secret:客户单密码
- grant_type:指定使⽤哪种颁发类型,password
- username:⽤户名
- password:密码
校验token:http://localhost:9999/oauth/check_token?token=a9979518-838c-49ffff-b14a-ebdb7fde7d08
刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=abcxyz&refresh_token=8b640340-30a3-4307-93d4-ed60cc54fbc8
资源服务器(希望访问被认证的微服务)Resource Server配置
资源服务配置类
package com.lagou.edu.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
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.RemoteTokenServices;
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;
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
private String sign_key = "lagou123"; // jwt签名密钥
@Autowired
private LagouAccessTokenConvertor lagouAccessTokenConvertor;
/**
* 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
/*// 设置当前资源服务的资源id
resources.resourceId("autodeliver");
// 定义token服务对象(token校验就应该靠token服务对象)
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 校验端点/接口设置
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
// 携带客户端id和客户端安全码
remoteTokenServices.setClientId("client_lagou");
remoteTokenServices.setClientSecret("abcxyz");
// 别忘了这一步
resources.tokenServices(remoteTokenServices);*/
// jwt令牌改造
resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 无状态设置
}
/**
* 场景:一个服务中可能有很多资源(API接口)
* 某一些API接口,需要先认证,才能访问
* 某一些API接口,压根就不需要认证,本来就是对外开放的接口
* 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http // 设置session的创建策略(根据需要创建即可)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
.antMatchers("/demo/**").authenticated() // demo为前缀的请求需要认证
.anyRequest().permitAll(); // 其他请求不认证
}
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
}
思考:当我们第⼀次登陆之后,认证服务器颁发token并将其存储在认证服务器中,后期我们访问资源服务器时会携带token,资源服务器会请求认证服务器验证token有效性,如果资源服务器有很多,那么认证服务器压⼒会很⼤…….
另外,资源服务器向认证服务器check_token,获取的也是⽤户信息UserInfo,能否把⽤户信息存储到令牌中,让客户端⼀直持有这个令牌,令牌的验证也在资源服务器进⾏,这样避免和认证服务器频繁的交互……
我们可以考虑使⽤ JWT 进⾏改造,使⽤JWT机制之后资源服务器不需要访问认证服务器……
9.4.4 JWT改造统⼀认证授权中⼼的令牌存储机制
JWT令牌介绍
通过上边的测试我们发现,当资源服务和授权服务不在⼀起时资源服务使⽤RemoteTokenServices 远程请求授权 服务验证token,如果访问量较⼤将会影响系统的性能。
解决上边问题: 令牌采⽤JWT格式即可解决上边的问题,⽤户认证通过会得到⼀个JWT令牌,JWT令牌中已经包括了⽤户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法⾃⾏完成令牌校验,⽆需每次都请求认证 服务完成授权。
1)什么是JWT?
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519),它定义了⼀种简介的、⾃包含的协议格式,⽤于 在通信双⽅传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使⽤HMAC算法或使⽤RSA的公 钥/私钥对来签名,防⽌被篡改。
2)JWT令牌结构
JWT令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz
Header
头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA),例如
{
"alg": "HS256",
"typ": "JWT"
}
将上边的内容使⽤Base64Url编码,得到⼀个字符串就是JWT令牌的第⼀部分。
Payload
第⼆部分是负载,内容也是⼀个json对象,它是存放有效信息的地⽅,它可以存放jwt提供的现成字段,⽐ 如:iss(签发者),exp(过期时间戳), sub(⾯向的⽤户)等,也可⾃定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第⼆部分负载使⽤Base64Url编码,得到⼀个字符串就是JWT令牌的第⼆部分。 ⼀个例⼦:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature
第三部分是签名,此部分⽤于防⽌jwt内容被篡改。 这个部分使⽤base64url将前两部分进⾏编码,编码后使⽤点(.)连接组成字符串,最后使⽤header中声明 签名算法进⾏签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第⼀部分。
base64UrlEncode(payload):jwt令牌的第⼆部分。
secret:签名所使⽤的密钥。
认证服务器端JWT改造(改造主配置类)
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
修改 JWT 令牌服务⽅法
资源服务器校验JWT令牌
不需要和远程认证服务器交互,添加本地tokenStore
package com.lagou.edu.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
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.RemoteTokenServices;
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;
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
private String sign_key = "lagou123"; // jwt签名密钥
@Autowired
private LagouAccessTokenConvertor lagouAccessTokenConvertor;
/**
* 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
/*// 设置当前资源服务的资源id
resources.resourceId("autodeliver");
// 定义token服务对象(token校验就应该靠token服务对象)
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 校验端点/接口设置
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
// 携带客户端id和客户端安全码
remoteTokenServices.setClientId("client_lagou");
remoteTokenServices.setClientSecret("abcxyz");
// 别忘了这一步
resources.tokenServices(remoteTokenServices);*/
// jwt令牌改造
resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 无状态设置
}
/**
* 场景:一个服务中可能有很多资源(API接口)
* 某一些API接口,需要先认证,才能访问
* 某一些API接口,压根就不需要认证,本来就是对外开放的接口
* 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http // 设置session的创建策略(根据需要创建即可)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
.antMatchers("/demo/**").authenticated() // demo为前缀的请求需要认证
.anyRequest().permitAll(); // 其他请求不认证
}
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
}
9.4.5 从数据库加载Oauth2客户端信息
创建数据表并初始化数据(表名及字段保持固定)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('client_lagou123',
'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token', NULL, NULL,
7200, 259200, NULL, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
配置数据源
server:
port: 8096
#注册到Eureka服务中心
eureka:
client:
service-url:
# 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok
defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
instance:
prefer-ip-address: true #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
# 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
application:
name: lagou-service-autodeliver
zipkin:
base-url: http://127.0.0.1:9411 # zipkin server的请求地址
sender:
# web 客户端将踪迹日志数据通过网络请求的方式传送到服务端,另外还有配置
# kafka/rabbit 客户端将踪迹日志数据传递到mq进行中转
type: web
sleuth:
sampler:
# 采样率 1 代表100%全部采集 ,默认0.1 代表10% 的请求踪迹数据会被采集
# 生产环境下,请求量非常大,没有必要所有请求的踪迹数据都采集分析,对于网络包括server端压力都是比较大的,可以配置采样率采集一定比例的请求的踪迹数据进行分析即可
probability: 1
#针对的被调用方微服务名称,不加就是全局生效
#lagou-service-resume:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
# springboot中暴露健康检查等断点接口
management:
endpoints:
web:
exposure:
include: "*"
# 暴露健康接口的细节
endpoint:
health:
show-details: always
#针对的被调用方微服务名称,不加就是全局生效
lagou-service-resume:
ribbon:
#请求连接超时时间
ConnectTimeout: 2000
#请求处理超时时间
##########################################Feign超时时长设置
ReadTimeout: 3000
#对所有操作都进行重试
OkToRetryOnAllOperations: true
####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),
####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),
####如果依然不行,返回失败信息。
MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用
MaxAutoRetriesNextServer: 0 #切换实例的重试次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
logging:
level:
# Feign日志只会对日志级别为debug的做出响应
com.lagou.edu.controller.service.ResumeServiceFeignClient: debug
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.cloud.sleuth: debug
# 开启Feign的熔断功能
feign:
hystrix:
enabled: false
hystrix:
command:
default:
execution:
isolation:
thread:
##########################################Hystrix的超时时长设置
timeoutInMilliseconds: 15000
oauth2:
server:
check-token-url: http://localhost:9999/oauth/check_token
认证服务器主配置类改造
@Autowired
private DataSource dataSource;
/**
* 客户端详情配置,
* ⽐如client_id,secret
* 当前这个服务就如同QQ平台,拉勾⽹作为客户端需要qq平台进⾏登录授权认证等,提前需要到
QQ平台注册,QQ平台会给拉勾⽹
* 颁发client_id等必要参数,表明客户端是谁
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws
Exception {
super.configure(clients);
// 从内存中加载客户端详情改为从数据库中加载客户端详情
clients.withClientDetails(createJdbcClientDetailsService());
}
@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new
JdbcClientDetailsService(dataSource);
return jdbcClientDetailsService;
}
9.4.6 从数据库验证⽤户合法性
创建数据表users(表名不需固定),初始化数据
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` char(10) DEFAULT NULL,
`password` char(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (4, 'lagou-user', 'iuxyzds');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
- 操作数据表的JPA配置以及针对该表的操作的Dao接⼝此处省略….
- 开发UserDetailsService接⼝的实现类,根据⽤户名从数据库加载⽤户信息 ```java package com.lagou.edu.service;
import com.lagou.edu.dao.UsersRepository; import com.lagou.edu.pojo.Users; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; 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;
import java.util.ArrayList;
@Service public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
private UsersRepository usersRepository;
/**
* 根据username查询出该用户的所有信息,封装成UserDetails类型的对象返回,至于密码,框架会自动匹配
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = usersRepository.findByUsername(username);
return new User(users.getUsername(),users.getPassword(),new ArrayList<>());
}
}
使⽤⾃定义的⽤户详情服务对象
```java
package com.lagou.edu.config;
import com.lagou.edu.service.JdbcUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.cglib.proxy.NoOp;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
/**
* 该配置类,主要处理用户名和密码的校验等事宜
*/
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
/**
* 注册一个认证管理器对象到容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编码对象(密码不进行加密处理)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 处理用户名和密码验证事宜
* 1)客户端传递username和password参数到认证服务器
* 2)一般来说,username和password会存储在数据库中的用户表中
* 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
// 实例化一个用户对象(相当于数据表中的一条用户记录)
/*UserDetails user = new User("admin","123456",new ArrayList<>());
auth.inMemoryAuthentication()
.withUser(user).passwordEncoder(passwordEncoder);*/
auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
}
}
9.4.7 基于Oauth2的 JWT 令牌信息扩展
OAuth2帮我们⽣成的JWT令牌载荷部分信息有限,关于⽤户信息只有⼀个user_name,有些场景下我们希望放⼊⼀些扩展信息项,⽐如,之前我们经常向session中存⼊userId,或者现在我希望在JWT的载荷部分存⼊当时请求令牌的客户端IP,客户端携带令牌访问资源服务时,可以对⽐当前请求的客户端真实IP和令牌中存放的客户端IP是否匹配,不匹配拒绝请求,以此进⼀步提⾼安全性。那么如何在OAuth2环境下向JWT令牌中存如扩展信息?
认证服务器⽣成JWT令牌时存⼊扩展信息(⽐如clientIp)
继承DefaultAccessTokenConverter类,重写convertAccessToken⽅法存⼊扩展信息
package com.lagou.edu.config;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
oAuth2Authentication.setDetails(map); // 将map放入认证对象中,认证对象在controller中可以拿到
return oAuth2Authentication;
}
}
9.4.8 资源服务器取出 JWT 令牌扩展信息
资源服务器也需要⾃定义⼀个转换器类,继承DefaultAccessTokenConverter,重写extractAuthentication提取⽅法,把载荷信息设置到认证对象的details属性中
将⾃定义的转换器对象注⼊
业务类⽐如Controller类中,可以通过SecurityContextHolder.getContext().getAuthentication()获取到认证对象,进⼀步获取到扩展信息
Object details =
SecurityContextHolder.getContext().getAuthentication().getDetails();
获取到扩展信息后,就可以做其他的处理了,⽐如根据userId进⼀步处理,或者根据clientIp处理,或者其他都是可以的了
9.4.9 其他
关于JWT令牌我们需要注意
- JWT令牌就是⼀种可以被验证的数据组织格式,它的玩法很灵活,我们这⾥是基于Spring Cloud Oauth2 创建、校验JWT令牌
- 我们也可以⾃⼰写⼯具类⽣成、校验JWT令牌
- JWT令牌中不要存放过于敏感的信息,因为我们知道拿到令牌后,我们可以解码看到载荷部分的信息
- JWT令牌每次请求都会携带,内容过多,会增加⽹络带宽占⽤