Jwt服务间认证

背景

新进部门项目的小伙伴经常会问一些关于Jwt权限认证的问题,结合项目架构以及网络资料,分享一些关于我们系统对Jwt的使用情况

Spring Security OAuth2实现使用JWT

概括

我们将讨论如何让Spring Security OAuth2实现使用JSON Web Tokens。

Maven 配置

首先,我们需要在我们的pom.xml中添加spring-security-jwt依赖项。

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>

我们需要为Authorization Server(认证服务)和Resource Server(资源服务)添加spring-security-jwt依赖项。

需要提到依赖中的几个关键类:

MacSigner:用于对称密钥对加密
Signer  jwtSigner = new MacSigner("hand");//默认HMACSHA256 算法加密
Signer  jwtSigner = new MacSigner("HMACSHA256","hand");//手动设置算法
RsaSigner:用于非对称加密,证书的生成于使用会在后面介绍
 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytool.jks"), "mypass".toCharArray());
 KeyPair demo = keyStoreKeyFactory.getKeyPair("mytool");
 Signer jwtSigner = new RsaSigner((RSAPrivateKey)demo.getPrivate());
JwtHelper:工具类,用于生成Jwt token,验证等。

授权服务器

接下来,我们将配置我们的授权服务器使用JwtTokenStore - 如下所示

//hcloud服务这一块已经弃用,改为手动生成token,hcloud的Jwt 生成策略在后面单讲

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.tokenStore(tokenStore())
             .accessTokenConverter(accessTokenConverter())
             .authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    //可以重写该类,一半默认已经满足需求
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("123");//用于签名,需要于验签服务key一致
    return converter;
}

@Bean
@Primary
public DefaultTokenServices tokenServices() {
    //一整套token的生成机制,可以重新,但是对于hcloud这一块已经不适用
    DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    defaultTokenServices.setSupportRefreshToken(true);
    return defaultTokenServices;
}
}

请注意,在JwtAccessTokenConverter中使用了一个对称密钥来签署我们的令牌 - 这意味着我们需要为资源服务器使用同样的确切密钥。

资源服务器

现在,我们来看看我们的资源服务器配置 - 这与授权服务器的配置非常相似:

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Override
public void configure(ResourceServerSecurityConfigurer config) {
    config.tokenServices(tokenServices());
}

@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("123");//需要与加密时一致
    return converter;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
    DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    return defaultTokenServices;
}
}

请记住,我们将这两个服务器定义为完全独立且可独立部署的服务器。这就是我们需要在新配置中再次声明一些相同的bean的原因。

令牌中的自定义声明

现在让我们设置一些基础设施,以便能够在访问令牌中添加一些自定义声明。框架提供的标准声明都很好,但大多数情况下我们需要在令牌中使用一些额外的信息来在客户端使用。 我们将定义一个TokenEnhancer来定制我们的Access Token与这些额外的声明。 在下面的例子中,我们将添加一个额外的字段“组织”到我们的访问令牌 - 与此CustomTokenEnhancer:

public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(
 OAuth2AccessToken accessToken, 
 OAuth2Authentication authentication) {
    Map<String, Object> additionalInfo = new HashMap<>();
    additionalInfo.put("organization", authentication.getName() + randomAlphabetic(4));
    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
    return accessToken;
}
}

然后,我们将把它连接到我们的授权服务器配置 - 如下所示:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
  Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore())
         .tokenEnhancer(tokenEnhancerChain)
         .authenticationManager(authenticationManager);
}

@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}

有了这个新的配置启动和运行 - 这是一个令牌令牌有效载荷看起来像:

{
"user_name": "john",
"scope": [
    "foo",
    "read",
    "write"
],
"organization": "johnIiCh",
"exp": 1458126622,
"authorities": [
    "ROLE_USER"
],
"jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f",
"client_id": "fooClientIdPassword"
}

不对称的KeyPair

在我们以前的配置中,我们使用对称密钥来签署我们的令牌:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}

我们还可以使用非对称密钥(公钥和私钥)来执行签名过程。

生成JKS Java KeyStore文件

我们首先使用命令行工具keytool生成密钥 - 更具体地说.jks文件:

keytool -genkeypair -alias mytest   -keyalg RSA   -keypass mypass   -keystore mytest.jks   -storepass mypass

该命令将生成一个名为mytest.jks的文件,其中包含我们的密钥 - 公钥和私钥。 还要确保keypass和storepass是一样的。

导出公钥

接下来,我们需要从生成的JKS中导出我们的公钥,我们可以使用下面的命令来实现:

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

示例回应如下所示:

-----BEGIN PUBLIC KEY-----

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp

OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2

/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3

DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR

xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr

lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK

eQIDAQAB

-----END PUBLIC KEY-----

-----BEGIN CERTIFICATE-----

MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1

czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2

MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV

BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN

AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj

Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM

urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX

eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj

iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn

WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD

VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3

1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0

yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp

/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN

hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V

FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF

lLFCUGhA7hxn2xf3x1JW

-----END CERTIFICATE-----

我们只取得我们的公钥,并将其复制到我们的资源服务器src / main / resources / public.txt中

-----BEGIN PUBLIC KEY-----

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp

OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2

/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3

DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR

xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr

lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK

eQIDAQAB

-----END PUBLIC KEY-----

Maven 配置

接下来,我们不希望JMS文件被maven过滤进程拾取 - 所以我们将确保将其排除在pom.xml中:

<build>
<resources>
<resource>
    <directory>src/main/resources</directory>
    <filtering>true</filtering>
    <excludes>
        <exclude>*.jks</exclude>
    </excludes>
</resource>
</resources>
</build>

如果我们使用Spring Boot,我们需要确保我们的JKS文件通过Spring Boot Maven插件添加到应用程序classpath - addResources:

<build>
<plugins>
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <addResources>true</addResources>
    </configuration>
</plugin>
</plugins>
</build>

授权服务器

现在,我们将配置JwtAccessTokenConverter使用mytest.jks中的KeyPair,如下所示:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory = 
  new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
return converter;
}

资源服务器

最后,我们需要配置我们的资源服务器使用公钥 - 如下所示:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;

try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);//此处需要注意与对称加密时设置的值不一样
return converter;
}

Spring cloud oauth2.0的源码解析

现在再来大致分析一下它的源码结构,Spring cloud oauth2.0的代码结构图如下:

image.png

可以看到Spring oauth2.0的代码结构分为了五层,client层负责应用客户端的管理。common层为公共类的所在地。config层为配置层。http.converter层为转换层。provider层是最重要的一层,它负责管理认证服务器和资源服务器。下面大致介绍每一层,欢迎一起讨论:

client层

如图:

image.png

这里只讲其中的几个比较典型的类:

(1)OAuth2ClientAuthenticationProcessingFilter:负责对客户端的访问进行过滤,验证token处理等操作。

(2)JdbcClientTokenServices:里面封装了对oauth_client_token表的一系列操作(如增删改查),此表保存的是此client的token信息。

(3)OAuth2RestTemplate:实现了OAuthRestOperations接口,主要是用来去调用受保护资源,会自己带上当前的token信息。

当然还有其它的一些类,比如DefaultOAuth2ClientContext实现了OAuth2ClientContext接口,里面存储了client的上下文信息。

common公共层

如图:

image.png

可见这一层主要是对json字符串处理、解析和实例化等操作,

config层

注解的定义,以及认证服务器和资源服务器的各个配置类的定义都在这个包,是贯穿整个代码非常核心的一层。如图:

image.png

可见,我们在代码里所用的三个基本注解EnableAuthorizationServer、EnableOAuth2Client、EnableResourceServer都在这里。还有

AuthorizationServerConfigurerAdapter、ResourceServerConfigurerAdapter这两个最核心的配置类也在这里,只要实现这两个核心配置类,复写他们的函数便可以实现

个性化的配置。(当然是按照自己的需求进行实际的复写喽~~~)具体的使用可以参照我Demo里面的代码~

converter层:略

provider层

最核心的一层,里面包含了endpoint、认证、以及四种授权方式的管理。如图:

image.png

可见,此层的代码包数量比较多。下面拆解分析进行分析:

(1)关于approval包:里面包含了授权的信息,如果想要自定义展示页面上的元素,需要研读此包里面的代码进行改写,如图:

(2)authentication包:这一层只讲一个类,就是OAuth2AuthenticationProcessingFilter类,他主要负责当资源服务器接收到请求,此类将进行过滤校验等操作。另

(3)client包:这里只讲一个JdbcClientDetailsService一个类,打开此类的源码可以看到它里面声明了很多的sql语句。此类主要是关系着client信息的管理。以jdbc的方式进行读取。

(4)code、implicit、password、refresh层这里就不再展开,就是对应着四种授权方式的管理。

(5)endpoint层:此层很重要,代码里面定义了基本的接口地址,如/oauth/authorize、/oauth/token、/oauth/confirm_access等等。如下图,这里只说一下WhitelabelApprovalEndpoint类,此类里面声明了@RequestMapping({"/oauth/confirm_access"})这个endpoint,这个endpoint就是生成我们的授权页面,就是那个有很多选项,让你选择允许还是拒绝的那个页面,这里此类并没有对应的web页面,而是在java代码里创建了一个html的ModelAndView视图,然后展示出来。所以如果你想要自定义授权页,最简单的方法就是把这个类copy到你的controller包里面,然后修改成你自己想要的。

@EnableResourceServer与@EnableAuthorizationServer

资源服务器与身份认证服务器。我们注意到其相关配置类是ResourceServerConfigurer,内部关联了ResourceServerSecurityConfigurer和HttpSecurity。前者与资源安全配置相关,后者与http安全配置相关。(类名比较类似,注意区分,以Adapter结尾的是适配器,以Configurer结尾的是配置器,以Builder结尾的是建造器,他们分别代表不同的设计模式,对设计模式有所了解可以更加方便理解其设计思路)

public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
    @Override
    public void configure(ResourceServerSecurityConfigurer resources <1> ) throws Exception {
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
    }
}

ResourceServerSecurityConfigurer显然便是我们分析的重点了。

ResourceServerSecurityConfigurer(了解)

其核心配置如下所示:

public void configure(HttpSecurity http) throws Exception {
    AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
    resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();//<1>
    resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
    resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);//<2>
    if (eventPublisher != null) {
        resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
    }
    if (tokenExtractor != null) {
        resourcesServerFilter.setTokenExtractor(tokenExtractor);//<3>
    }
    resourcesServerFilter = postProcess(resourcesServerFilter);
    resourcesServerFilter.setStateless(stateless);

    // @formatter:off
    http
        .authorizeRequests().expressionHandler(expressionHandler)
    .and()
        .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
        .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler)//<4>
            .authenticationEntryPoint(authenticationEntryPoint);
    // @formatter:on
}

这段是整个oauth2与HttpSecurity相关的核心配置,其中有非常多的注意点,顺带的都强调一下:

1.创建OAuth2AuthenticationProcessingFilter,即下一节所要介绍的OAuth2核心过滤器。

2.为OAuth2AuthenticationProcessingFilter提供固定的AuthenticationManager即OAuth2AuthenticationManager,它并没有将OAuth2AuthenticationManager添加到spring的容器中,不然可能会影响spring security的普通认证流程(非oauth2请求),只有被OAuth2AuthenticationProcessingFilter拦截到的oauth2相关请求才被特殊的身份认证器处理。

两种形式的请求会被拦截:

image.png
image.png

1.设置了TokenExtractor默认的实现—-BearerTokenExtractor,这个类在下一节介绍。

2.相关的异常处理器,可以重写相关实现,达到自定义异常的目的。

还记得我们在一开始的配置中配置了资源服务器,是它触发了相关的配置。

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {}

核心过滤器 OAuth2AuthenticationProcessingFilter(掌握)

回顾一下我们之前是如何携带token访问受限资源的:

http://localhost:8080/order/1?access%25252525255C_token=950a7cc9-5a8a-42c9-a693-40e817b1a4b0

唯一的身份凭证,便是这个access_token,携带它进行访问,会进入OAuth2AuthenticationProcessingFilter之中,其核心代码如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain){
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
    //从请求中取出身份信息,即access_token
    Authentication authentication = tokenExtractor.extract(request);
    if (authentication == null) {
    }

    else {
        request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
        if (authentication instanceof AbstractAuthenticationToken) {
            AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
            needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
        }
        //认证身份
        Authentication authResult = authenticationManager.authenticate(authentication);
        eventPublisher.publishAuthenticationSuccess(authResult);
        //将身份信息绑定到SecurityContextHolder中
        SecurityContextHolder.getContext().setAuthentication(authResult);
    }
}
catch (OAuth2Exception failed) {
    return;

}
chain.doFilter(request, response);
}

整个过滤器便是oauth2身份鉴定的关键,在源码中,对这个类有一段如下的描述

A pre-authentication filter for OAuth2 protected resources. Extracts an OAuth2 token from the incoming request and uses it to populate the Spring Security context with an {@link OAuth2Authentication} (if used in conjunction with an {@link OAuth2AuthenticationManager}).

OAuth2保护资源的预先认证过滤器。如果与OAuth2AuthenticationManager结合使用,则会从到来的请求之中提取一个OAuth2 token,之后使用OAuth2Authentication来填充Spring Security上下文。

其中涉及到了两个关键的类TokenExtractor,AuthenticationManager。相信后者这个接口大家已经不陌生,但前面这个类之前还未出现在我们的视野中。

OAuth2的身份管理器–OAuth2AuthenticationManager(掌握)

image.png

在之前的OAuth2核心过滤器中出现的AuthenticationManager其实在我们意料之中,携带access_token必定得经过身份认证,但是在我们debug进入其中后,发现了一个出乎意料的事,AuthenticationManager的实现类并不是我们在前面文章中聊到的常用实现类ProviderManager,而是OAuth2AuthenticationManager。

OAuth2AuthenticationManager

图1 新的AuthenticationManager实现类OAuth2AuthenticationManager

回顾我们第一篇文章的配置,压根没有出现过这个OAuth2AuthenticationManager,并且它脱离了我们熟悉的认证流程(第二篇文章中的认证管理器UML图是一张经典的spring security结构类图),它直接重写了容器的顶级身份认证接口,内部维护了一个ClientDetailService和ResourceServerTokenServices,这两个核心类在 Re:从零开始的Spring Security Oauth2(二)有分析过。在ResourceServerSecurityConfigurer的小节中我们已经知晓了它是如何被框架自动配置的,这里要强调的是OAuth2AuthenticationManager是密切与token认证相关的,而不是与获取token密切相关的。

其判别身份的关键代码如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

String token = (String) authentication.getPrincipal();

//最终还是借助tokenServices根据token加载身份信息

OAuth2Authentication auth = tokenServices.loadAuthentication(token);
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
}

auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}

说到tokenServices这个密切与token相关的接口,这里要强调下,避免产生误解。tokenServices分为两类,一个是用在AuthenticationServer端,第二篇文章中介绍的

public interface AuthorizationServerTokenServices {
//创建token

OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
//刷新token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
    throws AuthenticationException;
//获取token
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
}

而在ResourceServer端有自己的tokenServices接口:

public interface ResourceServerTokenServices {
//根据accessToken加载客户端信息
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
//根据accessToken获取完整的访问令牌详细信息。
OAuth2AccessToken readAccessToken(String accessToken);
}

具体内部如何加载,和AuthorizationServer大同小异,只是从tokenStore中取出相应身份的流程有点区别,不再详细看实现类了。

TokenExtractor(了解)

这个接口只有一个实现类,而且代码非常简单

public class BearerTokenExtractor implements TokenExtractor {
private final static Log logger = LogFactory.getLog(BearerTokenExtractor.class);
@Override
public Authentication extract(HttpServletRequest request) {
String tokenValue = extractToken(request);
if (tokenValue != null) {
    PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
    return authentication;
}
return null;
}

protected String extractToken(HttpServletRequest request) {
// first check the header...
String token = extractHeaderToken(request);
// bearer type allows a request parameter as well
if (token == null) {
}
return token;
}

protected String extractHeaderToken(HttpServletRequest request) {
    Enumeration<String> headers = request.getHeaders("Authorization");
    while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
        //从Header中获取token
    }
    return null;
}}

hcloud Jwt 服务认证解决方案

简介

当前hcloud环境下,单个服务不通过api-gateway,不需要任何权限就可以访问。另对当前hcloud的服务认证机制不了解。

请求过滤

新增过滤器,在所有过滤器之前,检查是否带有token信息,以及token的格式。标准oauth只对固定格式的请求头进行校验。

public class HttpBearerAuthorizeFilter implements Filter {
    private static final String BEARER = "Bearer ";
    private static final String AUTHORIZATION = "Authorization";
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // TODO Auto-generated method stub
        SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this,
                filterConfig.getServletContext());
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // TODO Auto-generated method stub
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        JSONObject failResponse = new JSONObject();
        String auth = httpRequest.getHeader(AUTHORIZATION);
        if(!StringUtils.isEmpty(auth)){
            String headStr = auth.substring(0,7);
            if(BEARER.equals(headStr)){
                chain.doFilter(request, response);
                return;
            }else{
                failResponse.put("message", "HttpHeader parameter [Authorization] Formal Error");
            }
        }else{
            failResponse.put("message", "Miss HttpHeader parameter [Authorization]");
        }
        failResponse.put("code", "401");
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        setHeader(response);
        response.setContentType(GatewayConstant.CONTENT_TYPE_JSON_UTF8);
        response.getWriter().println(failResponse);
        return;
    }
    @Override
    public void destroy() {
        // TODO Auto-generated method stub
    }
    private void setHeader(ServletResponse response) {
        // 此种方式 Spring 不会做跨域处理,需要手工设置
        if (response instanceof HttpServletResponse) {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader(GatewayConstant.ORIGIN_KEY, GatewayConstant.ORIGIN_VALUE);
            httpServletResponse.setHeader(GatewayConstant.ALLOW_METHODS_KEY, GatewayConstant.ALLOW_METHODS_VALUE);
            httpServletResponse.setHeader(GatewayConstant.MAX_AGE_KEY, GatewayConstant.MAX_AGE_VALUE);
            httpServletResponse.setHeader(GatewayConstant.ALLOW_HEADERS_KEY, GatewayConstant.ALLOW_HEADERS_VALUE);
            response.setCharacterEncoding(GatewayConstant.CHARACTER_ENCODE);
        }
    }
    interface GatewayConstant{
        String ORIGIN_KEY = "Access-Control-Allow-Origin";
        String ORIGIN_VALUE = "*";
        String ALLOW_METHODS_KEY = "Access-Control-Allow-Methods";
        String ALLOW_METHODS_VALUE = "POST, GET, OPTIONS, PUT, DELETE";
        String MAX_AGE_KEY = "Access-Control-Max-Age";
        String MAX_AGE_VALUE = "3600";
        String ALLOW_HEADERS_KEY = "Access-Control-Allow-Headers";
        String ALLOW_HEADERS_VALUE = "x-requested-with";
        String CHARACTER_ENCODE = "UTF-8";
        String CONTENT_TYPE_JSON_UTF8 = "application/json; charset=utf-8";
    }
}

过滤器不自动注入的spring环境中,手动注入。

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //权限控制在API Gateway控制, resource server 直接全部通过
        http
                .addFilterBefore(new HttpBearerAuthorizeFilter(),ChannelProcessingFilter.class)
                .authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }

Spring Security OAuth 有一整套的过滤器,我们将我们写的过滤器加载在这一整个过滤器之前。

ChannelProcessingFilter
ConcurrentSessionFilter
SecurityContextPersistenceFilter
LogoutFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
UsernamePasswordAuthenticationFilter
ConcurrentSessionFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter

如是所有进如服务的请求必须带有固定格式的token。

对称加密

Jwt token身份认证服务器端申明加密密钥,默认密钥是hand

@Value("${hap.jwt.key:hand}")
private String key;
/*
* JWT 对称签名字段
* */
@Bean
public Signer jwtSigner() {
    return new MacSigner(key);
}

具体生成加密密钥方式

ObjectMapper MAPPER = new ObjectMapper();
String token = MAPPER.writeValueAsString(details);
Signer jwtSigner = new MacSigner(key);
String jwt = "Bearer " + JwtHelper.encode(token, jwtSigner).getEncoded();

将 Jwt token设置到环境中

RequestContext ctx = RequestContext.getCurrentContext();
ctx.set(ACCESS\_TOKEN, jwt);
HystrixHeaderInterceptor.token.set(jwt);
Jwt token 资源服务器端,配置与身份认证服务器端相同的key
@Value("${hap.jwt.key:hand}")
private String key;
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    CustomJwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
    converter.setAccessTokenConverter(new CustomTokenConverter());
    converter.setSigningKey(key);
    return converter;
 }

非对称加密

制作证书:
keytool -genkeypair -alias mytest   -keyalg RSA   -keypass mypass   -keystore mytest.jks  -storepass mypass
提取公钥:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

将刚刚生成的证书拷贝到resources文件目录下在pom文件中配置刚刚生成的.jks

<build>
<resources>
    <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <excludes>
            <exclude>*.jks</exclude>
        </excludes>
    </resource>
</resources>
</build>

让刚刚的配置生效

<build>
<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <addResources>true</addResources>
        </configuration>
    </plugin>
</plugins>
</build>

Jwt token身份认证服务器端申明加密密钥

 @Value("${hap.jwt.key:hand}")
private String key;
/*
* JWT 对称签名字段
* */
@Bean
public Signer jwtSigner() {
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytool.jks"), "mypass".toCharArray());
    KeyPair demo = keyStoreKeyFactory.getKeyPair("mytool");
    return   new RsaSigner((RSAPrivateKey)demo.getPrivate());
}

具体生成加密密钥方式,并将Jwt token放置到环境中去

KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytool.jks"), "mypass".toCharArray());

KeyPair keyPair = keyStoreKeyFactory.getKeyPair("mytool");

jwtSigner = new RsaSigner((RSAPrivateKey)keyPair.getPrivate());

 if (logger.isDebugEnabled()) {
   logger.debug("CustomUserDetails:{}", BeanParser.object2Map(details));

}
ObjectMapper MAPPER =new ObjectMapper();
String token = MAPPER.writeValueAsString(details);
String jwt = "Bearer " + JwtHelper.encode(token, jwtSigner).getEncoded();
ctx.set(ACCESS\_TOKEN, jwt);
HystrixHeaderInterceptor.token.set(jwt);

Jwt token 资源服务器端,配置与身份认证服务器端相匹配的公钥

scurity:
  oauth2:
resource:
  jwt:
    keyValue: |
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxhlV3wv8rMFafzglAKqc
      7G0ZaUFeEnDeuB7LiMmPLvydOREbQj3hdp8JEkISAOlKObdDYadvtbM9JMTwEh/6
      QZh+2wx2cbQ7F/RAUeCFqU1qRF+LvTSArLZc2f48t1KvsbcAhQUwcVMe5ukhNe//
      CfIY6mlCFUWZdvw61yDiDnJ7dkwXaQdYdcgUzvSv754DB8xZV4iJdLlwXixfRTFh
      6pQ8xpGxcaETNEwRkBi2dLBFYjfBhgh5lpvO8RYcKbnrKKA0VJ4/GamYT2mxjMuP
      k+jsVVcOqpDKIwXZ00jxIN96PKVNXMFa6Dm2IPv15iaVcyN041EdB/+3plg0+ZsC
      4QIDAQAB
      -----END PUBLIC KEY-----

将公钥设置到验证资源中

  @Value("${scurity.oauth2.resource.jwt.keyValue}")
private String key;
/**
 * 返回converter
 *
 * @return converter
 */
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    CustomJwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
    converter.setAccessTokenConverter(new CustomTokenConverter());
    converter.setVerifierKey(key);
    return converter;
}

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小筱月

maven工程 java 实现文件上传 SSM ajax异步请求上传

<input type="file" class="fileupon33" name="fileupmulti" accept="image/jpeg,imag...

973
来自专栏酷玩时刻

支付宝Wap支付你了解多少?

为了方便开发者生成一对RSA密钥支付宝提供一键生成工具,具体如何生成与配置密钥详见签名专区。

1302
来自专栏Java帮帮-微信公众号-技术文章全总结

Java开发画板

Test类 public class Test { public static void main(String[] args){ ...

3625
来自专栏强仔仔

SpringBoot中实现邮件找回密码的功能

今天给大家介绍一下很常用的一个功能,就是邮件找回密码功能。找回密码一般会有:1.邮件找回密码、2短信找回密码、3问题找会密码。 关于邮件找回密码的原理思想为: ...

2158
来自专栏程序猿DD

Spring Security 入门(五):在 Spring-Boot中的应用

? 前言 本文作为入门级的DEMO,完全按照官网实例演示; 项目目录结构 ? Maven 依赖 <parent> <groupId>org.sprin...

41815
来自专栏程序猿DD

Spring Security (五) 动手实现一个IP_Login

在开始这篇文章之前,我们似乎应该思考下为什么需要搞清楚Spring Security的内部工作原理?按照第二篇文章中的配置,一个简单的表单认证不就达成了吗?更有...

1879
来自专栏向治洪

React Native调用Android相机图库

概述 在很多的React Native开发中,我们需要调用原生的api实现调用相机和图库的功能,网上用的最多的开源库如:react-native-image-p...

2389
来自专栏aoho求索

认证鉴权与API权限控制在微服务架构中的设计与实现(二)

引言: 本文系《认证鉴权与API权限控制在微服务架构中的设计与实现》系列的第二篇,本文重点讲解用户身份的认证与token发放的具体实现。本文篇幅较长,对涉及到的...

3454
来自专栏Kirito的技术分享

Spring Security(一)--Architecture Overview

一直以来我都想写一写Spring Security系列的文章,但是整个Spring Security体系强大却又繁杂。陆陆续续从最开始的guides接触它,项目...

2767
来自专栏日常分享

Struts2+DAO层实现实例01——搭建Struts2基本框架

711

扫码关注云+社区