spring-security-oauth2之AuthorizationServerConfigurerAdapter浅析

在java开发中,安全框架作为必备组件,典型的安全框架是采用Spring Security,在工程应用中又会引入Oauth2 + Jwt组件,下面就聊聊配置Oauth2服务时的配置类AuthorizationServerConfigurerAdapter。

1、AuthorizationServerConfigurerAdapter源码
在这里插入图片描述
其实现了AuthorizationServerConfigurer接口,其中存在3个方法:

  • AuthorizationServerSecurityConfigurer:配置令牌端点(Token Endpoint)的安全约束;
  • ClientDetailsServiceConfigurer:配置OAuth2客户端;
  • AuthorizationServerEndpointsConfigurer:配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services);

2、AuthorizationServerSecurityConfigurer
AuthorizationServerSecurityConfigurer继承自SecurityConfigurerAdapter,也就是一个 Spring Security安全配置提供给AuthorizationServer去配置AuthorizationServer的端点(/oauth/****)的安全访问规则、过滤器Filter。
在这里插入图片描述
具体提供的方法如下:
在这里插入图片描述
提供的令牌端点(Token Endpoint)如下:

  • /oauth/authorize:授权端点
  • /oauth/token:令牌端点
  • /oauth/confirm_access:用户确认授权提交端点
  • /oauth/error:授权服务错误信息端点
  • /oauth/check_token:用于资源服务访问的令牌解析端点
  • /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话

工程化时重写配置如下:

	@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进行认证:

  • 如果有一个AuthenticationProvider认证后的结果不为null,则表示该AuthenticationProvider已经认证成功,之后的AuthenticationProvider将不再继续认证。然后直接以该AuthenticationProvider的认证结果作为ProviderManager的认证结果。
  • 如果所有的AuthenticationProvider的认证结果都为null,则表示认证失败,将抛出一个ProviderNotFoundException。
    在这里插入图片描述
    校验认证请求最常用的方法是根据请求的用户名加载对应的UserDetails,然后比对UserDetails的密码与认证请求的密码是否一致,一致则表示认证通过。Spring Security内部默认实现为DaoAuthenticationProvider。其内部使用UserDetailsService来负责加载UserDetails。在认证成功以后会使用加载的UserDetails来封装要返回的Authentication对象,加载的UserDetails对象是包含用户权限等信息的。认证成功返回的Authentication对象将会保存在当前的SecurityContext中。

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);
    }
}
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值