欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于Spring Security的Oauth2授权实现方法

程序员文章站 2022-10-25 18:31:40
前言 经过一段时间的学习oauth2,在网上也借鉴学习了一些大牛的经验,推荐在学习的过程中多看几遍阮一峰的《理解oauth 2.0》,经过对oauth2的多种方式的实...

前言

经过一段时间的学习oauth2,在网上也借鉴学习了一些大牛的经验,推荐在学习的过程中多看几遍阮一峰的《理解oauth 2.0》,经过对oauth2的多种方式的实现,个人推荐spring security和oauth2的实现是相对优雅的,理由如下:

1、相对于直接实现oauth2,减少了很多代码量,也就减少的查找问题的成本。

2、通过调整配置文件,灵活配置oauth相关配置。

3、通过结合路由组件(如zuul),更好的实现微服务权限控制扩展。

oauth2概述

oauth2根据使用场景不同,分成了4种模式

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

在项目中我们通常使用授权码模式,也是四种模式中最复杂的,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。

oauth2授权主要由两部分组成:

  • authorization server:认证服务
  • resource server:资源服务

在实际项目中以上两个服务可以在一个服务器上,也可以分开部署。

准备阶段

核心maven依赖如下

    <dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-web</artifactid>
 </dependency>
 
 <dependency>
  <groupid>com.fasterxml.jackson.datatype</groupid>
  <artifactid>jackson-datatype-joda</artifactid>
 </dependency>
 <dependency>
  <groupid>org.thymeleaf.extras</groupid>
  <artifactid>thymeleaf-extras-springsecurity4</artifactid>
 </dependency>
 <dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-thymeleaf</artifactid>
 </dependency>
 <dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-security</artifactid>
 </dependency>
 <dependency>
  <groupid>org.springframework.security.oauth</groupid>
  <artifactid>spring-security-oauth2</artifactid>
 </dependency>
 
 <dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-jdbc</artifactid>
 </dependency>
 <dependency>
  <groupid>mysql</groupid>
  <artifactid>mysql-connector-java</artifactid>
 </dependency>
 <dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-data-jpa</artifactid>
 </dependency>

token的存储主流有三种方式,分别为内存、redis和数据库,在实际项目中通常使用redis和数据库存储。个人推荐使用mysql数据库存储。

初始化数据结构、索引和数据sql语句如下:

--
-- oauth sql -- mysql
--
 
drop table if exists oauth_client_details;
create table oauth_client_details (
 client_id varchar(255) primary key,
 resource_ids varchar(255),
 client_secret varchar(255),
 scope varchar(255),
 authorized_grant_types varchar(255),
 web_server_redirect_uri varchar(255),
 authorities varchar(255),
 access_token_validity integer,
 refresh_token_validity integer,
 additional_information text,
 autoapprove varchar (255) default 'false'
) engine=innodb default charset=utf8;
 
 
drop table if exists oauth_access_token;
create table oauth_access_token (
 token_id varchar(255),
 token blob,
 authentication_id varchar(255),
 user_name varchar(255),
 client_id varchar(255),
 authentication blob,
 refresh_token varchar(255)
) engine=innodb default charset=utf8;
 
 
drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
 token_id varchar(255),
 token blob,
 authentication blob
) engine=innodb default charset=utf8;
 
 
drop table if exists oauth_code;
create table oauth_code (
 code varchar(255),
 authentication blob
) engine=innodb default charset=utf8;
 
 
 
-- add indexes
create index token_id_index on oauth_access_token (token_id);
create index authentication_id_index on oauth_access_token (authentication_id);
create index user_name_index on oauth_access_token (user_name);
create index client_id_index on oauth_access_token (client_id);
create index refresh_token_index on oauth_access_token (refresh_token);
create index token_id_index on oauth_refresh_token (token_id);
create index code_index on oauth_code (code);
 
-- insert default data
insert into `oauth_client_details` values ('dev', '', 'dev', 'app', 'authorization_code', 'http://localhost:7777/', '', '3600', '3600', '{"country":"cn","country_code":"086"}', 'taiji');

核心配置

核心配置主要分为授权应用和客户端应用两部分,如下:

  • 授权应用:即oauth2授权服务,主要包括spring security、认证服务和资源服务两部分配置
  • 客户端应用:即通过授权应用进行认证的应用,多个客户端应用间支持单点登录

授权应用主要配置如下:

application.properties链接已初始化oauth2的数据库即可

application启动类,授权服务开启配置和spring security配置,如下:

@springbootapplication
@autoconfigureafter(jacksonautoconfiguration.class)
@order(securityproperties.access_override_order)
@enableauthorizationserver
public class application extends websecurityconfigureradapter {
  public static void main(string[] args) {
    springapplication.run(application.class, args);
  }
 
  // 启动的时候要注意,由于我们在controller中注入了resttemplate,所以启动的时候需要实例化该类的一个实例
  @autowired
  private resttemplatebuilder builder;
 
  // 使用resttemplatebuilder来实例化resttemplate对象,spring默认已经注入了resttemplatebuilder实例
  @bean
  public resttemplate resttemplate() {
    return builder.build();
  }
 
  @configuration
  public class webmvcconfig extends webmvcconfigureradapter {
    @override
    public void addviewcontrollers(viewcontrollerregistry registry) {
      registry.addviewcontroller("/login").setviewname("login");
    }
  }
 
 
  @override
  protected void configure(httpsecurity http) throws exception {
 
    http.headers().frameoptions().disable();
    http.authorizerequests()
        .antmatchers("/403").permitall() // for test
        .antmatchers("/login", "/oauth/authorize", "/oauth/confirm_access", "/appmanager").permitall() // for login
        .antmatchers("/image", "/js/**", "/fonts/**").permitall() // for login
        .antmatchers("/j_spring_security_check").permitall()
        .antmatchers("/oauth/authorize").authenticated();
    /*.anyrequest().fullyauthenticated();*/
    http.formlogin().loginpage("/login").failureurl("/login?error").permitall()
        .and()
        .authorizerequests().anyrequest().authenticated()
        .and().logout().invalidatehttpsession(true)
        .and().sessionmanagement().maximumsessions(1).expiredurl("/login?expired").sessionregistry(sessionregistry());
    http.csrf().csrftokenrepository(cookiecsrftokenrepository.withhttponlyfalse());
    http.rememberme().disable();
    http.httpbasic();
 
  }
 
}

资源服务开启,如下:

@configuration
@enableresourceserver
protected static class resourceserverconfiguration extends resourceserverconfigureradapter {
    @override
    public void configure(httpsecurity http) throws exception {
      http.antmatcher("/me").authorizerequests().anyrequest().authenticated();
    }
  }

oauth2认证授权服务配置,如下:

@configuration
public class authorizationserverconfiguration extends authorizationserverconfigureradapter {
 public static final logger logger = loggerfactory.getlogger(authorizationserverconfiguration.class);
 
 @autowired
  private authenticationmanager authenticationmanager;
 
  @autowired
  private datasource datasource;
  @bean
  public tokenstore tokenstore() {
    return new jdbctokenstore(datasource);
  }
 
 
  @override
  public void configure(authorizationserverendpointsconfigurer endpoints) throws exception {
 
    endpoints.authenticationmanager(authenticationmanager);
    endpoints.tokenstore(tokenstore());
    // 配置tokenservices参数
    defaulttokenservices tokenservices = new defaulttokenservices();
    tokenservices.settokenstore(endpoints.gettokenstore());
    tokenservices.setsupportrefreshtoken(false);
    tokenservices.setclientdetailsservice(endpoints.getclientdetailsservice());
    tokenservices.settokenenhancer(endpoints.gettokenenhancer());
    tokenservices.setaccesstokenvalidityseconds( (int) timeunit.minutes.toseconds(10)); //分钟
    endpoints.tokenservices(tokenservices);
  }
 
 
  @override
  public void configure(authorizationserversecurityconfigurer oauthserver) throws exception {
    oauthserver.checktokenaccess("isauthenticated()");
    oauthserver.allowformauthenticationforclients();
  }
 
  @bean
  public clientdetailsservice clientdetails() {
    return new jdbcclientdetailsservice(datasource);
  }
 
  @override
  public void configure(clientdetailsserviceconfigurer clients) throws exception {
      clients.withclientdetails(clientdetails());
    /*    
    *    基于内存配置项
    *    clients.inmemory()
        .withclient("community")
        .secret("community")
        .authorizedgranttypes("authorization_code").redirecturis("http://tech.taiji.com.cn/")
        .scopes("app").and() .withclient("dev")
        .secret("dev")
        .authorizedgranttypes("authorization_code").redirecturis("http://localhost:7777/")
        .scopes("app");*/
  }
}

客户端应用主要配置如下:

application.properties中oauth2配置,如下

security.oauth2.client.clientid=dev
security.oauth2.client.clientsecret=dev
security.oauth2.client.accesstokenuri=http://localhost:9999/oauth/token
security.oauth2.client.userauthorizationuri=http://localhost:9999/oauth/authorize
security.oauth2.resource.loadbalanced=true
security.oauth2.resource.userinfouri=http://localhost:9999/me
security.oauth2.resource.logout.url=http://localhost:9999/revoke-token
security.oauth2.default.rolename=role_user

oauth2config配置,授权oauth2sso配置和spring security配置,如下:

@configuration
@enableoauth2sso
public class oauth2config extends websecurityconfigureradapter{
 @autowired
 customssologouthandler customssologouthandler;
 
 @autowired
 oauth2clientcontext oauth2clientcontext;
 
 @bean
 public httpfirewall allowurlencodedslashhttpfirewall() {
   stricthttpfirewall firewall = new stricthttpfirewall();
   firewall.setallowurlencodedslash(true);
   firewall.setallowsemicolon(true);
   return firewall;
 }
 
 @bean
 @configurationproperties("security.oauth2.client")
 public authorizationcoderesourcedetails taiji() {
 return new authorizationcoderesourcedetails();
 }
 
 @bean
 public communitysuccesshandler customsuccesshandler() {
 communitysuccesshandler customsuccesshandler = new communitysuccesshandler();
 customsuccesshandler.setdefaulttargeturl("/");
 return customsuccesshandler;
 }
 
 @bean
 public customfailurehandler customfailurehandler() {
 customfailurehandler customfailurehandler = new customfailurehandler();
 customfailurehandler.setdefaultfailureurl("/index");
 return customfailurehandler;
 }
 
 @bean
 @primary
 @configurationproperties("security.oauth2.resource")
 public resourceserverproperties taijioauthorresource() {
 return new resourceserverproperties();
 }
 
 @bean
 @override
 public authenticationmanager authenticationmanagerbean() throws exception {
 list<authenticationprovider> authenticationproviderlist = new arraylist<authenticationprovider>();
 authenticationproviderlist.add(customauthenticationprovider());
 authenticationmanager authenticationmanager = new providermanager(authenticationproviderlist);
 return authenticationmanager;
 }
 
 @autowired
 public taijiuserdetailserviceimpl userdetailsservice;
 
 @bean
 public taijiauthenticationprovider customauthenticationprovider() {
 taijiauthenticationprovider customauthenticationprovider = new taijiauthenticationprovider();
 customauthenticationprovider.setuserdetailsservice(userdetailsservice);
 return customauthenticationprovider;
 }
 
 
 @autowired
 private menuservice menuservice;
 @autowired
 private roleservice roleservice;
 @bean
 public taijisecuritymetadatasource taijisecuritymetadatasource() {
 taijisecuritymetadatasource fismetadatasource = new taijisecuritymetadatasource();
// fismetadatasource.setmenuservice(menuservice);
 fismetadatasource.setroleservice(roleservice);
 return fismetadatasource;
 }
 
 @autowired
 private communityaccessdecisionmanager accessdecisionmanager;
 @bean
 public communityfiltersecurityinterceptor communityfiltersecurityinterceptor() throws exception {
 communityfiltersecurityinterceptor taijifiltersecurityinterceptor = new communityfiltersecurityinterceptor();
 taijifiltersecurityinterceptor.setfismetadatasource(taijisecuritymetadatasource());
 taijifiltersecurityinterceptor.setaccessdecisionmanager(accessdecisionmanager);
 taijifiltersecurityinterceptor.setauthenticationmanager(authenticationmanagerbean());
 return taijifiltersecurityinterceptor;
 }
 
 
 @override
 protected void configure(httpsecurity http) throws exception {
 http.authorizerequests()
//     .antmatchers("/").permitall() 
//  .antmatchers("/login").permitall() // 
//  .antmatchers("/image").permitall() // 
//  .antmatchers("/upload/*").permitall() // for
//  .antmatchers("/common/**").permitall() // for 
//  .antmatchers("/community/**").permitall()
  
//     .antmatchers("/").anonymous()
     .antmatchers("/personal/**").authenticated()
     .antmatchers("/notify/**").authenticated()
     .antmatchers("/admin/**").authenticated()
     .antmatchers("/manage/**").authenticated()
     .antmatchers("/**/personal/**").authenticated()
     .antmatchers("/user/**").authenticated()
  .anyrequest()
  .permitall()
//  .authenticated()
  .and()
  .logout()
  .logoutrequestmatcher(new antpathrequestmatcher("/logout"))
  .addlogouthandler(customssologouthandler)
  .deletecookies("jsessionid").invalidatehttpsession(true)
  .and()
  .csrf().disable()
  //.csrftokenrepository(cookiecsrftokenrepository.withhttponlyfalse())
  //.and()
  .addfilterbefore(loginfilter(), basicauthenticationfilter.class)
  .addfilterafter(communityfiltersecurityinterceptor(), filtersecurityinterceptor.class);///taijisecurity权限控制
 }
 
 @override
 public void configure(websecurity web) throws exception {
 // 解决静态资源被拦截的问题
 web.ignoring().antmatchers("/theme/**")
  .antmatchers("/community/**")
  .antmatchers("/common/**")
  .antmatchers("/upload/*");
 web.httpfirewall(allowurlencodedslashhttpfirewall());
 }
 
 
 public oauth2clientauthenticationprocessingfilter loginfilter() throws exception {
 oauth2clientauthenticationprocessingfilter ff = new oauth2clientauthenticationprocessingfilter("/login");
 oauth2resttemplate resttemplate = new oauth2resttemplate(taiji(),oauth2clientcontext);
 ff.setresttemplate(resttemplate);
 userinfotokenservices tokenservices = new userinfotokenservices(taijioauthorresource().getuserinfouri(), taiji().getclientid());
 tokenservices.setresttemplate(resttemplate);
 ff.settokenservices(tokenservices);
 ff.setauthenticationsuccesshandler(customsuccesshandler());
    ff.setauthenticationfailurehandler(customfailurehandler());
 return ff;
 }
}

授权成功回调类,认证成功用户落地,如下:

public class communitysuccesshandler extends savedrequestawareauthenticationsuccesshandler {
 
 protected final log logger = logfactory.getlog(this.getclass());
 
 private requestcache requestcache = new httpsessionrequestcache();
 @autowired
 private userservice userservice;
 @autowired
 private roleservice roleservice;
 @inject
 authenticationmanager authenticationmanager;
 @value("${security.oauth2.default.rolename}")
 private string defaultrole;
 @inject
 taijioperationlogservice taijioperationlogservice;
 
 @inject
 communityconfiguration communityconfiguration;
 
 @inject
 private objectmapper objectmapper;
 
 @scorerule(code="login_score")
 @override
 public void onauthenticationsuccess(httpservletrequest request, httpservletresponse response,
  authentication authentication) throws servletexception, ioexception {
 // 存放authentication到securitycontextholder
 securitycontextholder.getcontext().setauthentication(authentication);
 httpsession session = request.getsession(true);
 // 在session中存放security context,方便同一个session中控制用户的其他操作
 session.setattribute("spring_security_context", securitycontextholder.getcontext());
 oauth2authentication oauth2authentication = (oauth2authentication) authentication;
 object details = oauth2authentication.getuserauthentication().getdetails();
 userdto user = saveuser((map) details);//用户落地
 collection<grantedauthority> obtionedgrantedauthorities = obtiongrantedauthorities(user);
 usernamepasswordauthenticationtoken newtoken = new usernamepasswordauthenticationtoken(
  new user(user.getloginname(), "", true, true, true, true, obtionedgrantedauthorities), 
  authentication.getcredentials(), obtionedgrantedauthorities);
 
 newtoken.setdetails(details);
 object oath2details=oauth2authentication.getdetails();
 oauth2authentication = new oauth2authentication(oauth2authentication.getoauth2request(), newtoken);
 oauth2authentication.setdetails(oath2details);
 oauth2authentication.setauthenticated(true);
 securitycontextholder.getcontext().setauthentication(oauth2authentication);
 
 logutil.log2database(taijioperationlogservice, request, user.getloginname(), "user", "", "", "user_login", "登录", "onauthenticationsuccess","");
 session.setattribute("user", user);
 collection<grantedauthority> authorities = (collection<grantedauthority>) authentication.getauthorities();
 
 savedrequest savedrequest = requestcache.getrequest(request, response);
 if (savedrequest == null) {
  super.onauthenticationsuccess(request, response, authentication);
  return;
 }
 string targeturlparameter = gettargeturlparameter();
 if (isalwaysusedefaulttargeturl()
  || (targeturlparameter != null && stringutils.hastext(request.getparameter(targeturlparameter)))) {
  requestcache.removerequest(request, response);
  super.onauthenticationsuccess(request, response, authentication);
  return;
 }
 clearauthenticationattributes(request);
 // use the defaultsavedrequest url
 string targeturl = savedrequest.getredirecturl();
// logger.debug("redirecting to defaultsavedrequest url: " + targeturl);
 
 logger.debug("redirecting to last savedrequest url: " + targeturl);
 getredirectstrategy().sendredirect(request, response, targeturl);
// getredirectstrategy().sendredirect(request, response, this.getdefaulttargeturl());
 }
 
 public void setrequestcache(requestcache requestcache) {
 this.requestcache = requestcache;
 }
 
 //用户落地
 private userdto saveuser(map userinfo) {
 userdto dto=null;
 try {
  string json = objectmapper.writevalueasstring(userinfo);
  dto = objectmapper.readvalue(json,userdto.class);
 } catch (jsonprocessingexception e) {
  // todo auto-generated catch block
  e.printstacktrace();
 } catch (ioexception e) {
  // todo auto-generated catch block
  e.printstacktrace();
 }
 
 userdto user=userservice.findbyloginname(dto.getloginname());
 if(user!=null) {
  return user;
 }
 set<roledto> roles= new hashset<roledto>();
 roledto role = roleservice.findbyrolename(defaultrole);
 roles.add(role);
 dto.setroles(roles);
 list<userdto> list = new arraylist<userdto>();
 list.add(dto);
 dto.generatetokenforcommunity(communityconfiguration.getcontrollersalt());
 string id =userservice.saveuserwithrole(dto,communityconfiguration.getcontrollersalt());
 dto.setid(id);
 return dto;
 }
 
 /**
   * map转成实体对象
   *
   * @param map  map实体对象包含属性
   * @param clazz 实体对象类型
   * @return
   */
  public static <t> t map2object(map<string, object> map, class<t> clazz) {
    if (map == null) {
      return null;
    }
    t obj = null;
    try {
      obj = clazz.newinstance();
 
      field[] fields = obj.getclass().getdeclaredfields();
      for (field field : fields) {
        int mod = field.getmodifiers();
        if (modifier.isstatic(mod) || modifier.isfinal(mod)) {
          continue;
        }
        field.setaccessible(true);
        string filedtypename = field.gettype().getname();
        if (filedtypename.equalsignorecase("java.util.date")) {
          string datetimestamp = string.valueof(map.get(field.getname()));
          if (datetimestamp.equalsignorecase("null")) {
            field.set(obj, null);
          } else {
            field.set(obj, new date(long.parselong(datetimestamp)));
          }
        } else {
         string v = map.get(field.getname()).tostring();
          field.set(obj, map.get(field.getname()));
        }
      }
    } catch (exception e) {
      e.printstacktrace();
    }
    return obj;
  }
 
 
 
 // 取得用户的权限
 private collection<grantedauthority> obtiongrantedauthorities(userdto users) {
 collection<grantedauthority> authset = new hashset<grantedauthority>();
 // 获取用户角色
 set<roledto> roles = users.getroles();
 if (null != roles && !roles.isempty())
  for (roledto role : roles) {
 authset.add(new simplegrantedauthority(role.getid()));
  }
 return authset;
 }
}

客户端应用,单点登录方法,如下:

@requestmapping(value = "/loadtoken", method = { requestmethod.get })
 public void loadtoken(model model,httpservletresponse response,@requestparam(value = "clientid", required = false) string clientid) {
 string token = "";
 requestattributes ra = requestcontextholder.getrequestattributes();
 servletrequestattributes sra = (servletrequestattributes) ra;
 httpservletrequest request = sra.getrequest();
 httpsession session = request.getsession();
 if (session.getattribute("spring_security_context") != null) {
  securitycontext securitycontext = (securitycontext)session.getattribute("spring_security_context");
  authentication authentication = securitycontext.getauthentication();
  oauth2authenticationdetails oauth2authenticationdetails = (oauth2authenticationdetails) authentication.getdetails();
  token = oauth2authenticationdetails.gettokenvalue();
 }
 try {
  string url = "http://localhost:9999/rediect?clientid=dev&token="+token;
  response.sendredirect(url);
 } catch (ioexception e) {
  e.printstacktrace();
 }
 }

服务端应用,单点登录方法,如下:

@requestmapping("/rediect")
 public string rediect(httpservletresponse responsel, string clientid, string token) {
 oauth2authentication authentication = tokenstore.readauthentication(token);
 if (authentication == null) {
  throw new invalidtokenexception("invalid access token: " + token);
 }
 oauth2request request = authentication.getoauth2request();
 map map = new hashmap();
 map.put("code", request.getrequestparameters().get("code"));
 map.put("grant_type", request.getrequestparameters().get("grant_type"));
 map.put("response_type", request.getrequestparameters().get("response_type"));
 //todo 需要查询一下要跳转的client_id配置的回调地址
 map.put("redirect_uri", "http://127.0.0.1:8888");
 map.put("client_id", clientid);
 map.put("state", request.getrequestparameters().get("state"));
 request = new oauth2request(map, clientid, request.getauthorities(), request.isapproved(), request.getscope(),
  request.getresourceids(), map.get("redirect_uri").tostring(), request.getresponsetypes(),request.getextensions()); // 模拟用户登录
 authentication t = tokenstore.readauthentication(token);
 oauth2authentication auth = new oauth2authentication(request, t);
 oauth2accesstoken new_token = defaulttokenservices.createaccesstoken(auth);
 return "redirect:/user_info?access_token=" + new_token.getvalue();
 }
@requestmapping({ "/user_info" })
 public void user(string access_token,httpservletresponse response) {
 oauth2authentication auth=tokenstore.readauthentication(access_token);
 oauth2request request=auth.getoauth2request();
  map<string, string> map = new linkedhashmap<>();
  map.put("loginname", auth.getuserauthentication().getname());
  map.put("password", auth.getuserauthentication().getname());
  map.put("id", auth.getuserauthentication().getname());
  try {
 response.sendredirect(request.getredirecturi()+"?name="+auth.getuserauthentication().getname());
 } catch (ioexception e) {
 e.printstacktrace();
 }
}

个人总结

oauth2的设计相对复杂,需要深入学习多看源码才能了解内部的一些规则,如数据token的存储是用的实体序列化后内容,需要反序列才能在项目是使用,也许是为了安全,但在学习过程需要提前掌握,还有在token的过期时间不能为0,通常来讲过期时间为0代表长期有效,但在oauth2中则报错,这些坑需要一点点探索。

通过集成spring security和oauth2较大的提供的开发的效率,也提供的代码的灵活性和可用性。但封装的核心类需要大家都了解一下,通读下代码,以便在项目中可随时获取需要的参数。

示例代码

以下是个人的一套代码,供参考。

基于spring cloud的微服务框架集成oauth2的代码示例

oauth2数据结构,如下:

基于Spring Security的Oauth2授权实现方法基于Spring Security的Oauth2授权实现方法基于Spring Security的Oauth2授权实现方法基于Spring Security的Oauth2授权实现方法基于Spring Security的Oauth2授权实现方法

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。