【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