spring security_day5 開發(fā)基于app的認(rèn)證框架

一. Spring Security OAuth簡介

??上一章的開發(fā)l是以第三方應(yīng)用角色為中心進(jìn)行開發(fā)的鸯乃,我們做的工作就是如何實(shí)現(xiàn)第三方應(yīng)用斩萌,然后獲取服務(wù)提供者的訪問權(quán)限去訪問服務(wù)提供者的一些受保護(hù)的資源。而在這一章,我們要使用spring security oauth基于服務(wù)提供者進(jìn)行開發(fā),學(xué)習(xí)如何向其他客戶端提供令牌呻顽、并且可以驗(yàn)證這些令牌然后返回我們受保護(hù)的資源。
??服務(wù)提供商事實(shí)上包括兩種角色:授權(quán)服務(wù)器和資源服務(wù)器丹墨,它們可以處于同一個應(yīng)用程序中廊遍,當(dāng)然多個資源服務(wù)器也可以共享同一個授權(quán)服務(wù)器。

服務(wù)提供者

??spring social oauth的主要流程是:第三方應(yīng)用向認(rèn)證服務(wù)器獲取授權(quán)贩挣,認(rèn)證服務(wù)器向應(yīng)用發(fā)送token喉前,第三方應(yīng)用使用該token從資源服務(wù)器中獲取數(shù)據(jù)。
??spring social oauth默認(rèn)幫我們實(shí)現(xiàn)了四種傳統(tǒng)的授權(quán)模式的認(rèn)證邏輯王财,我們需要做的只是將我們自定義的認(rèn)證邏輯添加到認(rèn)證服務(wù)器中卵迂,使得我們的認(rèn)證服務(wù)器可以支持用戶名和密碼、手機(jī)號和第三方登錄的方式生成token绒净。

二. 服務(wù)提供者的簡單實(shí)現(xiàn)
@Configuration
@EnableAuthorizationServer      //該注解:表明當(dāng)前是一個認(rèn)證服務(wù)器
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("imooc")    //第三方應(yīng)用的標(biāo)識:比如慕課微信助手见咒,標(biāo)識哪個第三方應(yīng)用從服務(wù)提供者獲取權(quán)限
                .secret(passwordEncoder().encode("imoocsecret"))
                .redirectUris("http://www.baidu.com")
                .authorizedGrantTypes("authorization_code","refresh_token")
                .scopes("all");
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

@Component
public class AuthUserDetailService implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;
    // 服務(wù)提供者的哪個用戶進(jìn)行授權(quán)挂疆,比如微信中是哪個用戶賦予第三方應(yīng)用權(quán)限
    @Override
    public UserDetails loadUserByUsername(String username) {
        logger.info("用戶{}請求授權(quán)", username);
        //注意:一定要包括"role_user"角色
        return  new User(username, passwordEncoder.encode("123456"), AuthorityUtils.createAuthorityList("ROLE_USER"));
    }
}
@Configuration
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().and()
                .authorizeRequests().anyRequest().authenticated().and()
                .csrf().disable();
    }
}

??以上配置就完成了一個簡單的認(rèn)證服務(wù)器改览,我們可以測試一下根據(jù)傳統(tǒng)的四種授權(quán)模式是否可以訪問到我們想要的資源牛曹。
??1. 授權(quán)碼模式:
??(1)獲取授權(quán)碼
????a :訪問改地址假丧,輸入用戶名和密碼之后會跳往授權(quán)頁面:http://localhost:8989/oauth/authorize?response_type=code&client_id=imooc&redirect_uri=http://www.baidu.com&scope=all

授權(quán)頁面

???? b:點(diǎn)擊授權(quán)之后獲取授權(quán)碼

百度頁面

??(2)根據(jù)授權(quán)碼獲取Token(可以使用谷歌插件Restlet Client工具):
??發(fā)送POST請求到:http://localhost:8989/oauth/token
????a. 請求頭的設(shè)置:

請求頭
Authorization參數(shù)的值

????b:請求體要根據(jù)Oauth2協(xié)議規(guī)定的參數(shù)進(jìn)行添加

請求體

??認(rèn)證服務(wù)器會返回我們需要的token。

返回的結(jié)果

??2. 密碼模式:如果想使用這種模式荚坞,我們需要添加一些代碼

1. AppSecurityConfig類中墨闲,需要覆蓋一個方法今妄,將AuthenticationManager對象交給容器管理
     @Override
     @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
2. ImoocAuthorizationServerConfig類中郑口,需要將AuthenticationManager設(shè)置到AuthorizationServerEndpointsConfigurer中
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                        .userDetailsService(userDetailsService);
    }
3. ImoocAuthorizationServerConfig類中鸳碧,需要給該應(yīng)用添加密碼授權(quán)模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("imooc")
                .secret(passwordEncoder().encode("imoocsecret"))
                .redirectUris("http://www.baidu.com")
                .authorizedGrantTypes("authorization_code","password","refresh_token")
                .scopes("all");
    }
密碼模式

??密碼模式的請求頭和授權(quán)碼模式相同盾鳞,參數(shù)根據(jù)OAuth2協(xié)議規(guī)定參數(shù)進(jìn)行添加。
??在獲取到對應(yīng)的token之后瞻离,我們就可以使用該token從資源服務(wù)器中獲取受保護(hù)的資源了腾仅,資源服務(wù)器的配置非常簡單只需要加一個注解即可。

@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig {
}

??然后我們就可以在請求頭添加該token訪問資源服務(wù)器中的資源套利。

訪問資源
請求頭中需要添加 :  Authorization=token_type access_token
三. SpringSecurityOauth核心源碼解析
獲取Token所需要的類的整體流程

??最好可以邊看源碼邊借鑒上圖推励,梳理類之間的關(guān)系,對 token的生成的流程有一個大概的認(rèn)知肉迫,因?yàn)槲覀冃枰砑幼远x的生成邏輯验辞,我們需要知道可以從哪里入手,代碼如下:

    TokenEndpoint類:
    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        // 作為受保護(hù)的資源喊衫,當(dāng)程序運(yùn)行到這里跌造,說明已經(jīng)完成了第三方應(yīng)用的認(rèn)證工作
        //principal包含了第三方應(yīng)用的所有信息(比如慕課微信助手的clientid、clientSecret等等)族购,說明了是哪個應(yīng)用需要獲取 tokne信息
        //parameters:獲取token的請求所攜帶的參數(shù)

         //1. 首先根據(jù) ClientDetailsService 獲取第三方應(yīng)用的所有信息壳贪,clientDetails中包括clientId,clientSecret寝杖,scope违施,grantType等等。
        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        //2. 然后使用 DefaultOAuth2RequestFactory根據(jù) 請求參數(shù)和 clientDetail封裝TokenRequest對象
        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        //3. 驗(yàn)證請求token中的scope是否有效瑟幕,是否超過了該應(yīng)用的授權(quán)范圍
        if (authenticatedClient != null) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }

         //4. 如果是授權(quán)碼模式磕蒲,將請求參數(shù)中的scope設(shè)置為空,因?yàn)槭跈?quán)碼模式 token中的scope已經(jīng)在獲取授權(quán)碼時確定了收苏,所以token的scope應(yīng)該從code中獲取
        if (isAuthCodeRequest(parameters)) {
            if (!tokenRequest.getScope().isEmpty()) {
                logger.debug("Clearing scope of incoming token request");
                tokenRequest.setScope(Collections.<String> emptySet());
            }
        }

        //5. 如果是刷新令牌的請求亿卤,新token中的scope應(yīng)該和refresh_token中的一致
        if (isRefreshTokenRequest(parameters)) {
          tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
        }

        //6. 獲取token
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type");
        }
        return getResponse(token);
    }

??有上述代碼可知,主要的邏輯都在第六步中鹿霸,我們首先看下TokenGranter類排吴,spring security oauth對該類的是這樣描述的:

Interface for granters of access tokens. Various grant types are defined in the specification, and each of those has an implementation, leaving room for extensions to the specification as needed.

??大致的意意思:該類是一個授權(quán)接口,每種授權(quán)類型都應(yīng)該實(shí)現(xiàn)該類定義自己的實(shí)現(xiàn)(加上refresh_token懦鼠,目前包括五種授權(quán)類型钻哩,所以至少有五個實(shí)現(xiàn)類)。所以如果我們想要自定義我們的認(rèn)證邏輯肛冶,需要實(shí)現(xiàn)該類街氢。
??該類的作用將TokenRequest封裝成我們想要的token對象。debug結(jié)果顯示睦袖,珊肃,目前的確包括五種授權(quán)模式,由于密碼模式比較簡單,所以我們跟著密碼模式的流程走一遍:

所有的TokenGranter

??spring security oauth使用裝飾者模式伦乔,將這五個類封裝到CompositeTokenGranter中厉亏,由它根據(jù)授權(quán)類型自動選擇對應(yīng)的TokenGranter

@Deprecated
public class CompositeTokenGranter implements TokenGranter {

    private final List<TokenGranter> tokenGranters;

    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if (grant!=null) {
                return grant;
            }
        }
        return null;
    }
      ......
}  

??在調(diào)用ClientCredentialsTokenGranter的方法之前烈和,會調(diào)用父類AbstractTokenGrantergrant()方法:

 1. 密碼授權(quán)模式的方法:該方法沒有做什么主要的工作爱只,就是對父類返回token做了一層封裝,因?yàn)槟承┣闆r下密碼模式不應(yīng)該返回refresh_token
    @Override
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        OAuth2AccessToken token = super.grant(grantType, tokenRequest);
        if (token != null) {
            DefaultOAuth2AccessToken norefresh = new DefaultOAuth2AccessToken(token);
            if (!allowRefresh) {
                norefresh.setRefreshToken(null);
            }
            token = norefresh;
        }
        return token;
    }
  2. 父類的方法:使用 ClientDetailsService 對象獲取 clientDetails 對象招刹,然后和 tokenRequest 封裝成token 對象
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        String clientId = tokenRequest.getClientId();
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        validateGrantType(grantType, client);

        return getAccessToken(client, tokenRequest);
    }

   3.  調(diào)用TokenServices 對象將 clientDetail 和 TokenRequest 封裝成 結(jié)果token 對象
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

    4. 首先使用 DefaultOAuth2RequestFactory 對象將 clientDetail 和 tokenRequest 封裝成 OAuth2Authentication 對象
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, null);
    }

    createOAuth2Request方法如下恬试,使用 TokenRequest 和 client 封裝 OAuth2Request  對象,此時tokenRequest就包含了 client對象的一些細(xì)節(jié)
    public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) {
        return tokenRequest.createOAuth2Request(client);
    }

    5. 然后調(diào)用 DefaultTokenServices 類的 createAccessToken() 方法根據(jù) OAuth2Authentication創(chuàng)建結(jié)果 Token 對象
    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        //1. 首先根據(jù)認(rèn)證信息疯暑,從tokenStore中查詢是否給該用戶返回給Token训柴, 這就是為什么我們測試密碼模式和授權(quán)碼模式時返回的token是相同的
        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if (existingAccessToken != null) {
             //(1)如果Token已過期,則將該token從 tokenStore中移除妇拯,然后重新創(chuàng)建 token
            if (existingAccessToken.isExpired()) {
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
              //(2)重新存儲一下 token畦粮,因?yàn)檎J(rèn)證信息有可能改變
            else {
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }
                
        else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = createRefreshToken(authentication);
            }
        }
          //2.  創(chuàng)建 token ,然后將 token和refresh_token存儲起來
        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        // In case it was modified
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;
    }
6. 根據(jù)認(rèn)證信息和 refresh_token 創(chuàng)建 Token 對象 
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        //1.  創(chuàng)建 DefaultOAuth2AccessToken ,然后設(shè)置默認(rèn)值
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
        }
        token.setRefreshToken(refreshToken);
        token.setScope(authentication.getOAuth2Request().getScope());
        //2. 使用  token增強(qiáng)器 accessTokenEnhancer給 token添加附加信息乖阵,我們可以提供自定義 token增強(qiáng)器給token添加自定義的信息宣赔。
        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }
四. 重構(gòu)代碼
image.png

??如果你指定了成功處理器,那么無論使用哪種方式進(jìn)行認(rèn)證瞪浸,認(rèn)證完成之后都會跳往成功處理器進(jìn)行處理儒将,所以我們只需要原來成功處理器中的代碼修改成 向客戶端返回 token 的邏輯就可以了。
??所以我們按照以上圖片的流程對代碼進(jìn)行重構(gòu)

1. 首先在認(rèn)證服務(wù)器中做一些基本的配置对蒲,將成功處理器添加到流程中
@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(authenticationSuccessHandler).and()
                .authorizeRequests().anyRequest().authenticated().and()
                .csrf().disable();
    }
}
2. 成功處理器:根據(jù)之前源碼分析流程構(gòu)建 token 對象
@Component
public class ImoocSuccessHandler implements AuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private ClientDetailsService clientDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;
    @Autowired(required = false)
    private TokenEnhancer tokenEnhancer;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        // 1. BasicAuthenticationFilter類有獲取頭信息代碼钩蚊,可以借鑒
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Basic ")) {
            throw new RuntimeException("請求的格式不正確");
        }

        String[] tokens = extractAndDecodeHeader(header, request);
        String clientId = tokens[0];
        String clientSecret = tokens[1];

        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
        if(clientDetails == null){
            logger.info("{}對應(yīng)的信息不存在!", clientId);
            throw new RuntimeException("無效的clientId");
        }
        if( !passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())){
            throw new RuntimeException("無效的clientSecret");
        }
        // 根據(jù)之前分析流程獲取最終結(jié)果
        TokenRequest tokenRequest = new TokenRequest(null, clientId,  clientDetails.getScope(), "custom");
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

        OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

        accessToken =  tokenEnhancer == null? accessToken:tokenEnhancer.enhance(accessToken, oAuth2Authentication);

        logger.info("token為:{}", accessToken);
    }

    private String[] extractAndDecodeHeader (String header, HttpServletRequest request) throws
    UnsupportedEncodingException {
        
        byte[] base64Token = header.substring(6).getBytes("UTF-8");
        byte[] decoded;
        try {
            decoded = Base64.decode(base64Token);
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException(
                    "Failed to decode basic authentication token");
        }
        String token = new String(decoded, Charset.forName("UTF-8"));

        int delim = token.indexOf(":");

        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[]{token.substring(0, delim), token.substring(delim + 1)};
    }
    }

??以上的配置就算完成了用戶名和密碼登錄的基本授權(quán)流程,我們可以做一些測試:
?? (1)發(fā)送請求獲取 token蹈矮, 注意請求頭和之前相同砰逻。

發(fā)送請求獲取token
token數(shù)據(jù)

??(2)根據(jù) token 獲取資源

返回結(jié)果
五. Token相關(guān)

??1. TokenStore配置:默認(rèn)情況下,服務(wù)提供者的 token 是存儲在內(nèi)存中的泛鸟,當(dāng)服務(wù)重啟時蝠咆,發(fā)出去的 token 將變得無效,因?yàn)閼?yīng)用無法在內(nèi)存中獲取 token對應(yīng)的認(rèn)證信息北滥,所以可以將 token保存到持久化的介質(zhì)中刚操,以保證當(dāng)某些特殊情況機(jī)器導(dǎo)致重啟時發(fā)出去的 token 依然有效。

1. TokenStore配置類
@Configuration
public class TokenStoreConfig {
    @Bean
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory){
        return new RedisTokenStore(redisConnectionFactory);
    }
}
2. 通過授權(quán)服務(wù)器中的配置再芋, 將 tokenStore加到 入口類中
@Configuration
@EnableAuthorizationServer    
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                    .tokenStore(tokenStore)
                    ......
    }
}

??2. 使用 JWT 替換 token
?? JWT ( JSON WEB TOKEN )菊霜,主要用作交換信息和授權(quán),主要包含三個部分:Header(包括所使用的 token 類型和所使用的簽名算法)济赎、Payload(主要數(shù)據(jù)鉴逞,jwt 存儲數(shù)據(jù)的部分)记某,Signature(簽名算法,判斷數(shù)據(jù)的有效性(是否發(fā)生被惡意篡改构捡、丟失等改變))辙纬。
?? spring security oauth中的token中包含的數(shù)據(jù)是沒有任何業(yè)務(wù)含義的,它的作用主要是保證了自身在持久化介質(zhì)中的唯一性叭喜,可以根據(jù) token從介質(zhì)中可以獲取到用戶認(rèn)證的信息,它的初始化方式是 DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());蓖谢,而JWT不需要依賴任何持久化介質(zhì)捂蕴,用戶的身份信息是存儲在 token中的,所以它也不存在由于服務(wù)重啟導(dǎo)致token變得無效的問題闪幽。

@Configuration
public class TokenStoreConfig {
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("imooc");  //使用密簽加密和解密 token
        return jwtAccessTokenConverter;
    }
    /**
     *  添加附加信息
     * */
    @Bean
    public TokenEnhancer tokenEnhancer(){
        return new TokenEnhancer() {
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                Map<String, Object> info = new HashMap<>();
                info.put("鍵", "自定義數(shù)據(jù)");
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
                return accessToken;
            }
        };
    }
}

@Configuration
@EnableAuthorizationServer  
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private TokenEnhancer tokenEnhancer;
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> chain = new ArrayList<>();
            chain.add(tokenEnhancer);
            chain.add(jwtAccessTokenConverter);
            tokenEnhancerChain.setTokenEnhancers(chain);
            endpoints
                    .tokenStore(tokenStore)
                    .tokenEnhancer(tokenEnhancerChain)
                    ......
    }
}

??將 OauthToken 替換為 JWT的配置就完成了啥辨,我們可以做一些測試:

??(1)獲取 token:有以下結(jié)果可知,的確已經(jīng)替換成功盯腌,不再是32位的UUID字符串溉知,而是包含三個部分的 JWT,每個部分由"." 隔開腕够。

獲取 token

??(2)訪問資源:將生成的 token 添加到請求頭中级乍,獲取到了受保護(hù)的資源

測試結(jié)果

??如果想要查看我們生成的 JWT 包含了什么信息,可以點(diǎn)擊該網(wǎng)址查看帚湘。

jwt 內(nèi)容

?? SpringSecurity沒有給我們提供可以獲取 JWT 中自定義的數(shù)據(jù)的途徑玫荣,所以我們需要自定義接口來解析 JWT獲取我們想要的數(shù)據(jù)。

1.添加依賴
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
2. 解析 token
    @GetMapping("/me")
    public Object getMe(HttpServletRequest request){
        String authorization = request.getHeader("Authorization");
        String token = StringUtils.substringAfter(authorization, "bearer ");
        Claims claims = Jwts.parser().setSigningKey("imooc".getBytes(Charset.forName("UTF-8"))).parseClaimsJws(token).getBody();
        return claims;
    }
六. 使用 JWT 實(shí)現(xiàn) SSO

單點(diǎn)登錄(SingleSignOn大诸,SSO)捅厂,就是通過用戶的一次性鑒別登錄。當(dāng)用戶在身份認(rèn)證服務(wù)器上登錄一次以后资柔,即可獲得訪問單點(diǎn)登錄系統(tǒng)中其他關(guān)聯(lián)系統(tǒng)和應(yīng)用軟件的權(quán)限焙贷,同時這種實(shí)現(xiàn)是不需要管理員對用戶的登錄狀態(tài)或其他信息進(jìn)行修改的,這意味著在多個應(yīng)用系統(tǒng)中贿堰,用戶只需一次登錄就可以訪問所有相互信任的應(yīng)用系統(tǒng)辙芍。這種方式減少了由登錄產(chǎn)生的時間消耗,輔助了用戶管理羹与,是目前比較流行的

應(yīng)用交互

??使用三個系統(tǒng):應(yīng)用A沸手、應(yīng)用B、認(rèn)證服務(wù)器注簿,進(jìn)行模擬單點(diǎn)登錄契吉,當(dāng)任何一個應(yīng)用經(jīng)過認(rèn)證服務(wù)器的認(rèn)證和授權(quán)之后,其他的應(yīng)用都無需進(jìn)行認(rèn)證直接可以向認(rèn)證服務(wù)器請求授權(quán)诡渴,并根據(jù)所授予的權(quán)限訪問資源服務(wù)器中的資源捐晶。
??所以我們需要搭建三個項(xiàng)目菲语, APP1、APP2惑灵、APPServer
??(1)首先創(chuàng)建三個項(xiàng)目的父項(xiàng)目:sso-demo山上,所導(dǎo)的依賴和之前的父項(xiàng)目相同
??(2)創(chuàng)建認(rèn)證服務(wù)器

1. 添加依賴,注意這三個項(xiàng)目的依賴相同
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
    </dependencies>

2. 服務(wù)端的一些配置
@Configuration
public class SsoServerConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().and()
                .authorizeRequests().anyRequest().authenticated()
                .and().csrf().disable();
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new PasswordEncoder() {
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            public boolean matches(CharSequence charSequence, String s) {
                return StringUtils.equals(charSequence, s);
            }
        };
    }
}
3. 認(rèn)證服務(wù)器類的一些配置
@Configuration
@EnableAuthorizationServer
public class SsoAuthrizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("app1")
                .secret("appsecret1")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://localhost:9091/app1/login")
                .scopes("all")
                .autoApprove(true)      //自動授權(quán)
              .and()
                .withClient("app2")
                .secret("appsecret2")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://localhost:9092/app2/login")
                .scopes("all")
                .autoApprove(true);
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore())
                  .accessTokenConverter(jwtAccessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("isAuthenticated()"); //表示用戶經(jīng)過身份認(rèn)證之后英支,才可以訪問 tokenkey
    }
    // JWT 的一些配置
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("imooc");
        return jwtAccessTokenConverter;
    }
}

4. 端口和系統(tǒng)用戶的一些配置
server.port=9090
server.servlet.context-path=/server
spring.security.user.password=123456

??(2)創(chuàng)建客戶端應(yīng)用(兩個客戶端的配置基本相同)

1. 客戶端應(yīng)用的配置類
@SpringBootApplication
@EnableOAuth2Sso    //開啟單點(diǎn)登錄功能
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

2. 客戶端應(yīng)用的配置文件
server.port=9092
server.servlet.context-path=/app2

security.oauth2.client.client-secret=appsecret2
security.oauth2.client.client-id=app2

#用戶認(rèn)證的地址
security.oauth2.client.user-authorization-uri=http://localhost:9090/server/oauth/authorize
#用戶獲取token的地址
security.oauth2.client.access-token-uri=http://localhost:9090/server/oauth/token
#由于配置了 tokenKey是需要已認(rèn)證的用戶才可以訪問佩憾,所以配置用戶獲取tokenkey的地址
security.oauth2.resource.jwt.key-uri=http://localhost:9090/server/oauth/token_key

3. 測試使用的前臺頁面
    <h1>客戶端2</h1>
    <a href="http://localhost:9091/app1/sso1demo.html">跳轉(zhuǎn)到應(yīng)用1</a>

??由于我們設(shè)置的是自動授權(quán),所以不會顯示授權(quán)頁面(可以選擇確認(rèn)授權(quán)或者拒絕授權(quán))干花,默認(rèn)的授權(quán)頁面是由WhitelabelApprovalEndpoint類提供的妄帘,如果想要自定義授權(quán)頁面,只需要仿照該類自定義即可池凄。

1. controller類
@RestController
@SessionAttributes("authorizationRequest")
public class GrantPage{
    @Autowired
    private ObjectMapper objectMapper;
    @RequestMapping("/oauth/confirm_access")
    public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request)  {
        // model中包含了授權(quán)請求中的所有信息抡驼,包括 回調(diào)地址、授權(quán)范圍 Scope等等肿仑,你可以自定義將某些信息顯示到頁面中
        //比如提示用 當(dāng)前哪個應(yīng)用正在請求授權(quán)致盟、授權(quán)范圍等等
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("/base_grant.html");
        return modelAndView;
    }
}

2. 前臺頁面
<h2>自定義授權(quán)頁面</h2>
//注意兩個表單提交地址是相同的 /oauth/authorize,由于這里我存在相對路徑問題所以我寫成了絕對路徑
// 判斷用戶是否授權(quán)是根據(jù)user_oauth_approval的值來判斷的尤慰,具體邏輯在`AuthorizationEndpoint`類中馏锡,想看的可以看下
<form method="post" action="http://localhost:9090/server/oauth/authorize">
    <input name="user_oauth_approval" value="true" type="hidden"/>
    <button class="btn" type="submit"> 同意授權(quán)</button>
</form>

<form method="post" action="http://localhost:9090/server/oauth/authorize">
    <input name="user_oauth_approval" value="false" type="hidden"/>
    <button class="btn" type="submit"> 拒絕授權(quán)</button>
</form>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伟端,隨后出現(xiàn)的幾起案子眷篇,更是在濱河造成了極大的恐慌,老刑警劉巖荔泳,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蕉饼,死亡現(xiàn)場離奇詭異,居然都是意外死亡玛歌,警方通過查閱死者的電腦和手機(jī)昧港,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來支子,“玉大人创肥,你說我怎么就攤上這事≈蹬螅” “怎么了叹侄?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長昨登。 經(jīng)常有香客問我趾代,道長,這世上最難降的妖魔是什么丰辣? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任撒强,我火速辦了婚禮禽捆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘飘哨。我一直安慰自己胚想,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布芽隆。 她就那樣靜靜地躺著浊服,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胚吁。 梳的紋絲不亂的頭發(fā)上牙躺,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機(jī)與錄音囤采,去河邊找鬼。 笑死惩淳,一個胖子當(dāng)著我的面吹牛蕉毯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播思犁,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼代虾,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了激蹲?” 一聲冷哼從身側(cè)響起棉磨,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎学辱,沒想到半個月后乘瓤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡策泣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年衙傀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萨咕。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡统抬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出危队,到底是詐尸還是另有隱情聪建,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布茫陆,位于F島的核電站金麸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏簿盅。R本人自食惡果不足惜钱骂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一叔锐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧见秽,春花似錦愉烙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至禀苦,卻和暖如春蔓肯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背振乏。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工蔗包, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人慧邮。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓调限,卻偏偏與公主長得像,于是被迫代替她去往敵國和親误澳。 傳聞我的和親對象是個殘疾皇子耻矮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容