spring-authorization-server 是 Spring 新推出的认证服务器。当前主要支持的目标是 Oauth 与 OpenId
以下场景可以考虑引入 Oauth 2.0 认证:
①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖
今后有这么两项
Spring 官方
讨论组
OAuth2 官方文档
参考网址
启动官方的例子代码,其中包含三个应用程序
逻辑是用户输入:http://127.0.0.1:8080,跳转到 9000 登录,登录后通过 code 得到 Token,然后拿 token 到resource server
中得到数据。
OAuth 2.0 定义了四种授权方式
其中【简化模式】不推荐了,所以在今后的文档中不再描述。
Spring 内置了下面的模式,可以通过AuthorizationGrantType
查看相关的代码。但是现在 Spring 只支持一部分,见Spring 的说明。
名称 | 说明 | 备注 |
---|---|---|
AUTHORIZATION_CODE | 授权码模式 | |
IMPLICIT | 简化模式 | 不推荐,当前不支持 |
REFRESH_TOKEN | 刷新 Token | |
CLIENT_CREDENTIALS | 客户端模式 | 直接根据 client 的 id 和密钥即可获取 token,当前支持 |
PASSWORD | 密码模式 | 通过用户名与密码直接得到 Token,当前不支持 |
JWT_BEARER | jwt 模式 |
client_credentials
如果想深入了解,可以参考这些网址
将要测试的授权模式,都添加到程序中,然后启动。
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("messaging-client").clientSecret("{noop}secret")//授权模式.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).authorizationGrantType(AuthorizationGrantType.PASSWORD).authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
这里为简化测试,不需要用户去确认scope
,就添加了SCOPE_openid
的test/*
@EnableWebSecuritypublic class ResourceServerConfig {// @formatter:off@BeanSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.mvcMatcher("/messages/**").authorizeRequests().mvcMatchers("/messages/**").access("hasAuthority('SCOPE_message.read')").mvcMatchers("/test/**").access("hasAuthority('SCOPE_openid')").and().oauth2ResourceServer().jwt();return http.build();}// @formatter:on}
添加 controller
@GetMapping("/test")public String[] test() {return new String[] {"test 1", "test 2", "test 3"};}
第一步:浏览器中输入下面 URL
http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid message.read message.write&state=some-state&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc
第二步: 在登录页面,输入用户名与密码
第三部:在跳转到的链接中,将 code 复制过来
在 poostman 中输入下面的项目。
其中:redirect_url 可以指定多个,但是这里一定要与得到 Code 时指定的 url 一样,不然会出现错误。
返回的内容如下:
{"access_token": "eyJraWQiOiI5ZDVmOTA3OS0wMDg0LTRmNzAtYmNmNC1hMTc2YTI0OWM2ZTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsImF1ZCI6Im1lc3NhZ2luZy1jbGllbnQiLCJuYmYiOjE2Mzc0OTkyNTIsInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTAwMCIsImV4cCI6MTYzNzQ5OTU1MiwiaWF0IjoxNjM3NDk5MjUyfQ.ExBF3PfwZ9dv8GwrFutw9ZjLWjW6nj6q32AuDmTNA5fbq7SBx_igLZC1DlULmnoqTS5PTYwN0oTMHSyy2BJX70PMQ3jhFpVzaAeayuuWEiFf0g60pn565HpaxUzvDfH3FjUnNh1JiF8nOpzW9lZinpPSkVuZvGuU6Uw6_ZgBvLsdiuzJ0MugTh3Qkg6P79XSydUrjksk-Ja2EKvn08k3kIckqSTRDoDZGKvMVfnEbq8trtXX8D2OQh6pe32JMZ-vqxz6wDJr8fS3SSDSh_U7fce32hZl5dP2b-FmFNSO5W1JXGQ0veRh-7Z2bN7kSVtlXjm4_J3-0NTI2iHCz5jstQ","refresh_token": "PYqVy7ek-5u0nlFDoaeh2H1OfPDadcvQ3-AYnE1Cd5xOsSUTdGJof1K6Nr4dpVSosEiDNNawGmrii0i1eISN2SiLKHkPbAiUoofksaY314o382jal5apH24D-Z9lbord","scope": "openid","id_token": "eyJraWQiOiI5ZDVmOTA3OS0wMDg0LTRmNzAtYmNmNC1hMTc2YTI0OWM2ZTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsImF1ZCI6Im1lc3NhZ2luZy1jbGllbnQiLCJhenAiOiJtZXNzYWdpbmctY2xpZW50IiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjkwMDAiLCJleHAiOjE2Mzc1MDEwNTIsImlhdCI6MTYzNzQ5OTI1Mn0.JdL0dKEqDMPaTBnpVypIvGZNBc1AFoOIKUglvD4PaRRpgDL6ikXLcgCuLALfBjd-f6F_Y2VqRfzdSsN39aOSpAkGU59-gWJ2B0qApKuEgFKW113TuCIECgjsvD5psEcCVr68EMXnjS19vPZ9Yv675KXs9WSq7q5YyWCrJy8vO1hu0K2QdxwDS-YiIe9t7wi5jGkXwfY4mldR5kz3cr0KGoMcZUNftMexRI2j57wrFpCHm68zUg6gupWxU6TPtVBjswIduAaYSfkIi_Hji0SdihQWAcvv5Er4Ki3b_A4NE66rhJDwWJS4Xuzqiw6OG86EVEhOZF_2_r7Ztl3H48Erag","token_type": "Bearer","expires_in": 300}
使用 postman 来访问http://localhost:8090/test
实际上就是在 Headers 中添加
Authorization=Bearer eyJraWQiOiI1ZTc4YjYxYS1iYjQxLTRjY2QtYmZkZC1lNjE1Yzg4NjNjMGIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsImF1ZCI6Im1lc3NhZ2luZy1jbGllbnQiLCJuYmYiOjE2Mzc1MDMwMTUsInNjb3BlIjpbIm9wZW5pZCIsIm1lc3NhZ2UucmVhZCJdLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTAwMCIsImV4cCI6MTYzNzUwMzMxNSwiaWF0IjoxNjM3NTAzMDE1fQ.purpxamZpOyhj9VNgnKgPV9L-At0wsAJbonfzxgAv6ItJaXpfZfbBvnfyGNcggIq_wATr0FTBs4Gm1EuHo1VOSRK1yHjkqzFRTWvAr1xY3IAoGq-4QG2yPPh_gbOtgB76wZgkUPb0ulPkipb1RCCUC5UYzVsP6uRE3tyUpI98WP97tZoa7RbgypcoNyFrvM0jpELrl0X2RNKYR8rrCSndySk5AK-865VHrpbBX52F9pToLg8XPf-IGNEem-K__bHnLNvaFY_VDfqlPaC0qcHP8skTbpUbXUMSU2b9mFWZthWrTfMekO0SVYPKwA9to60Ea5U891mG_O8vfQyqVN8kA
思考:
直接根据 client 的 id 和密钥即可获取 token,在 PostMan 中使用 Post
http://localhost:9000/oauth2/token?grant_type=client_credentials&client_id=messaging-client&client_secret=secret
然后就得到数据了
{"access_token": "eyJraWQiOiI3YzYwZmY5OC0zNDBjLTRkNDEtOTE2Mi1lZjEzNjQyYzk0OTkiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtZXNzYWdpbmctY2xpZW50IiwiYXVkIjoibWVzc2FnaW5nLWNsaWVudCIsIm5iZiI6MTYzNzU1MTU5OSwic2NvcGUiOlsib3BlbmlkIiwibWVzc2FnZS5yZWFkIiwibWVzc2FnZS53cml0ZSJdLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTAwMCIsImV4cCI6MTYzNzU1MTg5OSwiaWF0IjoxNjM3NTUxNTk5fQ.i_joxPJo92ardIdtmLftougiTEOTMf9Q1fYEfFhD_DEZZn-A5n3mySq46qcxA78hgKAvDjOs5PFnSAjViLA5pqMCirl-CyOkWBfx14rQ9xNAAPvb1qlKVh0jwKH090mgJ0LdytwoT6Ci-FBidzeIaU7teYEH1tnLt3zvUFVmVh4dQCbBB0enPEGqsBYyi2hVXnHF_8xOK9-tc-hWI8yxQoxYJ3bpOcv3db_hzibe1QpBHUfdXPJuVhsnIP4HtWw_Wo-ow5aYO78mRVuYzdoVh-CmhHhJsNGnbBWRbAMgYdGvfO3W5jTFEhImZ6KFGNsbFBBTRkyySQQ9vKSszIJx1Q","scope": "openid message.read message.write","token_type": "Bearer","expires_in": 300}
客户端获取 code 后,需要去服务器将 code 换成 token,这时候用到了客户端验证的方法。
在认证服务器上配置认证方式
//客户端认证方式.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
也就是将客户端用户名与密码,放到 POST 参数中。
例如下面的方式
http://localhost:9000/oauth2/token?grant_type=authorization_code&client_id=messaging-client&client_secret=secret&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&code=R-X4cYeDPoUYx0nzlZRIO3Zwr_x_FesUUYnwtZYQeAOjdeeWdScngZT2wa3cKqJkDQ0GWzhCqpmhM7kUz2LYAVptGp1NPZxAJjq4ETfqE0FQfOsTzj1olRtd5wVNnBkR
这时候需要将 client_id 与 client_secret 的内容,设置到http basic
中。
③ 得到 code
登录完毕后,在地址栏中输入/oauth2/authorize
得到响应的 code
测试输入正确的密码
http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=DeEOyLOKLiUPykkoPuYsmEJ2GOcOc_4NMgF7uzFqofdZmXi4OyvWgARjwE13hVFsercSHyf-drgSKfQ5szfblN3BMAs9xK1kr66NB_XVt6MC4dTGLjfkXVd9IZ1AvqVz&state=state
由于 React 要远程调用获取 Token,需要进行跨域 cors 改造,这里有几点注意事项。
改造都在服务器端进行。
下面是一个简单的,今后可以根据 client 来配置那些可以跨域。
public class RegisteredClientCorsConfigurationSource implements CorsConfigurationSource {@Overridepublic CorsConfiguration getCorsConfiguration(HttpServletRequest request) {CorsConfiguration config = new CorsConfiguration();config.setAllowCredentials(true);config.setAllowedOriginPatterns(Arrays.asList("/**"));config.setAllowedOrigins(Arrays.asList("http://127.0.0.1:8000"));config.setAllowedMethods(Arrays.asList("*"));config.setAllowedHeaders(Arrays.asList("*"));//自定义的输出头,这样客户端就可以读取到了//config.setExposedHeaders(Arrays.asList("/**"));// 根据client_id得到client的return url 这段代码省略了return config;}}
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
非常重要,不然会得不到 Token。但是这样会引发一个严重的问题,因为所有的路径,包含静态图片调用,都会运行一下这个过滤器。
@Configurationpublic class FiltersConfiguration {@Configurationpublic static class FiltersCorsConfiguration {@Bean@ConditionalOnMissingBean(name = "corsConfigurationSource")public CorsConfigurationSource corsConfigurationSource(){return new RegisteredClientCorsConfigurationSource();}@Beanpublic FilterRegistrationBean<CorsFilter> casCorsFilter(@Qualifier("corsConfigurationSource")final CorsConfigurationSource corsConfigurationSource){FilterRegistrationBean bean = new FilterRegistrationBean<>(new CorsFilter(corsConfigurationSource));bean.setName("sasCorsFilter");bean.setAsyncSupported(true);bean.setOrder(Ordered.HIGHEST_PRECEDENCE);return bean;}}}
OPTIONS
操作在进行跨域操作之前,浏览器会发送一个OPTIONS
请求,看看服务器是否允许跨域操作,所以关于这个操作,要放开。不然不能跨域。
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
CSRF一般指跨站请求伪造。 跨站请求伪造。如果要启动这个功能,需要单独启动。
.csrf().disable()
如果不限制这个,就算在 postman 中,用 post 来调用一个 API 接口,也会出现错误。
怎么启动这个功能呢? 在前后台分离的情况下,如何启用这个功能呢?不启动这个功能,会是一个隐患。
以 AntDesign 的代码为例子。主要实现的功能如下:
http://127.0.0.1:9000/oauth2/authorize
,这个页面会出发login
进行登录。http://127.0.0.1:8000/oauth2/authorized
页面。这个页面相当于以前的 login,但是一个自动登录的页面。code
,通过post
去访问http://127.0.0.1:9000/oauth2/token
获得token
app.tsx
getInitialState
函数,如果没有得到当前登录的用户,就跳转到OAuth2
登录页面。antDesignPro
一样放到useModel('@@initialState')
中,也可以放到 token 中。/oauth2/authorized
用来收到 code,并且得到 tokenstate
跳转到页面官网的例子有
@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =new OAuth2AuthorizationServerConfigurer<>();authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint ->authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();http.requestMatcher(endpointsMatcher).authorizeRequests(authorizeRequests ->authorizeRequests.anyRequest().authenticated()).csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)).apply(authorizationServerConfigurer);return http.formLogin(Customizer.withDefaults()).build();}
从系统中得到两个类,分别是:registeredClientRepository
与authorizationConsentService
用来得到注册用户信息与当前已经确认过的信息。
@GetMapping(value = "/oauth2/consent")public String consent(Principal principal, Model model,@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,@RequestParam(OAuth2ParameterNames.SCOPE) String scope,@RequestParam(OAuth2ParameterNames.STATE) String state) {// Remove scopes that were already approvedSet<String> scopesToApprove = new HashSet<>();Set<String> previouslyApprovedScopes = new HashSet<>();RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);OAuth2AuthorizationConsent currentAuthorizationConsent =this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());Set<String> authorizedScopes;if (currentAuthorizationConsent != null) {authorizedScopes = currentAuthorizationConsent.getScopes();} else {authorizedScopes = Collections.emptySet();}for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {if (authorizedScopes.contains(requestedScope)) {previouslyApprovedScopes.add(requestedScope);} else {scopesToApprove.add(requestedScope);}}model.addAttribute("clientId", clientId);model.addAttribute("state", state);model.addAttribute("scopes", withDescription(scopesToApprove));model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));model.addAttribute("principalName", principal.getName());return "consent";}
使用了bootstrap 4.5.2
与默认的模板引擎。
今后可以使用 tailwind.css。
不用自己写 css,不用 bootstrap,写样式有 tailwindcss 就足够了
OAuth2ClientAuthenticationFilter extends OncePerRequestFilter
遇到 /oauth2/token
调用ProviderManager
中的 authenticate 函数,根据多个provider
中的OAuth2ClientAuthenticationProvider
得到[authenticationResult]。
得到[authenticationResult]结果后干什么呢?
调用另外一个认证OAuth2AuthorizationCodeAuthenticationProvider
OAuth2TokenEndpointFilter extends OncePerRequestFilter
这个调用啥东东
Http11Processor
Error parsing HTTP request header
Error state [CLOSE_CONNECTION_NOW] reported while processing request
NioEndpoint
URLClassLoaderorg.apache.tomcat.util.buf.UDecoder$1org.h2.value.ValueLob