在java开发中,安全框架作为必备组件,典型的安全框架是采用Spring Security,在工程应用中又会引入Oauth2 + Jwt组件,下面就聊聊配置Oauth2服务时的配置类AuthorizationServerConfigurerAdapter。
1、AuthorizationServerConfigurerAdapter源码
其实现了AuthorizationServerConfigurer接口,其中存在3个方法:
2、AuthorizationServerSecurityConfigurer
AuthorizationServerSecurityConfigurer继承自SecurityConfigurerAdapter,也就是一个 Spring Security安全配置提供给AuthorizationServer去配置AuthorizationServer的端点(/oauth/****)的安全访问规则、过滤器Filter。
具体提供的方法如下:
提供的令牌端点(Token Endpoint)如下:
工程化时重写配置如下:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
// 自定义异常处理端口
security.authenticationEntryPoint(customAuthenticationEntryPoint);
security.accessDeniedHandler(customAccessDeniedHandler);
security
// oauth/token_key
.tokenKeyAccess("permitAll()")
// oauth/check_token
.checkTokenAccess("isAuthenticated()")
// 允许客户表单认证
.allowFormAuthenticationForClients();
}
部分源码如下:
public final class AuthorizationServerSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private AuthenticationEntryPoint authenticationEntryPoint;
private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler();
private PasswordEncoder passwordEncoder;
private String realm = "oauth2/client";
private boolean allowFormAuthenticationForClients = false;
private String tokenKeyAccess = "denyAll()";
private String checkTokenAccess = "denyAll()";
private boolean sslOnly = false;
// 过滤器
private List<Filter> tokenEndpointAuthenticationFilters = new ArrayList();
public void init(HttpSecurity http) throws Exception {
// 发生异常时的入口配置
this.registerDefaultAuthenticationEntryPoint(http);
if (this.passwordEncoder != null) {
ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(this.clientDetailsService());
// 注入passwordEncoder
clientDetailsUserDetailsService.setPasswordEncoder(this.passwordEncoder());
((AuthenticationManagerBuilder)http.getSharedObject(AuthenticationManagerBuilder.class)).userDetailsService(clientDetailsUserDetailsService).passwordEncoder(this.passwordEncoder());
} else {
http.userDetailsService(new ClientDetailsUserDetailsService(this.clientDetailsService()));
}
// 配置/oaut/***端点 httpBasic安全规则
((HttpSecurity)((HttpSecurity)http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and()).csrf().disable()).httpBasic().realmName(this.realm);
// ssl 通道安全
if (this.sslOnly) {
((RequiresChannelUrl)http.requiresChannel().anyRequest()).requiresSecure();
}
}
public void configure(HttpSecurity http) throws Exception {
this.frameworkEndpointHandlerMapping();
// 针对/oauth/token端点添加ClientCredentialsTokenEndpointFilter
if (this.allowFormAuthenticationForClients) {
this.clientCredentialsTokenEndpointFilter(http);
}
Iterator var2 = this.tokenEndpointAuthenticationFilters.iterator();
// 在BasicAuthenticationFilter之前添加过滤器
while(var2.hasNext()) {
Filter filter = (Filter)var2.next();
http.addFilterBefore(filter, BasicAuthenticationFilter.class);
}
// 设置accessDeniedHandler
http.exceptionHandling().accessDeniedHandler(this.accessDeniedHandler);
}
private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(this.frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
clientCredentialsTokenEndpointFilter.setAuthenticationManager((AuthenticationManager)http.getSharedObject(AuthenticationManager.class));
OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
authenticationEntryPoint.setTypeName("Form");
authenticationEntryPoint.setRealmName(this.realm);
clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
clientCredentialsTokenEndpointFilter = (ClientCredentialsTokenEndpointFilter)this.postProcess(clientCredentialsTokenEndpointFilter);
// 设置clientCredentialsTokenEndpointFilter过滤器进行密码比对
http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
return clientCredentialsTokenEndpointFilter;
}
}
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
private AuthenticationEntryPoint authenticationEntryPoint;
private boolean allowOnlyPost;
public ClientCredentialsTokenEndpointFilter() {
this("/oauth/token");
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (this.allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[]{"POST"});
} else {
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断是否认证
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
} else if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
} else {
if (clientSecret == null) {
clientSecret = "";
}
clientId = clientId.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, clientSecret);
// 开始认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
}
3、ClientDetailsServiceConfigurer
ClientDetailsServiceConfigurer也继承自SecurityConfigurerAdapter,主要是注入ClientDetailsService实例对象,能够使用内存或者JDBC来实现客户端详情服务,默认提供了2个实现类JdbcClientDetailsService、InMemoryClientDetailsService。
具体提供的方法如下:
工程化时重写配置如下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(customClientDetailsService);
}
@Service
@RequiredArgsConstructor
public class CustomClientDetailsService implements ClientDetailsService {
private ClientDetailsService clientDetailsService;
private final PasswordEncoder passwordEncoder;
/**
* 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次,类似于Serclet的inti()方法。
* 被@PostConstruct修饰的方法会在构造函数之后,init()方法之前运行。
*/
@PostConstruct
public void init() {
InMemoryClientDetailsServiceBuilder inMemoryClientDetailsServiceBuilder = new InMemoryClientDetailsServiceBuilder();
inMemoryClientDetailsServiceBuilder
.withClient("client1")
.secret(passwordEncoder.encode("1"))
.authorizedGrantTypes("authorization_code", "password", "implicit", "client_credentials", "refresh_token")
.resourceIds("resource1")
.redirectUris("http://localhost:8080/rest/code")
.scopes("insert", "update", "del", "select", "replace", "all");
try {
clientDetailsService = inMemoryClientDetailsServiceBuilder.build();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
if (clientId == null) {
throw new ClientRegistrationException("客户端不存在");
}
return clientDetailsService.loadClientByClientId(clientId);
}
}
部分源码如下:
public class JdbcClientDetailsService implements ClientDetailsService, ClientRegistrationService {
private static final Log logger = LogFactory.getLog(JdbcClientDetailsService.class);
private JdbcClientDetailsService.JsonMapper mapper = createJsonMapper();
private static final String CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove";
private static final String CLIENT_FIELDS = "client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove";
private static final String BASE_FIND_STATEMENT = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details";
private static final String DEFAULT_FIND_STATEMENT = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details order by client_id";
private static final String DEFAULT_SELECT_STATEMENT = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?";
private static final String DEFAULT_INSERT_STATEMENT = "insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
private static final String DEFAULT_UPDATE_STATEMENT = "update oauth_client_details set " + "resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove".replaceAll(", ", "=?, ") + "=? where client_id = ?";
private static final String DEFAULT_UPDATE_SECRET_STATEMENT = "update oauth_client_details set client_secret = ? where client_id = ?";
private static final String DEFAULT_DELETE_STATEMENT = "delete from oauth_client_details where client_id = ?";
private RowMapper<ClientDetails> rowMapper = new JdbcClientDetailsService.ClientDetailsRowMapper();
private String deleteClientDetailsSql = "delete from oauth_client_details where client_id = ?";
private String findClientDetailsSql = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details order by client_id";
private String updateClientDetailsSql;
private String updateClientSecretSql;
private String insertClientDetailsSql;
private String selectClientDetailsSql;
private PasswordEncoder passwordEncoder;
private final JdbcTemplate jdbcTemplate;
private JdbcListFactory listFactory;
// 根据clientId加载ClientDetails
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
try {
ClientDetails details = (ClientDetails)this.jdbcTemplate.queryForObject(this.selectClientDetailsSql, new JdbcClientDetailsService.ClientDetailsRowMapper(), new Object[]{clientId});
return details;
} catch (EmptyResultDataAccessException var4) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
}
public class InMemoryClientDetailsService implements ClientDetailsService {
private Map<String, ClientDetails> clientDetailsStore = new HashMap();
public InMemoryClientDetailsService() {
}
// 根据clientId加载ClientDetails
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
ClientDetails details = (ClientDetails)this.clientDetailsStore.get(clientId);
if (details == null) {
throw new NoSuchClientException("No client with requested id: " + clientId);
} else {
return details;
}
}
public void setClientDetailsStore(Map<String, ? extends ClientDetails> clientDetailsStore) {
this.clientDetailsStore = new HashMap(clientDetailsStore);
}
}
4、AuthorizationServerEndpointsConfigurer
AuthorizationServerEndpointsConfigurer其实是一个装载类,装载Endpoints所有相关的类配置,如AuthorizationServer、TokenServices、TokenStore、ClientDetailsService、UserDetailsService,也就是说进行密码验证的一些工具类或服务类,均在这个地方进行注入。
具体提供的方法如下:
部分源码如下:
public final class AuthorizationServerEndpointsConfigurer {
private AuthorizationServerTokenServices tokenServices;
private ConsumerTokenServices consumerTokenServices;
private AuthorizationCodeServices authorizationCodeServices;
private ResourceServerTokenServices resourceTokenServices;
private TokenStore tokenStore;
private TokenEnhancer tokenEnhancer;
// 用来生成AccessToken的转换器
private AccessTokenConverter accessTokenConverter;
private ApprovalStore approvalStore;
private TokenGranter tokenGranter;
private OAuth2RequestFactory requestFactory;
private OAuth2RequestValidator requestValidator;
private UserApprovalHandler userApprovalHandler;
private AuthenticationManager authenticationManager;
private ClientDetailsService clientDetailsService;
private String prefix;
private Map<String, String> patternMap = new HashMap();
private Set<HttpMethod> allowedTokenEndpointRequestMethods = new HashSet();
private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping;
private boolean approvalStoreDisabled;
private List<Object> interceptors = new ArrayList();
private DefaultTokenServices defaultTokenServices;
private UserDetailsService userDetailsService;
private boolean tokenServicesOverride = false;
private boolean userDetailsServiceOverride = false;
private boolean reuseRefreshToken = true;
private WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator;
private RedirectResolver redirectResolver;
// 设置PreAuthenticatedAuthenticationProvider
private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper(userDetailsService));
tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
}
}
}
4.1、AuthenticationManager
AuthenticationManager是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法authenticate(),该方法只接收一个代表认证请求的Authentication对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的Authentication对象进行返回。
在Spring Security中,AuthenticationManager的默认实现是ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的AuthenticationProvider列表,然后会依次使用每一个AuthenticationProvider进行认证:
4.2、JwtAccessTokenConverter
JwtAccessTokenConverter是用来生成token的转换器,而token令牌默认是有签名的,且资源服务器需要验证这个签名。此处的加密及验签包括两种方式:
对称加密需要授权服务器和资源服务器存储同一key值,而非对称加密可使用密钥加密,暴露公钥给资源服务器验签。非对称加密方式如下:
/**
* 使用非对称加密算法来对Token进行签名
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter converter = new MyJwtAccessTokenConverter ();
// 导入证书
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
return converter;
}
通过 JDK 工具生成 JKS 证书文件,并将 keystore.jks 放入resource目录下:
keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore keystore.jks -storepass mypass
此时还可以自定义JwtAccessTokenConverter用于添加额外用户信息,如下:
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInformation = new LinkedHashMap<>();
Map<String, Object> info = new LinkedHashMap<>();
info.put("username", ((User)authentication.getPrincipal()).getUsername());
info.put("user", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
additionalInformation.put("info", info);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
return super.enhance(accessToken, authentication);
}
}