spring security OAUTH2全景图
第一步:在Gitee授权服务器上“创建应用”其实也就是注册client到授权服务器
进入gitee,创建自己的第三方应用 https://gitee.com/oauth/applications/new
- 进入gitee,创建自己的第三方应用 https://gitee.com/oauth/applications/new
点击第三方应用
点击创建应用
2填写应用相关信息,勾选应用所需要的权限。其中: 回调地址是用户对client确认授权后,码云将浏览器重定向到的应用client的地址,也是回传授权码的地址。
【注意】上图的“应用回调地址”就是我们注册的应用client的接收授权码和access-token的地址其中,/login/oauth2/code必须是这样,这是由Client引入的oauth2AuthenticationFilter的内部默认路径。而后面的gitee是registrationid,我们在自己的Client代码中配置的一致,Client的oauth2AuthenticationFilter要靠这个值去对应配置在程序中的clientid和clientSecret然后发给gitee认证服务器。且将来gitee授权服务器收到授权请求后,会将配置的这个应用回调地址与请求参数中的redirect_uri匹配,正确才回传授权码以及access_token。
创建应用过程中你提供的参数包括这些
应用主页:http://localhost:8080
应用回调地址:http://localhost:8080/login/oauth2/code/gitee 【红色的由Oauth2Authenticationfilter默认的】而“”gitee”是我们在应用代码中配置的。
第二步:开发Client应用项目
pom主要依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId> 【注】oauth2应用client依赖的starter
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.进行client应用的安全配置
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login(); 【指定认证方式:配置filterchain,向filterChain中加入了一个Oauth2AuthenticationFilter的过滤器】
http.authorizeRequests() 开始配置授权
.anyRequest()
.authenticated();【任何请求,必须都是经过认证的合法用户。认证会用oauth2authenticationFilter的使用的authenticationProvider就是去Gitee授权服务器上一边认证user,同时也为Client授权访问资源服务器gitee
3.client应用上要注册一个client实例。
ClientRegistration cr =
ClientRegistration.withRegistrationId("gitee") 【起个名字,代表client,如clientId和clientSecret】
.clientId("1b7fad4402b92690721ee8ead02b92690721d") 【此处要换成你在gitee上创建应用得到的】
.clientSecret("819c94b402b92690721ee8ead02b402b92690721ee8ead02b9293d15")【此处要换成你在gitee上创建应用得到的】
.scope(new String[]{"user_info "}) 【读取用户权限,参见你gitee上创建应用时的授权勾选】
.authorizationUri("https://gitee.com/oauth/authorize") 【这要看gitee的api,是user认证以及client认证获取授权码的地址】
.tokenUri("https://gitee.com/oauth/token ") 【这要看gitee的api,是client得到授权码后去换token的gitee地址】
.userInfoUri("https://gitee.com/api/v5/user") 【资源服务器api地址-也是client用access-token去获取用户user详情的“用户详情资源服务器地址”-这里也是gitee】】
.userNameAttributeName("id")
.clientName("gitee") 【为我们的应用client起了个名字】
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) 【注是授权码模式】
.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") 本应用配置的gitee发回授权码的地址
http://localhost:8080/login/oauth2/code/gitee
.build();
redirectUriTemplate: 在OAuth2AuthenticationFilter的逻辑中【这是你依赖的filter逻辑写死的】,action的值就是login.而registrationId是我们配的gitee。这个地址要和Gitee上创建应用时配置的应用回调地址完全一致。http://localhost:8080/login/oauth2/code/gitee
完整安全配置文件
@Configuration public class ProjectConfig extends WebSecurityConfigurerAdapter { @Bean 【供filter- OAuth2LoginAuthenticationFilter使用】 public ClientRegistrationRepository clientRepository() { ClientRegistration c = clientRegistration(); return new InMemoryClientRegistrationRepository(c); } private ClientRegistration clientRegistration() { ClientRegistration cr = ClientRegistration.withRegistrationId("gitee") 【标识一个Client,filter据此请求路径查找clientid和clientsecret】 .clientId("1b7fad4489aeb5411f52fdf20694a5d4489aeb5411f52fdf20698eee8ead02b92690721d") .clientSecret("819489aeb5411f52fdf2069d2b54489aeb5411f52fdf2069a83489aeb5411f52fdf") .scope(new String[]{"user_info"}) 【要看gitee上注册client时的勾选权限的范围】 .authorizationUri("https://gitee.com/oauth/authorize") 【user到gitee认证和获得授权码地址】 .tokenUri("https://gitee.com/oauth/token") 【client到gitee申请token的地方】 .userInfoUri("https://gitee.com/api/v5/user")【client带着token到gitee获取用户信息的地方】 .userNameAttributeName("id") .clientName("gitee") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) 【授权码方式】 .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") .build();
redirectUriTemplate: 在Filter的逻辑中,action就是login.而registrationId是我们配的gitee。Gitee上应用回调地址要和这个地址完全一致。http://localhost:8080/login/oauth2/code/gitee
return cr; }
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login(); 【增加OAuth2LoginAuthenticationFilter】
http.authorizeRequests()
.anyRequest().authenticated();
}
}
}
}
4.定义受spring security保护的资源 【但这个是Client应用本身要保护的资源,不是需要access_token去访问的资源服务器的资源。Client应用接收到access_token只是为了到资源服务器取得user_info,取得user_info后再封装OAuth2AuthenticationToken封装到authentication->securityContext.再由OauthAuthenticationFilter通过securityContextHolder将securityContext放入Reqeust线程中。所以Client应用的controller能取出OAuth2AuthenticationToken】
@Controller
public class MainController {
private Logger logger =
Logger.getLogger(MainController.class.getName());
@GetMapping("/")
public String main(OAuth2AuthenticationToken token) {
logger.info(String.valueOf(token));
return "main.html";
}
}
我们可以看一下Oauth2provider通过giteee认证服务器user登录授权后返回的OAuth2AuthenticationToken 是通过access_token从资源服务器获取的user_info来构造的
至此,access_token被Client使用获得user_info,将usernme,authorities,isAuthenticated=true后。之后的令牌就是OAuth2AuthenticationToken 了。而不是access_token
】
3.client应用的securityFilterChain中不再有usernamePasswordAuthenticationFilter和basicAuthenticationFitler,因为你配置的本client的http.oauth2Login().那么本client的“认证凭证”就由oauth2AuthenticationFilter来提供oauth2AuthenticationToken了。
org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken@52eddef4:
Principal: Name: [5244586],
【注意二:ROLE_USER是本client分配的默认的用户user角色,而认证过的用户username来自user_info中的id。另外带scope_的权限是用于访问gitee的资源服务器的】
【注意三】当user被client的OAuth2AuthenticationFitler重定向到【授权服务器gitee后,】首先由gitee承担了user的认证,然后user授权给了本client去访问gitee资源服务器的资源,获得user_info后相当于 得到了UserDetails.不必在用请求中密码加密对比userDetails中的密码。因为user认证过程是在授权服务器进行的。
scope_权限是user授权给clientid去访问资源服务器的权限,role_基于用户user,它们的作用都是访问控制。被授权给第三方Client访问的资源服务器的API一定可以被该用户user访问;能被该用户user访问的API则不一定可以被授权给第三方client访问。另外,user的username就是拿到user_info的id,而client应用中的默认的Role_user角色,这样user就有带有username和authorities的令牌了
就是说client应用不再维护userDetails了,而是靠授权服务器gitee提供的资源服务器上的user_info来的userDetails
第三部分 流程详解:
先看图
解释流程:
①.浏览器发出请求 ①http://localhost:8080/ 本client的FilterSecurityInterceptor发现你的配置protected void configure(HttpSecurity http) throws Exception { http.oauth2Login(); http.authorizeRequests() .anyRequest().authenticated(); }
任何请求都要认证,所以抛出异常,到达ExceptionTranslationFilter-->authenticationEntryPoint 而由于你配置了http.oauth2Login()所以,就将浏览器重定向到你配置的clientRegistry地址
.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
也就是:http://localhost:8080/oauth2/authorization/gitee
这也是Oauth2AuthenticationFilter会拦截的地址。
【只要把授权服务器当做一个userDetails的数据库就行了】
首先发回让浏览器重定向到Client认证地址
HTTP/1.1 302
Set-Cookie: JSESSIONID=25D9E74527189708AD15B5997D4D4F50; Path=/; HttpOnly
Location: http://localhost:8080/oauth2/authorization/gitee
Content-Length: 0
② Client 发出重定向到浏览器的响应(如上) 。让浏览器请求Client中Oauth2AuthenticationFilter特定地址。
redirect: http://localhost:8080/oauth2/authorization/gitee
【这个gitee就是我们为client配置的registrationid,而/oauth2/authorization是filter固定的,而gitee是我们配置的registrationid,所以,跟上】
③浏览器向client发出Get 请求http://localhost:8080/oauth2/authorization/gitee
④Client的Oauth2AuthenticationFilter根据url路径中的gitee(对应的registrationid取出clientid和clientsecret,scope其他“我们代码配置的client”信息,)再次向浏览器发出重定向,这次带着clientid和scope等配置在client中的client信息以及权限scope在重定向响应指令中中,这次是。换句话说,Client通过重定向浏览器将client的信息发往Gitee。从而能让user到gitee上为该Clientid授权。而user要在gitee上登录和授权,从而Gitee能知道,浏览器user同意授权给client去访问(gitee上维护着username/password和clientid/clientsecret)。
redirect:登录 - Gitee.com
【千万注意redirect_uri是当做字符串发送的,没有变化过,而且redirect_uri字符串是由Client生成的(代码配置)。redirect_uri作用是当浏览器user带着clientid和scope权限等到Gitee,首先浏览器user要在Gitee上通过user认证授权-输入user在gitee上用户名密码同时点击同意授权。后Gitee的回调地址和重定向地址。
gitee认证user通过并且user为clientid同意授权后。Gitee再向浏览器user发重定向,将浏览器重定向到redirect_uri的地址来发送code;另外,无论是浏览器还是Client,和Gitee交互都是HTTPS】
⑤浏览器按照指令发出重定向到gitee请求授权码
Get 请求登录 - Gitee.com
【注意】之后的请求是在浏览器user和Gitee之间用HTTPs交互的
【注】其中redirect_uri是当做字符串发给gitee的,将来用户输入自己gitee上的username和password登录了gitee,并授权通过后。Gitee将确认参数来的redirect_uri和clientid对应的注册在gitee上的client的redirect_uri是否一致。如果一致,gitee再次向浏览器发出向这个地址的重定向:redirect_uri=http://localhost:8080/login/oauth2/code/gitee 。浏览器根据这个重定向响应指令发出对地址redirect_uri的请求,从而将授权码转交给client
用户在gitee上认证并授权后,gitee服务器向浏览器发送重定向就是上面的参数redirect_uri的值来的
redirect: http://localhost:8080/login/oauth2/code/gitee并带上授权码
省略一些过程
⑥Gitee发现浏览器user没有登录Gitee--则提示用户登录-输入用户在gitee上usernma/password.
⑦Gitee向浏览器发送确认权限授权的页面,
⑧浏览器用户user点击确认信息给Gitee
⑨当Gitee认证服务器认证了user,同时也获得了clientid和用户授权的权限后,发回重定向指令到浏览器,带着【授权码】--实际上是 HTTP 302响应
⑩接着浏览器被重定向,向Client发出请求,带着授权码GET请求
【千万注意】这个地址http://localhost:8080/login/oauth2/code/gitee
就是我们代码中配置的
【注意】redirectUriTemplate其中:
1. baseUrl的值http://localhost:8080/就是我们的应用Client的根路径。
2.{action}不是我们配置的,而是OauthAuthenticationFilter拦截请求Filter中固定的-login,不同的路径对应不同拦截功能。我们没有自己写filter,采用的是默认的。我们只要配置了http.oauth2Login();就会有此默认的拦截器OauthAuthenticationFilter,他的逻辑是固定的。
3.oauth2/code也是OauthAuthenticationFilter固定的。因为我们没有自己写filter。
4.gitee是我们为Client起的名字,OauthAuthenticationFilter拦截请求后,会根据路径上的这个名字gitee 这个id取出clientid clientsecret 权限scope等等。
【所以********】 我们在Gitee上配置的客户端clientid和clientsecret一定要和Client传来的一致,且¥¥¥¥我们在gitee上配置的应用回调Url才是Gitee发出请求的依据。Gitee是根据配置的这个回调Url来进行请求的。所以,这个回调url必须和我们应用client配置一致才对。gitee对用户user认证并得到user对client的scope确认后,才会根据【gitee上的应用回调地址做出请求】。
而我们.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")这里的配置是配置应用client。当收到这样的请求,client会向gitee发出post请求。以便用得到的授权码到gitee上换取access_token【重点】所以gitee上的用户配置的回调地址决定了Gitee向哪个地址发送认证码
而我们代码配置的.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}"
是这个地址的决定的Oauth2AuthenticationFilter的有效拦截功能启用。 两个地址必须一致才能收到Gitee来的授权码
11 当Client filter收到授权码后,拦截,根据到来的请求http://localhost:8080/login/oauth2/code/gitee:中的registrationid--gitee取出参数,比如获取token的 url以及收到的code,以及clientid对应的client_secret以及redirect_uri等。 然后向Gitee发送Post请求,其中Redirect_uri是:{baseUrl}/{action}/oauth2/code/{registrationId}也就是http://localhost:8080/login/oauth2/code/gitee.是Gitee对授权码和clientid/clientsecret认证后发回access_token给应用client的地址。 Client的拦截器Oauth2AuthenticationFilter根据请求,从back-channel向Gitee发送POST请求. POST https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret} .
我用postman 数据要放在body中,要不会返回错误:“授权方式无效,或者登录回调地址无效、过期或已被撤销”。
接着停留在Oauth2AuthenticationFilter 中处理方法中,等待Gitee的后端back-channel来的Gitee的response
12 Gitee对授权码和clientid secret等做验证后,按照为gitee设定的应用地址(创建应用时设定的)响应结果
13.Gitee认证client和code后返回给client
200 ok
{
access_token: “266ffabcef cdefcd2342 cdefcd23”
refresh_token:”353434355345abcdefcd234234234efefef3534545”
scope: “user_inf” 之前user向gitee确认的同意授权给Client的细分的权限
}
Oauth2AuthenticationFilter从back-channel收到了来自Gitee授权服务器的access_token
14.Client应用的filter根据配置的Gitee资源服务器的api利用 access_token去取得user_inf 根据应用回调Url返回用户信息.【至此完成了】用户认证。我们client的用户认证已经设置成了Oauth2AuthenticationFilter认证方式,不再是usernamePasswordAuthenticationFilter
Client中的Oauth2AuthenticationFilter相当于通过
Oauth2AuthenticationManager
Oauth2AuthenticationProvider
UserDetailsService
层层返回UserDetails【到Oauth2AuthenticationFilter,然后Oauth2AuthenticationFilter增加一个默认角色Role_user】后将user_info打包为
Oauth2AuthenticationToken然后放到
SecurityContext-->再通过securityContextHolder放入Request主线程中供后续使用。
然后Oauth2AuthenticationFilter将用access_token获取的
user_info包装成了Oauth2AuthenticationToken(里面既有相当于username的user
id,也有user本身的角色Role_user,也有user授权给client去访问资源服务器的Scope权限)并且设置为isAuthenticated.放入-->securityContext--> 通过securityContextHolder放入Reqeust请求线程中供后续使用。
到此时都是Client从后端back-channel按照你的配置自动地完成user认证的
只是Oauth2authenticationProvider收到access_token后取得用户信息。只有取回user_info然后,将user_info 内容取出,并设置Oauth2AuthenticationToken 中的username,authorities并将Oauth2AuthenticationToken为isAuthenticated为true)。后才是认证完成。只是Oauth2AuthenticationFitler从后端用access_token从back-channel收到 user_info后包装为Oauth2AutheticationToken放入了securityContext中。
【重点**】************************
1.OAuth2Authenticationtoken不同于access_token,access_token是client被user授权去访问资源服务器资源的令牌。只发生在Client去访问其他资源服务器时用。而OAuth2Authenticationtoken才是Client应用中已认证用户的Authentication。OAuth2Authenticationtoken包含了Client利用access_token到“资源服务器上取得的user”的数据--比如将userinfo中的id作为中的usernmae并赋予Role_user角色,Auth2AuthenticationProvider会默认给授权client的user一个角色Role_user.
2.Client的user用户认证是通过OAuth2AuthenticationFilter去完成的。标志是,当经过上述一系列操作拿到access_token并取得user_info后,相当于Fitler通过AuthenticationManager--》AuthenticationProvider--》UserDetailServcie获得userDetails。只有这样AuthenticationProvider将从资源服务器获得的user_info直接当做Client应用中的已经认证的用户。【并用user_info构造OAuth2Authenticationtoken里面有其他认证过滤器所具有的标志user身份标志的username: 而角色是该filter自动赋予的 Role_user】.
我们可以打印一下OAuth2Authenticationtoken
Granted Authorities: [[ROLE_USER, SCOPE_emails, SCOPE_projects, SCOPE_user_info]],
User Attributes: [{id=5244586, login=hoaringtiger, name=alexTan,
省略其他。。。。。。
; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: D48E621DC9CF771EDD4ED3B96D2A791D; Granted Authorities: ROLE_USER, SCOPE_emails, SCOPE_projects, SCOPE_user_inf
发现负责Client用户认证的过滤器OAuth2AuthenticationFilter通过将user重定向到Oauth2授权服务器去到进行user认证,client取得授权码,换取access_token再用access_token读取Oauth2资源服务器上的user_info.整个过程就是OAuth2AuthenticationFilter使用了不同于其他如UsernamePasswordAuthenticationFitler的一个AuthenticationProvider:OAuth2AuthenticationProvider。也就是AuthenticationProvider不再利用userDetailsService的LoadUserDetailsByUsername来获得userDetails。而是只要成功AuthenticationProvider获得了user_info就标志着user认证完成。那么标志user的id是什么呢?我们看我们Client中的配置:
.userNameAttributeName("id") 表示从资源服务器中取得的user_info中的属性名为id就当做Client中的userName也,也就是Client中user的标识!!!我们看下
ClientRegistration cr = ClientRegistration.withRegistrationId("gitee")
.clientId("1b7fad4489aeb5411f52fdf20694a5d4dab952908d158eee8ead02b92690721d")
.clientSecret("819c94bdbc1a6d2b54a832116019c6e5562e6d56c9e593d15d4dbaeec09ec2f8")
.scope(new String[]{"user_info"})
.authorizationUri("https://gitee.com/oauth/authorize")
.tokenUri("https://gitee.com/oauth/token")
.userInfoUri("https://gitee.com/api/v5/user")
.userNameAttributeName("id") 【取得的user详情中的属性id的值作为Client的已认证的用户的用户名。】
而在OAuth2AuthenticationToken中,OAuth2AuthenticationProvider已经将取得的用户id放入了OAuth2AuthenticationToken。其中id就是已经在本Client通过认证的username。同时,设置为Authenticated
Granted Authorities: [[ROLE_USER, SCOPE_emails, SCOPE_projects, SCOPE_user_info]],
User Attributes: [{id=5244586, login=hoaringtiger, name=alexTan,
省略其他。。。。。。
Credentials: [PROTECTED];
Authenticated: true;
Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0:
RemoteIpAddress: 0:0:0:0:0:0:0:1;
SessionId: D48E621DC9CF771EDD4ED3B96D2A791D;
Granted Authorities: ROLE_USER, SCOPE_emails, SCOPE_projects, SCOPE_user_inf
显然,user通过登录Oauth2授权服务器的授权client访问Oauth2资源服务器获取user_info后“间接地”通过了Client的认证。而通过security认证的标志就是认证过滤器比如OAuth2AuthenticationFilter将带有username和authorities以及状态为Authenticated: true; 的一个Authentication---OAuth2AuthenticationToken放入securityContext,然后通过securityContextHolder放到Request线程中。至于“之后的Client授权过滤器还是其他微服务的就要看本token中的权限了”。说白了就是通过Oauth2的机制获得了UserDetails包括用户名username。这是OAuth2AuthenticationFilter不同于其他认证过滤器如UsernamePasswordAuthenticationFitler的地方,也是让人迷惑的地方
【以上才完成了user的oauth2认证。然后security将user的原始请求http://localhost:8080/取出去向浏览器再次重定向】
15.再次通过securitycontextPersistenceFilter后将reqeust中的securityContext取出(含有OAuth2Authenticationtoken)并清空request中securityContext ,并以与当前front-channel(浏览器)保持的的sessionid保存session。发出从定向到浏览器. 并setcookie:jsessionid=***
16.浏览器根据重定向响应指令发出http://localhost:8080/请求,携带cookie:jessionid到Client应用被securitycontextPersistenceFilter拦截,根据sessionid再次取出了OAuth2Authenticationtoken。并将此含有Authentication的securityContext放入reqeust中。而这个OAuth2AuthenticationToken是isAuthenticated的。直接到授权过滤器FilterSecurityInterceptor后,你配置的任何Client上的资源都是http.authorizerequest().anyrequest().authenticated.符合条件,放行,直接到了DistpacherServlet--》
17Controller以OAuth2AuthenticationToken为参数,所以被DispatcherServlet通过securityContextHolder从请求中拿出来提供给controller。
@Controller public class MainController { private Logger logger = Logger.getLogger(MainController.class.getName()); @GetMapping("/") public String main(OAuth2AuthenticationToken token) { //logger.info(String.valueOf(token)); System.out.println(String.valueOf(token.getPrincipal())); return "main.html"; } }
那么到达DispatcherServlet--》controller返回结果。
【另外一个重点。Oauth认证服务器和资源服务器只相当于我们应用中的securityFilterChain的OauthFilter利用authenticationProvider拿到用户详情,设定Authentication的isAuthenticated为true 到SecurityContextHolder中。
到达DispatcherServlet--controller后就结束此请求,或者颁发JWT-token给浏览器客户端,或者则需要有set-cookie。
还有:之所以把我们的应用称为Client,是相对于Oauth server和resource server而言我们的应用中集成了Oauth2 server的客户端】
换句话说。Client针对前端浏览器就是普通的的web应用(),而针对资源服务器必须用access_token来访问。
代码请参考《spring security in action》manning公司
的ssia-ch12-ex1。