token處理之二使用JWT替換默認的token
JWT(Json Web Token) 特點:
1,自包含:jwt token包含有意義的信息
spring security oauth默認生成的token是uuid,是無意義的,本身並不包含任何信息。這個token所包含的信息,如果用redis存儲token ,會在redis里存儲這些信息(數據庫也一樣):
這樣當用這個token去訪問接口的時候,需要根據這個token 從redis中取出來存儲的相關的信息,才能知道這個token所包含的信息。這中存儲策略的特點是,如果redis或數據庫服務掛了,這個token 就失效了,因為這個token本身是不包含信息的。
而JWT自包含的意思就是,JWT令牌本身是有信息的,拿到令牌后,解析令牌就能拿到包含的信息,不用去存儲里取。
2,密簽
發的令牌根據指定的秘鑰簽名(防止信息被篡改,不是加密)
3,可擴展
所包含的信息可以擴展
要將uuid換成JWT,需要做兩件事,使用JWT ,有兩個增強器:
1,第一個叫JwtAccessTokenConverter,作用是添加JWT簽名,將uuid的token轉為jwt,用秘鑰簽名
2,第二個叫 TokenEnhancer ,作用是往jwt里添加自定義的信息。由於默認生成uuid token的方法是private,所以通過ImoocJwtTokenEnhancer 往jwt里添加一些自定義的信息
最后在認證服務器ImoocAuthenticationServerConfig里,拿到增強器鏈TokenEnhancerChain,判斷系統里這兩個增強器是不是空,非空的話把這兩個增強器連起來,加到TokenEndpoint 。
實現:
1,配置JwtAccessTokenConverter,新建一個配置類,先配置TokenStore 為JwtTokenStore ,然后JwtTokenStore 需要JwtAccessTokenConverter 這個轉換器,在轉換器里設置簽名。
/** * token存儲到redis,默認是在內存不行 * ClassName: TokenStoreConfig * @Description: token存儲策略 * @author lihaoyang * @date 2018年3月15日 */ @Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; /** * 配置redis存儲token * @Description: 配置文件有 imooc.security.oauth2.storeType = redis 時才生效 * @param @return * @return TokenStore * @throws * @author lihaoyang * @date 2018年3月16日 */ @Bean @ConditionalOnProperty(prefix = "imooc.security.oauth2" , name = "storeType" , havingValue = "redis") public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } /** * JWT配置 * ClassName: JwtTokenConfig * @Description:\ * @ConditionalOnProperty是說,有前綴imooc.security.oauth2.storeType = jwt 的配置時,這個類里的配置才生效 * matchIfMissing =true 意思是當配置文件里不配置imooc.security.oauth2.storeType = jwt時,配置是生效的 * @author lihaoyang * @date 2018年3月16日 */ @Configuration @ConditionalOnProperty(prefix = "imooc.security.oauth2" , name = "storeType" , havingValue = "jwt" , matchIfMissing = true) public static class JwtTokenConfig{ @Autowired private SecurityProperties securityProperties; @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey(securityProperties.getOauth2().getJetSigningKey());//jwt簽名 return jwtAccessTokenConverter; } } }
2,配置增強器 ImoocJwtTokenEnhancer往jwt增加自定義信息:
/** * jwt增強器 * ClassName: ImoocJwtTokenEnhancer * @Description: * 往jwt的 token增加自己的信息 * spring默認生成token的方法在DefaultTokenService里,是private,生成的是uuid,沒辦法重寫,只能是增強器把uuid轉換成jwt,添加一些信息 * @author lihaoyang * @date 2018年3月16日 */ public class ImoocJwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { //往jwt添加的自定義信息 Map<String , Object> info = new HashMap<>(); info.put("company", "imooc"); info.put("product_code", "100"); ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info); return accessToken; } },
3,在認證服務器ImoocAuthenticationServerConfig判斷是否存在2個增強器,並添加到TokenEndpoint ( /oauth/token處理的入口點)
/** * 認證服務器 * ClassName: ImoocAuthenticationServerConfig * @Description: * extends AuthorizationServerConfigurerAdapter 自定義token生成 * @author lihaoyang * @date 2018年3月12日 */ @Configuration @EnableAuthorizationServer //這個注解就是實現了一個認證服務器 public class ImoocAuthenticationServerConfig extends AuthorizationServerConfigurerAdapter{ /* * 不繼承AuthorizationServerConfigurerAdapter,這些bean會自己找,配了,就要自己實現 */ @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; //配置文件 @Autowired private SecurityProperties securityProperties; //token存在redis,默認是在內存 @Autowired private TokenStore tokenStore; /** * jwt需要的兩個增強器之一:將uuid轉換為jwt * 有jwt配置時才生效 */ @Autowired(required = false) private JwtAccessTokenConverter jwtAccessTokenConverter; /** * jwt需要的兩個增強器之二:往jwt添加自定義信息 */ @Autowired(required = false) private TokenEnhancer jwtTokenEnhancer; /** * 配置TokenEndpoint 是 /oauth/token處理的入口點 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); /** * 使用JWT ,有兩個增強器: * 1,使用JwtAccessTokenConverter將uuid的token轉為jwt,用秘鑰簽名 * 2,由於默認生成uuid token的方法是private,所以通過ImoocJwtTokenEnhancer 往jwt里添加一些自定義的信息 * * 在這里拿到增強器的鏈,把這兩個增強器連起來 */ if(jwtAccessTokenConverter != null && jwtTokenEnhancer != null){ //拿到增強器鏈 TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancers = new ArrayList<TokenEnhancer>(); enhancers.add(jwtAccessTokenConverter); enhancers.add(jwtTokenEnhancer); enhancerChain.setTokenEnhancers(enhancers); endpoints.tokenEnhancer(enhancerChain) .accessTokenConverter(jwtAccessTokenConverter); } } /** * 功能:認證服務器會給哪些第三方應用發令牌 * 覆蓋了該方法,application.properties里配置的 * security.oauth2.client.clientId = imooc * security.oauth2.client.clientSecret = imoocsecret * 就失效了 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //1,寫死 // clients.jdbc(dataSource)就是qq場景用的,有第三方公司注冊過來,目前場景是給自己的應用提供接口,所以用內存就行 // clients.inMemory() // //~========================== 在這里配置和寫配置文件一樣================ // .withClient("imooc") //第三方應用用戶名 // .secret("imoocsecret") //密碼 // .accessTokenValiditySeconds(7200)//token有效期 // .authorizedGrantTypes("password","refresh_token") //支持的授權模式 // .scopes("all","read","write") //相當於oauth的權限,這里配置了,請求里的必須和這里匹配 // //~=======如果有多個client,這里繼續配置 // .and() // .withClient("xxxxx"); //2,讀取配置文件 InMemoryClientDetailsServiceBuilder builder = clients.inMemory(); //判斷是否配置了客戶端 if(ArrayUtils.isNotEmpty(securityProperties.getOauth2().getClients())){ for (OAuth2ClientProperties config : securityProperties.getOauth2().getClients()) { builder.withClient(config.getClientId()) .secret(config.getClientSecret()) .accessTokenValiditySeconds(config.getAccessTokenValiditySeconds()) .authorizedGrantTypes("password","refresh_token") //這些也可以配置也可以寫死,看心情 .scopes("all","read","write"); } } } }
重新獲取token:
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjExNjYwNDksInVzZXJfbmFtZSI6IjEzODEyMzQ5ODc2IiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iLCJST0xFX1VTRVIiXSwianRpIjoiYzRkYWQzYTMtM2I0Ni00N2FlLTgzYzAtYzkyNjg2MzU5ZjI0IiwiY2xpZW50X2lkIjoiaW1vb2MiLCJzY29wZSI6WyJhbGwiLCJyZWFkIiwid3JpdGUiXX0.R6-l64ogfHGRACNLiz3_-d-KT8AnN0jmSYzplpkSy-0", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiIxMzgxMjM0OTg3NiIsInNjb3BlIjpbImFsbCIsInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJjNGRhZDNhMy0zYjQ2LTQ3YWUtODNjMC1jOTI2ODYzNTlmMjQiLCJleHAiOjE1MjM3NTc5ODksImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6ImQ3N2Y3ZjVkLTE1OTctNGI5My1hNzU5LWUyYWVlYTBjM2UxMSIsImNsaWVudF9pZCI6Imltb29jIn0.ZKMUKmprgtFbsWBAFAI_BKsBVQ9RUGdbhViG3OyyIJU", "expires_in": 59, "scope": "all read write", "jti": "c4dad3a3-3b46-47ae-83c0-c92686359f24" }
此時redis中就沒有token信息了
訪問https://www.jsonwebtoken.io/ 可以解析jwt
至此已完成JWT配置,但是如果想在controller 獲取到往JWT里自定義的信息,還需要添加一些配置
在demo項目(應用的項目)添加maven依賴:
<!-- 解析jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
在controller里:
@GetMapping("/me2") public Object getCurrentUser2(Authentication user,HttpServletRequest request) throws Exception{ String header = request.getHeader("Authorization"); String token = StringUtils.substringAfter(header, "bearer "); Claims claims = Jwts.parser().setSigningKey(securityProperties.getOauth2().getJwtSigningKey().getBytes("UTF-8")).parseClaimsJws(token).getBody(); String company = (String) claims.get("company"); String productId = (String) claims.get("product_id"); System.err.println("token decode ------>company:"+company+",productId:"+productId); return user; }
但是我一直打印不了,不知道為什么?誰知道告訴我
Token刷新:
token是有有效期的,當token過期后,響應信息會提示token過期,又得重新登錄才能訪問接口, 有個token的刷新機制,我們請求 token的時候,會返回一個 refresh_token ,這個就是在token過期后,可以拿着它去換取一個新的token,這個過程應該是在用戶無感知的情況下進行的。實驗表明,對於沒有過期的token也可以刷新,會返回一個新的token,但是之前的還可以用(這樣做沒有什么意義)
刷新token訪問的url還是 http://127.0.0.1:8080/oauth/token
需要的參數
具體代碼放在了github:https://github.com/lhy1234/spring-security