微服务API开放授权平台的设计与实现

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 微服务API开放授权平台的设计与实现

本文所介绍的项目是一个基于oath2协议的应用,实现的的功能逻辑与QQ互联微博开放平台类似,都是同一套认证授权流程。

项目结构简单易懂,却不偷工减料,在学习完本文内容后,读者可以直接获取文中的项目代码用于学习或者copy到公司的生产项目中修改后使用,真正达到学以致用的目的。

所涉及技术栈:

  • java
  • springboot 2.0.1.RELEASE
  • spring security 2.0.1.RELEASE
  • spring cloud oauth2 2.0.0.M7
  • mybatis 2.0.1

本项目包括功能有

  • 新用户
  1. 用户注册自动分配角色权限
  2. 用户只能访问自己所拥有的角色权限访问路径
  • 开放平台
  1. 用户可以申请获取客户ID和客户密钥
  2. 用户可以通过客户ID获取授权码
  3. 用户可以通过客户ID和密钥以及授权码获取access token 和referrsh token和scope
  • 资源api服务(order-service/open-api-service)
  1. 可自定义配置需授权url
  2. 可自定义配置受限url的访问scope
  3. 未授权用户或访问权限不足用户,页面提示相信息
  4. 用户通过access token 来访问对应url

项目概览

首先来看一下项目的结构图,了解项目的大致布局

a15232cbb14b0bfe91786501315463e.png

  • .idea:idea工具生成的文件
  • docker: 存放Dockerfile文件用于构建容器镜像
  • config: 项目中关于spring security和web的配置
  • controller:请求控制器
  • entity: 对象实体类包
  • handler:逻辑处理类包
  • mapper:存放mybatis mapper接口类
  • service: 业务逻辑处理类
  • util: 通用的工具类
  • postman:postman工具的测试用例
  • sql:项目的初始化sql语句
  • templates: 页面模板文件

接下来正式介绍关于项目的细节,由于项目本身就已经有不少中文注释,所以在讲解的时候会收缩起一些代码的具体实现,如果读者不习惯可以在这里点在线比对阅读或者fork到自己的项目里阅读==> 项目代码

用户是如何被拦截认证的

@Description 
@ date
 web 安全拦截西置
@ component 
 aEnablewebSecurity 
 public class SecurityConfig 
@ Autowired 
 private MyAuthenticationFailureHandler failurehandler ;
 QAutowired 
 private MyAuthenticationSuccessHandter successHandlter ;
@ Aautowired 
 extends WebSecurityConfigurerAdapter 
  1. 自定类 SecurityConfig 继承了 WebSecurityConfigurerAdapter,说明SecurityConfig将会是一个 Http安全配置适配器
  2. 通过 @EnableWebSecurity打开了 HttpSecurity 的安全配置,则该类将生效
*配置 HttpSecurity 拦截资源
 @param http 
 @throws Exception aoverride 
 protected void 
 f configure ( HttpSecurity http ) throws Exception {
 List < Permission > permissions = permissionMapper . findallPermission ();
 ExpressionUrLAuthorizationConfigurercHttpSecurity >. ExpressionInterceptUrlRegistry authorizeRequests =
 http . authorizeRequests ();
 permissions . forEach ( permission -→ authorizeRequests . antMatchers ( permission . getUrl ()). hasAnyAuthority ( permission . getPermTag ()));
 authorizeRequests . antMatchers (-. antPattems :“/ login ”).permitAl1()
 xpressionUrlAuthorizationConfigurer < HttpSecurity >. ExpressioninterceptUrlRegistry 
. antMatchers (. antPatterns :"/ oauth /**”). permitAll () Expressi 
 sionUrlAuthorizationConfigurer < HttpSecurity >. ExpressioninterceptUrlRegistry 
. antMatchers (-
.-. antPatterns "/ addUser "). permi tAll () Expressic 
 onUrlAuthorizationconfigurer < HttpSecurity >. ExpressionlnterceptUrlRegistry 
. antMatchers (. antPatterns :"/ register ").permitAl1() ExpresionUrlAuthorizationConfigurer < HttpSecurity >. ExpressionInterceptUrlRegistry 
. antMatchers (. antPattems :“/**“). fullyAuthenticated () and (). formLogin (). loginPage (“/ login ") FomLoginconfigurer < Htpsecurity >
. successHandler ( successHandler ). failureHandler ( failureHandler ) FormLoginConfigurer < HttpSecurity >
. and (). csrf (). disable ();
  1. 重写 WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 方法
  2. 指定哪里请求url是不需要拦截直接放行的
  3. 指定哪些请求是需要拦截校验的
 private Passwordtncoder passwordtncoder ;
/**.用户认证信息....../.
@0verride
 protected void configure ( AuthenticationManagerBuilderd auth ) t 
 thrOws ExCeptiOn f 
 auth . userDetailsservice ( myUserDetailsService ). passwordEncoder ( new PasswordEncoder (){
 @OVerrlde 
 publi String encode ( CharSequence rawPassword ){ return passwordEncoder . encode ( rawPassword );
 @Override 
 public boolean matches ( CharSequence rawPassword , String encodedPassword )[
 return passwordEncoder . matches ( rawPassword , encodedPassword );
  1. 重写 WebSecurityConfigurerAdapter 的 configure(AuthenticationManagerBuilder auth) 方法
  2. 指定处理认证的service服务类为 MyUserDetailsService
  3. 比对输入的密码和数据库中的用户密码是否一致
import java . util . List ;
 import java . util . stream . Collectors ;/* k . Qauthor . jianspeng ....*/
 acomponent 
@slf4j
 public class MvuserDetailsservice 
 QAutowired 
 private userMapper userMapperi i aoverride 
 public UserDetails loadUserByUsername ( String username )
/1.根据教据库蓝闻,用户是否登陆
 User user = userMapper . findByUsername ( username );/2.查询该用户信息权限
 if ( user nul7)[
/设置用户视限
 List < Permission > permissions =. userMapper . findPermissionByUsername ( username );
tog.info("用户信息权:(,权:”, username , permissions . tostring ());
 e implements UserDetailsService 
 if (! CollectionUtils . isEmpty permissions ))
 ListerantedAuthority > simpleGrantedAuthorities = user . setAuthorities ( simpleGrantedAuthorities );
 permissions . stream (). map ( permission - new SimpleGrantedAuthority permission . getPermTag ()). collect ( collectors . tolist ())
 return user;
  1. 自定类 MyUserDetailsService 实现了 spring security的UserDetailsService 接口
  2. 重写 loadUserByUsername(String username) 方法, 其中username是页面传的属性和值,属性是固定的
  3. 获取用户权限表对应的权限详情,并把内容设置到 UserDetails.Authorities属性中
  4. 返回 UserDetails的子类 User

用户注册自动分配角色权限

用户注册则往用户表插入数据,同时往用户角色表也插入一份数据。

当然也可以设计的更复杂些,比如根据来源、时间、白名单、内部推荐等设置不同的权限,读者可自行扩展。

@param user user 
@return String 
@ PostMapping ("/ addUser ")@ ResponseBody 
 public String addUser ( User user ){
 user . setId ( userMapper . getMaxId ()+1);
 user . setPassword ( passwordEncoder . encode ( user . getPassword ())); user . setCreateDate ( new Date ());
 user . setEnabled ( true );
 user . setAccountNonExpired ( true );
 user . setAccountNonLocked ( true );
 user . setCredentialsNonExpired ( true );
 userMapper . insert ( user );
 userRoleMapper . insert ( user . getId (),
 roleld :1);
 return "注册用户成功";

用户只能访问自己所拥有的角色权限访问路径

 import java . util . List ;
 import java . util . stream . Collectors ;/**.@ author . jiangpeng ......*/
 acomponent 
@SLf4J
 public class MyuserDetailsService implements UserDetailsService {
 QAutowired 
 private UserMapper userMapperi 
@0verride
 public UserDetails loadUserByUsername ( String username ) throws UsernameNotFoundExcepti0n
//1.根据数据库查询,用户是否登陆
 User user = userMapper . findByUsername ( username );
//2.询该用户信息权限
 if ( user 丰 null ){
 List < Permission > permissions = userMapper . findPermissionByUsername ( username ); Log . info ("用户信息权限:,权限:仔", username , permissions . tostring ());
 if (! CollectioUtils . isEmptypermissions ))(
 List < GrantedAuthority ? simpleGrantedAuthorities =
 permissions . stream (). map ( permission - new SimpleGrantedAuthority permission . getPermTag ())). collect ( collectors . toList ()
 user . setAuthorities ( simpleGrantedAuthorities );
 return user;
  1. 取出当前用户的所有权限
  2. 取出权限的权限标识字段,并包装成一个List 集合
  3. 保存包装的权限list集合到 UserDetails 对象中
*配置 Httpsecurity 栏截资源
 aparam http 
@ throws Exception 
 Override 
 protected void configure ( HttpSecurity http ) throws Exception i 
 ListcPermission > permissions = permissionMapper . findAllPermission ();
 ExpressionUrlAuthorizationConfigurercHttpSecurity >. ExpressionInterceptUrlRegistry authorizeRequests =
 http . authorizeRequests (); l 
 permissions . forEach ( permission -> authorizeRequests . antMatchers ( permission . getUrl ()). hasAnyAuthority ( permission . getPermTag ()));
 authorizeRequests . antMatchers (.. antPatterns :“/ login ").permitAl1() ExpressionUrlAuthorizationConfigurer < HttpSecurity >. Expressioninte 
. antMatchers (. antPatterns :“/ oauth /**"). permitAll () ExpressionUrlAuthorizationConfigurer < HtpSecurity >. Expressioninrpttrg 
  1. 取出所有权限表中内容
  2. http.authorizeRequests()获取当前的认证对象
  3. 把权限表中的内容全部设置到 authorizeRequests中, antMatchers表示拦截的url, hasAnyAuthority表示可以访问的权限标识
  4. 因为在上图中user已经设置了自己所拥有权限的权限标识,所以可以访问被拦截的url

54e492b2567604c4da7f05325d036e8.png

用户可以申请获取客户ID和客户密钥

首先来看一下表结构,oauth_client_details 为spring cloud oath2自带的表, user_client_secret 为我们自己创建的表

 cfent 1
 client _426
 client _770
 client _92
( Null )
( Null )
( Null )
( Null )
 client _ secret 
$2a$10$7YJO.unJklmfILm5fV4kLugKPTZZjm/Zshc3HtBwoLsGoggcD2$2a$10$4cjnVXy3KZP.WJm646cmZu9o7JMkMMW5aay2FbI1kXiW56$2a$10$OMBqs3ilrnCG1LbNPBM9nexvbBo5VOWY2LVahSt3crBLrbt$2a$10$WkQsDlNu9IFck8AarbGII.48NrmgX6i3Bh/HWm7kqnJ1UAE

ba37b6f633419093859b1c87092030c.png

*申请获取客户 ID 和客户密钥
aretUrn Str1ng
 asetwapping ("/ registerClientId ")@ ResponseBody 
 public String registerClientId (){
 User principal =( User ) SecurityContextHolder . getContext (). getAuthentication (). getPrincipal (); if ( principal = null ){
 throw new NullPointerException ("用户未登录");
 Random random = new Random ();
 OauthClientDetails clientDetails = new OauthclientDetails ();
 clientDetails . setClientId (" client _"+ random . nextInt ( bound :999));
 clientDetails . setClientSecret ( passwordEncoder . encode ( rawPassword :“123456")); clientDetails . setscope (" all ");
 clientDetails . setAuthorizedGrantTypes (" password , client _ credentials , refresh _ token , authorization _ code "); clientDetails . setWebserverRedirectUri (“http://localhost:8080/code”);
 clientDetails . setAuthorities (" ROLE _ ADMIN , ROLE _ USER ");
 clientDetails . setAccessTokenValidity (7200);
 clientDetails . setRefreshTokenValidity (7200); oauthclientDetailsMapper . insert ( clientDetails );
 UserclientSecret userclientSecret = new UserClientSecret ( principal . getId (), clientDetails . getclientId ()); userClientSecretMapper . save ( userClientsecret );
 return "申请客户 ID 和客户密钥成功";
  1. 生成 OauthClientDetails 数据并保存至数据库中
  2. 获取当前登录人信息,并绑定OauthClientDetails的clientId至user_client_secret表中

用户可以通过客户ID获取授权码

具体的实现在spring-security-oauth包中的, 非本项目内的自我实现

客户ID获取授权码 请求url:

http://localhost:8080/oauth/authorize?response_type=code&client_id=client_92&redirect_uri=http://localhost:8080/code

c571975ec0af1f4b77ae4ccf6f0a2ff.png

e3c828f4910d65cac63ed5de19e0d77.png

  1. 因为是要获取授权码,response_type=code为固定值
  2. 客户申请的客户ID
  3. oauth_client_details 表中的 web_server_redirect_uri

用授权码获取access_token

具体的实现在spring-security-oauth包中的

org.springframework.security.oauth2.provider.endpoint.TokenEndpoint类

,有兴趣的同学可以在里面进行debug调试

用授权码获取access_token 请求url:

http://localhost:8080/oauth/token?grant_type=authorization_code&code=8fGtOV&client_id=client_92&client_secret=123456&redirect_uri=http://localhost:8080/code&scope=all

37ae9c2a60e24a8225e15841467662e.png

  1. 通过客户ID获取到的授权码
  2. 客户申请的客户ID
  3. 客户申请的客户ID配套的客户秘钥
  4. oauth_client_details 表中的 web_server_redirect_uri
  5. oauth_client_details 表中的 scope
access token:"bfcc2788-1652-4ff4-bcc7-058a8c0de3b6" token_type : "bearer"
refresh token:"4af56824-afd4-46e4-8474-f6acf7329660" expires_in:7199 scope:"all"
# 出现如下类似错误标识code失效,重新在获取授权码操作即可
{
    "error": "invalid_grant",
    "error_description": "Invalid authorization code: iq30f9"
}

商户id和商户秘钥获取accessToken和刷新accessToken

493486727338411368.jpg

"access_token":"ae502865-8a96-4ad4-9d28-e8ed35bbdecc","token_type": "bearer",
"refreshtoken":"96600e32-08c7-4bc6-b2e0-ec05502a3eed""expires in": 7199,
"scope":"all" 
  • grant_type: 授权类型,这里设置为密码模式
  • username: 用户名
  • password: 用户密码
  • client_id: 申请得到的客户Id
  • client_secret: 申请得到的客户秘钥
  • scope:范围标识,取自 oauth_client_details 表中的 scope

刷新 accessToken

刷新 accessToken的 请求url:

http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=4741d043-e202-4de0-ae21-4f5c7ec5626e&client_id=client_1&client_secret=123456

验证accessToken是否有效

验证accessToken的 请求url:

http://localhost:8080/oauth/check_token?token=1131809b-ee12-4aea-9823-ea4454b96f2d
{
"active": true,"exp":1573730171
"user name": "admin".
"authorities":「 
"addOrder","showOrder",
"update0rder""delete0rder"
"client_id":"client_1", 
"scope": [  
"all" 

资源api服务

到了这里,我们来看另一个项目 order-service, 其实取名叫 order-api-resource 比较合适,不过也懒得改了。这个项目的结构与代码比较稀少,所以学习起来更为轻松。

如何自定义配置需拦截授权的url

package com.mty.jpsite.configi
import …
@author jiangpeng*资源服务器处理逻辑配置@date 2019/10/1416:48白 */
-@Configuration
..开启资源服务器配责@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
@
public void configure(HttpSecurity http) throws Exception {
// 对 api/order 等自定义请求进行拦截
http.authorizeRequests().antMatchers(…antPatterns: "/api/order/**").access( attribute: "#oauth2.hasScope('read order
//拦截对 /api/product/的 get 请求
.antMatchers(HttpMethod.GET,…antPatterns:"/api/product/**").authenticated()// 拦截对 /api/trade/的 post 请求
.antMatchers(HttpMethod.POST,…antPatterns:"/api/trade/**").authenticated();

通过 antMatchers()authenticated() 方法来配置,查看图中红框内的内容, 这些都是代表需要拦截验证的请求路径url,除了拦截请求外,还可以指定拦截请求方式,比如访问 /api/trade/ ,只对 POST 访问做拦截, GET 请求的访问一律放行。

如何自定义配置受限url的访问scope

@Override
public void configure(HttpSecurity http) throws Exception {
// 对 api/order 等自定义请求进行拦截
http.authorizeRequests().antMatchers( .antPatterns: "/api/order/**  .access( attribute: "#oauth2.hasScope('read_order')") 
//拦截对 /api/product/的 get 请求
.antMatchers(HttpMethod.GET,…antPatterns:"/api/product/**").authenticated()//拦截对 /api/trade/的 post 请求
.antMatchers(HttpMethod.POST,…antPatterns:"/api/trade/**").authenticated();

通过 access() 方法指定访问的该 url 需要的 scope,取值为 oauth_client_details 表中的 scope 值, 如果scope值与代码中定义的不一致,则会出现如下错误:

"error": "insufficient_scope",
"error_description": "Insufficient scope for this resource",
"scope": "read_order" 

未授权用户或访问权限不足用户,页面提示相应信息

用户如何通过 access token 来访问对应url

level:  
org.springframework.security: DEBUG 
security:
oauth2:
resource: 
  # 从认证授权中心上验证token 
token-info-uri: http://localhost:8080/oauth/check_token
  prefer-token-info: true 
client: 
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize 
###appid  
clientId:client 92  
###appSecret  
  clientSecret:123456
  1. 配置验证 accessToken 是否有效的服务地址,其实就是上面发放 accessToken 的服务
  2. 需要配置商户id和商户秘钥获取accessToken,用来通过的开发平台的校验。有的小伙伴可能奇怪了,文中上面也有介绍验证accessToken是否有效,但是并没有需要验证登录,这里为什么要呢? 我们来看下源码:

spring-security-oauth2-2.2.1.RELEASE-sources.jar 中的 RemoteTokenServices.java 用于远程调用 token 服务(开发平台)进行操作

@Override
public OAuth2AuthenticationloadAuthentication(StringaccessToken)throws AuthenticationException,InvalidTokenException
MultiValueMap<String,String> formData = new LinkedMultiValueMap<<(); formData.add(tokenName,accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization",getAuthorizationHeader(clientId, clientSecret));  1 
Map<String,Object>map=postForMap(checkTokenEndpointUrl, formData, headers);
if(map.containsKey("error")){
logger.debug("check_token returned error: "+ map.get("error"));
throw new InvalidTokenException(accessToken); 
  1. 因为给http header 设置了Authorization属性表示需要验证,所以取了上述配置中的 clientId 和 clientSecret,否则发送请求会被开放平台验证失败
  2. postForMap() 包装参数并执行请求发送

项目代码点这里

问题

/oauth/check_token 401



相关文章
|
2天前
|
监控 Java API
淘客返利平台的微服务架构实现
淘客返利平台的微服务架构实现
|
2天前
|
JSON 安全 Java
淘客返利平台的API设计与安全
淘客返利平台的API设计与安全
|
2天前
|
缓存 运维 监控
探索微服务架构中的API网关模式
在微服务架构的海洋中,API网关是连接客户端与众多微服务群岛之间的桥梁。本文将深入探讨API网关的设计原则、核心功能以及在现代软件架构中的关键作用,同时分析其在实际应用中的效益和面临的挑战。
|
3天前
|
安全 API 数据安全/隐私保护
​验证码邮件API有哪些?分析最好的3个接口平台
验证码邮件API如AOKSend、SendGrid和Mailgun是用户身份验证的关键工具。这些API提供高效、可靠的邮件发送服务,确保验证码的安全传输。AOKSend以其快速发送和易用性著称,SendGrid则以全面功能和扩展性见长,而Mailgun则以灵活性和高送达率闻名。开发者可以根据需求选择合适的API,通过示例代码轻松集成到应用中,增强安全性和用户体验。
|
4天前
|
监控 Kubernetes API
探索微服务架构中的API网关模式
【6月更文挑战第22天】在微服务架构的海洋中,API网关是一艘引领航行的旗舰。它不仅是服务的守门人,更是流量的指挥官和信息的翻译官。本文将深入探讨API网关的核心作用、设计考量与实现策略,为构建高效、可靠的微服务系统提供航标。
|
4天前
|
JSON 负载均衡 监控
探索微服务架构中的API网关模式
【6月更文挑战第22天】在微服务架构的海洋中,API网关犹如一座灯塔,指引着服务间的通信与集成。本文将深入探讨API网关的核心概念、设计原则及其在现代后端系统中的关键作用,同时通过实例分析其对系统性能和可维护性的影响,为读者提供一种视角,理解如何高效地构建和管理微服务架构下的API网关。
|
6天前
|
Java Linux API
微信API:探究Android平台下Hook技术的比较与应用场景分析
微信API:探究Android平台下Hook技术的比较与应用场景分析
|
6天前
|
负载均衡 API 开发者
深入理解微服务架构中的API网关模式
在微服务架构中,API网关不仅仅是一个请求转发的节点,它承担着请求路由、负载均衡、认证授权、限流熔断等关键职责。本文将深入探讨API网关的设计原则、实现技术以及在实际部署中的最佳实践,帮助开发者构建一个高效、安全且可扩展的微服务入口。
|
7天前
|
弹性计算 负载均衡 API
微服务架构下的API网关模式解析
在现代软件工程中,微服务架构因其灵活性和可维护性而受到青睐。本文将探讨API网关模式在微服务架构中的关键角色,分析其设计原则、实现方式及面临的挑战,并结合实际案例阐述如何有效整合API网关以提升系统整体性能和安全性。
|
8天前
|
安全 API 数据安全/隐私保护
发送邮件API接口有哪些平台?
在数字化时代,企业借助邮件发送API如AokSend、Mailgun、Amazon SES、Postmark和Sendinblue自动化邮件发送。这些平台提供高可靠性、灵活性、扩展性和多功能集成,支持邮件营销、事务邮件和客户沟通。例如,AokSend以其丰富的功能和易集成著称,而Mailgun则适合需要高级功能的开发者。Amazon SES以高扩展性和经济实惠吸引快速增长的企业,Postmark专长于快速的事务性邮件,Sendinblue则结合了邮件和短信营销。每个平台都有相应的Python示例代码展示如何使用其API发送邮件。

热门文章

最新文章