Spring?Boot如何接入Security權(quán)限認證服務(wù)
SpringBoot實戰(zhàn):Spring Boot接入Security權(quán)限認證服務(wù)
引言
Spring Security
是一個功能強大且高度可定制的身份驗證和訪問控制的框架,提供了完善的認證機制和方法級的授權(quán)功能,是一個非常優(yōu)秀的權(quán)限管理框架。其核心是一組過濾器鏈,不同的功能經(jīng)由不同的過濾器。本文將通過一個案例將 Spring Security
整合到 SpringBoot
中,要實現(xiàn)的功能就是在認證服務(wù)器上登錄,然后獲取Token,再訪問資源服務(wù)器中的資源。
一、基本介紹
登錄驗證:
通過 JWT
為每個用戶生成一個唯一且有期限的 Token
,用戶每次請求都會重新生成過期時間,在規(guī)定的時間內(nèi),用戶未進行操作 Token
就會過期,當(dāng)用戶再次請求時則會再次執(zhí)行登錄流程,而 Token
的過期時間應(yīng)根據(jù)實際的業(yè)務(wù)場景規(guī)定。
權(quán)限認證:
權(quán)限認證通過Spring Security
框架來實現(xiàn),在用戶成功登錄之后,當(dāng)嘗試訪問系統(tǒng)資源時(即發(fā)起接口調(diào)用),服務(wù)端會根據(jù)用戶所屬的角色來判斷其是否具備相應(yīng)的訪問權(quán)限。若用戶未獲得該資源的訪問權(quán)限,則服務(wù)端應(yīng)當(dāng)返回明確的權(quán)限不足提示信息,以確保系統(tǒng)的安全性與用戶體驗。
通過如圖來講解我們的實現(xiàn)目標(biāo):登錄驗證
和 權(quán)限認證
二、環(huán)境準(zhǔn)備
創(chuàng)建 auth_user
系統(tǒng)用戶表,并準(zhǔn)備測試數(shù)據(jù)。
CREATE TABLE `auth_user` ( `id` varchar(36) NOT NULL, `username` varchar(100) DEFAULT NULL, `password` varchar(100) DEFAULT NULL, `role` varchar(100) DEFAULT NULL, `account_non_expired` int(11) DEFAULT '0', `account_non_locked` int(11) DEFAULT '0', `credentials_non_expired` int(11) DEFAULT '0', `is_enabled` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf32; INSERT INTO auth_user (id, username, password, `role`, account_non_expired, account_non_locked, credentials_non_expired, is_enabled) VALUES ('1', 'user', '15tT+y0b+lJq2HIKUjsvvg==', 'USER', 1, 1, 1, 1), ('2', 'admin', '15tT+y0b+lJq2HIKUjsvvg==', 'ADMIN', 1, 1, 1, 1);
三、登錄代碼實現(xiàn)
1.為項目導(dǎo)入相關(guān)依賴
在pom.xml
文件中到入依賴,除了 Security
之外 還引入了 AES
和 JWT
相關(guān)依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- AES加密 --> <dependency> <groupId>org.apache.directory.studio</groupId> <artifactId>org.apache.commons.codec</artifactId> <version>1.8</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> </dependencies>
創(chuàng)建項目所需實體類:
在工程中創(chuàng)建一個新的實體類AuthUser
,該實體類需要實現(xiàn)Spring Security
的UserDetails
接口,并特別地,需要重寫getAuthorities()
方法來從數(shù)據(jù)庫中動態(tài)讀取并設(shè)置用戶的角色權(quán)限。此外,為了確保用戶賬戶處于正常激活狀態(tài),isAccountNonExpired()
、isAccountNonLocked()
、isCredentialsNonExpired()
、isEnabled()
這四個方法也必須被重寫,并且應(yīng)該基于數(shù)據(jù)庫查詢的結(jié)果或業(yè)務(wù)邏輯,無條件地返回true(假設(shè)在這個場景下,所有用戶賬戶都被視為有效、未過期、未鎖定且憑據(jù)未過期)。
這樣的設(shè)計確保了AuthUser
類能夠準(zhǔn)確地反映用戶的安全狀態(tài)和權(quán)限信息,同時允許Spring Security
框架利用這些信息進行訪問控制。通過從數(shù)據(jù)庫動態(tài)加載權(quán)限信息,系統(tǒng)能夠靈活地適應(yīng)不同用戶的權(quán)限需求,提升系統(tǒng)的安全性和靈活性。
public class AuthUser implements Serializable, UserDetails { private static final long serialVersionUID = 1L; private String id; private String username; private String password; private String role; private Integer accountNonExpired; private Integer accountNonLocked; private Integer credentialsNonExpired; private Integer isEnabled; @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 獲取用戶所有權(quán)限 String[] roles = role.split(","); // 遍歷 roles,取出每一個權(quán)限進行認證,添加到簡單的授予認證類 List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (String role : roles) { authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); } // 返回到已經(jīng)被授予認證的權(quán)限集合, 這里面的角色所擁有的權(quán)限都已經(jīng)被 spring security 所知道 return authorities; } @Override public boolean isAccountNonExpired() { return this.accountNonExpired != null && this.accountNonExpired == 1; } @Override public boolean isAccountNonLocked() { return this.accountNonLocked != null && this.accountNonLocked == 1; } @Override public boolean isCredentialsNonExpired() { return this.credentialsNonExpired != null && this.credentialsNonExpired == 1; } @Override public boolean isEnabled() { return this.isEnabled != null && this.isEnabled == 1; } // 略去其它 Get、Set 方法 }
創(chuàng)建 Service 服務(wù)
創(chuàng)建名為 AuthUserService
的接口,并實現(xiàn) UserDetailsService
類,重寫 loadUserByUsername()
方法( Security
認證登錄調(diào)用的接口)。
public interface AuthUserService extends UserDetailsService { } @Service("authUserService") public class AuthUserServiceImpl implements AuthUserService { @Resource private AuthUserDao authUserDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { AuthUser authUser = authUserDao.queryByName(username); if (authUser == null) { throw new IllegalArgumentException("User [" + username + "] doesn't exist."); } return authUser; } }
AutUserDao
是用來解讀數(shù)據(jù)庫信息的類, queryByName()
是通過 username
從 auth_user
數(shù)據(jù)表進行精準(zhǔn)查詢。
Congtroller 層方法
創(chuàng)建兩個接口分別供不同角色測試。
@RestController @RequestMapping("api/resource") public class ResourceController { @GetMapping("user") public String demo1() { return "User demo."; } @GetMapping("admin") public String demo2() { return "Admin demo."; } }
四、工具類
AES加密
在前后端數(shù)據(jù)傳輸過程中明文密碼傳輸存在相當(dāng)大的隱患,可以采用加密的方式,對信息進行隱藏,話不多說上代碼。
public class AESUtil { private final static String ALGORITHM = "AES/CBC/NoPadding"; private final static String DEFAULT_IV = "1234567890123456"; private final static String DEFAULT_KEY = "1234567890123456"; public static String encrypt(String data) throws Exception { return encrypt(data, DEFAULT_KEY, DEFAULT_IV); } public static String desEncrypt(String data) throws Exception { return desEncrypt(data, DEFAULT_KEY, DEFAULT_IV); } public static String encrypt(String data, String key, String iv) throws Exception { Cipher cipher = Cipher.getInstance(ALGORITHM); int blockSize = cipher.getBlockSize(); byte[] dataBytes = data.getBytes(); int length = dataBytes.length; if (length % blockSize != 0) { length = length + (blockSize - (length % blockSize)); } byte[] plaintext = new byte[length]; System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length); SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted = cipher.doFinal(plaintext); return new Base64().encodeToString(encrypted); } public static String desEncrypt(String data, String key, String iv) throws Exception { byte[] encrypted1 = new Base64().decode(data); Cipher cipher = Cipher.getInstance(ALGORITHM); SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] bytes = cipher.doFinal(encrypted1); return new String(bytes); } }
JWT生成
通過引入JWT
(JSON Web Tokens),我們可以高效地管理用戶的登錄狀態(tài)。JWT
能夠生成一串包含過期時間的Token
值,該值以字符串形式存在。當(dāng)Token
達到其設(shè)定的過期時間時,嘗試對其進行解析將會觸發(fā)ExpiredJwtException
異常。通過捕獲這個ExpiredJwtException
異常,我們能夠有效地判斷用戶的登錄狀態(tài)是否已經(jīng)過期。在上述描述中,createJWT()
函數(shù)負責(zé)生成Token,而parseJWT()
函數(shù)則負責(zé)解析Token
。這樣的機制既方便了Token的生成與管理,也簡化了用戶登錄狀態(tài)的驗證過程。
public class TokenUtil { /** * 密鑰 */ public static final String JWT_KEY = "ibudai"; /** * 過期時間 */ public static final Long JWT_TTL = TimeUnit.MINUTES.toMillis(5); /** * 生成 Token */ public static String createJWT(String data, Long ttlMillis) { String uuid = UUID.randomUUID().toString().replaceAll("-", ""); JwtBuilder builder = getJwtBuilder(data, ttlMillis, uuid); return builder.compact(); } /** * 解析 Token */ public static Claims parseJWT(String token) { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); } /** * 生成加密后的秘鑰 */ private static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JWT_KEY); return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm algorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if (ttlMillis == null) { ttlMillis = JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) // 計算內(nèi)容 .setSubject(subject) // 簽發(fā)者 .setIssuer("budai") // 簽發(fā)時間 .setIssuedAt(now) // 加密算法簽名 .signWith(algorithm, secretKey) .setExpiration(expDate); } }
五、權(quán)限配置
接下來正式配置 Security
權(quán)限模塊。
新建SecurityConfig
類,并使其繼承自WebSecurityConfigurerAdapter
,隨后在該類中重寫configure(AuthenticationManagerBuilder auth)
方法。在這個方法內(nèi)部,我們將利用AuthUserService
(即之前創(chuàng)建的用于從數(shù)據(jù)庫中讀取用戶角色數(shù)據(jù)的類)來配置用戶認證信息。這樣的配置確保了Spring Security
能夠基于數(shù)據(jù)庫中存儲的用戶和角色信息來執(zhí)行身份驗證。
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthUserService authUserService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 動態(tài)讀取數(shù)據(jù)庫信息 auth.userDetailsService(authUserService) // 自定義 AES 方式加密 .passwordEncoder(new AESEncoder()); } }
配置好上述代碼,首先來手動配置兩個角色 budia
, admian
以及相應(yīng)的角色權(quán)限和密碼。
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 手動配置 auth.inMemoryAuthentication() .withUser("budai").password("123456").roles("USER") .and() .withUser("admin").password("123456").roles("ADMIN", "USER") .and() // 自定義賬號信息解析方式 .passwordEncoder(new AESEncoder()); }
自定義加密
Security
中默認提供了強哈希加密方式 BCryptPasswordEncoder
,但也可根據(jù)實際需求自定義加密邏輯,這通過實現(xiàn) PasswordEncoder
接口并重寫其方法來完成。在自定義的 PasswordEncoder
實現(xiàn)中,matches
方法的 charSequence
參數(shù)實際上是用戶登錄時傳入的密碼(明文),該密碼在驗證前可能已經(jīng)過解密處理(如果前端使用了AES等加密方式)。而 matches
方法的另一個參數(shù) s(或根據(jù)具體實現(xiàn)可能命名為其他變量),則是從數(shù)據(jù)庫中讀取的、已經(jīng)加密存儲的用戶密碼值。由于前端工程中實施了AES數(shù)據(jù)加密,因此在服務(wù)器端進行密碼驗證之前,需要先對接收到的加密密碼進行解密操作。
public class AESEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { String str = charSequence.toString(); try { String plain; if (!Objects.equals(str, "userNotFoundPassword")) { plain = AESUtil.desEncrypt(str); } else { plain = str; } return AESUtil.encrypt(plain); } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean matches(CharSequence charSequence, String s) { try { String plain = AESUtil.desEncrypt(charSequence.toString()); String result = AESUtil.encrypt(plain); return Objects.equals(result, s); } catch (Exception e) { throw new RuntimeException(e); } } }
權(quán)限分配
完成用戶角色的創(chuàng)建之后,接下來的步驟是為不同的角色分配相應(yīng)的資源權(quán)限。這通常在SecurityConfig
類中通過重寫configure(HttpSecurity http)
方法來實現(xiàn)。在該方法中,可以配置哪些接口(如freeAPI
、userAPI
和adminAPI
)可以被特定用戶角色訪問。這些接口的配置信息可以存儲在yml
文件中,并通過Spring
的注解機制動態(tài)獲取。
當(dāng)未認證用戶嘗試訪問受保護的資源時,Spring Security
會自動將請求重定向到登錄頁面,但在這里,我們通過formLogin().loginProcessingUrl("/api/auth/verify")
指定了一個自定義的登錄接口地址/api/auth/verify,以支持通過API請求方式進行用戶認證。用戶提交登錄請求后,AuthUserService
中的loadUserByUsername()
方法將被調(diào)用,以驗證用戶的用戶名和密碼,并確定其角色。
對于認證成功、認證失敗以及無權(quán)限訪問的情況,我們采用了匿名函數(shù)(或Lambda
表達式,具體取決于實現(xiàn)方式)來處理這些事件的邏輯。這些處理邏輯可能包括重定向到特定頁面、返回錯誤信息或執(zhí)行其他自定義操作。
public class AESEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { String str = charSequence.toString(); try { String plain; if (!Objects.equals(str, "userNotFoundPassword")) { plain = AESUtil.desEncrypt(str); } else { plain = str; } return AESUtil.encrypt(plain); } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean matches(CharSequence charSequence, String s) { try { String plain = AESUtil.desEncrypt(charSequence.toString()); String result = AESUtil.encrypt(plain); return Objects.equals(result, s); } catch (Exception e) { throw new RuntimeException(e); } } }
六、邏輯處理
成功處理
用戶成功通過認證后,系統(tǒng)會執(zhí)行兩個關(guān)鍵步驟來管理登錄狀態(tài)和權(quán)限控制。首先,會生成一個JWT
(JSON Web Token)Token
值,該Token
用于后續(xù)請求的登錄狀態(tài)管理。JWT是基于登錄用戶的用戶名、密碼(通常是密碼的哈希值,而非明文)及角色信息序列化后的JSON數(shù)據(jù)計算得出的,確保了數(shù)據(jù)的安全性和可驗證性。其次,用戶的角色信息會被封裝成一個Authentication
認證碼,該認證碼是username:password
(注意:這里的password
部分應(yīng)替換為更安全的信息,如用戶ID或角色的哈希值,因為直接包含密碼是不安全的)經(jīng)過Base64
編碼后的值,用于后續(xù)的權(quán)限過濾。
這兩個認證信息——JWT Token
和Authentication
認證碼——都會通過HTTP
響應(yīng)的請求頭返回給前端。前端接收到這些信息后,會將其存儲起來,并在后續(xù)發(fā)出的所有請求中,在請求頭中攜帶這兩個參數(shù)。后端則通過配置過濾器與Spring Security框架,實現(xiàn)對這些請求頭的解析,從而驗證用戶的登錄狀態(tài)和訪問權(quán)限,完成登錄狀態(tài)的管理與權(quán)限訪問控制。
失敗處理
用戶未通過 Security 認證時,需要通過驗證碼狀態(tài)等信息來響應(yīng)給前端, 在這里我們通過新建的返回類? 來返回結(jié)果給前端。
private void failureHandle(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { String msg; if (exception instanceof LockedException) { msg = "Account has been locked, please contact the administrator."; } else if (exception instanceof BadCredentialsException) { msg = "Account credential error, please recheck."; } else { msg = "Account doesn't exist, please recheck."; } response.setContentType("application/json;charset=UTF-8"); response.setStatus(203); ResultData<Object> result = new ResultData<>(203, msg, null); response.getWriter().write(objectMapper.writeValueAsString(result)); }
無權(quán)攔截
在用戶沒有經(jīng)過 權(quán)限認證的情況下訪問資源,則需要進行攔截并返回響應(yīng)的狀態(tài)信息。
private void unAuthHandle(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { String msg = "Please login and try again."; response.setContentType("application/json;charset=UTF-8"); response.setStatus(203); ResultData<Object> result = new ResultData<>(203, msg, null); response.getWriter().write(objectMapper.writeValueAsString(result)); }
七、Filter配置
Bean注入
@Configuration public class FilterConfig { /** * 設(shè)置放行資源 * * 例:/api/auth/verify */ @Value("${auth.api.verify}") private String verifyAPI; @Bean public FilterRegistrationBean<AuthFilter> orderFilter1() { FilterRegistrationBean<AuthFilter> filter = new FilterRegistrationBean<>(); filter.setName("auth-filter"); // Set effect url filter.setUrlPatterns(Collections.singleton("/**")); // Set ignore url, when multiply the value spilt with "," filter.addInitParameter("excludedUris", verifyAPI); filter.setOrder(-1); filter.setFilter(new AuthFilter()); return filter; } }
攔截邏輯
我們新建一個名為AuthFilter
的自定義過濾器類并實現(xiàn)Filter
接口時,我們需要重點關(guān)注doFilter()
方法的實現(xiàn)。如之前所述,一旦用戶通過登錄認證成功,系統(tǒng)會將JWT Token
和Authentication
認證信息寫入HTTP響應(yīng)的請求頭中,并返回給前端。之后,前端在發(fā)起任何需要認證或權(quán)限驗證的請求時,都應(yīng)在請求頭中包含這兩個參數(shù)。
在請求到達后端時,首先會觸發(fā)Spring Security
的認證流程。Spring Security
會使用請求頭中的Authentication
認證信息(盡管通常不直接使用username:password
格式的Base64編碼,而是可能使用更安全的認證令牌,如預(yù)共享密鑰生成的Token或基于HTTP頭部的認證方式)進行初步的身份驗證。這一部分是Spring Security
內(nèi)部自動處理的,我們無需直接操作。
一旦通過Spring Security
的身份驗證,請求將繼續(xù)流向我們配置的AuthFilter
。在AuthFilter
的doFilter()
方法中,我們需要編寫邏輯來解析請求頭中的JWT Token。這個Token包含了用戶的會話信息,如用戶名、角色以及Token的簽發(fā)和過期時間等。我們將驗證這個Token是否有效(比如檢查它是否未過期),如果Token已過期,我們需要構(gòu)造一個包含相應(yīng)錯誤信息的響應(yīng),并通過HTTP狀態(tài)碼(如401 Unauthorized)返回給前端。前端接收到這個響應(yīng)后,可以根據(jù)需要重定向用戶到登錄頁面。
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; int status; String msg; String token = req.getHeader("Token"); if (StringUtils.isNotBlank(token)) { boolean isExpired = false; try { TokenUtil.parseJWT(token); } catch (ExpiredJwtException e) { isExpired = true; } if (!isExpired) { filterChain.doFilter(req, servletResponse); return; } else { status = 203; msg = "Login expired."; } } else { status = 203; msg = "Please login and try again."; } response.setContentType("application/json;charset=UTF-8"); response.setStatus(status); ResultData<Object> result = new ResultData<>(status, msg, null); response.getWriter().write(objectMapper.writeValueAsString(result)); }
八、跨域處理
在工程中新建 CorsConfig
類實現(xiàn) WebMvcConfigurer
接口并重寫 addCorsMappings()
方法配置跨域信息
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/** * 設(shè)置跨域訪問地址,逗號分隔 * * 例:http://localhost:8080,http://127.0.0.1:8080 */ @Value("${auth.host.cors}") private String hosts; @Override public void addCorsMappings(CorsRegistry registry) { String[] crosHost = hosts.trim().split(","); // 設(shè)置允許跨域的路徑 registry.addMapping("/**") // 設(shè)置允許跨域請求的域名 .allowedOriginPatterns(crosHost) // 是否允許cookie .allowCredentials(true) // 設(shè)置允許的請求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 設(shè)置允許的header屬性 .allowedHeaders("*") // 跨域允許時間 .maxAge(TimeUnit.SECONDS.toMillis(5)); } }
到此這篇關(guān)于Spring Boot接入Security權(quán)限認證服務(wù)的文章就介紹到這了,更多相關(guān)Spring Boot接入Security權(quán)限認證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis-generator-gui根據(jù)需求改動示例
這篇文章主要為大家介紹了mybatis-generator-gui根據(jù)需求改動示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09SpringBoot兩種方式接入DeepSeek的實現(xiàn)
本文主要介紹了SpringBoot兩種方式接入DeepSeek的實現(xiàn),包括HttpClient方式和基于spring-ai-openai的方式,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03Springmvc 4.x利用@ResponseBody返回Json數(shù)據(jù)的方法
這篇文章主要介紹了Springmvc 4.x利用@ResponseBody返回Json數(shù)據(jù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04