程序的目的主要是,在自己開發的web項目中,即提供前端頁面調用訪問得接口(帶有安全機制),也提供第三方調用的API(基於授權認證的).
在整合的過程中發現SpringSecurity不能到即處理自己的web請求也處理第三方調用請求。所以采用攔截器攔截處理本地的web請求,spring-security-oauth對第三方認證請求進行認證與授權。如果對Oauth2.0不熟悉請參考Oauth2.0介紹,程序主要演示password模式和client模式。
官方樣例:
1.pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springboot</groupId> <artifactId>testSpringBoot</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>18_SpringBoot_codeStandard</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <!-- 繼承父包 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath></relativePath> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--jdbc --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- Spring Boot Mybatis 依賴 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency> <!--mysql驅動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--連接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.25</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.2</version> </dependency> <!-- freemarker --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--alibab json --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.44</version> </dependency> <!-- 存放token --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--oauth --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> </dependency> <!--單元測試 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> <!--maven的插件 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <executable>true</executable> </configuration> </plugin> </plugins> <pluginManagement> <plugins> <plugin> <!-- 配置java版本 不配置的話默認父類配置的是1.6 --> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!-- 配置Tomcat插件 --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> </plugin> </plugins> </pluginManagement> </build> </project>

2.application.properties中增加redis配置
#設置session超時時間 server.session.timeout=2000 spring.redis.host=127.0.0.1 spring.redis.port=6379 #配置oauth2過濾的優先級 security.oauth2.resource.filter-order=3
3.第三方調用API
package com.niugang.controller; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class OauthController { @GetMapping("/api/product/{id}") public String getProduct(@PathVariable String id) { // for debug Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return "product id : " + id; } @GetMapping("/api/order/{id}") public String getOrder(@PathVariable String id) { // for debug Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return "order id : " + id; } }

AuthExceptionEntryPoint.java 自定義token授權失敗返回信息
package com.niugang.exception; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 自定義AuthExceptionEntryPoint用於tokan校驗失敗返回信息 * * @author niugang * */ public class AuthExceptionEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws ServletException { Map<String, Object> map = new HashMap<>(); //401 未授權 map.put("error", "401"); map.put("message", authException.getMessage()); map.put("path", request.getServletPath()); map.put("timestamp", String.valueOf(new Date().getTime())); response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); try { ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), map); } catch (Exception e) { throw new ServletException(); } } }

CustomAccessDeniedHandler.java 自定義token授權失敗返回信息
package com.niugang.exception; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Autowired private ObjectMapper objectMapper; @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> map = new HashMap<>(); map.put("error", "403"); map.put("message", accessDeniedException.getMessage()); map.put("path", request.getServletPath()); map.put("timestamp", String.valueOf(new Date().getTime())); response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write(objectMapper.writeValueAsString(map)); } }

以下為password模式通過用戶名和密碼獲取token失敗,自定義錯誤信息。
CustomOauthException.java
package com.niugang.exception; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; /** * * @ClassName: CustomOauthException * @Description:password模式錯誤處理,自定義登錄失敗異常信息 * @author: niugang * @date: 2018年9月5日 下午9:44:38 * @Copyright: 863263957@qq.com. All rights reserved. * */ @JsonSerialize(using = CustomOauthExceptionSerializer.class) public class CustomOauthException extends OAuth2Exception { public CustomOauthException(String msg) { super(msg); } }

CustomOauthExceptionSerializer.java
package com.niugang.exception; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Date; import java.util.Map; /** * * @ClassName: CustomOauthExceptionSerializer * @Description:password模式錯誤處理,自定義登錄失敗異常信息 * @author: niugang * @date: 2018年9月5日 下午9:45:03 * @Copyright: 863263957@qq.com. All rights reserved. * */ public class CustomOauthExceptionSerializer extends StdSerializer<CustomOauthException> { private static final long serialVersionUID = 1478842053473472921L; public CustomOauthExceptionSerializer() { super(CustomOauthException.class); } @Override public void serialize(CustomOauthException value, JsonGenerator gen, SerializerProvider provider) throws IOException { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); gen.writeStartObject(); gen.writeStringField("error", String.valueOf(value.getHttpErrorCode())); gen.writeStringField("message", value.getMessage()); // gen.writeStringField("message", "用戶名或密碼錯誤"); gen.writeStringField("path", request.getServletPath()); gen.writeStringField("timestamp", String.valueOf(new Date().getTime())); if (value.getAdditionalInformation()!=null) { for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) { String key = entry.getKey(); String add = entry.getValue(); gen.writeStringField(key, add); } } gen.writeEndObject(); } }

CustomWebResponseExceptionTranslator.java
package com.niugang.exception; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; import org.springframework.stereotype.Component; /** * * @ClassName: CustomWebResponseExceptionTranslator * @Description:password模式錯誤處理,自定義登錄失敗異常信息 * @author: niugang * @date: 2018年9月5日 下午9:46:36 * @Copyright: 863263957@qq.com. All rights reserved. * */ @Component public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator { @Override public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception { OAuth2Exception oAuth2Exception = (OAuth2Exception) e; return ResponseEntity .status(oAuth2Exception.getHttpErrorCode()) .body(new CustomOauthException(oAuth2Exception.getMessage())); } }

4.配置授權認證服務器
package com.niugang.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import com.niugang.exception.AuthExceptionEntryPoint; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; @Configuration /* * 在當前應用程序上下文中啟用授權服務器(即AuthorizationEndpoint和TokenEndpoint)的便利注釋, * 它必須是一個DispatcherServlet上下文。服務器的許多特性可以通過使用AuthorizationServerConfigurer類型的@ * bean來定制(例如,通過擴展AuthorizationServerConfigurerAdapter)。用戶負責使用正常的Spring安全特性( * @EnableWebSecurity等)來保護授權端點(/oauth/授權),但是令牌端點(/oauth/ * Token)將通過客戶端憑證上的HTTP基本身份驗證自動獲得。 * 客戶端必須通過一個或多個AuthorizationServerConfigurers提供一個ClientDetailsService來注冊。 */ @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { //模擬第三方調用api private static final String DEMO_RESOURCE_ID = "api"; @Autowired AuthenticationManager authenticationManager; @Autowired RedisConnectionFactory redisConnectionFactory; @Autowired private UserDetailsService userDetailsService; @Autowired private WebResponseExceptionTranslator customWebResponseExceptionTranslator; /**accessTokenValiditySeconds:設置token無效時間,秒 * refreshTokenValiditySeconds:設置refresh_token無效時間秒 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //配置兩個客戶端,一個用於password認證一個用於client認證 clients.inMemory().withClient("client_1")//基於客戶端認證的 .resourceIds(DEMO_RESOURCE_ID) .authorizedGrantTypes("client_credentials", "refresh_token") .scopes("select") .authorities("client") .secret("123456")/*.refreshTokenValiditySeconds(3600).accessTokenValiditySeconds(60)*/ .and().withClient("client_2")//基於密碼的 .resourceIds(DEMO_RESOURCE_ID) .authorizedGrantTypes("password", "refresh_token") .scopes("select") .authorities("client") .secret("123456")/*.refreshTokenValiditySeconds(3600).accessTokenValiditySeconds(60)*/; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(new RedisTokenStore(redisConnectionFactory)) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService);//密碼模式需要在數據庫中進行認證 endpoints.exceptionTranslator(customWebResponseExceptionTranslator);//錯誤異常 } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { //允許表單認證 oauthServer.allowFormAuthenticationForClients(); oauthServer.authenticationEntryPoint(new AuthExceptionEntryPoint()); } }

5.配置資源服務器
package com.niugang.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import com.niugang.exception.AuthExceptionEntryPoint; import com.niugang.exception.CustomAccessDeniedHandler; /** * 配置資源服務器 * * @author niugang * */ @Configuration @EnableResourceServer /** * 為OAuth2資源服務器提供方便的注釋,使Spring security過濾器能夠通過傳入的OAuth2令牌驗證請求。用戶應該添加這個注釋, * 並提供一個名為ResourceServerConfigurer的@Bean(例如,通過ResourceServerConfigurerAdapter), * 它指定了資源的詳細信息(URL路徑和資源id)。為了使用這個過濾器,您必須在您的應用程序中的某個地方使用@EnableWebSecurity, * 或者在您使用這個注釋的地方,或者在其他地方。 * * * */ public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{ private static final String DEMO_RESOURCE_ID = "api"; @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; @Override public void configure(ResourceServerSecurityConfigurer resources) { //resourceId:指定可訪問的資源id //stateless:標記,以指示在這些資源上只允許基於標記的身份驗證。 resources.resourceId(DEMO_RESOURCE_ID).stateless(true); resources.authenticationEntryPoint(new AuthExceptionEntryPoint()); resources.accessDeniedHandler(customAccessDeniedHandler); } @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .authorizeRequests() .antMatchers("/api/**").authenticated();//配置api訪問控制,必須認證過后才可以訪問 } }

6.配置springsecurity
package com.niugang.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity /** * 雖然和oauth認證優先級,起了沖突但是啟動也會放置不安全的攻擊 * @author niugang * */ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/oauth/**").permitAll(); } }

7.增加攔截器
package com.niugang.interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; public class LogInterceptor implements HandlerInterceptor { private static Logger logger = LoggerFactory.getLogger(LogInterceptor.class); /** * 執行攔截器之前 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.info("interceptor....在執行前...url:{}", request.getRequestURL()); String user = (String)request.getSession().getAttribute("user"); if(user==null){ response.sendRedirect("/myweb/login"); } return true; //返回false將不會執行了 } /** * 調用完處理器,渲染視圖之前 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { logger.info("interceptor.......url:{}", request.getRequestURL()); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }

8.配置攔截器
package com.niugang.config; import java.util.concurrent.TimeUnit; import org.springframework.context.annotation.Configuration; import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration @EnableWebMvc public class MvcConfig extends WebMvcConfigurerAdapter { /** * 授權攔截的路徑 addPathPatterns:攔截的路徑 excludePathPatterns:不攔截的路徑 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new com.niugang.interceptor.LogInterceptor()).addPathPatterns("/**").excludePathPatterns("/login/**", "/static/*","/api/**");//"/api/**",不攔截第三方調用的api super.addInterceptors(registry); } /** * 修改springboot中默認的靜態文件路徑 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //addResourceHandler請求路徑 //addResourceLocations 在項目中的資源路徑 //setCacheControl 設置靜態資源緩存時間 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/") .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()); super.addResourceHandlers(registry); } }

9.配置配置springsecurity數據庫認證
package com.niugang.service; import java.util.ArrayList; import java.util.List; import javax.annotation.Resource; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.niugang.bean.UserQuery; import com.niugang.entity.User; import com.niugang.exception.CheckException; /** * 授權認證業務類 * * @author niugang UserDetailsService spring security包里面的 * 重寫loadUserByUsername方法 * */ @Service public class UserDetailsServiceImpl implements UserDetailsService { //UserService自定義的,從數據查詢信息 @Resource private UserService userService; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserQuery user = new UserQuery(); user.setName(username); // 查詢用戶是否存在 List<User> queryList = userService.queryListByPage(user); if (queryList != null & queryList.size() == 1) { // 查詢用戶擁有的角色 List<GrantedAuthority> list = new ArrayList<GrantedAuthority>(); //如果是admin用戶登錄,授予SUPERADMIN權限 if(username.equals("admin")){ list.add(new SimpleGrantedAuthority("SUPERADMIN")); } org.springframework.security.core.userdetails.User authUser = new org.springframework.security.core.userdetails.User( queryList.get(0).getName(), queryList.get(0).getPassword(), list); return authUser; } return null; } }

如訪問:http:://localhost:8080/myweb/index,沒有登錄就會跳轉到登錄頁面通過以上配置,對於所有web請求,如果沒有登錄都會跳轉到登錄頁面,攔截器不會攔截調用api的請求。
訪問http:://localhost:8080/myweb/api/order/1會提示沒有權限需要認證,默認錯誤與我們自定義返回信息不一致,並且描述信息較少。那么如何自定義Spring Security Oauth2
異常信息,上面也已經有代碼實現
(默認的)
(自定義的)
獲取token
進行如上配置之后,啟動springboot應用就可以發現多了一些自動創建的endpoints(項目啟動的時候也會打印mappings):
{[/oauth/authorize]}
{[/oauth/authorize],methods=[POST]
{[/oauth/token],methods=[GET]}
{[/oauth/token],methods=[POST]}
{[/oauth/check_token]}
{[/oauth/error]
通過單元測試,獲取client模式的token
package com.niugang; import java.util.HashMap; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.RestTemplate; @RunWith(SpringRunner.class) @SpringBootTest public class Test { @org.junit.Test public void queryToken() { RestTemplate restTemplate = new RestTemplate(); HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("grant_type", "client_credentials"); hashMap.put("scope", "select"); hashMap.put("client_id", "client_1"); hashMap.put("client_secret", "123456"); ResponseEntity<String> postForEntity = restTemplate.postForEntity("http://localhost:8080/myweb/oauth/token?grant_type={grant_type}&scope={scope}&client_id={client_id}&client_secret={client_secret}", String.class, String.class, hashMap); String body = postForEntity.getBody(); System.out.println(body); } }

{"access_token":"5bf8c55d-874d-41fc-94bc-01e2cb8f7142","token_type":"bearer","expires_in":43199,"scope":"select"}
expires_in:訪問令牌數秒內的生命周期。例如,值“3600”表示訪問令牌將在響應生成后一小時內過期
然后在訪問:訪問http://localhost:8080/myweb/api/order/1?access_token=bbc81328-69f6-4ff0-8c4c-512f1b8beea3
密碼模式也是一樣就是放說需要的參數變了
注意此列中的密碼模式是基於數據認證的,所以獲取token之前確保數據庫有對應的username和password
/** * 密碼模式 */ @org.junit.Test public void queryToken2() { RestTemplate restTemplate = new RestTemplate(); HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("username", "haha"); hashMap.put("password", "123456"); hashMap.put("grant_type", "password"); hashMap.put("scope", "select"); hashMap.put("client_id", "client_2"); hashMap.put("client_secret", "123456"); ResponseEntity<String> postForEntity = restTemplate.postForEntity( "http://localhost:8080/myweb/oauth/token?username={username}&password= {password}&grant_type={grant_type}&scope={scope}&client_id={client_id}&client_secret= {client_secret}", String.class, String.class, hashMap); String body = postForEntity.getBody(); System.out.println(body); }

{"access_token":"39aa6302-6614-4b94-8553-a96d9ba0f893","token_type":"bearer","refresh_token":"7f2f41dd-4406-4df4-997a-d80178431db8","expires_in":43199,"scope":"select"} //密碼模式返回了refresh_token
源碼地址:https://gitee.com/niugangxy/springboot
微信公眾號