javascript
Spring Security框架
Spring Security框架
關(guān)于用戶身份認(rèn)證與授權(quán)
Spring Security是用于解決認(rèn)證與授權(quán)的框架。
添加依賴
<!-- Spring Boot Security:處理認(rèn)證與授權(quán) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>啟動項目,在啟動的日志中,可以看到類似以下內(nèi)容:
Using generated security password: 2abb9119-b5bb-4de9-8584-9f893e4a5a92Spring Security有默認(rèn)登錄的賬號和密碼(以上提示的值),密碼是隨機的,每次啟動項目都會不同。
Spring Security默認(rèn)要求所有的請求都是必須先登錄才允許的訪問,可以使用默認(rèn)的用戶名user和自動生成的隨機密碼來登錄。在測試登錄時,在瀏覽器訪問當(dāng)前主機的任意網(wǎng)址都可以(包括不存在的資源),會自動跳轉(zhuǎn)到登錄頁(是由Spring Security提供的,默認(rèn)的URL是:http://localhost:8080/login),當(dāng)?shù)卿洺晒?#xff0c;會自動跳轉(zhuǎn)到此前訪問的URL(跳轉(zhuǎn)登錄頁之前的URL),另外,還可以通過 http://localhost:8080/logout 退出登錄。
Spring Security的依賴項中包括了Bcrypt算法的工具類,Bcrypt是一款非常優(yōu)秀的密碼加密工具,適用于對需要存儲下來的密碼進行加密處理。
測試?yán)?:
public class BcryptPasswordEncoderTests {private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();@Testpublic void testEncode() {// 原文相同的情況,每次加密得到的密文都不同for (int i = 0; i < 10; i++) {String rawPassword = "123456";String encodedPassword = passwordEncoder.encode(rawPassword);System.out.println("rawPassword = " + rawPassword);System.out.println("encodedPassword = " + encodedPassword);}// rawPassword = 123456// encodedPassword = $2a$10$HWuJ9WgPazrwg9.isaae4u7XdP7ohH7LetDwdlTWuPC4ZAvG.Uc7W}@Testpublic void testMatches() {String rawPassword = "123456";String encodedPassword = "$2a$10$hI4wweFOGJ7FMduSmCjNBexbKFOjYMWl8hkug0n0k1LNR5vEyhhMW";boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword);System.out.println("match result : " + matchResult);}}Spring Security的認(rèn)證機制中包含:當(dāng)客戶端提交登錄后,會自動調(diào)用UserDetailsService接口(Spring Security定義的)的實現(xiàn)類對象中的UserDetails loadUserByUsername(String username)方法(根據(jù)用戶名加載用戶數(shù)據(jù)),將得到UserDetails類型的對象,此對象中應(yīng)該至少包括此用戶名對應(yīng)的密碼、權(quán)限等信息,接下來,Spring Security會自動完成密碼的對比,并確定此次客戶端提交的信息是否允許登錄!類似于:
// Spring Security的行為 UserDetails userDetails = userDetailsService.loadUserByUsername("chengheng"); // Spring Security將從userDetails中獲取密碼,用于驗證客戶端提交的密碼,判斷是否匹配所以,要實現(xiàn)Spring Security通過數(shù)據(jù)庫的數(shù)據(jù)來驗證用戶名與密碼(而不是采用默認(rèn)的user用戶名和隨機的密碼),則在passport包下創(chuàng)建security.UserDetailsServiceImpl類,實現(xiàn)UserDetailsService接口,并重寫接口中的抽象方法:
@Service public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate AdminMapper adminMapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {System.out.println("根據(jù)用戶名查詢嘗試登錄的管理員信息,用戶名=" + s);/*通過用戶名從緩存中獲取用戶信息*/AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);System.out.println("通過持久層進行查詢,結(jié)果=" + admin);if (admin == null) {System.out.println("根據(jù)用戶名沒有查詢到有效的管理員數(shù)據(jù),將拋出異常");throw new BadCredentialsException("登錄失敗,用戶名不存在!");}System.out.println("查詢到匹配的管理員數(shù)據(jù),需要將此數(shù)據(jù)轉(zhuǎn)換為UserDetails并返回");UserDetails userDetails = User.builder().username(admin.getUsername()).password(admin.getPassword())/* accountExpired 賬號是否過期 */.accountExpired(false)/* accountLocked 賬號是否鎖定 */.accountLocked(false)/* disabled 賬號是否禁用 */.disabled(admin.getIsEnable() != 1)/* credentialsExpired 證書是否過期 */.credentialsExpired(false)/* authorities 用戶權(quán)限 */.authorities(admin.getPermissions().toArray(new String[] {})).build();System.out.println("轉(zhuǎn)換得到UserDetails=" + userDetails);return userDetails;}}完成后,再配置密碼加密器即可:
@Configuration public class SecurityConfiguration {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}重啟項目,可以發(fā)現(xiàn)在啟動過程中不再生成隨機的密碼值,在瀏覽器上訪問此項目的任何URL,進入登錄頁,即可使用數(shù)據(jù)庫中的管理員數(shù)據(jù)進行登錄。
在Spring Security,默認(rèn)使用Session機制存儲成功登錄的用戶信息(因為HTTP協(xié)議是無狀態(tài)協(xié)議,并不保存客戶端的任何信息,所以,同一個客戶端的多次訪問,對于服務(wù)器而言,等效于多個不同的客戶端各訪問一次,為了保存用戶信息,使得服務(wù)器端能夠識別客戶端的身份,必須采取某種機制),當(dāng)下,更推薦使用Token或相關(guān)技術(shù)(例如JWT)來解決識別用戶身份的問題。
JWT
JWT = JSON Web Token,它是通過JSON格式組織必要的數(shù)據(jù),將數(shù)據(jù)記錄在票據(jù)(Token)上,并且,結(jié)合一定的算法,使得這些數(shù)據(jù)會被加密,然后在網(wǎng)絡(luò)上傳輸,服務(wù)器端收到此數(shù)據(jù)后,會先對此數(shù)據(jù)進行解密,從而得到票據(jù)上記錄的數(shù)據(jù)(JSON數(shù)據(jù)),從而識別用戶的身份,或者處理相關(guān)的數(shù)據(jù)。
其實,在客戶端第1次訪問服務(wù)器端時,是“空著手”訪問的,不會攜帶任何票據(jù)數(shù)據(jù),當(dāng)服務(wù)器進行響應(yīng)時,會將JWT響應(yīng)到客戶端,客戶端從第2次訪問開始,每次都應(yīng)該攜帶JWT發(fā)起請求,則服務(wù)器都會收到請求中的JWT并進行處理。
要使用JWT,需要添加相關(guān)的依賴項,可以實現(xiàn)生成JWT、解析JWT的框架較多,目前,主流的JWT框架可以是jjwt:
JWT的組成部分:Header(頭),Payload(載荷),Signature(簽名)
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --> <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>則在根項目中管理以上依賴,并在csmall-passport中添加以上依賴。
測試使用JWT:
public class JwtTests {// 密鑰String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";@Testpublic void testGenerateJwt() {// ClaimsMap<String, Object> claims = new HashMap<>();claims.put("id", 9527);claims.put("name", "家丁");// JWT的組成部分:Header(頭),Payload(載荷),Signature(簽名)String jwt = Jwts.builder()// Header:指定算法與當(dāng)前數(shù)據(jù)類型// 格式為: { "alg": 算法, "typ": "jwt" }.setHeaderParam(Header.CONTENT_TYPE, "HS256").setHeaderParam(Header.TYPE, Header.JWT_TYPE)// Payload:通常包含Claims(自定義數(shù)據(jù))和過期時間.setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))// Signature:由算法和密鑰(secret key)這2部分組成.signWith(SignatureAlgorithm.HS256, secretKey)// 打包生成.compact();// eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NTM2NzMwMn0.qBBHearv8iHPNjtDGtO2ci_-KAL4CALHnwzaG_ljsQgSystem.out.println(jwt);}@Testpublic void testParseJwt() {String jwt = "eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NTM2NzMwMn0.qBBHearv8iHPNjtDGtO2ci_-KAL4CALHnwzaG_ljsQg";Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();Object id = claims.get("id");Object name = claims.get("name");System.out.println("id=" + id);System.out.println("name=" + name);}}當(dāng)JWT數(shù)據(jù)過期時,異常信息例如:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-06-16T15:47:57Z. Current time: 2022-06-16T16:08:32Z, a difference of 1235869 milliseconds. Allowed clock skew: 0 milliseconds.當(dāng)JWT解析失敗(數(shù)據(jù)有誤)時,異常信息例如:
io.jsonwebtoken.MalformedJwtException: Unable to read JSON value: {"cty"�"HS256","typ":"JWT","alg":"HS256"}當(dāng)生成JWT和解析JWT的密鑰不一致時,異常信息例如:
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.要在Spring Security中使用JWT,至少需要:
-
不能讓Spring Security按照原有模式來處理登錄(原有模式中,登錄成功后,自動裝用戶信息存儲到Session中,且跳轉(zhuǎn)頁面),需要
-
需要自動裝配AuthenticationManager對象
- 使得SecurityConfiguration配置類繼承自WebSecurityConfigurerAdapter類,重寫其中的xx方法,在此方法中直接調(diào)用父級方法即可,并在此方法上添加@Bean注解
-
創(chuàng)建AdminLoginDTO類,此類中應(yīng)該包含用戶登錄時需要提交的用戶名、密碼
-
創(chuàng)建IAdminService接口
-
在IAdminService接口中添加登錄的抽象方法
String login(AdminLoginDTO adminLoginDTO); -
創(chuàng)建AdminServiceImpl類,實現(xiàn)以上接口
- 在實現(xiàn)過程中,調(diào)用AuthenticationManager實現(xiàn)認(rèn)證,當(dāng)認(rèn)證成功后,生成JWT并返回
-
創(chuàng)建AdminController類,在類中處理登錄請求
-
在SecurityConfiguration中配置Spring Security,對特定的請求進行放行(默認(rèn)所有請求都必須先登錄)
-
配置代碼
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 禁用防跨域攻擊http.csrf().disable();// URL白名單String[] urls = {"/admins/login"};// 配置各請求路徑的認(rèn)證與授權(quán)http.authorizeRequests() // 請求需要授權(quán)才可以訪問.antMatchers(urls) // 匹配一些路徑.permitAll() // 允許直接訪問(不需要經(jīng)過認(rèn)證和授權(quán)).anyRequest() // 匹配除了以上配置的其它請求.authenticated(); // 都需要認(rèn)證} } @Data public class AdminLoginDTO implements Serializable {private String username;private String password; } public interface IAdminService {String login(AdminLoginDTO adminLoginDTO);} @Service public class AdminServiceImpl implements IAdminService {@Autowiredprivate AuthenticationManager authenticationManager;@Overridepublic String login(AdminLoginDTO adminLoginDTO) {// 準(zhǔn)備被認(rèn)證數(shù)據(jù)Authentication authentication= new UsernamePasswordAuthenticationToken(adminLoginDTO.getUsername(), adminLoginDTO.getPassword());// 調(diào)用AuthenticationManager驗證用戶名與密碼// 執(zhí)行認(rèn)證,如果此過程沒有拋出異常,則表示認(rèn)證通過,如果認(rèn)證信息有誤,將拋出異常authenticationManager.authenticate(authentication);// 如果程序可以執(zhí)行到此處,則表示登錄成功// 生成此用戶數(shù)據(jù)的JWTString jwt = "This is a JWT."; // 臨時return jwt;} } @RestController @RequestMapping(value = "/admins", produces = "application/json; charset=utf-8") public class AdminController {@Autowiredprivate IAdminService adminService;// http://localhost:8080/admins/login?username=root&password=123456@RequestMapping("/login")public String login(AdminLoginDTO adminLoginDTO) {String jwt = adminService.login(adminLoginDTO);return jwt;} }測試
完成以上配置后,可以通過 http://localhost:8080/admins/login?username=root&password=123456 這類URL測試登錄,使用數(shù)據(jù)庫中的用戶名和密碼進行嘗試。
當(dāng)通過以上URL進行訪問時,其內(nèi)部過程大概是:
- Spring Security的相關(guān)配置會進行URL的檢查,來判斷是否允許訪問此路徑
- 所以,需要在SecurityConfiguration中將以上路徑設(shè)置為白名單
- 如果沒有將以上路徑配置到白名單,將直接跳轉(zhuǎn)到登錄頁,因為默認(rèn)所有請求都必須先登錄
- 由AdminController接收到請求后,調(diào)用了IAdminService接口的實現(xiàn)類對象來處理登錄
- IAdminService接口的實現(xiàn)是AdminServiceImpl
- 在AdminServiceImpl中,調(diào)用了AuthenticationManager處理登錄的認(rèn)證
- AuthenticationManager對象調(diào)用authenticate()方法進行登錄處理
- 內(nèi)部實現(xiàn)中,會自動調(diào)用UserDetailsService實現(xiàn)對象的loadUserByUsername()方法以獲取用戶信息,并自動完成后續(xù)的認(rèn)證處理(例如驗證密碼是否正確),所以,在步驟中,具體執(zhí)行的是UserDetailsServiceImpl類中重寫的方法,此方法返回了用戶信息,Spring Security自動驗證,如果失敗(例如賬號已禁用、密碼錯誤等),會拋出異常
- 以上調(diào)用的authenticate()方法如果未拋出異常,可視為認(rèn)證成功,即登錄成功
- 當(dāng)?shù)卿洺晒r,應(yīng)該返回此用戶的JWT數(shù)據(jù)
- AuthenticationManager對象調(diào)用authenticate()方法進行登錄處理
Spring Security + JWT
此前,在處理登錄的業(yè)務(wù)中,當(dāng)視為登錄成功時,返回的字符串并不是JWT數(shù)據(jù),則應(yīng)該將此數(shù)據(jù)改為必要的JWT數(shù)據(jù)。
@Service public class AdminServiceImpl implements IAdminService {// ===== 原有其它代碼 =====/*** JWT數(shù)據(jù)的密鑰*/private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";@Overridepublic String login(AdminLoginDTO adminLoginDTO) {// ===== 原有其它代碼 =====// 如果程序可以執(zhí)行到此處,則表示登錄成功// 生成此用戶數(shù)據(jù)的JWT// ClaimsUser user = (User) authenticate.getPrincipal();System.out.println("從認(rèn)證結(jié)果中獲取Principal=" + user.getClass().getName());Map<String, Object> claims = new HashMap<>();claims.put("username", user.getUsername());claims.put("permissions", user.getAuthorities());System.out.println("即將向JWT中寫入數(shù)據(jù)=" + claims);// JWT的組成部分:Header(頭),Payload(載荷),Signature(簽名)String jwt = Jwts.builder()// Header:指定算法與當(dāng)前數(shù)據(jù)類型// 格式為: { "alg": 算法, "typ": "jwt" }.setHeaderParam(Header.CONTENT_TYPE, "HS256").setHeaderParam(Header.TYPE, Header.JWT_TYPE)// Payload:通常包含Claims(自定義數(shù)據(jù))和過期時間.setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))// Signature:由算法和密鑰(secret key)這2部分組成.signWith(SignatureAlgorithm.HS256, secretKey)// 打包生成.compact();// 返回JWT數(shù)據(jù)return jwt;}}在控制器中,應(yīng)該響應(yīng)JSON格式的數(shù)據(jù),所以,需要添加依賴包含JsonResult類的依賴。將控制器中處理請求的方法的返回值類型改為JsonResult<String>,并調(diào)整返回值:
// http://localhost:8080/admins/login?username=root&password=123456 @RequestMapping("/login") public JsonResult<String> login(AdminLoginDTO adminLoginDTO) {String jwt = adminService.login(adminLoginDTO);return JsonResult.ok(jwt); }此時,重啟項目,在瀏覽器中,使用正確的用戶名和密碼訪問,響應(yīng)的結(jié)果例如:
{"state":20000,"message":null,"data":"eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJwZXJtaXNzaW9ucyI6W3siYXV0aG9yaXR5IjoiL2Ftcy9hZG1pbi9kZWxldGUifSx7ImF1dGhvcml0eSI6Ii9hbXMvYWRtaW4vcmVhZCJ9LHsiYXV0aG9yaXR5IjoiL2Ftcy9hZG1pbi91cGRhdGUifSx7ImF1dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9kZWxldGUifSx7ImF1dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9yZWFkIn0seyJhdXRob3JpdHkiOiIvcG1zL3Byb2R1Y3QvdXBkYXRlIn1dLCJleHAiOjE2NTU0MzQwMzcsInVzZXJuYW1lIjoicm9vdCJ9.8ZIfpxxjJlwNo-E3JhXwH4sZR0J5-FU-HAOMu1Tg-44" }注意:以上只是訪問/admins/login時會執(zhí)行所編寫的流程(發(fā)送用戶名和密碼,得到含JWT的結(jié)果),并不代表真正意義的實現(xiàn)了“登錄”!
登錄的流程應(yīng)該是:
客戶端提交用戶名和密碼到服務(wù)器端 >>> 服務(wù)器端認(rèn)證成功后響應(yīng)JWT >>> 客戶端在后續(xù)的請求中都攜帶JWT >>> 服務(wù)器端驗證JWT來決定是否允許訪問。
為了便于體現(xiàn)“客戶端在后續(xù)的請求中都攜帶JWT”的操作,可以在項目中添加使用Knife4j。
當(dāng)使用Knife4j時,需要在白名單中添加相關(guān)的放行資源路徑,否則,Knife4j的頁面將無法使用:
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter {// ===== 原有其它代碼 =====@Overrideprotected void configure(HttpSecurity http) throws Exception {// ===== 原有其它代碼 =====// URL白名單String[] urls = {"/admins/login","/doc.html", // 從本行開始,以下是新增"/**/*.js","/**/*.css","/swagger-resources","/v2/api-docs","/favicon.ico"};// ===== 原有其它代碼 =====} }在后續(xù)的訪問中,必須在請求中攜帶JWT數(shù)據(jù), 服務(wù)器端才可以嘗試解析此JWT數(shù)據(jù),從而判斷用戶是否已登錄或允許訪問。
為了便于測試,在控制器中添加一個測試訪問的請求配置:
// 以下是測試訪問的請求 @GetMapping("/hello") public String sayHello() {return "hello~~~"; }由于以上 /admins/hello 路徑并不在白名單中,如果直接訪問,會出現(xiàn)403錯誤。
在規(guī)范的使用方式中,JWT數(shù)據(jù)必須攜帶在請求頭(Request Header)的Authorization屬性中。
按照以上規(guī)范,則服務(wù)器端在每次接收到請求后,首先,就應(yīng)該先判斷請求頭中是否存在Authorization、Authorization的值是否有效等操作,通常,是通過過濾器來實現(xiàn)以上檢查的。
在passport的根包下的security包下創(chuàng)建JwtAuthenticationFilter過濾器類,需要繼承自O(shè)ncePerRequestFilter類:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {System.out.println("JwtAuthenticationFilter.doFilterInternal()");} }所有的過濾器都必須注冊后才可以使用,且同一個項目中允許存在多個過濾器,形成過濾器鏈,以上用于驗證JWT的過濾器應(yīng)該運行在Spring Security處理登錄的過濾器之前,需要在自定義的SecurityConfiguration中的configure()方法中將以上自定義的過濾器注冊在Spring Security的相關(guān)過濾器之前:
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter {// 新增@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;// ===== 原有其它代碼 =====@Overrideprotected void configure(HttpSecurity http) throws Exception {// ===== 原有其它代碼 =====// 注冊處理JWT的過濾器// 此過濾器必須在Spring Security處理登錄的過濾器之前http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);} }完成后,重啟項目,無論對哪個路徑發(fā)出請求,在控制臺都可以看出輸出了過濾器中的輸出語句內(nèi)容,并且,在瀏覽器將顯示一片空白。
關(guān)于JwtAuthenticationFilter,它需要實現(xiàn):
- 嘗試從請求頭中獲取JWT數(shù)據(jù)
- 如果無JWT數(shù)據(jù),應(yīng)該直接放行,Spring Security還會進行后續(xù)的處理,例如白名單的請求將允許訪問,其它請求將禁止訪問
- 如果存在JWT數(shù)據(jù),應(yīng)該嘗試解析
- 如果解析失敗,應(yīng)該視為錯誤,可以要求客戶端重新登錄,客戶端就可以得到新的、正確的JWT,客戶端在下一次提交請求時,使用新的JWT即可正確訪問
- 將解析得到的數(shù)據(jù)封裝到Authentication對象中
- Spring Security的上下文中存儲的數(shù)據(jù)類型是Authentication類型
- 為避免存入1次后,Spring Security的上下文中始終存在Authentication,在此過濾器執(zhí)行的第一時間,應(yīng)該清除上下文中的數(shù)據(jù)
要使用Spring Security實現(xiàn)授權(quán)訪問,首先,必須保證用戶登錄后,在Spring Security上下文中存在權(quán)限相關(guān)信息(目前,此項已完成,在JwtAuthenticationFilter的最后,已經(jīng)存入權(quán)限信息)。
然后,需要在配置類上使用@EnableGlobalMethodSecurity注解開啟“通過注解配置權(quán)限”的功能,所以,在SecrutiyConfiguration類上添加:
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) // 新增 public class SecurityConfiguration extends WebSecurityConfigurerAdapter {// ===== 類中原有代碼 ===== }最后,在任何你需要設(shè)置權(quán)限的處理請求的方法上,通過@PreAuthorize注解來配置要求某種權(quán)限,例如:
@GetMapping("/hello") @PreAuthorize("hasAuthority('/ams/admin/read')") // 新增 public String sayHello() {return "hello~~~"; }完成后,重啟項目,使用具有/ams/admin/read權(quán)限的用戶可以直接訪問,不具有此權(quán)限的用戶則不能訪問
總結(jié)
以上是生活随笔為你收集整理的Spring Security框架的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mamp nginx php7,MAMP
- 下一篇: Spring4 学习教程