权限-JWT令牌(七)

JWT介绍

通过上一篇文章的测试我们发现,当资源服务和授权服务不在一起时资源服务使用RemoteTokenServices 远程请求授权 服务验证token,如果访问量较大时将会影响系统的性能 。

 @Bean
    public ResourceServerTokenServices tokenService() {
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:9001/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }

解决上边问题

令牌采用JWT格式即可解决上边的问题,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信

息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证

服务完成授权。

什么是JWT?

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于

在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公

钥/私钥对来签名,防止被篡改。

官网:https://jwt.io/

标准:https://tools.ietf.org/html/rfc7519

JWT令牌的优点:

1)jwt基于json,非常方便解析。

2)可以在令牌中自定义丰富的内容,易扩展。

3)通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

4)资源服务使用JWT可不依赖认证服务即可完成授权。

JWT缺点:

1)JWT令牌较长,占存储空间比较大。

JWT令牌结构

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)

一个例子如下:

下边是Header部分的内容

{ 
"alg": "HS256", 
"typ": "JWT" 
} 

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

Payload

第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比

如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

一个例子:

{ 
"sub": "1234567890", 
"name": "456", 
"admin": true 
} 

Signature

第三部分是签名,此部分用于防止jwt内容被篡改。

这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明

签名算法进行签名。

一个例子:

HMACSHA256( 
base64UrlEncode(header) + "." + 
base64UrlEncode(payload), 
secret) 

base64UrlEncode(header):jwt令牌的第一部分。

base64UrlEncode(payload):jwt令牌的第二部分。

secret:签名所使用的密钥。

配置JWT令牌服务

我们通过改造上一篇文章来实现JWT令牌服务

在uaa中配置jwt令牌服务,即可实现生成jwt格式的令牌。

推荐文章

  1. TokenConfig
package org.dong.oauth.uaa.config;

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;

@Configuration
public class TokenConfig {
    //定义JWT令牌服务
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
    //密钥
    private String SIGNING_KEY = "uaa123";

    /**
     * 存储令牌的三种方式
     * 1. JdbcTokenStore(jdbc方式)
     * 2. JWTTokenStore(JWT方式)
     * 3. InMemoryTokenStore(内存方式,下面我们使用内存方式存储普通令牌)
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    //使用密钥生成令牌
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //对称秘钥,资源服务器使用该秘钥来验证
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}

2、定义JWT令牌服务

AuthorizationServer

该类相比上篇文章新增的内容为

@Autowired 
private JwtAccessTokenConverter accessTokenConverter;

//AuthorizationServerTokenServices方法中
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
services.setTokenEnhancer(tokenEnhancerChain);

修改后的全部代码

/**
 * 授权服务的配置
 * @EnableAuthorizationServer注解标明它是授权服务
 *AuthorizationServerConfigurerAdapter类implements了AuthorizationServerConfigurer类
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    //因为配置客服端详情的时候客服端的授权类型authorizedGrantTypes有password
    //所以我们需要配置AuthenticationManager和userDetailsService
    //这个认证管理器我配置到SecurityConfig类中
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    /**
     * 1. 配置客服端详情服务ClientDetailsServiceConfigurer
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer client) throws Exception {
        client.inMemory()//使用内存存储
                .withClient("c1")//with一个客户端 参数是客服端id
                .secret(new BCryptPasswordEncoder().encode("secret"))//密钥
                .resourceIds("res1")//客服端可以访问的资源id
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")//该客户端允许的授权类型
                .scopes("all")//允许的授权范围
                .autoApprove(false)//false会跳转到授权页面,让用户授权。true则不会
                .redirectUris("http://www.baidu.com");//客服端的回调地址
//        .and()
//        .withClient("id")
//        .....    这样就可以配置多个客服端了
    }

    /**
     * 2. 配置令牌服务
     * (存储方式见类TokenConfig类)
     * (令牌管理服务见下方tokenServices)
     */
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        //配置客服端详情服务
        services.setClientDetailsService(clientDetailsService);
        //支持刷新令牌
        services.setSupportRefreshToken(true);
        //令牌存储方式
        services.setTokenStore(tokenStore);
        //设置令牌的增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);
        //令牌有效时期为2小时
        services.setAccessTokenValiditySeconds(7200);
        //刷新令牌默认有效期3天
        services.setRefreshTokenValiditySeconds(259200);
        return services;
    }

    /**
     * 3. 配置令牌的访问端点
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                //认证管理器,当你选择了资源所有者密码( password )授权类型的时候,请设置这个属性注入一个AuthenticationManager对象。
                .authenticationManager(authenticationManager)
                //授权码服务
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenServices())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    /**
     * 4. 配置令牌端点的安全约束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                //对应/oauth/token_key 的URL是公开的(不用登录就能访问)
                .tokenKeyAccess("permitAll()")
                ///oauth/check_token 是公开的
                .checkTokenAccess("permitAll()")
                //表单认证(用于客服端申请令牌)
                .allowFormAuthenticationForClients();
        //不公开的url就需要进行认证
    }


    /*
     *因为配置客服端详情的时候客服端的授权类型authorizedGrantTypes有authorization_code
     *所以需要配置authorizationCodeServices
     * 下面我们暂时采用内存的方式
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

}

再次运行uaa模块,用密码授权的方式访问结果如下

http://localhost:9001/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&redirect_uri=http://www.baidu.com&username=dong&password=123456

jwtpassword

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJkb25nIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYyMDgxMDQ3OCwiYXV0aG9yaXRpZXMiOlsicDEiLCJwMiJdLCJqdGkiOiIyYTgxMzc4Zi1lZGFiLTRhOTEtOGMzNi02ZjI0OWE2YzY1ZGEiLCJjbGllbnRfaWQiOiJjMSJ9.TLgtP4QGomJYRXqqqSQ_PRGSNSfklxmCWnn5wW_nUIM",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJkb25nIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjJhODEzNzhmLWVkYWItNGE5MS04YzM2LTZmMjQ5YTZjNjVkYSIsImV4cCI6MTYyMTA2MjQ3OCwiYXV0aG9yaXRpZXMiOlsicDEiLCJwMiJdLCJqdGkiOiIzYzMyOGQ1NS1kMzdiLTQ0MjAtOTM5My0wYzlmOTU2N2QyZDEiLCJjbGllbnRfaWQiOiJjMSJ9.Bye92EHg6Dcp66OTrPDQ-IuGN9eV7Dqs9YhAQftVCRg",
    "expires_in": 7199,
    "scope": "all",
    "jti": "2a81378f-edab-4a91-8c36-6f249a6c65da"
}

从结果总我们发现access_token非常的长,这是因为和以前使用内存方式存储令牌来比JWT令牌中已经包括了用户相关的信息 ,权限等内容。客户端只需要携带JWT访问资源服务

校验jwt令牌

资源服务需要和授权服务拥有一致的签字、令牌服务等:

1、将授权服务中的TokenConfig类拷贝到资源服务中 (order)

package org.dong.oauth.order.config;

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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class TokenConfig {
    //定义JWT令牌服务
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
    //密钥 需要和授权服务的密钥保持一致。这是对称加密的方式
    private String SIGNING_KEY = "uaa123";

    /**
     * 存储令牌的三种方式
     * 1. JdbcTokenStore(jdbc方式)
     * 2. JWTTokenStore(JWT方式)
     * 3. InMemoryTokenStore(内存方式,下面我们使用内存方式存储普通令牌)
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //对称秘钥,资源服务器使用该秘钥来验证
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}

2、屏蔽资源 服务原来的令牌服务类

修改ResourceServerConfig类的地方有

//注入tokenStore
@Autowired
TokenStore tokenStore;

//configure(ResourceServerSecurityConfigurer resources)
//删除下面这一行代码
tokenServices(tokenService()) //验证令牌的服务
//替换为
.tokenStore(tokenStore)
    
 //删除ResourceServerTokenServices tokenService()方法
 //因为使用JWT,不需要使用远程服务请求授权服务器校验token了

完整代码为

package org.dong.oauth.order.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.config.http.SessionCreationPolicy;
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.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
//表明是资源服务
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    TokenStore tokenStore;

    /**
     * 1. 资源id需要和uaa模块中AuthorizationServer配置的资源id一致
     * .resourceIds("res1")//客服端可以访问的资源id
     */
    public static final String RESOURCE_ID = "res1";

    /**
     * 2. 安全校验
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources
                .resourceId(RESOURCE_ID) //资源id
                .tokenStore(tokenStore)
                .stateless(true);
    }

    /**
     * 3. 安全拦截机制
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**")
                .access("#oauth2.hasScope('all')")
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

}

启动order服务进行测试

访问:http://localhost:9002/order/r1 在Headers中输入Authorization的值(Bearer+access_token)

jwtorder

结果

访问资源1

访问用户dong没有权限访问的的资源http://localhost:9002/order/r3

{
    "error": "access_denied",
    "error_description": "不允许访问"
}

到此我们已经完成了JWT的配置,接下来我们来完善一些环境配置,以前很多东西都是使用的内存方式,接下来我们把它改为数据库的方式。请浏览下一篇文章。
参考课程黑马

posted @ 2021-05-13 11:29  懒鑫人  阅读(330)  评论(0)    收藏  举报