转:https://blog.csdn.net/silmeweed/article/details/101603227
https://www.cnblogs.com/fp2952/p/8973613.html
AuthorizationServerConfigurerAdapter中:
ClientDetailsServiceConfigurer 主要是注入ClientDetailsService实例对象(AuthorizationServerConfigurer 的一个回调配置项,唯一配置注入) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),系统提供的二个ClientDetailsService实现类:JdbcClientDetailsService、InMemoryClientDetailsService。
Spring Security OAuth2的配置方法是编写@Configuration类继承AuthorizationServerConfigurerAdapter,然后重写void configure(ClientDetailsServiceConfigurer clients)方法,如:
回调配置ClientDetailsServiceConfigurer
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用JdbcClientDetailsService客户端详情服务
clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
这里使用Jdbc实现客户端详情服务,数据源dataSource不做叙述,使用框架默认的表,schema链接:
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
/**
*
* 配置从哪里获取ClientDetails信息。
* 在client_credentials授权方式下,只要这个ClientDetails信息。
* @param clientsDetails
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clientsDetails) throws Exception {
//认证信息从数据库获取
clientsDetails.withClientDetails(clientDetailsService);
// 测试用,将客户端信息存储在内存中
clientsDetails.inMemory()
.withClient("client") // client_id
.secret("secret") // client_secret
.authorizedGrantTypes("authorization_code") // 该client允许的授权类型
.scopes("app") // 允许的授权范围
.autoApprove(true); //登录后绕过批准询问(/oauth/confirm_access)
}
JdbcClientDetailsService类
public class JdbcClientDetailsService implements ClientDetailsService, ClientRegistrationService {
//操作oauth_client_details数据库SQL语句,另外可以自行注入
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, " + CLIENT_FIELDS_FOR_UPDATE;
private static final String BASE_FIND_STATEMENT = "select client_id, " + CLIENT_FIELDS + " from oauth_client_details";
private static final String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
private static final String DEFAULT_INSERT_STATEMENT = "insert into oauth_client_details (" + CLIENT_FIELDS
+ ", client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
private static final String DEFAULT_UPDATE_STATEMENT = "update oauth_client_details " + "set "
+ CLIENT_FIELDS_FOR_UPDATE.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 rowMapper = new ClientDetailsRowMapper();
//1.用于client_secret密码入库与出库时转化
private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();
//2.数据库存操作。
private final JdbcTemplate jdbcTemplate;
private JdbcListFactory listFactory;
public JdbcClientDetailsService(DataSource dataSource) {
Assert.notNull(dataSource, "DataSource required");
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(jdbcTemplate));
}
/**
* 核心方法。加载ClientDetails by clientId
*/
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
ClientDetails details;
try {
details = jdbcTemplate.queryForObject(selectClientDetailsSql, new ClientDetailsRowMapper(), clientId);
}
catch (EmptyResultDataAccessException e) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
return details;
}
}
InMemoryClientDetailsService类
public class InMemoryClientDetailsService implements ClientDetailsService {
private Map clientDetailsStore = new HashMap();
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
ClientDetails details = clientDetailsStore.get(clientId);
return details;
}
}
AuthorizationServerEndpointsConfigurer其实是一个装载类,装载Endpoints所有相关的类配置(AuthorizationServer、TokenServices、TokenStore、ClientDetailsService、UserDetailsService)。
/**
* 注入相关配置:
* 1. 密码模式下配置认证管理器 AuthenticationManager
* 2. 设置 AccessToken的存储介质tokenStore, 默认使用内存当做存储介质。
* 3. userDetailsService注入
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
endpoints
.authenticationManager(authenticationManager)
//末确认点.userDetailsService(userDetailsService) //MUST:密码模式下需设置一个AuthenticationManager对象,获取 UserDetails信息
.tokenStore(tokenStore)//token的保存方式
.tokenEnhancer(tokenEnhancerChain);//token里加点信息
}
AuthorizationServerEndpointsConfigurer类:
public final class AuthorizationServerEndpointsConfigurer {
private AuthorizationServerTokenServices tokenServices;
private ConsumerTokenServices consumerTokenServices;
private AuthorizationCodeServices authorizationCodeServices;
private ResourceServerTokenServices resourceTokenServices;
private TokenStore tokenStore;
private TokenEnhancer tokenEnhancer;
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 patternMap = new HashMap();
private Set allowedTokenEndpointRequestMethods = new HashSet();
private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping;
private boolean approvalStoreDisabled;
private List
JwtAccessTokenConverter是用来生成token的转换器,而token令牌默认是有签名的,且资源服务器需要验证这个签名。此处的加密及验签包括两种方式:
对称加密、非对称加密(公钥密钥)
对称加密需要授权服务器和资源服务器存储同一key值,而非对称加密可使用密钥加密,暴露公钥给资源服务器验签,本文中使用非对称加密方式,配置于AuthorizationServerConfigurerAdapter如下:
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
// 配置JwtAccessToken转换器
.accessTokenConverter(jwtAccessTokenConverter())
// refresh_token需要userDetailsService
.reuseRefreshTokens(false).userDetailsService(userDetailsService);
//.tokenStore(getJdbcTokenStore());
}
/**
* 使用非对称加密算法来对Token进行签名
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessToken();
// 导入证书
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
此处我们自定义JwtAccessToken用于添加额外用户信息
/**
* Created by fp295 on 2018/4/16.
* 自定义JwtAccessToken转换器
*/
public class JwtAccessToken extends JwtAccessTokenConverter {
/**
* 生成token
* @param accessToken
* @param authentication
* @return
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
// 设置额外用户信息
BaseUser baseUser = ((BaseUserDetail) authentication.getPrincipal()).getBaseUser();
baseUser.setPassword(null);
// 将用户信息添加到token额外信息中
defaultOAuth2AccessToken.getAdditionalInformation().put(Constant.USER_INFO, baseUser);
return super.enhance(defaultOAuth2AccessToken, authentication);
}
/**
* 解析token
* @param value
* @param map
* @return
*/
@Override
public OAuth2AccessToken extractAccessToken(String value, Map map){
OAuth2AccessToken oauth2AccessToken = super.extractAccessToken(value, map);
convertData(oauth2AccessToken, oauth2AccessToken.getAdditionalInformation());
return oauth2AccessToken;
}
private void convertData(OAuth2AccessToken accessToken, Map map) {
accessToken.getAdditionalInformation().put(Constant.USER_INFO,convertUserData(map.get(Constant.USER_INFO)));
}
private BaseUser convertUserData(Object map) {
String json = JsonUtils.deserializer(map);
BaseUser user = JsonUtils.serializable(json, BaseUser.class);
return user;
}
}
JwtAccessToken 类中从authentication里的getPrincipal(实际为UserDetails接口)获取用户信息,所以我们需要实现自己的UserDetails
/**
* Created by fp295 on 2018/4/29.
* 包装org.springframework.security.core.userdetails.User类
*/
public class BaseUserDetail implements UserDetails, CredentialsContainer {
private final BaseUser baseUser;
private final org.springframework.security.core.userdetails.User user;
public BaseUserDetail(BaseUser baseUser, User user) {
this.baseUser = baseUser;
this.user = user;
}
@Override
public void eraseCredentials() {
user.eraseCredentials();
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return user.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return user.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return user.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
public BaseUser getBaseUser() {
return baseUser;
}
}
AuthenticationManager是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法authenticate(),该方法只接收一个代表认证请求的Authentication对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的Authentication对象进行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在Spring Security中,AuthenticationManager的默认实现是ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的AuthenticationProvider列表,然后会依次使用每一个AuthenticationProvider进行认证,如果有一个AuthenticationProvider认证后的结果不为null,则表示该AuthenticationProvider已经认证成功,之后的AuthenticationProvider将不再继续认证。然后直接以该AuthenticationProvider的认证结果作为ProviderManager的认证结果。如果所有的AuthenticationProvider的认证结果都为null,则表示认证失败,将抛出一个ProviderNotFoundException。
校验认证请求最常用的方法是根据请求的用户名加载对应的UserDetails,然后比对UserDetails的密码与认证请求的密码是否一致,一致则表示认证通过。
Spring Security内部的DaoAuthenticationProvider就是使用的这种方式。其内部使用UserDetailsService来负责加载UserDetails。在认证成功以后会使用加载的UserDetails来封装要返回的Authentication对象,加载的UserDetails对象是包含用户权限等信息的。认证成功返回的Authentication对象将会保存在当前的SecurityContext中。
实现UserDetailsService
UserDetailsService只定义了一个方法 loadUserByUsername,根据用户名可以查到用户并返回的方法。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.debug("权限框架-加载用户");
List auths = new ArrayList<>();
BaseUser baseUser = new BaseUser();
baseUser.setUserName(username);
baseUser = baseUserService.selectOne(baseUser);
if (baseUser == null) {
logger.debug("找不到该用户 用户名:{}", username);
throw new UsernameNotFoundException("找不到该用户!");
}
if(baseUser.getStatus()==2)
{
logger.debug("用户被禁用,无法登陆 用户名:{}", username);
throw new UsernameNotFoundException("用户被禁用!");
}
List roles = baseRoleService.selectRolesByUserId(baseUser.getId());
if (roles != null) {
//设置角色名称
for (BaseRole role : roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleCode());
auths.add(authority);
}
}
return new org.springframework.security.core.userdetails.User(baseUser.getUserName(), baseUser.getUserPassword(), true, true, true, true, auths);
}
AuthorizationServerSecurityConfigurer继承SecurityConfigurerAdapter.也就是一个 Spring Security安全配置提供给AuthorizationServer去配置AuthorizationServer的端点(/oauth/****)的安全访问规则、过滤器Filter。
类继承关系:
可配置属性项:
1. ClientDetail加密方式
2. allowFormAuthenticationForClients 允许表单认证。针对/oauth/token端点。
3. 添加开发配置tokenEndpointAuthenticationFilters
4. tokenKeyAccess、checkTokenAccess访问权限。
/**
* 配置:安全检查流程,用来配置令牌端点(Token Endpoint)的安全与权限访问
* 默认过滤器:BasicAuthenticationFilter
* 1、oauth_client_details表中clientSecret字段加密【ClientDetails属性secret】
* 2、CheckEndpoint类的接口 oauth/check_token 无需经过过滤器过滤,默认值:denyAll()
* 对以下的几个端点进行权限配置:
* /oauth/authorize:授权端点
* /oauth/token:令牌端点
* /oauth/confirm_access:用户确认授权提交端点
* /oauth/error:授权服务错误信息端点
* /oauth/check_token:用于资源服务访问的令牌解析端点
* /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
**/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()//允许客户表单认证
.passwordEncoder(new BCryptPasswordEncoder())//设置oauth_client_details中的密码编码器
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.passwordEncoder(oauthClientPasswordEncoder);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()");
}
AuthorizationServerSecurityConfigurer类
public final class AuthorizationServerSecurityConfigurer extends
SecurityConfigurerAdapter {
private AuthenticationEntryPoint authenticationEntryPoint;
private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler();
//client secrets加密器
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 tokenEndpointAuthenticationFilters = new ArrayList();
@Override
public void init(HttpSecurity http) throws Exception {
//1.异常发生时的入口配置
registerDefaultAuthenticationEntryPoint(http);
//1. passwordEncoder注入到ClientDetailsUserDetailsService(
// UserDetailsService对象存储在HttpSecurity.SharedObject里。
if (passwordEncoder != null) {
ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(clientDetailsService());
clientDetailsUserDetailsService.setPasswordEncoder(passwordEncoder());
http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(clientDetailsUserDetailsService)
.passwordEncoder(passwordEncoder());
}
else {
http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService()));
}
//2.配置/oaut/***端点 httpBasic安全规则
http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable()
.httpBasic().realmName(realm);
//3. ssl 通道安全
if (sslOnly) {
http.requiresChannel().anyRequest().requiresSecure();
}
}
/**
* 开发配置:
* 1. allowFormAuthenticationForClients 允许表单认证。针对/oauth/token端点添加ClientCredentialsTokenEndpointFilter
* 2. 添加开发配置tokenEndpointAuthenticationFilters。(在 BasicAuthenticationFilter之前)
* 3. 添加在 accessDeniedHandler
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// ensure this is initialized
frameworkEndpointHandlerMapping();
//2. 针对/oauth/token端点添加ClientCredentialsTokenEndpointFilter
if (allowFormAuthenticationForClients) {
clientCredentialsTokenEndpointFilter(http);
}
//3.添加开发配置tokenEndpointAuthenticationFilters。(在 BasicAuthenticationFilter之前)
for (Filter filter : tokenEndpointAuthenticationFilters) {
http.addFilterBefore(filter, BasicAuthenticationFilter.class);
}
//4. 添加在 accessDeniedHandler,处理访问AccessDeniedException发生时处理。
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
/**
* 配置异常发生时入口点AuthenticationEntryPoint
* @param http
*/
private void registerDefaultAuthenticationEntryPoint(HttpSecurity http) {
ExceptionHandlingConfigurer exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {
return;
}
//1.端点入口:BasicAuthenticationEntryPoint设置response配置。
// response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
// response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
if (authenticationEntryPoint==null) {
BasicAuthenticationEntryPoint basicEntryPoint = new BasicAuthenticationEntryPoint();
basicEntryPoint.setRealmName(realm);
authenticationEntryPoint = basicEntryPoint;
}
//发生异常时,会调用到BasicAuthenticationEntryPoint.commence()
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
}
/**
* 核心:
* "/oauth/token"端点的添加过滤器clientCredentialsTokenEndpointFilter(clientId, client_secert)密码比对。而且
* 放到BasicAuthenticationFilter之前。
* http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
*/
private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(
frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
clientCredentialsTokenEndpointFilter
.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
authenticationEntryPoint.setTypeName("Form");
authenticationEntryPoint.setRealmName(realm);
clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter);
http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
return clientCredentialsTokenEndpointFilter;
}
/**
* 居然使用共享方式
*/
private ClientDetailsService clientDetailsService() {
return getBuilder().getSharedObject(ClientDetailsService.class);
}
private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping() {
return getBuilder().getSharedObject(FrameworkEndpointHandlerMapping.class);
}
private PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return StringUtils.hasText(encodedPassword) ? passwordEncoder.matches(rawPassword, encodedPassword)
: true;
}
@Override
public String encode(CharSequence rawPassword) {
return passwordEncoder.encode(rawPassword);
}
};
}
}
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
public ClientCredentialsTokenEndpointFilter() {
this("/oauth/token");
}
public ClientCredentialsTokenEndpointFilter(String path) {
super(path);
setRequiresAuthenticationRequestMatcher(new ClientCredentialsRequestMatcher(path));
// If authentication fails the type is "Form"
((OAuth2AuthenticationEntryPoint) authenticationEntryPoint).setTypeName("Form");
}
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
exception = new BadCredentialsException(exception.getMessage(), new BadClientCredentialsException());
}
authenticationEntryPoint.commence(request, response, exception);
}
});
setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// no-op - just allow filter chain to continue to token endpoint
}
});
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
//1.获取请求参数:clientId、clientSecret用来认证
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
//2.如果认证过就不需要再认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
}
if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
}
if (clientSecret == null) {
clientSecret = "";
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
//4.开始认证
return this.getAuthenticationManager().authenticate(authRequest);
}