• 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();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    部分源码如下:

    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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    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);
                }
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    3、ClientDetailsServiceConfigurer
    ClientDetailsServiceConfigurer也继承自SecurityConfigurerAdapter,主要是注入ClientDetailsService实例对象,能够使用内存或者JDBC来实现客户端详情服务,默认提供了2个实现类JdbcClientDetailsService、InMemoryClientDetailsService。
    具体提供的方法如下:
    在这里插入图片描述
    工程化时重写配置如下:

    	@Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(customClientDetailsService);
        }
    
    • 1
    • 2
    • 3
    • 4
    @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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    部分源码如下:

    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);
            }
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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)));
            }
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    通过 JDK 工具生成 JKS 证书文件,并将 keystore.jks 放入resource目录下:

    keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore keystore.jks -storepass mypass
    
    • 1

    此时还可以自定义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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • 相关阅读:
    深入理解JVM虚拟机_2 基于SPI破解双亲委派机制
    牛客 2024 【牛客&赛文X】春招冲刺 ONT73 体育课测验(二) 【中等 图/拓扑排序 Java,Go,PHP】
    好心情精神心理医生:出现这些早期症状,你可能得了双相情感障碍
    【Linux】统信各版本UOS安装vnc
    [附源码]Python计算机毕业设计Django冬奥会网上商城
    java-php-net-python-篮球网站文献综述计算机毕业设计程序
    node-rsa公钥私钥加密解密
    【无标题】
    关于 .NET 在不同操作系统中 IO 文件路径拼接方法,升级 .NET 7 后注意到的一个知识点
    python创建临时文件
  • 原文地址:https://blog.csdn.net/leijie0322/article/details/126636224