普通令牌的问题
客户端申请到令牌,接下来客户端携带令牌去访问资源,到资源服务器将会校验令牌的合法性。
资源服务器如何校验令牌的合法性?
我们以OAuth2的密码模式为例进行说明:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQyNTQ2NzIsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6Ijg4OTEyYjJkLTVkMDUtNGMxNC1iYmMzLWZkZTk5NzdmZWJjNiIsImNsaWVudF9pZCI6ImMxIn0.wkDBL7roLrvdBG2oGnXeoXq-zZRgE9IVV2nxd-ez_oA
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
1.Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
一个例子如下:
下边是Header部分的内容
{
"alg": "HS256",
"typ": "JWT"
}
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
2. Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的信息字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
一个例子:
{
"sub": "1234567890",
"name": "456",
"admin": true
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
为什么JWT可以防止篡改?
第三部分使用签名算法对第一部分和第二部分的内容进行签名,常用的签名算法是 HS256,常见的还有md5,sha 等,签名算法需要使用密钥进行签名,密钥不对外公开,并且签名是不可逆的,如果第三方更改了内容那么服务器验证签名就会失败,要想保证验证签名正确必须保证内容、密钥与签名前一致。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
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 java.util.Arrays;
/**
* 生成JWT
**/
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "mq123";
@Autowired
TokenStore tokenStore;
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@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;
}
}
重启服务。
使用httpclient通过密码模式申请令牌
### 密码模式
POST localhost:63070/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123
生成jwt的示例如下:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzMzE2OTUsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6ImU5ZDNkMGZkLTI0Y2ItNDRjOC04YzEwLTI1NmIzNGY4ZGZjYyIsImNsaWVudF9pZCI6ImMxIn0.-9SKI-qUqKhKcs8Gb80Rascx-JxqsNZxxXoPo82d8SM",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJlOWQzZDBmZC0yNGNiLTQ0YzgtOGMxMC0yNTZiMzRmOGRmY2MiLCJleHAiOjE2NjQ1ODM2OTUsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6ImRjNTRjNTRkLTA0YTMtNDIzNS04MmY3LTFkOWZkMmFjM2VmNSIsImNsaWVudF9pZCI6ImMxIn0.Wsw1Jc-Kd_GFqEugzdfoSsMY6inC8OQsraA21WjWtT8",
"expires_in": 7199,
"scope": "all",
"jti": "e9d3d0fd-24cb-44c8-8c10-256b34f8dfcc"
}
1、access_token,生成的jwt令牌,用于访问资源使用。
2、token_type,bearer是在RFC6750中定义的一种token类型,在携带jwt访问资源时需要在head中加入bearer jwt令牌内容
3、refresh_token,当jwt令牌快过期时使用刷新令牌可以再次生成jwt令牌。
4、expires_in:过期时间(秒)
5、scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。
6、jti:令牌的唯一标识。
我们可以通过check_token接口校验jwt令牌
###校验jwt令牌
POST localhost:63070/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJzdHUxIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY2NDM3MTc4MCwiYXV0aG9yaXRpZXMiOlsicDEiXSwianRpIjoiZjBhM2NkZWItMzk5ZC00OGYwLTg4MDQtZWNhNjM4YWQ4ODU3IiwiY2xpZW50X2lkIjoiYzEifQ.qy46CSCJsH3eXWTHgdcntZhzcSzfRQlBU0dxAjZcsUw
{
"aud": [
"res1"
],
"user_name": "zhangsan",
"scope": [
"all"
],
"active": true,
"exp": 1664371780,
"authorities": [
"p1"
],
"jti": "f0a3cdeb-399d-48f0-8804-eca638ad8857",
"client_id": "c1"
}
携带令牌访问资源服务
拿到了jwt令牌下一步就要携带令牌去访问资源服务中的资源,项目各个微服务就是资源服务,比如:A服务,客户端申请到jwt令牌,携带jwt去B服务查询信息,此时B服务要对jwt进行校验,只有jwt合法才可以继续访问。
1、在B服务的的工程中添加依赖
<!--认证相关-->
<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>
2、在资源服务配置包中创建TokenConfig和ResouceServerConfig 配置类。
可以在ResouceServerConfig类中配置需要认证的url。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
/**
* @description 资源服务配置
*
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
//资源服务标识
public static final String RESOURCE_ID = "xuecheng-plus";
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll();
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
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 java.util.Arrays;
/**
*JWT配置类
**/
@Configuration
public class TokenConfig {
String SIGNING_KEY = "mq123";
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
启动服务
使用httpclient测试:
1、访问根据id查询信息接口
### 查询课程信息
GET http://localhost:63040/content/course/2
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
### 携带token访问资源服务
GET http://localhost:63040/content/course/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzMzM0OTgsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6IjhhM2M2OTk1LWU1ZGEtNDQ1Yy05ZDAyLTEwNDFlYzk3NTkwOSIsImNsaWVudF9pZCI6ImMxIn0.73eNDxTX5ifttGCjwc7xrd-Sbp_mCfcIerI3lGetZto
在请求头中添加Authorization,内容为Bearer 令牌,Bearer用于通过oauth2.0协议访问资源。
如果携带jwt令牌且jwt正确则正常访问资源服务的内容。
如果不正确则报令牌无效的错误:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
测试获取用户身份
jwt令牌中记录了用户身份信息,当客户端携带jwt访问资源服务,资源服务验签通过后将前两部分的内容还原即可取出用户的身份信息,并将用户身份信息放在了SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId){
//取出当前用户身份
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(principal);
return courseBaseInfoService.getCourseBaseInfo(courseId);
}
测试时需要注意:
1、首先在资源服务配置中指定安全拦截机制 /course/开头的请求需要认证,即请求/course/{courseId}接口需要携带jwt令牌且签证通过。
2、认证服务生成jwt令牌将用户身份信息写入令牌,目前还是将用户信息硬编码并暂放在内存中。
如下:
WebSecurityConfig.java
@Bean
public UserDetailsService userDetailsService() {
//这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
3、我们在使用密码模式生成jwt令牌时用的是zhangsan的信息,所以jwt令牌中存储了zhangsan的信息,那么在资源服务中应该取出zhangsan的信息才对。
清楚了以上内容,下边重启内容管理服务,跟踪取到的用户身份是否正确。
因篇幅问题不能全部显示,请点此查看更多更全内容