Spring Security構建Rest服務-1205-Spring Security OAuth開發APP認證框架之Token處理


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM