前言
筆者學(xué)習(xí)Spring Boot有一段時(shí)間了藏否,附上Spring Boot系列學(xué)習(xí)文章门驾,大家有興趣可以參考參考:
- 5分鐘入手Spring Boot;
- Spring Boot數(shù)據(jù)庫交互之Spring Data JPA;
- Spring Boot數(shù)據(jù)庫交互之Mybatis;
- Spring Boot視圖技術(shù);
- Spring Boot之整合Swagger;
- Spring Boot之junit單元測試踩坑;
- 如何在Spring Boot中使用TestNG;
- Spring Boot之整合logback日志;
- Spring Boot之整合Spring Batch:批處理與任務(wù)調(diào)度;
- Spring Boot之整合Spring Security: 訪問認(rèn)證;
在上一篇文章Spring Boot之整合Spring Security:訪問認(rèn)證中,我們一起學(xué)習(xí)了Spring Security的訪問認(rèn)證實(shí)現(xiàn)娃善,旨在探索如何用Spring Security進(jìn)行訪問認(rèn)證控制论衍,簡單的說就是:
-
未登錄狀態(tài)下,站點(diǎn)的所有訪問均跳轉(zhuǎn)到登錄頁面聚磺,包括API;
而這樣的操作或設(shè)置遠(yuǎn)不能代表真實(shí)場景坯台,一般我們會面臨以下問題:
1. 未登錄狀態(tài)下,訪問API應(yīng)返回HTTP 狀碼401瘫寝,并伴隨提示性response body蜒蕾;
2. 不同用戶需要不同的訪問權(quán)限,即權(quán)限管理焕阿;
今天我們就來探索如何實(shí)現(xiàn)這2個需求咪啡!
項(xiàng)目代碼仍用已上傳的Git Hub倉庫,歡迎取閱:
整體步驟
- 準(zhǔn)備不同角色的用戶暮屡;
- 準(zhǔn)備測試接口撤摸;
- 美化登錄頁面;
- 授權(quán)管理配置栽惶;
- 驗(yàn)證授權(quán)效果愁溜;
1. 準(zhǔn)備不同角色的用戶;
1). 規(guī)范化角色外厂;
在上一篇文章Spring Boot之整合Spring Security:訪問認(rèn)證,我們在多處使用角色信息:
.roles("admin")
...
.roles("user")
...
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");
...
像這種多處使用的數(shù)據(jù)代承,應(yīng)該做個集中管理與限制汁蝶,因此,我們在項(xiàng)目中創(chuàng)建constant包论悴,創(chuàng)建一個枚舉類:UserTypeEnum掖棉,代碼如:
package com.github.dylanz666.constant;
/**
* @author : dylanz
* @since : 09/07/2020
*/
public enum UserTypeEnum {
ADMIN,
USER
}
然后把所有角色進(jìn)行重構(gòu)替換,這樣我們將角色進(jìn)行集中管理與限制膀估,更為嚴(yán)謹(jǐn)幔亥;
- User實(shí)體類增加userType;
package com.github.dylanz666.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String userType;
}
- 創(chuàng)建用戶時(shí)存儲角色信息察纯,查詢時(shí)也查詢出角色信息帕棉;
package com.github.dylanz666.service;
import com.github.dylanz666.constant.UserTypeEnum;
import com.github.dylanz666.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserDetailsImpl userService;
@Autowired
private UserDetails userDetails;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//Spring Security要求必須加密密碼
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//模擬從數(shù)據(jù)庫中取出用戶信息针肥,使用的sql如: SELECT * FROM USER WHERE USER_NAME='cherrys'
List<User> userList = new ArrayList<>();
User firstUser = new User();
firstUser.setUsername("cherrys");
firstUser.setPassword(passwordEncoder.encode("123"));
firstUser.setUserType(UserTypeEnum.USER.toString());
userList.add(firstUser);
User secondUser = new User();
secondUser.setUsername("randyh");
secondUser.setPassword(passwordEncoder.encode("456"));
secondUser.setUserType(UserTypeEnum.USER.toString());
userList.add(secondUser);
List<User> mappedUsers = userList.stream().filter(s -> s.getUsername().equals(username)).collect(Collectors.toList());
//判斷用戶是否存在
User user;
if (CollectionUtils.isEmpty(mappedUsers)) {
logger.info(String.format("The user %s is not found !", username));
throw new UsernameNotFoundException(String.format("The user %s is not found !", username));
}
user = mappedUsers.get(0);
return new UserDetailsImpl(user);
}
}
- 使用角色信息時(shí),均在限定范圍內(nèi):
.roles(UserTypeEnum.ADMIN.toString())
...
.roles(UserTypeEnum.UAER.toString())
...
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.currentUser.getUserType());
...
2. 準(zhǔn)備測試接口香伴;
我們準(zhǔn)備4種接口慰枕,用于Demo授權(quán)管理:
- 任何角色登錄均可訪問;
- 無需登錄即可訪問;
- ADMIN角色登錄方可訪問即纲;
- USER及比USER權(quán)限大的角色登錄方可訪問具帮;
package com.github.dylanz666.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@RestController
public class HelloController {
@GetMapping("/hello")//任何角色登錄均可訪問;
public String sayHello() {
return "Hello!";
}
@GetMapping("/ping")//無需登錄即可訪問低斋;
public String ping() {
return "Success!";
}
@GetMapping("/admin/hello")//ADMIN角色登錄方可訪問蜂厅;
public String adminHello() {
return "Hello admin!";
}
@GetMapping("/user/hello")//USER及比USER權(quán)限大的角色登錄方可訪問;
public String userHello() {
return "Hello user!";
}
}
3. 美化登錄頁面膊畴;
在上一期文章中掘猿,我們使用了自定義的登錄頁面,但樣子實(shí)在丑巴比,有同學(xué)也許想看下术奖,我們?nèi)绾巫约鹤鰝€美麗的登錄頁面,因此我也稍微美化了一下:
1). 在resources文件夾下創(chuàng)建static文件夾轻绞,用于放置靜態(tài)資源采记,如圖片、CSS文件政勃、js文件等唧龄,我們用于放一張登錄背景圖:
2). 更新resources/templates/login.html模板文件:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Security Example</title>
</head>
<body class="body">
<div class="main">
<div class="welcome">Welcome</div>
<hr/>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="username" class="input">
</div>
<div>
<input type="password" name="password" placeholder="password" class="input">
</div>
<div th:style="'vertical-align:middle'">
<button class="button">Sign In</button>
</div>
</form>
</div>
</body>
<style>
.body {
background-image: url("/20200907.jpg");
background-repeat: no-repeat;
background-position: fixed;
background-size: cover
}
.welcome {
font-size:36px;color: white;
}
.main {
border:5px solid white;
border-radius: 5px;
width: 320px;
height: 220px;
margin: 120px auto;
display: table;
text-align: center;
line-height: 40px;
vertical-align: middle;
display: table;
}
.button {
margin-top: 10px;
border: none;
background-color: #4CAF50;
color: white;
padding: 12px 30px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
border-radius: 3px;
}
.input {
margin-top: 10px;
border: 2px solid #a1a1a1;
background: white;
width: 200px;
height: 18px;
padding: 12px 30px;
border-radius: 5px;
padding: 10px 28px;
text-decoration: none;
display: inline-block;
font-size: 14px;
border-radius: 3px;
}
</style>
</html>
3). 登錄頁面效果:
感覺漂亮多了吧!
4. 授權(quán)管理配置奸远;
1). 自定義無權(quán)限報(bào)錯實(shí)體類既棺;
在授權(quán)之前,我們先自定義一個無權(quán)限報(bào)錯實(shí)體類懒叛,定義當(dāng)無權(quán)限訪問時(shí)丸冕,告知客戶端的信息。在domain包下創(chuàng)建AuthorizationException實(shí)體類薛窥,代碼如下:
package com.github.dylanz666.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @author : dylanz
* @since : 09/07/2020
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class AuthorizationException implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String status;
private String uri;
private String message;
@Override
public String toString() {
return "{" +
"\"code\":\"" + code + "\"," +
"\"status\":\"" + status + "\"," +
"\"message\":\"" + message + "\"," +
"\"uri\":\"" + uri + "\"" +
"}";
}
}
2). 開放靜態(tài)資源訪問胖烛;
修改config包下的WebMvcConfig類,如:
package com.github.dylanz666.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home.html").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello.html").setViewName("hello");
registry.addViewController("/login.html").setViewName("login");
}
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
}
}
3). 授權(quán)管理配置诅迷;
- 修改WebSecurityConfig如下:
package com.github.dylanz666.config;
import com.github.dylanz666.constant.UserTypeEnum;
import com.github.dylanz666.domain.AuthorizationException;
import com.github.dylanz666.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthorizationException authorizationException;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/*.jpg", "/*.png", "/*.css", "/*.js");
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html", "/ping").permitAll()//這3個url不用訪問認(rèn)證
.antMatchers("/admin/**").hasRole(UserTypeEnum.ADMIN.toString())
.antMatchers("/user/**").hasRole(UserTypeEnum.USER.toString())
.anyRequest()
.authenticated()//其他url都需要訪問認(rèn)證
.and()
.formLogin()
.loginPage("/login.html")//登錄頁面的url
.loginProcessingUrl("/login")//登錄表使用的API
.permitAll()//login.html和login不需要訪問認(rèn)證
.and()
.logout()
.permitAll()//logout不需要訪問認(rèn)證
.and()
.csrf()
.disable()
.exceptionHandling()
.accessDeniedHandler(((httpServletRequest, httpServletResponse, e) -> {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json");
authorizationException.setCode(HttpServletResponse.SC_FORBIDDEN);
authorizationException.setStatus("FAIL");
authorizationException.setMessage("FORBIDDEN");
authorizationException.setUri(httpServletRequest.getRequestURI());
PrintWriter printWriter = httpServletResponse.getWriter();
printWriter.write(authorizationException.toString());
printWriter.flush();
printWriter.close();
}))
.authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
if (httpServletRequest.getRequestURI().equals("/hello.html")) {
httpServletResponse.sendRedirect("/login.html");
return;
}
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.setContentType("application/json");
authorizationException.setCode(HttpServletResponse.SC_UNAUTHORIZED);
authorizationException.setStatus("FAIL");
authorizationException.setUri(httpServletRequest.getRequestURI());
authorizationException.setMessage("UNAUTHORIZED");
PrintWriter printWriter = httpServletResponse.getWriter();
printWriter.write(authorizationException.toString());
printWriter.flush();
printWriter.close();
});
httpSecurity.userDetailsService(userDetailsService());
httpSecurity.userDetailsService(userDetailsService);
}
@Bean
@Override
public UserDetailsService userDetailsService() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserDetails dylanz =
User.withUsername("dylanz")
.password(bCryptPasswordEncoder.encode("666"))
.roles(UserTypeEnum.ADMIN.toString())
.build();
UserDetails ritay =
User.withUsername("ritay")
.password(bCryptPasswordEncoder.encode("888"))
.roles(UserTypeEnum.USER.toString())
.build();
UserDetails jonathanw =
User.withUsername("jonathanw")
.password(bCryptPasswordEncoder.encode("999"))
.roles(UserTypeEnum.USER.toString())
.build();
return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_" + UserTypeEnum.ADMIN.toString() + " > ROLE_" + UserTypeEnum.USER.toString());
return roleHierarchy;
}
}
我們來解讀一下:
- "/.jpg", "/.png", "/.css", "/.js" 這幾種類型的資源訪問佩番,均不需要認(rèn)證;
- 對API進(jìn)行授權(quán)罢杉,不同API需要不同的角色:
//以admin開頭的API趟畏,需要ADMIN或更大權(quán)限的角色;
.antMatchers("/admin/**").hasRole(UserTypeEnum.ADMIN.toString())
//以user開頭的API滩租,需要USER或更大權(quán)限的角色赋秀;
.antMatchers("/user/**").hasRole(UserTypeEnum.USER.toString())
- 當(dāng)權(quán)限不足時(shí)利朵,我們自定義了權(quán)限不足邏輯:
(1). 訪問的資源時(shí),由于權(quán)限不足沃琅,角色權(quán)限不足哗咆,則API報(bào)403,且API返回我們自定義的無權(quán)限報(bào)錯信息益眉;
(2). 當(dāng)用戶訪問資源時(shí)晌柬,由于用戶未登錄,則API報(bào)401郭脂,且API返回我們自定義的無權(quán)限報(bào)錯信息年碘;
(3). 當(dāng)用戶訪問資源時(shí),權(quán)限不足且訪問的是/hello.html頁面展鸡,則重定向到登錄頁面/login.html屿衅,這樣避免權(quán)限不足訪問/hello.html頁面時(shí)也報(bào)401;
.exceptionHandling()
.accessDeniedHandler(((httpServletRequest, httpServletResponse, e) -> { httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json");
authorizationException.setCode(HttpServletResponse.SC_FORBIDDEN);
authorizationException.setStatus("FAIL");
authorizationException.setMessage("FORBIDDEN");
authorizationException.setUri(httpServletRequest.getRequestURI());
PrintWriter printWriter = httpServletResponse.getWriter();
printWriter.write(authorizationException.toString());
printWriter.flush();
printWriter.close();
}))
.authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
if (httpServletRequest.getRequestURI().equals("/hello.html")) {
httpServletResponse.sendRedirect("/login.html");
return;
}
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.setContentType("application/json");
authorizationException.setCode(HttpServletResponse.SC_UNAUTHORIZED);
authorizationException.setStatus("FAIL");
authorizationException.setUri(httpServletRequest.getRequestURI());
authorizationException.setMessage("UNAUTHORIZED");
PrintWriter printWriter = httpServletResponse.getWriter();
printWriter.write(authorizationException.toString());
printWriter.flush();
printWriter.close();
});
4). 角色繼承莹弊;
在實(shí)際使用場景中涤久,有些角色擁有其他角色的所有權(quán)限,這時(shí)忍弛,如果為每個角色都單獨(dú)創(chuàng)建完整的權(quán)限表响迂,那么有時(shí)候會相當(dāng)冗余。
因此细疚,這時(shí)候我們就要用到Spirng Security中的角色繼承蔗彤;
在WebSecurityConfig類中,我寫了一個角色繼承的例子:
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_" + UserTypeEnum.ADMIN.toString() + " > ROLE_" + UserTypeEnum.USER.toString());
return roleHierarchy;
}
即:USER繼承于ADMIN角色疯兼,USER角色只擁有ADMIN的部分功能然遏,而ADMIN擁有USER角色的所有功能;
注意角色繼承的寫法吧彪,每個角色前要加ROLE_待侵,繼承時(shí)用 > 符號連接,符號左邊權(quán)限大姨裸,符號右邊權(quán)限小诫给。
5). 資源多角色訪問配置;
假設(shè)我們有些/any開頭的API啦扬,可以給多個角色使用,如SUPERVISOR角色(假設(shè)有這個角色)和USER角色凫碌,我們可以在WebSecurityConfig中這么配置:
.antMatchers("/any/**").hasAnyRole(UserTypeEnum.SUPERVISOR.toString(), UserTypeEnum.USER.toString())
5. 驗(yàn)證授權(quán)效果扑毡;
啟動項(xiàng)目:
開始驗(yàn)證:
1). 訪問不用訪問認(rèn)證的API;
2). 訪問需要任意角色通過訪問認(rèn)證的API盛险;
-
登錄前:
需任意角色認(rèn)證API瞄摊,登錄前 登錄后:
3). 訪問需要ADMIN角色通過訪問認(rèn)證的API;
-
登錄前:
需要ADMIN角色認(rèn)證API换帜,登錄前 -
登錄后:
需要ADMIN角色認(rèn)證API楔壤,登錄后
4). 訪問需要USER角色通過訪問認(rèn)證的API;
-
登錄前:
需要USER角色認(rèn)證API惯驼,登錄前 -
登錄后:
需要USER角色認(rèn)證API蹲嚣,登錄后 -
訪問權(quán)限外API:(注意,此時(shí)的API status code為403祟牲,可以從瀏覽器的Network中查看)
訪問權(quán)限外API
5). 訪問不存在的API隙畜;
我沒有額外定制不存在的錯誤信息或錯誤頁面,默認(rèn)為:
Controller中授權(quán)管理
除了上述在WebSecurityConfig統(tǒng)一對API進(jìn)行授權(quán)说贝,我們還可以在項(xiàng)目的Controller中進(jìn)行授權(quán)管理议惰,步驟:
1. WebSecurityConfig類添加注解:@EnableGlobalMethodSecurity(prePostEnabled = true),如:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
2. Controller內(nèi)增加另外的API乡恕,并在API中配置權(quán)限言询,而不是在WebSecurityConfig中配置:
@GetMapping("/controller/hello")
@PreAuthorize(value="isAuthenticated()")//任何角色登錄均可訪問;
public String controllerAnyHello() {
return "Hello controller any!";
}
@GetMapping("/controller/admin/hello")
@PreAuthorize("hasRole('ADMIN')")//ADMIN角色登錄方可訪問傲宜;
public String controllerAdminHello() {
return "Hello controller admin!";
}
@GetMapping("/controller/both/hello")
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")//ADMIN或USER角色登錄方可訪問运杭;
public String controllerBothHello() {
return "Hello controller both!";
}
簡單分析一下:
1). 使用@EnableGlobalMethodSecurity注解后,Controller中的@PreAuthorize方可生效蛋哭;
2). @PreAuthorize注解內(nèi)可以指定權(quán)限县习,如:
- @PreAuthorize(value="isAuthenticated()"),代表//任何角色登錄均可訪問谆趾;
- @PreAuthorize("hasRole('ADMIN')")躁愿,代表ADMIN角色登錄方可訪問;
- @PreAuthorize("hasAnyRole('ADMIN', 'USER')")沪蓬,可用hasAnyRole為指定的多個角色進(jìn)行授權(quán)彤钟;
- 這種注解方式,hasRole和hasAnyRole內(nèi)的角色不能引用UserTypeEnum內(nèi)的值跷叉,只能手填hard code逸雹;
效果:
與在WebSecurityConfig配置的效果是一樣的,但該方式可對每個API進(jìn)行單獨(dú)配置云挟,不會導(dǎo)致WebSecurityConfig配置在復(fù)雜應(yīng)用里頭的配置很長梆砸,并且對開發(fā)人員更加直觀,授權(quán)管理也更加靈活园欣;
總結(jié)
至此帖世,我們學(xué)會了對API、資源進(jìn)行授權(quán)管理沸枯,若結(jié)合之前學(xué)的訪問認(rèn)證日矫,則我們已能夠?qū)?yīng)用進(jìn)行靈活的訪問控制和權(quán)限控制赂弓,可以滿足大部分認(rèn)證授權(quán)場景!
如果本文對您有幫助哪轿,麻煩點(diǎn)贊+關(guān)注盈魁!
謝謝!