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;
}
......
}