普通令牌问题
普通令牌的问题
: 以OAuth2的密码模式为例进行说明,客户端每次访问资源时, 资源服务都需要远程请求认证服务去校验令牌的合法性
导致执行性能低
如果能够让资源服务自己校验令牌的合法性,这样就可以省去远程请求认证服务的成本并提高性能
常见两种认证方式
有状态认证
: 基于Session
的方式,用户登录成功后需要将用户的身份信息存储在服务端的Session中,这样会加大服务端的存储压力并且不适合在分布式系统中应用
无状态认证
: 基于令牌
的方式,将用户身份信息存储在令牌中,可以在分布式系统中实现认证
JWT的格式
如果令牌采用JWT格式就可以不依赖认证服务在资源服务中自行校验令牌
Json Web Token(JWT)是一种使用Json格式传递数据
的网络令牌技术,它是一个开放的行业标准(RFC 7519)
JWT令牌的优缺点
JWT令牌由头部,负载,签名
三部分组成,每部分中间使用点.
分隔
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQyNTQ2NzIsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6Ijg4OTEyYjJkLTVkMDUtNGMxNC1iYmMzLWZkZTk5NzdmZWJjNiIsImNsaWVudF9pZCI6ImMxIn0.wkDBL7roLrvdBG2oGnXeoXq-zZRgE9IVV2nxd-ez_oA
Header
: 头部是个Json对象,包括令牌的类型(即JWT)及使用的签名算法(如HMAC、MD5、HS526、SHA256或RSA)
{
"alg": "HS256",
"typ": "JWT"
}
// 将上面的内容使用Base64Url编码得到的字符串就是JWT令牌的第一部分
Payload
: 负载也是一个Json对象,它是存放有效信息的地方如用户信息,但是不建议存放敏感信息因为此部分可以解码还原得到原始内容
存放JWT提供的现成字段
: iss(签发者)、exp(过期时间戳)、sub(面向的用户)自定义字段
:{
"sub": "1234567890",
"name": "456",
"admin": true
}
// 将上面的内容使用Base64Url编码得到的字符串就是JWT令牌的第二部分
Sugbature
: 使用Base64Url编码
将前两部分进行编码,然后使用点.
连接组成字符串,最后使用Header中声明的签名算法对Header和Payload
中的内容进行签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
密钥
: 在使用签名算法对Header和Payload
中的内容进行进行签名时需要使用密钥
(这个密钥不对外公开)
对称加密(效率高)
: 认证服务和资源服务使用相同的密钥,如果一旦密钥泄露别人就可以解析JWT令牌中的内容,然后使用同样的密钥和签名算法伪造JWT令牌非对称加密(效率低但更安全)
: 认证服务自己保留私钥,将公钥下发给受信任的客户端如资源服务,公钥和私钥是配对的,成对的公钥和私钥才可以正常加密、解密并且签名是不可逆的
如果第三方更改了JWT令牌中的内容
那么服务器验证时就会失败,要想保证签名正确,必须保证内容、密钥与签名前一致
那么服务器验证前面就会失败,要想保证签名正确,必须保证内容、密钥与签名前一致
测试认证服务生成JWT令牌
第一步: 在认证服务工程的config/TokenConfig
中配置令牌的生成和存储策略
@Configuration
public class TokenConfig {
// 认证服务生成JWT令牌时的密钥
private String SIGNING_KEY = "mq123";
@Autowired
TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
// 采用Jwt的方式存储令牌
return new JwtTokenStore(accessTokenConverter());
}
// Jwt令牌转换器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
//令牌管理服务
@Bean(name = "authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
第二步: 认证服务会将WebSecurityConfig
配置类中配置的用户身份信息和令牌其他的相关信息写入JWT令牌
@Bean
public UserDetailsService userDetailsService() {
// 1. 配置用户信息服务
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 2. 创建用户信息这里暂时写死,后面需要从数据库中动态查询, Kyle的权限是p1,Lucy的权限是p2
// User是Spring Security提供的工具类
manager.createUser(User.withUsername("Kyle").password("123").authorities("p1").build());
manager.createUser(User.withUsername("Lucy").password("456").authorities("p2").build());
return manager;
}
第三步: 重启认证服务,使用HttpClient通过密码模式指定用户的身份信息申请令牌
# 密码模式
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=Kyle&password=123
第四步: 查看响应的Jwt令牌
响应参数 | 描述 |
---|---|
access_token | 生成的JWT令牌,用于客户端访问资源时使用 |
token_type | bearer是在RFC6750中定义的一种token类型,在携带JWT访问资源时需要在head中加入bearer jwt令牌内容 |
refresh_token | 当JWT令牌快过期时使用刷新令牌可以再次生成JWT令牌 |
expires_in | 过期时间(秒) |
scope | 令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权 |
jti | 令牌的唯一表示 |
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJLeWxlIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY3ODQyNzQ5NiwiYXV0aG9yaXRpZXMiOlsicDEiXSwianRpIjoiY2IyOTI0ZjYtOGZiOS00N2ViLThjNGEtMWFmMjkzZWU4NTg4IiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.aVZOsHBEuowof41HgV2auyDrRh9ZiNfwn4qoQWjla7o",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJLeWxlIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImNiMjkyNGY2LThmYjktNDdlYi04YzRhLTFhZjI5M2VlODU4OCIsImV4cCI6MTY3ODY3OTQ5NiwiYXV0aG9yaXRpZXMiOlsicDEiXSwianRpIjoiNjFhNWRmOGItZTc3ZS00YmVkLWE3OTQtZTlmMjJkM2FmMTYyIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.JqEL9V4Yn8tWYtvH46wtbAgJQ1dEoseuWyQhDdZNveo",
"expires_in": 7199,
"scope": "all",
"jti": "cb2924f6-8fb9-47eb-8c4a-1af293ee8588"
}
第五步: 通过check_token接口
校验响应的JWT令牌即解析令牌中的信息
// 校验JWT令牌
POST {{auth_host}}/auth/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJLeWxlIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY3ODQyOTg5MywiYXV0aG9yaXRpZXMiOlsicDEiXSwianRpIjoiMzNhMzg4YWMtNzNmYS00ODBmLWEzMWUtOTdmOTJmMjBkNWZkIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.cTcfIzL2avSp2XEsPvGU2IoJ060ooln1hARZCrvCxp4
// 响应结果
{
"aud": [
"xuecheng-plus"
],
"user_name": "Kyle",
"scope": [
"all"
],
"active": true,
"exp": 1678429893,
"authorities": [
"p1"
],
"jti": "33a388ac-73fa-480f-a31e-97f92f20d5fd",
"client_id": "XcWebApp"
}
本项目各个微服务就是资源服务,如当客户端申请到JWT令牌后可以携带JWT令牌去内容管理服务
查询课程信息,此时内容管理服务需要对携带的JWT令牌进行校验
第一步: 在内容管理服务的content-api
工程中添加依赖
<!--认证相关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
第二步: 在内容管理服务的content-api
工程中添加TokenConfig
令牌策略配置类
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "mq123";
// 使用密钥校验令牌
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
// 存储令牌
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
}
第三步: 添加资源服务配置类ResourceServerConfig
,配置需要身份认证的URL即对访问的资源进行安全控制,只有携带jwt令牌且签证通过后才能访问
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "xuecheng-plus";
// Jwt令牌
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用CSRF保护
.authorizeRequests() // 配置对请求的授权策略
.antMatchers("/r/**", "/course/**").authenticated() // 指定以"/r/"和"/course/"开头的请求路径需要进行身份认证才能访问
.anyRequest().permitAll(); // 允许其他所有请求(除了以上的请求)不需要进行身份认证就可以访问
}
}
第四步: 重启内容管理服务,使用HttpClient
直接访问内容管理服务的接口(本质还是资源)并查看响应结果
# 直接根据课程id查询课程基本信息
GET {{content_host}}/content/course/22
Content-Type: application/json
# 响应结果
{
"error": "unauthorized",# 未认证
"error_description": "Full authentication is required to access this resource"
}
第五步: 携带JWT令牌访问内容管理服务的接口,在请求头中添加Authorization
,内容为Bearer Jwt令牌
,Bearer用于通过oauth2.0协议访问资源
###### 首先通过密码模式访问认证服务获取Jwt令牌,令牌中存储的是当前用户的身份信息
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=Kyle&password=123
# 携带正确的JWT令牌访问内容管理服务中的资源
GET {{content_host}}/content/course/160
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJLeWxlIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY3ODQzOTMwOSwiYXV0aG9yaXRpZXMiOlsicDEiXSwianRpIjoiNTAxNDNiZTItOGM3ZC00MmUzLWEwNDMtMTQwMGQ5NWQ5MmZiIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.o3nWLeRkJncEnnZ0egFmBpyC8Keq-L8IY6k0Uc0a96c
# 如果没有携带jwt令牌或内容错误则报令牌无效的错误
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
测试获取用户身份
客户端携带JWT令牌访问资源服务时, 由于JWT令牌中记录了用户身份信息,资源服务校验签名通过后将Header(令牌类型)和Payload(有效信息)
两部分内容还原
SecurityContextHolder
上下文中,SecurityContext
会与当前线程进行绑定,方便在接口中获取用户信息在查询课程的接口中添加获取用户身份信息的业务,重启内容管理服务查看控制台是否会输出申请该令牌的用户身份信息
@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable Long courseId) {
// 取出当前用户的身份
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 当前用户身份为:Kyle
System.out.println("当前用户身份为:" + principal);
return courseBaseInfoService.getCourseBaseInfo(courseId);
}