spring security 整合 oauth2
前言
之前一篇部落格學過security的核心,這次整合一下oauth2,它也是市場上比較流行的介面驗證的一種方式了
引入pom
文中提及的整合oauth2的方式是建立在boot 的基礎上的.在引入的boot 和security的start之後,我們還需要引入oauth2,注意,它不是start,另外我們計劃將token儲存在redis中,所以我們還需要引入redis的start
<!-- 將token儲存在redis中 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
學習程式碼
security基本配置,主要是使用者許可權,以及設定oauth的認證路徑為所以角色都可訪問\
@Configuration public class AppSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers().anyRequest() .and() .authorizeRequests() .antMatchers("/oauth/*").permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { System.out.println("開始認證角色.............."); //這裡設定的角色 系統會自動加上role_字首 既ROLE_ADMIN //inMemoryAuthentication 從記憶體中獲取 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("user_1").password(new BCryptPasswordEncoder().encode("12345678")).roles("client"); //inMemoryAuthentication 從記憶體中獲取 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("user_2").password(new BCryptPasswordEncoder().encode("12345678")).roles("USER"); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { //oauth2的配置中需要這個bean return super.authenticationManagerBean(); } }
oauth2配置;如你所見oauth是建立在sercurity的基礎上的,所以資源伺服器和認證伺服器的configure與security基本的configure如出一轍;所謂資源伺服器配置就是值使用者請求資源時,哪些資源能被訪問,能被怎樣的許可權所訪問的配置,認證伺服器的配置主要是指採用哪種方式進行認證,認證的客戶端的賬號密碼的配置.關於認證方式有四種,這裡只提了兩種;
-
授權碼模式(authorization code)
-
簡化模式(implicit)
-
密碼模式(resource owner password credentials)
-
客戶端模式(client credentials)
@Configuration
public class Oauth2ServerConfig {
private static final String DEMO_RESOURCE_ID="order";
/**
* 資源伺服器配置
*/
@Configuration
@EnableResourceServer
protected static class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public ResourceServerConfig() {
super();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().anyRequest()
.and()
.anonymous()
.and()
.authorizeRequests()
//設定請求資源需要認證
.antMatchers("/getOrderInfo/**").authenticated();
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private AuthenticationManager authenticationManager;
@Bean
protected PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
public AuthorizationServerConfig() {
super();
}
@Override
/**
* 配置authorizationServer安全認證的相關資訊,建立clientCredentialsTokenEndPointFilter核心過濾器
*/
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Override
/**
* 配置oauth2的客戶端相關資訊
*/
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client1")
.resourceIds(DEMO_RESOURCE_ID)
//客戶端模式
.authorizedGrantTypes("client_credentials","refresh_token")
.scopes("select")
.authorities("ROLE_CLIENT")
//系統預設只接受加密的密碼
.secret(passwordEncoder.encode("123456"))
.and().withClient("cilent2")
.resourceIds(DEMO_RESOURCE_ID)
//password模式
.authorizedGrantTypes("password","refresh_token")
.scopes("select")
.authorities("client")
//a62d747e-91fb-42c5-8857-b47243179ecd
.secret(passwordEncoder.encode("123456"));
}
@Override
/**
* 配置AuthorizationServerEndpointsConfigurer眾多相關類,包括配置身份認證器,配置認證方式,TokenStore,TokenGranter,OAuth2RequestFactory
*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager);
}
}
}
兩個資源請求路徑的測試controller
@RestController
public class TestEndPoint {
@GetMapping("/getProductInfo/{id}")
public String getProductInfo(@PathVariable String id){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getDetails());
System.out.println(authentication.getPrincipal());
return "product id:" +id;
}
@GetMapping("/getOrderInfo/{id}")
public String getOrderInfo(@PathVariable String id){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getDetails());
System.out.println(authentication.getPrincipal());
return "order id:" +id;
}
}
核心說明
1.請求進入ClientCredentialsTokenEndpointFilter中的doFilter方法,跑到attemptAuthentication()開始進行認證
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
***********
Authentication authResult;
//開始認證
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
******************
}
2.獲取使用者傳入的認證伺服器的client_id和client_secret 組裝成UsernamePasswordAuthenticationToken,傳入AuthenticationManager(預設是providerManager)進行認證.
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
**************************************
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
**************************************
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
return this.getAuthenticationManager().authenticate(authRequest);
}
3.providerManager迴圈呼叫註冊的provider進行認證,一般用的是daoAutheticationProvider
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
***************************
try {
//呼叫provider進行認證
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
******************************
throw lastException;
}
4.daoAuthenticationProvider會呼叫userService獲取資料庫或記憶體中的使用者資訊,這裡我們是存在記憶體中的.
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
//從快取中獲取使用者資訊
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//準備呼叫userdetailService去獲取使用者資訊
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
}
}
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//呼叫userService去獲取使用者資訊(這裡是指認證伺服器的資訊)
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
************************
}
5.provider拿到使用者輸入的伺服器的賬號密碼的token以及儲存在資料庫或者記憶體中的賬號密碼資訊之後便在additionalAuthenticationChecks中開始認證(主要是呼叫加密器進行密碼匹配),如果沒有丟擲異常則算成功
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
6.認證成功繼續往後走,provider將會將userDetail裡的許可權塞入token,返回給providermanager
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
try {
//載入userDetail
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
***********************
try {
//賬號認證前檢查,是否可用,是否過期,是否鎖住
preAuthenticationChecks.check(user);
//賬號認證
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
*************
//賬號認證後檢查,是否過期
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//建立塞入許可權的token並返回給providerManager
return createSuccessAuthentication(principalToReturn, authentication, user);
}
7.providerManager拿到token之後將會移除密碼,來保證系統安全,然後返回ClientCredentialsTokenEndpointFilter
try {
//認證
result = provider.authenticate(authentication);
if (result != null) {
//移除token的密碼
copyDetails(authentication, result);
break;
}
}
8.認證成功後,進入successfulAuthetication方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
*******************
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
****************
successfulAuthentication(request, response, chain, authResult);
}
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
***************************
successHandler.onAuthenticationSuccess(request, response, authResult);
}
9.認證成功之後,請求開始走如同我們一般請求的流程,進入dispatchServlet,dispatcher處理我們請求之後,通過/oauth/token對映到TokenEndPoint類
//這個mapping很關鍵
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
//獲取伺服器賬號
String clientId = getClientId(principal);
//通過賬號獲取伺服器資訊,賬號,密碼,許可權,域等
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
//獲取tokenRequest物件,主要包含了伺服器i資訊的賬號密碼 許可權,認證方式,以及請求引數
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
*****************************
//進入tokenGranter準備辦法accessToken
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
10.請求進入tokenGranter
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));
}
11.實際頒發者是defaulTokenService,這個類負責重新整理,新增,移除accessToken等一切相關accessToken的相關操作
@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();
// The token store could remove the refresh token when the
// access token is removed, but we want to
// be sure...
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);
}
// But the refresh token itself might need to be re-issued if it has
// expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
//建立accessToken
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
****************
return accessToken;
}
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
//我們收到的accessToken就是這個類序列化後的樣子
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;
}
12 返回dispatchServlet中的doFilter,認證流程結束
後記
有空再分析一下資源請求的原始碼,寫部落格還是很費時間的..................