【spring security】oauth2.0認證授權實現


1、基本實現

授權服務

SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

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

    @Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated();
        http.formLogin().loginProcessingUrl("/login")
                .usernameParameter("username").passwordParameter("password")
                .defaultSuccessUrl("/default", true);
    }

}

這里設置了BCryptPasswordEncoder密碼器以及用戶信息服務,放行了oauth相關接口用於獲取token和校驗token。

AuthorizationConfig

//授權服務器配置
@Configuration
@EnableAuthorizationServer //開啟授權服務
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private ClientDetailsServiceImpl clientDetailsService;

    /**
     *  授權服務器端點的 非安全性配置(請求到 TokenEndpoint )
     *  配置令牌(token)的訪問端點和令牌服務(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }

    /**
     *  授權服務器端點的 安全性配置(請求到 TokenEndpoint 之前)
     *  配置令牌端點的安全約束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允許表單提交
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("permitAll()");
    }

    /**
     * 配置客戶端詳情
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);

    }
}

@EnableAuthorizationServer注解開啟授權服務,密碼模式設置authenticationManager,oauth模式中需要讓客戶端登錄這里設置客戶端詳情查詢服務clientDetailsService。

UserDetailsService

實現UserDetailsService重寫loadUserByUsername(String username)方法實現通過賬號查詢用戶信息邏輯

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("用戶認證,username:{}", username);
        User user = userService.getUserByUsername(username);
        return BeanCopyUtil.copyProperties(user, UserInfoDetail::new);
    }
}

UserDetails 用戶信息實體,getAuthorities()權限信息寫死了兩個,可以去數據庫里查

@Data
public class UserInfoDetail implements UserDetails {

    private Integer id;

    private String username;

    private String nickname;

    private String password;

    private Boolean enabled;

    /**
     * 用戶手機號
     */
    private String mobile;

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        List<GrantedAuthority> list = new ArrayList<>();
        SimpleGrantedAuthority authority1 = new SimpleGrantedAuthority("ROLE_USER");
        SimpleGrantedAuthority authority2 = new SimpleGrantedAuthority("ROLE_SYS");
        list.add(authority1);
        list.add(authority2);

        return list;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

ClientDetailsService

@Slf4j
@Service
public class ClientDetailsServiceImpl implements ClientDetailsService {

    @Autowired
    private OauthClientService oauthClientService;

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        log.info("客戶端認證,clientId:{}", clientId);
        OauthClient oauthClient = oauthClientService.getClientByClientId(clientId);
        BaseClientDetails baseClientDetails = new BaseClientDetails();
        baseClientDetails.setClientId(oauthClient.getClientId());
        baseClientDetails.setClientSecret(oauthClient.getClientSecret());
        baseClientDetails.setScope(StringUtils.commaDelimitedListToSet(oauthClient.getScope()));
        baseClientDetails.setAuthorizedGrantTypes(StringUtils.commaDelimitedListToSet(oauthClient.getAuthorizedGrantTypes()));

        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        if (!StringUtils.isEmpty(oauthClient.getAuthorities())) {
            String[] authorities = oauthClient.getAuthorities().split(",");
            for (String authority : authorities) {
                authorityList.add(new SimpleGrantedAuthority(authority));
            }
        }
        baseClientDetails.setAuthorities(authorityList);
        baseClientDetails.setResourceIds(StringUtils.commaDelimitedListToSet(oauthClient.getResourceIds()));
        baseClientDetails.setRegisteredRedirectUri(StringUtils.commaDelimitedListToSet(oauthClient.getWebServerRedirectUri()));
        return baseClientDetails;
    }

}

通過客戶端ID查詢客戶端詳情,client_id(客戶端ID)、client_secret(客戶端秘鑰)、scope(作用域)、authorized_grant_types(授權類型)、authorities(權限)、resource_ids(資源ID)、registered_redirect_uri(重定向地址)、access_token_validity(accesstoken有效時間)......

客戶端授權類型配置的authorization_code,password,refresh_token授權碼模式、密碼模式。還有implicit(隱式模式) client_credentials(客戶端模式)

資源服務

ResourceServerConfig

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public static final String RESOURCE_ID = "userinfo";

    @Primary
    @Bean
    public RemoteTokenServices remoteTokenServices() {
        final RemoteTokenServices tokenServices = new RemoteTokenServices();
        //設置授權服務器check_token端點完整地址
        tokenServices.setCheckTokenEndpointUrl("http://localhost:5000/oauth/check_token");
        //設置客戶端id與secret,注意:client_secret值不能使用passwordEncoder加密!
        tokenServices.setClientId("c123456");
        tokenServices.setClientSecret("123456");
        return tokenServices;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/aaa/aaa").hasAuthority("ROLE_USER")
                .antMatchers("/bbb/aaa").hasRole("123456")
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                .anyRequest().authenticated()
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
    }

}

定義了資源ID “userinfo”,客戶端信息里resource_ids需要包含userinfo。寫了兩個啥都沒干的接口做了個權限控制.antMatchers("/aaa/aaa").hasAuthority("ROLE_USER").antMatchers("/bbb/aaa").hasRole("123456")

2、認證流程分析---密碼模式

在測試的過程中有一個問題讓我很奇怪,認證流程走下來查詢了很多次客戶端詳情。密碼模式查詢客戶端詳情大概查詢了4次,為什么要查這么多次,每一次又是因為啥???debug走了好多好多遍才找到............................................

ClientCredentialsTokenEndpointFilter

訪問oauth2令牌端點前的一個過濾器,作用是進行客戶端身份驗證,也就是客戶端登錄,對,客戶端也要登錄用client_id、client_secret

public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {

    // 攔截了/oauth/token這個端點
	public ClientCredentialsTokenEndpointFilter() {
		this("/oauth/token");
	}
    
    ......

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {
		
        // 判斷是否是post請求
		if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
		}

		String clientId = request.getParameter("client_id");
		String clientSecret = request.getParameter("client_secret");

		// 判斷是否已經完成認證
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null && authentication.isAuthenticated()) {
			return authentication;
		}

		......

		clientId = clientId.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
				clientSecret);

		return this.getAuthenticationManager().authenticate(authRequest);

	}
    
    .......

}

使用請求參數clientId、clientSecret封裝成UsernamePasswordAuthenticationToken用作一個身份的標識,調用認證邏輯的處理器AuthenticationManager,在AuthenticationManager的實現類ProviderManager中維護了一個AuthenticationProvider的list這里面的才是真正的認證邏輯,根據身份標識UsernamePasswordAuthenticationToken去匹配認證邏輯的提供者AuthenticationProvider,匹配到的提供者是DaoAuthenticationProvider。在DaoAuthenticationProvider里第一次查詢了客戶端信息並做了相關校驗,此時DaoAuthenticationProvider中裝配的userDetailsService是ClientDetailsUserDetailsService通過自定義的clientDetailsService查詢客戶端信息並且轉換成用戶信息。(基本就是之前寫過的spring security的認證流程,區別就是spring security的認證流程中DaoAuthenticationProvider中裝配的userDetailsService是自定義的userDetailsService直接去查詢的用戶信息

TokenEndpoint

令牌請求的端點,提供了獲取令牌的接口 /oauth/token

public class TokenEndpoint extends AbstractEndpoint {

	......
	
	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

		......

		String clientId = getClientId(principal);
        // (1)
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
		// (2)
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        ......
        
        // (3)
		if (authenticatedClient != null) {
			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
		}
		
        ......
		// (4)
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type");
		}

		return getResponse(token);

	}
	
	......
	
}

在前面過濾器中已經將認證成功的客戶端token--->UsernamePasswordAuthenticationToken放入了SecurityContext中,所以在這個TokenEndpoint中可以通過參數Principal principal把客戶端信息注入進來

// ClientCredentialsTokenEndpointFilter的父類
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
        
    ......
        
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
        
        ......
        try {
        	// 調用子類ClientCredentialsTokenEndpointFilter獲取經過認證的UsernamePasswordAuthenticationToken
			authResult = attemptAuthentication(request, response);
			......
			sessionStrategy.onAuthentication(authResult, request, response);
		}
        
        ......
        
        successfulAuthentication(request, response, chain, authResult);
        
    }
    
    protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		......
		// 將通過認證的UsernamePasswordAuthenticationToken放入SecurityContext
		SecurityContextHolder.getContext().setAuthentication(authResult);

		......
        
	}

}

(1)第二次查詢了客戶端信息

(2)構建TokenRequest

public class DefaultOAuth2RequestFactory implements OAuth2RequestFactory {

    public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {

		String clientId = requestParameters.get(OAuth2Utils.CLIENT_ID);
		
        ......
        
		String grantType = requestParameters.get(OAuth2Utils.GRANT_TYPE);

		Set<String> scopes = extractScopes(requestParameters, clientId);
		TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);

		return tokenRequest;
	}
    
    private Set<String> extractScopes(Map<String, String> requestParameters, String clientId) {
		Set<String> scopes = OAuth2Utils.parseParameterList(requestParameters.get(OAuth2Utils.SCOPE));
		ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

		if ((scopes == null || scopes.isEmpty())) {
			scopes = clientDetails.getScope();
		}

		if (checkUserScopes) {
			scopes = checkUserScopes(scopes, clientDetails);
		}
		return scopes;
	}
    
}

這里主要封裝了一些參數,其中scopes是請求的token可以使用的作用域。如果請求參數中沒有scopes,就使用客戶端的scopes。第三次查詢了客戶端詳情

(3)校驗scopes

判斷請求token的作用域 在不在 客戶端的作用域范圍內

(4)調用TokenGranter使用tokenRequest生成token

TokenGranter

頒發token的頂級接口。有兩個實現

AbstractTokenGranter定義了各個授權類型的規范

  • ResourceOwnerPasswordTokenGranter ==> password密碼模式
  • AuthorizationCodeTokenGranter ==> authorization_code授權碼模式
  • ClientCredentialsTokenGranter ==> client_credentials客戶端模式
  • ImplicitTokenGranter ==> implicit簡化模式
  • RefreshTokenGranter ==>refresh_token 刷新token專用
public abstract class AbstractTokenGranter implements TokenGranter {
	
	protected final Log logger = LogFactory.getLog(getClass());

	private final AuthorizationServerTokenServices tokenServices;

	private final ClientDetailsService clientDetailsService;
	
	private final OAuth2RequestFactory requestFactory;
	
	private final String grantType;	// 授權模式標識

	......

	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		// 根據授權模式標識匹配到具體的授權者
		if (!this.grantType.equals(grantType)) {
			return null;
		}
		
		String clientId = tokenRequest.getClientId();
        // 第四次查詢了客戶端詳情
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        // 判斷客戶端支持的授權類型 包不包括 當前授權類型
		validateGrantType(grantType, client);

		if (logger.isDebugEnabled()) {
			logger.debug("Getting access token for: " + clientId);
		}
		// 進行校驗授權
		return getAccessToken(client, tokenRequest);

	}

	protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
		return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
	}

	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
		OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
		return new OAuth2Authentication(storedOAuth2Request, null);
	}

	protected void validateGrantType(String grantType, ClientDetails clientDetails) {
		Collection<String> authorizedGrantTypes = clientDetails.getAuthorizedGrantTypes();
		if (authorizedGrantTypes != null && !authorizedGrantTypes.isEmpty()
				&& !authorizedGrantTypes.contains(grantType)) {
			throw new InvalidClientException("Unauthorized grant type: " + grantType);
		}
	}

    ......

}

tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));

AuthorizationServerTokenServices tokenServices; token相關服務,提供了創建token,刷新token,獲取token的實現。

getOAuth2Authentication(client, tokenRequest)授權邏輯,密碼模式中重寫了該方法。定義了授權類型的標識,使用用戶的賬號密碼又走了一遍AuthenticationManager認證授權的操作

public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {

	private static final String GRANT_TYPE = "password";

	private final AuthenticationManager authenticationManager;

	public ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager,
			AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
		this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
	}

	protected ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
			ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
		super(tokenServices, clientDetailsService, requestFactory, grantType);
		this.authenticationManager = authenticationManager;
	}

	@Override
	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

		Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
		String username = parameters.get("username");
		String password = parameters.get("password");
		// Protect from downstream leaks of password
		parameters.remove("password");

		Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
		((AbstractAuthenticationToken) userAuth).setDetails(parameters);
		try {
			userAuth = authenticationManager.authenticate(userAuth);
		}
		catch (AccountStatusException ase) {
			//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
			throw new InvalidGrantException(ase.getMessage());
		}
		catch (BadCredentialsException e) {
			// If the username/password are wrong the spec says we should send 400/invalid grant
			throw new InvalidGrantException(e.getMessage());
		}
		if (userAuth == null || !userAuth.isAuthenticated()) {
			throw new InvalidGrantException("Could not authenticate user: " + username);
		}
		
		OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);		
		return new OAuth2Authentication(storedOAuth2Request, userAuth);
	}
}

CompositeTokenGranter維護了一個List<TokenGranter>里面放的是各個授權模式的實例,循環調用這些實例通過grantType匹配到真正請求的授權模式,生成token。

public class CompositeTokenGranter implements TokenGranter {

	private final List<TokenGranter> tokenGranters;

	public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
		this.tokenGranters = new ArrayList<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;
	}
	
	public void addTokenGranter(TokenGranter tokenGranter) {
		if (tokenGranter == null) {
			throw new IllegalArgumentException("Token granter is null");
		}
		tokenGranters.add(tokenGranter);
	}

}

AuthorizationServerTokenServices

token服務接口定義了創建token、刷新token、獲取token三個方法

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
		ConsumerTokenServices, InitializingBean {
	// refreshToken的有效時間
	private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
	// accessToken的有效時間
	private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
	// 是否支持refreshToken
	private boolean supportRefreshToken = false;
	// RefreshToken是否能夠重新使用  刷新AccessToken的時候 是否 生成新的RefreshToken
	private boolean reuseRefreshToken = true;
	// token商店 token存儲相關
	private TokenStore tokenStore;
	// 客戶端信息服務
	private ClientDetailsService clientDetailsService;
	// 可對token進行增強,默認token只是一個UUID字符串---->JWT
	private TokenEnhancer accessTokenEnhancer;
	// 認證授權的處理器
	private AuthenticationManager authenticationManager;

	......

	@Transactional
	public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

		OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
		OAuth2RefreshToken refreshToken = null;
		if (existingAccessToken != null) {
			if (existingAccessToken.isExpired()) {
				if (existingAccessToken.getRefreshToken() != null) {
					refreshToken = existingAccessToken.getRefreshToken();
					
					tokenStore.removeRefreshToken(refreshToken);
				}
				tokenStore.removeAccessToken(existingAccessToken);
			}
			else {
				// Re-store the access token in case the authentication has changed
				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);
			}
		}

		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;

	}

	......

	private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
		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());

		return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
	}
	
    ......

}


免責聲明!

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



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