微信授权就是这个原理,Spring Cloud OAuth2 授权码模式
上一篇文章spring cloud oauth2 实现单点登录介绍了使用 password 模式进行身份认证和单点登录。本篇介绍 spring cloud oauth2 的另外一种授权模式-授权码模式。
授权码模式的认证过程是这样的:
1、用户客户端请求认证服务器的认证接口,并附上回调地址;
2、认证服务接口接收到认证请求后调整到自身的登录界面;
3、用户输入用户名和密码,点击确认,跳转到授权、拒绝提示页面(也可省略);
4、用户点击授权或者默认授权后,跳转到微服务客户端的回调地址,并传入参数 code;
5、回调地址一般是一个 restful 接口,此接口拿到 code 参数后,再次请求认证服务器的 token 获取接口,用来换取 access_token 等信息;
6、获取到 access_token 后,拿着 token 去请求各个微服务客户端的接口。
注意上面所说的用户客户端可以理解为浏览器、app 端,微服务客户端就是我们系统中的例如订单服务、用户服务等微服务,认证服务端就是用来做认证授权的服务,相对于认证服务端来说,各个业务微服务也可以称作是它的客户端。
认证服务端配置
认证服务端继续用上一篇文章的配置,代码不需要任何改变,只需要在数据库里加一条记录,来支持新加的微服务客户端的认证
我们要创建的客户端的 client-id 为 code-client,client-secret 为 code-secret-8888,但是同样需要加密,可以用如下代码获取:
system.out.println(new bcryptpasswordencoder().encode("code-secret-8888"));
除了以上这两个参数,要将 authorized_grant_types 设置为 authorization_code,refresh_token,web_server_redirect_uri 设置为回调地址,稍后微服务客户端会创建这个接口。
然后将这条记录组织好插入数据库中。
insert into oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) values ('code-client', '$2a$10$jendqzrtqqdr6sxgqk.l0obadgipyhtarfardtelki76i/ir1fdn6', 'all', 'authorization_code,refresh_token', 'http://localhost:6102/client-authcode/login', null, 3600, 36000, null, true);
创建授权模式的微服务
引入 maven 包
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-oauth2</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-redis</artifactid> </dependency> <dependency> <groupid>io.jsonwebtoken</groupid> <artifactid>jjwt</artifactid> <version>0.9.1</version> </dependency> <dependency> <groupid>com.squareup.okhttp3</groupid> <artifactid>okhttp</artifactid> <version>3.14.2</version> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-thymeleaf</artifactid> </dependency>
引入 okhttp 和 thymeleaf 是因为要做一个简单的页面并模拟正常的认证过程。
配置文件 application.yml
spring: application: name: client-authcode server: port: 6102 servlet: context-path: /client-authcode security: oauth2: client: client-id: code-client client-secret: code-secret-8888 user-authorization-uri: http://localhost:6001/oauth/authorize access-token-uri: http://localhost:6001/oauth/token resource: jwt: key-uri: http://localhost:6001/oauth/token_key key-value: dev authorization: check-token-access: http://localhost:6001/oauth/check_token
创建 resourceconfig
@configuration @enableresourceserver @enableglobalmethodsecurity(prepostenabled = true) public class resourceserverconfig extends resourceserverconfigureradapter { @bean public tokenstore jwttokenstore() { return new jwttokenstore(jwtaccesstokenconverter()); } @bean public jwtaccesstokenconverter jwtaccesstokenconverter() { jwtaccesstokenconverter accesstokenconverter = new jwtaccesstokenconverter(); accesstokenconverter.setsigningkey("dev"); accesstokenconverter.setverifierkey("dev"); return accesstokenconverter; } @autowired private tokenstore jwttokenstore; @override public void configure(resourceserversecurityconfigurer resources) throws exception { resources.tokenstore(jwttokenstore); } @override public void configure(httpsecurity http) throws exception { http.authorizerequests().antmatchers("/login").permitall(); } }
使用 jwt 作为 token 的存储,注意允许 /login
接口无授权访问,这个地址是认证的回调地址,会返回 code 参数。
创建 application.java启动类
@springbootapplication public class application { public static void main(string[] args) { springapplication.run(application.class, args); } }
到这步可以先停一下了。我们把认证服务端和刚刚创建的认证客户端启动起来,就可以手工测试一下了。回调接口不是还没创建呢吗,没关系,我们权当那个地址现在就是为了接收 code 参数的。
1、在浏览器访问 /oauth/authorize 授权接口,接口地址为:
http://localhost:6001/oauth/authorize?client_id=code-client&response_type=code&redirect_uri=http://localhost:6102/client-authcode/login
注意 response_type 参数设置为 code,redirect_uri 设置为数据库中插入的回调地址。
2、输入上面地址后,会自动跳转到认证服务端的登录页面,输入用户名、密码,这里用户名是 admin,密码是 123456
3、点击确定后,来到授权确认页面,页面上有 authorize 和 deny (授权和拒绝)两个按钮。可通过将 autoapprove 字段设置为 0 来取消此页面的展示,默认直接同意授权。
4、点击同意授权后,跳转到了回调地址,虽然是 404 ,但是我们只是为了拿到 code 参数,注意地址后面的 code 参数。
5、拿到这个 code 参数是为了向认证服务器 /oauth/token 接口请求 access_token ,继续用 rest client 发送请求,同样的,你也可以用 postman 等工具测试。
注意 grant_type 参数设置为 authorization_code,code 就是上一步回调地址中加上的,redirect_uri 仍然要带上,回作为验证条件,如果不带或者与前面设置的不一致,会出现错误。
请求头 authorization ,仍然是 basic + 空格 + base64(client_id:client_secret),可以通过 https://www.sojson.com/base64.html 网站在线做 base64 编码。
code-client:code-secret-8888 通过 base64 编码后结果为 y29kzs1jbgllbnq6y29kzs1zzwnyzxqtodg4oa==
post http://localhost:6001/oauth/token?grant_type=authorization_code&client=code-client&code=bbce34&redirect_uri=http://localhost:6102/client-authcode/login accept: */* cache-control: no-cache authorization: basic y29kzs1jbgllbnq6y29kzs1zzwnyzxqtodg4oa==
发送请求后,返回的 json 内容如下:
{ "access_token": "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyj1c2vyx25hbwuioijhzg1pbiisimp3dc1lehqioijkv1qg5omp5bgv5l-h5ogviiwic2nvcguiolsiywxsil0simv4cci6mtu3mjywmtmzmiwiyxv0ag9yaxrpzxmiolsiuk9mrv9bre1jtijdlcjqdgkioii2owrmy2m4yy1izmziltrinditytzhzi1hn2izzwuyzji1ztmilcjjbgllbnrfawqioijjb2rllwnsawvudcj9.wlggnbkndg2pwkqjbzwo6qmumq0qluzlgiwjxazahsu", "token_type": "bearer", "refresh_token": "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyj1c2vyx25hbwuioijhzg1pbiisimp3dc1lehqioijkv1qg5omp5bgv5l-h5ogviiwic2nvcguiolsiywxsil0simf0asi6ijy5zgzjyzhjlwjmzmitngi0mi1hnmfmlwe3yjnlztjmmjvlmyisimv4cci6mtu3mjyzmzczmiwiyxv0ag9yaxrpzxmiolsiuk9mrv9bre1jtijdlcjqdgkioijknzk2owrhms04ntg4ltq2yzmtyjdlns1jmgm5nzcxntm5y2yilcjjbgllbnrfawqioijjb2rllwnsawvudcj9.tez0pqohst9-ozdojwm6cf1sowvpc6w-5jw9yjzjxek", "expires_in": 3599, "scope": "all", "jwt-ext": "jwt 扩展信息", "jti": "69dfcc8c-bffb-4b42-a6af-a7b3ee2f25e3" }
和上一篇文章 password 模式拿到的 token 内容是一致的,接下来的请求都需要带上 access_token 。
6、把获取到的 access_token 代入到下面的请求中 ${access_token} 的位置,就可以请求微服务中的需要授权访问的接口了。
get http://localhost:6102/client-authcode/get accept: */* cache-control: no-cache authorization: bearer ${access_token}
接口内容如下:
@org.springframework.web.bind.annotation.responsebody @getmapping(value = "get") @preauthorize("hasanyrole('role_admin')") public object get(authentication authentication) { //authentication authentication = securitycontextholder.getcontext().getauthentication(); authentication.getcredentials(); oauth2authenticationdetails details = (oauth2authenticationdetails) authentication.getdetails(); string token = details.gettokenvalue(); return token; }
经过以上的手工测试,证明此过程是通的,但是还没有达到自动化。如果你集成过微信登录,那你一定知道我们在回调地址中做了什么,拿到返回的 code 参数去 token 接口换取 access_token 对不对,没错,思路都是一样的,我们的回调接口中同样要拿 code 去换取 access_token。
为此,我做了一个简单的页面,并且在回调接口中请求获取 token 的接口。
创建简单的登录页面
在 resources 目录下创建 templates 目录,用来存放 thymeleaf 的模板,不做样式,只做最简单的演示,创建 index.html 模板,内容如下:
<!doctype html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <title>古时的风筝-oauth2 client</title> </head> <body> <div> <a href="http://localhost:6001/oauth/authorize?client_id=code-client&response_type=code&redirect_uri=http://localhost:6102/client-authcode/login">登录</a> <span th:text="'当前认证用户:' + ${username}"></span> <span th:text="${accesstoken}"></span> </div> </body> </html>
回调接口及其他接口
@slf4j @controller public class codeclientcontroller { /** * 用来展示index.html 模板 * @return */ @getmapping(value = "index") public string index(){ return "index"; } @getmapping(value = "login") public object login(string code,model model) { string tokenurl = "http://localhost:6001/oauth/token"; okhttpclient httpclient = new okhttpclient(); requestbody body = new formbody.builder() .add("grant_type", "authorization_code") .add("client", "code-client") .add("redirect_uri","http://localhost:6102/client-authcode/login") .add("code", code) .build(); request request = new request.builder() .url(tokenurl) .post(body) .addheader("authorization", "basic y29kzs1jbgllbnq6y29kzs1zzwnyzxqtodg4oa==") .build(); try { response response = httpclient.newcall(request).execute(); string result = response.body().string(); objectmapper objectmapper = new objectmapper(); map tokenmap = objectmapper.readvalue(result,map.class); string accesstoken = tokenmap.get("access_token").tostring(); claims claims = jwts.parser() .setsigningkey("dev".getbytes(standardcharsets.utf_8)) .parseclaimsjws(accesstoken) .getbody(); string username = claims.get("user_name").tostring(); model.addattribute("username", username); model.addattribute("accesstoken", result); return "index"; } catch (exception e) { e.printstacktrace(); } return null; } @org.springframework.web.bind.annotation.responsebody @getmapping(value = "get") @preauthorize("hasanyrole('role_admin')") public object get(authentication authentication) { //authentication authentication = securitycontextholder.getcontext().getauthentication(); authentication.getcredentials(); oauth2authenticationdetails details = (oauth2authenticationdetails) authentication.getdetails(); string token = details.gettokenvalue(); return token; } }
其中 index() 方法是为了展示 thymeleaf 模板,login 方法就是回调接口,这里用了 okhttp3 用作接口请求,请求认证服务端的 /oauth/token 接口来换取 access_token,只是把我们手工测试的步骤自动化了。
访问 index.html 页面
我们假设这个页面就是一个网站的首页,未登录的用户会在网站上看到登录按钮,我们访问这个页面:,看到的页面是这样的
接下来,点击登录按钮,通过上面的模板代码看出,点击后其实就是跳转到了我们手工测试第一步访问的那个地址,之后的操作和上面手工测试的是一致的,输入用户名密码、点击同意授权。
接下来,页面跳转回回调地址<http://localhost:6102/client-authcode/login?code=xxx 的时候,login 方法拿到 code 参数,开始构造 post 请求体,并把 authorization 加入请求头,然后请求 oauth/token 接口,最后将拿到的 token 和 通过 token 解析后的 username 返回给前端,最后呈现的效果如下:
最后,拿到 token 后的客户端,就可以将 token 加入到请求头后,去访问需要授权的接口了。
结合上一篇文章,我们就实现了 password 和 授权码两种模式的 oauth2 认证。
本篇源码微服务客户端对应的源码地址为:
相关阅读
不要吝惜你的「推荐」呦
欢迎关注,不定期更新本系列和其他文章古时的风筝
,进入公众号可以加入交流群
上一篇: C++引用与常量
下一篇: Java IO编程——转换流