上一篇文章Spring Cloud OAuth2 实现单点登陆介绍了使用 password 模式进行身份认证和单点登陆。本篇介绍 Spring Cloud OAuth2 的另一种受权模式-受权码模式。html
受权码模式的认证过程是这样的:前端
一、用户客户端请求认证服务器的认证接口,并附上回调地址;java
二、认证服务接口接收到认证请求后调整到自身的登陆界面;mysql
三、用户输入用户名和密码,点击确认,跳转到受权、拒绝提示页面(也可省略);git
四、用户点击受权或者默认受权后,跳转到微服务客户端的回调地址,并传入参数 code;github
五、回调地址通常是一个 RESTful 接口,此接口拿到 code 参数后,再次请求认证服务器的 token 获取接口,用来换取 access_token 等信息;web
六、获取到 access_token 后,拿着 token 去请求各个微服务客户端的接口。redis
注意上面所说的用户客户端能够理解为浏览器、app 端,微服务客户端就是咱们系统中的例如订单服务、用户服务等微服务,认证服务端就是用来作认证受权的服务,相对于认证服务端来讲,各个业务微服务也能够称做是它的客户端。spring
认证服务端继续用上一篇文章的配置,代码不须要任何改变,只须要在数据库里加一条记录,来支持新加的微服务客户端的认证sql
咱们要建立的客户端的 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 参数的。
一、在浏览器访问 /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 设置为数据库中插入的回调地址。
二、输入上面地址后,会自动跳转到认证服务端的登陆页面,输入用户名、密码,这里用户名是 admin,密码是 123456
三、点击肯定后,来到受权确认页面,页面上有 Authorize 和 Deny (受权和拒绝)两个按钮。可经过将 autoapprove 字段设置为 0 来取消此页面的展现,默认直接赞成受权。
四、点击赞成受权后,跳转到了回调地址,虽然是 404 ,可是咱们只是为了拿到 code 参数,注意地址后面的 code 参数。
五、拿到这个 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 。
六、把获取到的 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/index,看到的页面是这样的
接下来,点击登陆按钮,经过上面的模板代码看出,点击后其实就是跳转到了咱们手工测试第一步访问的那个地址,以后的操做和上面手工测试的是一致的,输入用户名密码、点击赞成受权。
接下来,页面跳转回回调地址<http://localhost:6102/client-authcode/login?code=xxx 的时候,login 方法拿到 code 参数,开始构造 post 请求体,并把 Authorization 加入请求头,而后请求 oauth/token 接口,最后将拿到的 token 和 经过 token 解析后的 username 返回给前端,最后呈现的效果以下:
最后,拿到 token 后的客户端,就能够将 token 加入到请求头后,去访问须要受权的接口了。
结合上一篇文章,咱们就实现了 password 和 受权码两种模式的 oauth2 认证。
本篇源码微服务客户端对应的源码地址为: 点击查看 github 源码
相关阅读
不要吝惜你的「推荐」呦
欢迎关注,不按期更新本系列和其余文章
古时的风筝
,进入公众号能够加入交流群