服务端开发实战

Spring Security OAuth2实现使用JWT

2018-11-14  本文已影响70人  AaronSimon

Spring Security Oauth2-授权码模式(Finchley版本)一文中介绍了OAuth2的授权码模式的实现,本文将在这篇文章的基础上使用JWT生成token。

一、准备工作

  1. 下载代码
    大家可以在github上下载Spring Security Oauth2-授权码模式(Finchley版本)的源码。
  2. 添加JWT依赖
    授权服务和资源服务是两个分开的服务,需要在两个服务中添加JWT依赖
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-jwt</artifactId>
      <version>1.0.7.RELEASE</version>
    </dependency>
    

二、案例介绍

JWT认证提供了对称加密和非对称加密的实现。

2.1 对称加密

2.1.1 授权服务

(1) 定义token的生成方式
AccessToken转换器用来定义token的生成方式,这里使用JWT生成token

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

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

(2) 告知spring security token的生成方式

/**
   * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
   * @param endpoints
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    //指定认证管理器
    endpoints.authenticationManager(authenticationManager);
    //指定token存储位置
    endpoints.tokenStore(tokenStore());
    // token生成方式
    endpoints.accessTokenConverter(accessTokenConverter());
    endpoints.userDetailsService(userDetailsService);
  }

2.1.2 资源服务

资源服务的配置与授权服务大致相同

/**
 * 资源服务器配置
 *
 * @author simon
 * @create 2018-11-14 11:03
 **/
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

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

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

  @Override
  public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    resources.tokenServices(defaultTokenServices;
  }
}

2.2 非对称加密

使用非对称密钥(公钥和私钥)来执行签名过程,需要先生成一个证书并导出公钥。

2.2.1 生成证书

(1) 生成JKS Java KeyStore文件
使用命令行工具keytool生成证书

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

此命令将生成一个名为mytest.jks的文件,其中包含我们的密钥(公钥和私钥)。

(2) 导出公钥
我们可以使用下面的命令从生成的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-----

这里我们只需要复制公钥到资源服务的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-----

2.2.2 授权服务

将刚刚生成的证书复制到授权服务器的resources目录下。配置JwtAccessTokenConverter使用mytest.jks 中的KeyPair

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

2.2.3 资源服务

配置资源服务器使用公钥:

@Bean
public JwtAccessTokenConverter accessTokenConverter(){
  JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  Resource resource =  new ClassPathResource("public.txt");
  String publicKey;
  try {
    publicKey = inputStream2String(resource.getInputStream());
  } catch (final IOException e) {
    throw new RuntimeException(e);
  }
  converter.setVerifierKey(publicKey);
  return converter;
}

String inputStream2String(InputStream is) throws IOException {
  BufferedReader in = new BufferedReader(new InputStreamReader(is));
  StringBuffer buffer = new StringBuffer();
  String line = "";
  while ((line = in.readLine()) != null) {
    buffer.append(line);
  }
  return buffer.toString();
}

2.3 添加额外信息

额外信息的添加与加密方式无关

2.3.1 自定义生成token携带的信息

可以自定义一个TokenEnhancer将额外的信息添加到token中。TokenEnhancer 接口提供public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication)方法用于token信息的添加
(1) 自定义TokenEnhancer

/**
 * 自定义token生成携带的信息
 *
 * @author simon
 * @create 2018-11-14 10:16
 **/
public class CustomTokenEnhancer implements TokenEnhancer {
  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
    final Map<String, Object> additionalInfo = new HashMap<>();
    //获取登录信息
    UserDetails user = (UserDetails) oAuth2Authentication.getUserAuthentication().getPrincipal();
    additionalInfo.put("userName", user.getUsername());
    additionalInfo.put("authorities", user.getAuthorities());
    ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
    return oAuth2AccessToken;
  }
}

(2) 将自定义的TokenEnhancer加入到TokenEnhancerChain中

/**
   * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
   * @param endpoints
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    //指定认证管理器
    endpoints.authenticationManager(authenticationManager);
    //指定token存储位置
    endpoints.tokenStore(tokenStore());

    endpoints.accessTokenConverter(accessTokenConverter());
    endpoints.userDetailsService(userDetailsService);
    //自定义token生成方式
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customerEnhancer(),accessTokenConverter()));
    endpoints.tokenEnhancer(tokenEnhancerChain);

2.3.2 自定义token中添加的信息

(1)授权服务自定义JwtAccessTokenConverte
JwtAccessTokenConverter是我们用来生成token的转换器,所以我们需要配置这里面的部分信息来实现token中携带额外的信息。

JwtAccessTokenConverter默认使用DefaultAccessTokenConverter来处理token的生成、转换、获取。DefaultAccessTokenConverter中使用UserAuthenticationConverter来处理token与userinfo的获取、转换。因此我们需要重写下UserAuthenticationConverter对应的转换方法就可以

/**
 * 自定义CustomerAccessTokenConverter 这个类的作用主要用于AccessToken的转换,
 * 默认使用DefaultAccessTokenConverter 这个装换器
 * DefaultAccessTokenConverter有个UserAuthenticationConverter,这个转换器作用是把用户的信息放入token中,默认只是放入user_name
 * <p>
 * 自定义这个方法,加入了额外的信息
 * <p>
 * @author simon
 * @create 2018-11-14 10:26
 **/
public class CustomerAccessTokenConverter extends DefaultAccessTokenConverter {

  public CustomerAccessTokenConverter() {
    super.setUserTokenConverter(new CustomerUserAuthenticationConverter());
  }

  private class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter{
    @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
      LinkedHashMap <String, Object> response = new LinkedHashMap <>();
      response.put("details", authentication.getDetails());
      response.put("test","hello");
      if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
        response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
      }
      return response;
    }
  }
}

(2) 授权服务告诉JwtAccessTokenConverter替换默认的方式

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

(3)资源服务自定义JwtAccessTokenConverte

/**
 * 自定义CustomerAccessTokenConverter 这个类的作用主要用于AccessToken的转换,
 * 默认使用DefaultAccessTokenConverter 这个装换器
 * DefaultAccessTokenConverter有个UserAuthenticationConverter,这个转换器作用是把用户的信息放入token中,
 * 默认只是放入username
 * <p>
 * 自定义了下这个方法,加入了额外的信息
 * <p>
 * @author simon
 * @create 2018-11-14 10:26
 **/
public class CustomerAccessTokenConverter extends DefaultAccessTokenConverter {

  public CustomerAccessTokenConverter() {
    super.setUserTokenConverter(new CustomerUserAuthenticationConverter());
  }

  private class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter {

    // 资源服务获得自定义信息  
    @Override
    public Authentication extractAuthentication(Map<String, ?> map) {
      Collection <? extends GrantedAuthority> authorities = this.getAuthorities(map);
      return new UsernamePasswordAuthenticationToken(map, "N/A", authorities);
    }

    private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
      if (!map.containsKey("authorities")) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(new String[]{"USER"}));
      } else {
        Object authorities = map.get("authorities");
        if (authorities instanceof String) {
          return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
        } else if (authorities instanceof Collection) {
          return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection) authorities));
        } else {
          throw new IllegalArgumentException("Authorities must be either a String or a Collection");
        }
      }
    }

  }
}

2.4 测试

启动服务

2.4.1 获取code

浏览器访问http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://baidu.com

进入登录页面,输入用户名:admin;密码:admin。

登录成功进入授权页面,点击授权,获得code
https://www.baidu.com/?code=NW8eB1

2.4.2 获取token

使用POSTMAN发送post请求获取token


postman请求

2.4.3 访问资源服务获取资源

使用POSTMAN发送get请求获取资源


postman请求

2.4.4 解析token

新增测试类解析token

@Test
  public void contextLoads() {
    //填写token
    String token = "";
    Jwt jwt = JwtHelper.decode(token);
    System.err.println(jwt.toString());
  }

解析后的信息如下:

{"alg":"RS256","typ":"JWT"} {"test":"hello","scope":["test"],"details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"34B189EA6F1DA4834E5AEA31E91A2460"},"exp":1542277115,"userName":"admin","authorities":[{"authority":"USER"}],"jti":"8e4a72d3-affb-4977-b174-cb9ee4f2e08b","client_id":"client1"} [256 crypto bytes]

结果中包含添加的额外信息

github源码下载

上一篇下一篇

猜你喜欢

热点阅读