上一节咱们已经讲解了token的基本配置,包括过时时间,密匙等。这一节主要讲解使用JWT替换默认的Token配置。前端
JWT的全称是Json Web Token;Json开放的Token标准。 JWT的特色:web
最后返回给接口的串是无心义的字符串,自己并不包含任何有意义信息,信息是单独存起来的,就像: redis
当你用令牌去访问的时候,咱们还须要根据这个令牌从存储redis里面读取出来信息,而后知道用户信息。这种令牌机制存在的特色会依赖一个存储。一旦存储出现问题,redis或者数据库挂掉了的话,你这个存储就毫无用处了。由于这个令牌自己不包含任何信息的。而JWT他的特色是自包含,这个令牌自己里面是有信息的。你拿到这个令牌以后,直接解析令牌就能够知道用户信息。而不须要从存储里面去读取任何信息。spring
咱们在app工程的TokenStoreConfig里面配置咱们的JWT。 咱们不是加一个静态方法而是加一个静态类:JwtTokenConfig。咱们须要在里面配置一系列bean。 Token存储bean。Jwt令牌Token转换bean数据库
@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Configuration public static class JwtTokenConfig{ @Autowired private SecurityProperties securityProperties; /** * 只负责token存储,无论token生成 * @return */ @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 负责token生成过程当中的处理的 * JwtAccessTokenConverter功能能够进行密签,就是进行签名。 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.设置签名秘钥 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; } } }
咱们将秘钥配置放到:properties文件里面。json
public class OAuth2Properties { /** * jwt密签,加密时候使用这个密签,解密时候也是使用这个密签 * 本身必定要保存好,别人一旦知道你的秘钥,就能够签发你的jwt令牌,就能够随意进入你系统 */ private String jwtSigningKey = "yxm"; private OAuth2ClientProperties[] clients = {};//默认空数组 public OAuth2ClientProperties[] getClients() { return clients; } public void setClients(OAuth2ClientProperties[] clients) { this.clients = clients; } public String getJwtSigningKey() { return jwtSigningKey; } public void setJwtSigningKey(String jwtSigningKey) { this.jwtSigningKey = jwtSigningKey; } }
从上面能够知道咱们的tokenStore配置了两个,那么咱们如何告诉系统,咱们想用JwtTokenStore呢?因此咱们就须要外加一些配置。数组
@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "redis") public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Configuration /** * 前缀(prefix):表明的是以"."隔开的配置文件中,最后一个点前面的全部字符串。 * name:表明的是以"."隔开的配置文件中,最后一个点后面的字符串。 */ @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "jwt",matchIfMissing = true) public static class JwtTokenConfig{ @Autowired private SecurityProperties securityProperties; /** * 只负责token存储,无论token生成 * @return */ @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 负责token生成过程当中的处理的 * JwtAccessTokenConverter功能能够进行密签,就是进行签名。 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.设置签名秘钥 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; } } }
由于咱们在TokenStoreConfig多配置了一个bean,因此咱们须要将其加入到认证服务器配置上去。安全
@Configuration @EnableAuthorizationServer public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private SecurityProperties securityProperties; @Autowired private TokenStore redisTokenStore; @Autowired(required = false) //由于这个类在JwtTokenConfig有效状况下才有效 private JwtAccessTokenConverter jwtAccessTokenConverter; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * 系统端点配置:endpoints * 1.使用咱们本身的受权管理器(AuthenticationManager)和自定义的用户详情服务(UserDetailsService) */ endpoints.tokenStore(redisTokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); if(jwtAccessTokenConverter != null){ endpoints.accessTokenConverter(jwtAccessTokenConverter); } } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { /** * 系统第三方客户端配置: * 所谓的客户端就是有哪些应用会访问咱们的系统, * 咱们的认证服务器会决定给哪些第三方应用client去发送令牌。 * 若是这个配置后咱们以前配置文件中配置的clientId和clientSecret将不会起做用了 */ //目前咱们的应用场景是在咱们的app和咱们的前端;咱们不容许第三方来注册,因此用内存 /* clients.inMemory().withClient("yxm") .secret("yxmsecret") .accessTokenValiditySeconds(7200)//发出去的令牌,有效期是多少? 这里设置为2小时 .authorizedGrantTypes("refresh_token","password")//针对当前应用客户端:yxm,所能支持的受权模式是哪些?以前设置有4种类加上刷新总共5种:这里只支持配置的:"refresh_token","password"。 .scopes("all","read","write")//发出去的权限有哪些?以前前端请求携带了scoope,此配置的scope用来指定前端发送scope的值必须在配置的里面或者不携带scope;默认为此处配置的scope .and() .withClient("startshineye") .secret("startshineyesecret") .accessTokenValiditySeconds(3600) .authorizedGrantTypes("password") .scopes("read");*/ InMemoryClientDetailsServiceBuilder builder = clients.inMemory(); if(ArrayUtils.isNotEmpty(securityProperties.getOauth2().getClients())) {//判断咱们的配置是否为空 for (OAuth2ClientProperties client:securityProperties.getOauth2().getClients()){ builder.withClient(client.getClientId()) .secret(client.getClientSecret()) .accessTokenValiditySeconds(client.getAccessTokenValiditySeconds()) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .scopes("all"); } } } }
咱们使用password测试: 服务器
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJhdXRob3JpdGllcyI6WyJhZG1pbiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI3MzczNjY1NC05Y2M1LTRlOTctOWRhYS04MDY5MzU1MWRjNWIiLCJjbGllbnRfaWQiOiJzdGFydHNoaW5leWUiLCJzY29wZSI6WyJhbGwiXX0.WlBgeBkU5giUQWeDZqAAMF8i-8DrvqvdT7TglwsBzsE", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiNzM3MzY2NTQtOWNjNS00ZTk3LTlkYWEtODA2OTM1NTFkYzViIiwiZXhwIjoxNTg2MjMwNTQyLCJhdXRob3JpdGllcyI6WyJhZG1pbiIsIlJPTEVfVVNFUiJdLCJqdGkiOiJjNGM4MDlmYi0zNjFkLTRkNzYtOGU5Zi1hOTY5ZGI2MmVlOGIiLCJjbGllbnRfaWQiOiJzdGFydHNoaW5leWUifQ.VrrLpj24xLefgYd6j1ALfCRAghBoVHEfkKBMQb2veO8", "scope": "all", "jti": "73736654-9cc5-4e97-9daa-80693551dc5b" }
以前咱们说过,jwtToken是包含信息的:
咱们进入网站:https://jwt.io/输入咱们的jwtAccessToken app
咱们用户的me接口以下:
@GetMapping("/me") public Object me(@AuthenticationPrincipal UserDetails user){ return user; }
curl -i -X GET \ -H "Authorization:bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODM2NDcwNTEsInVzZXJfbmFtZSI6IuW8oOS4iSIsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6IjI5OGJhZDdkLThjZmUtNDIyZC1hZTg2LTgzN2U3Nzk2MjNmNiIsImNsaWVudF9pZCI6Inl4bSIsInNjb3BlIjpbImFsbCJdfQ.wPKwjJzT-EJ6BAZOSaAQwBBFBZKwrkmT2ymbkp_mdLA" \ 'http://127.0.0.1:8088/user/me'
请求示例:
咱们发现返回的是NO CONTENT
分析:
@GetMapping("/me") public Object me(@AuthenticationPrincipal UserDetails user){ return user; }
如今的Authentication对象里面的Principal并非一个UserDetails,而是一个字符串 因此获取不到值
咱们修改:
@GetMapping("/me") public Object me(Authentication user){ return user; }
咱们再次请求:获取的结果为:
咱们发现返回的Authentication并非按照咱们以前在jwt.io里面解码成的payload:
针对于JwtToken-可扩展,咱们在TokenStoreConfig下的JwtTokenConfig配置TokenEnhancer
他是一个接口,咱们须要本身实现。
public class MyJwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String,Object> info = new HashMap<>(); info.put("company","alibaba"); /** * 往accessToken里面添加额外信息 */ ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info); return accessToken; } }
写完上面类以后,咱们把它配置到Spring容器里面。注解: @ConditionalOnMissingBean(name="jwtTokenEnhancer")//业务系统本身能够添加一个:jwtTokenEnhancer来覆盖此定义的加强器。
@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "redis") public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Configuration /** * 前缀(prefix):表明的是以"."隔开的配置文件中,最后一个点前面的全部字符串。 * name:表明的是以"."隔开的配置文件中,最后一个点后面的字符串。 */ @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "jwt",matchIfMissing = true) public static class JwtTokenConfig{ @Autowired private SecurityProperties securityProperties; /** * 只负责token存储,无论token生成 * @return */ @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 负责token生成过程当中的处理的 * JwtAccessTokenConverter功能能够进行密签,就是进行签名。 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.设置签名秘钥 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; } @Bean @ConditionalOnMissingBean(name="jwtTokenEnhancer")//业务系统本身能够添加一个:jwtTokenEnhancer来覆盖 public TokenEnhancer jwtTokenEnhancer(){ return new MyJwtTokenEnhancer(); } } }
@Configuration @EnableAuthorizationServer public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private SecurityProperties securityProperties; @Autowired private TokenStore redisTokenStore; @Autowired(required = false) //由于这个类在JwtTokenConfig有效状况下才有效 private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired(required = false) private TokenEnhancer jwtTokenEnhancer; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * 系统端点配置:endpoints * 1.使用咱们本身的受权管理器(AuthenticationManager)和自定义的用户详情服务(UserDetailsService) */ endpoints.tokenStore(redisTokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); if(jwtAccessTokenConverter != null && jwtTokenEnhancer != null){ /** * 1.因为咱们以前经过DefaultTokenService建立的AccessToken默认是经过UUID来建立的,而且主框架没有给咱们提供共有的建立accessToken方法 * 2.建立的默认方法: private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken), * 因此咱们智能在endpoints里面配置加强器。 * 3.为了知足1,2;咱们须要建立加强器链,来将jwtAccessTokenConverter和jwtTokenEnhancer链接起来。 */ TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancers = new ArrayList<>(); enhancers.add(jwtTokenEnhancer); enhancers.add(jwtAccessTokenConverter); endpoints .tokenEnhancer(enhancerChain) .accessTokenConverter(jwtAccessTokenConverter); } }
咱们再次启动下服务:
而后访问获取accessToken:
而后咱们拿着access_token去jwt.io网站去查找:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJzY29wZSI6WyJhbGwiXSwiY29tcGFueSI6ImFsaWJhYmEiLCJleHAiOjE1ODM2NzU1NTIsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6ImRhZTg0MWZhLTlkNTktNGRhMC05NTZhLTgyODI3ZTJjYjRmNSIsImNsaWVudF9pZCI6Inl4bSJ9.gSNL-MJU1whYNqmL4IVGSJsoSpGo7VkrND8O9_c0VBQ
而后咱们携带咱们的access_token去获取Authentication信息。
咱们能够发现:返回的Authentication数据字段里面没有company信息,缘由是由于:咱们的加强是在Authentication生成以后进行的加强、Spring只会管理JwtToken标准的形式,不会改变你自定义的数据格式。若是须要自定义咱们的数据格式,咱们须要:在Authentication里面单独添加信息。
作这个事情的时候,咱们须要添加一个依赖;就是以前https://jwt.io/对应的网站包依赖
<!--添加JWT依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
//2.解密获取用户信息 Claims claims = Jwts.parser().setSigningKey(securityProperties.getOauth2().getJwtSigningKey().getBytes("UTF-8")).parseClaimsJws(token).getBody();
缘由是由于Jwts设置SigningKey默认不是utf-8的格式
而咱们在JwtAccessTokenConverter的时候是使用utf-8格式的。
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.设置签名秘钥-----默认是按照utf-8格式的 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; }
咱们重启服务尝试一下:
先获取令牌:
根据令牌咱们获取用户信息:
从expires_in": 7199咱们知道令牌的失效时间是:接近2小时,一旦在2小时以后访问时候,用户访问将会失效。咱们不能让用户老是登陆,从新获取令牌。
咱们获取令牌时候,会返回一个refresh_token
curl -i -X POST \ -H "Content-Type:application/x-www-form-urlencoded" \ -H "Authorization:Basic eXhtOnl4bXNlY3JldA==" \ -d "grant_type=refresh_token" \ -d "refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiZDU5Yzk1ZWYtZTVlNC00ZDhhLWI1MDUtNWM2ZjMwYmRiOGEwIiwiY29tcGFueSI6ImFsaWJhYmEiLCJleHAiOjE1ODYyNjg5MDcsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6IjE5YjlmODZjLWJkZDItNGRjNi04NzkzLWVlYTRhYWZlMzFhOSIsImNsaWVudF9pZCI6Inl4bSJ9.PZyZ-2S3JfJwJdAb2dL4orIuFc0Mjc9v-9QqZx7jPN8" \ -d "scop=all" \ 'http://127.0.0.1:8088/oauth/token'
此时咱们就会再次获取到对应有效时间为2小时的token